14 điểm bởi cybrshin 2026-01-04 | 9 bình luận | Chia sẻ qua WhatsApp

Ban đầu tôi dùng claude code và cursor, nhưng sau khi nghỉ việc năm ngoái, từ tháng 11 tôi chuyển sang antigravity và cứ mỗi ngày lại làm đủ kiểu dự án web service đồ chơi. Rồi đột nhiên 3 ngày trước (1/2), tôi захотел thử làm một game thủ thành.
Vốn dĩ tôi là frontend web developer nên lười bọc thành app, cứ để nó chỉ chạy trong môi trường trình duyệt desktop thôi. Tôi cũng nghĩ phần vận hành dựa trên canvas và xử lý file hình ảnh sẽ tốn quá nhiều thời gian, nên bắt đầu với ý tưởng tận dụng emoji tối đa.

Định là xong trong một ngày, nhưng càng làm càng vui nên cứ sửa với thêm cái này cái kia, thành ra mất tới 3 ngày. T_T

Tech stack cơ bản như sau.

  • Frontend: Next.js 14+ (App Router), TypeScript, HTML5 Canvas
  • Backend: FastAPI, Python 3.9+, Pydantic
  • Database: Supabase (PostgreSQL)
  • Deployment: Vercel

Sau nhiều lần chỉnh sửa, logic cuối cùng của game được chốt như sau.

1. Sức tấn công của tháp

Mỗi khi tháp tăng cấp thông qua merge, tầm đánh (Range) sẽ tăng thêm 10, còn hiệu suất sức tấn công được điều chỉnh thành 1,8 lần (cân bằng game).

  • Công thức: Damage = floor(BaseDamage * (1.8 ^ (Level - 1)))
    • Lv 1: 10
    • Lv 2: 18
    • Lv 3: 32
    • Lv 4: 57

2. Mẫu xuất hiện của địch (Wave Logic)

Để tránh sự đơn điệu, nhiều kiểu wave khác nhau được áp dụng.

  • Wave thường (Horde Mix): Địch của tier hiện tại (70%) và tier trước đó (30%) sẽ xuất hiện xen lẫn.
  • Wave boss (Every 6th): Boss là 1 kẻ địch mạnh nhất của thời đại hiện tại xuất hiện, xung quanh có lính thường hộ tống.
    • Số lượng địch trong wave boss: Wave / 6 (wave 6 có 1 con, wave 12 có 2 con...)

4. Thưởng vàng (Active)

Khi tiêu diệt địch hoặc hoàn thành wave, bạn sẽ nhận được vàng.

ps. Tôi không cân nhắc nhiều tới môi trường di động. Có thêm touch event nên trên Fold hay Pad thì vẫn ổn, nhưng với màn hình nhỏ thì hầu như chưa tối ưu lắm~

ps. Nếu có điểm cần cải thiện hoặc bug thì hãy báo qua bình luận, lúc nào có thời gian tôi sẽ phản ánh dần.

https://tower.dsp.ai.kr/

9 bình luận

 
roxie 2026-02-27

Không thể tin nổi luôn... 700 wave sao............???

 
cybrshin 2026-02-28

Làm được nhé haha

 
wancomplete 2026-01-07

Gần đây tôi cũng thử làm game và dùng https://verse8.io, thấy nó còn tự tạo cả hình ảnh và hoạt ảnh sprite 2D nên tôi không phải tốn thời gian làm asset mà có thể chỉ tập trung vào logic game. Hơn nữa, vì có thể triển khai ngay trên nền tảng nên cũng không cần phải bận tâm riêng về hosting, thật sự rất tiện.

 
huiya 2026-01-06

Khá thú vị. Việc emoji được sử dụng tích cực mang lại cảm giác rất mới mẻ.
Tôi không hay chơi game thủ thành nên cũng không chắc đây có phải là nhận xét đúng hay không, nhưng việc không tách riêng thời gian wave và thời gian chuẩn bị, khiến người chơi vẫn có thể đổi vị trí tháp ngay cả khi wave đang tới, tạo cho tôi cảm giác khá lạ.
Nếu cực đoan hơn thì có thể liên tục đổi vị trí để tấn công ở mọi thời điểm, nên tôi cảm thấy tầm bắn trở nên vô nghĩa.

 
cybrshin 2026-01-06

Cảm ơn mọi người đã chơi vui.
Trong lúc làm, tôi cũng cảm thấy việc di chuyển vị trí hơi kỳ, nhưng tôi thấy đó theo cách nào đó cũng có thể trở thành điểm mạnh của trò chơi này, đồng thời là một yếu tố năng động và có thể điều khiển được, nên tôi cứ để nguyên như vậy.
Thực ra nếu muốn di chuyển nhanh thì sẽ rất khó nếu không hợp nhất các tháp.
Có nên thêm hình phạt cho việc di chuyển hay không, v.v., có vẻ vẫn là những điểm cần tiếp tục suy nghĩ về cân bằng game.

 
huiya 2026-01-07

À, hóa ra bạn đã cân nhắc hết rồi. Tôi đã suy nghĩ quá nông cạn.

 
cybrshin 2026-01-07

Không phải vậy. Cảm ơn ý kiến của bạn.

 
waylake 2026-01-04

Xin chào, tôi đã chơi rất vui và xin gửi phản hồi liên quan đến bảo mật.

  • Hiện tại HMAC secret (khóa dùng để ký) đang bị lộ ở frontend
    • Có thể chỉnh sửa trực tiếp gold, wave, lives, towers từ frontend

Tôi báo để bạn biết vì có thể phát sinh vấn đề về gói cước của Vercel và Supabase.

const state = {  
  towers: [],    
  gold: 9999,  
  wave: 1,       
  lives: 1       
};  
  
const state_raw = JSON.stringify(state);  
const user_id = "11111";  
const secret = "11111";  
  
(async () => {  
  const key = await crypto.subtle.importKey(  
    "raw",  
    new TextEncoder().encode(secret),  
    { name: "HMAC", hash: "SHA-256" },  
    false,  
    ["sign"]  
  );  
  const signatureBuffer = await crypto.subtle.sign(  
    "HMAC",  
    key,  
    new TextEncoder().encode(state_raw)  
  );  
  const signature = Array.from(new Uint8Array(signatureBuffer))  
    .map(b => b.toString(16).padStart(2, "0")).join("");  
    
  fetch("https://tower.dsp.ai.kr/api/sync";, {  
    method: "POST",  
    headers: {  
      "Content-Type": "application/json",  
      "Accept": "*/*"  
    },  
    body: JSON.stringify({  
      user_id,  
      state_raw,  
      signature  
    })  
  })  
  .then(res => res.text())  
  .then(console.log)  
  .catch(console.error);  
})();  
  
 
cybrshin 2026-01-04

À, cảm ơn~