Documentation

DEPLOY BÓNG MỚI

Mục lục
  1. Tổng quan hệ thống
  2. Cấu trúc folder bóng
  3. config.json
  4. weapon.js — Vũ khí
  5. ai.js — Lối chơi / AI
  6. skill.js — Kỹ năng đặc biệt
  7. anim.js — Animation
  8. Bảng tra cứu API đầy đủ
  9. Ví dụ đầy đủ: "Lính Tên Lửa"
  10. Những gì KHÔNG thể thay đổi

1. Tổng quan hệ thống

Mỗi đấu sĩ trong game là một package độc lập gồm 1 ảnh sprite + 4 file code JavaScript. Bạn upload cả folder qua trang Admin, hệ thống tự đọc và ghép thành 1 bóng chiến đấu được.

4 file code chia đúng 4 vai trò, mỗi vai trò chạy độc lập với nhau mỗi khung hình (frame):

FileVai tròChạy khi nào
weapon.jsVẽ vũ khí + tính va chạm cận chiếnMỗi frame, 1 lần
ai.jsQuyết định di chuyểnMỗi frame, đầu tiên
skill.jsKỹ năng đặc biệt (buff, phi tiêu, vùng nổ...)Mỗi frame, sau AI
anim.jsChọn frame sprite hiển thịMỗi frame, cuối cùng
Không cần viết cả 4 từ đầu. Khi bạn mở một bóng có sẵn trong Admin để sửa, cả 4 ô đều hiển thị code đang chạy thật — copy sang bóng mới rồi chỉnh lại là cách nhanh nhất để bắt đầu.

2. Cấu trúc folder bóng

Tạo 1 folder bất kỳ, đặt tên gì cũng được, bên trong chứa:

my-new-fighter/ ├── config.json // tùy chọn — chỉ số + cách cắt sprite ├── sprite.png // bắt buộc — 1 ảnh, nhiều frame xếp thành lưới ├── weapon_xxx.js // bắt buộc — tên chứa "weapon" ├── ai_xxx.js // bắt buộc — tên chứa "ai" hoặc "behavior" ├── skill_xxx.js // bắt buộc — tên chứa "skill" └── anim_xxx.js // bắt buộc — tên chứa "anim"

Tên file linh hoạt — hệ thống chỉ cần tìm thấy từ khóa trong tên (không phân biệt hoa/thường). weapon_katana.js, WeaponCode.js, my-weapon-v2.js đều nhận diện đúng là "weapon". Nếu thiếu 1 trong 4 vai trò, hoặc thiếu ảnh, Admin sẽ báo lỗi rõ ràng và không lưu.

Lưu ý ảnh sprite: cần là lưới ảnh đều nhau (vd 4 cột × 1 hàng = 4 frame animation cùng kích thước). Khai báo số cột/hàng trong config.json bằng cols/rows — hitbox va chạm của bóng sẽ tự tính bằng kích thước 1 frame (chiều rộng ảnh ÷ cols, chiều cao ÷ rows).

3. config.json

Hoàn toàn tùy chọn — nếu không có, hệ thống dùng giá trị mặc định. Tất cả field cũng tùy chọn riêng lẻ.

{
  "cols": 4,
  "rows": 1,
  "maxHP": 200,
  "maxSpeed": 4,
  "baseDamage": 8,
  "radius": 28,
  "color": "#4caf50",
  "skillName": "Tên kỹ năng hiển thị",
  "skillDesc": "Mô tả ngắn kỹ năng làm gì"
}
FieldMặc địnhGhi chú
cols / rows1 / 1Số cột/hàng cắt sprite sheet
maxHP200Máu tối đa
maxSpeed4Tốc độ di chuyển tối đa
baseDamage8Sát thương mặc định cho vũ khí/phi tiêu nếu code không tự ghi đè
radiustự tính theo ảnhBán kính va chạm — thường để trống, dùng giá trị tự động
color#888888Màu fallback khi ảnh chưa tải xong

4. weapon.js — Vũ khí chạy mỗi frame

Nhận 3 tham số: (self, ctx, api). Nhiệm vụ: vẽ vũ khí bằng api.drawLine/drawCircle/drawTriangleTip, rồi return vị trí đầu vũ khí để engine tự tính va chạm.

Vũ khí cận chiến (vd kiếm quay quanh thân)

self.weaponAngle = (self.weaponAngle || 0) + 0.12 * self.weaponBoost;
const len = 46, width = 10;
const tipX = self.x + Math.cos(self.weaponAngle) * len;
const tipY = self.y + Math.sin(self.weaponAngle) * len;

api.drawLine(self.x, self.y, tipX, tipY, "#c9d6e3", width);
api.drawCircle(tipX, tipY, width * 0.55, "#e8eef5");

// Bắt buộc: trả về vị trí đầu vũ khí + bán kính va chạm để gây damage
return { tipX, tipY, hitRadius: width / 2 + 14 };

Bóng không có cận chiến (chỉ đánh qua skill)

// Không vẽ gì hoặc chỉ vẽ trang trí, rồi trả về null
return null;

Giá trị trả về (object hoặc null):

FieldBắt buộc?Ý nghĩa
tipX, tipYTọa độ đầu vũ khí
hitRadiusKhông (mặc định 8)Bán kính vùng gây damage quanh đầu vũ khí
originX, originYKhông (mặc định = tâm bóng)Điểm bắt đầu của vũ khí — engine tính va chạm theo cả đoạn thẳng từ đây đến tipX/tipY, không chỉ điểm đầu mũi
damageOverrideKhông (mặc định dùng Sát thương cơ bản)Ghi đè damage riêng cho đòn cận chiến này

5. ai.js — Lối chơi / AI chạy mỗi frame

Nhận (self, enemy, api). Nhiệm vụ duy nhất: chỉnh self.vx / self.vy để điều khiển hướng di chuyển. Vật lý va chạm (nảy tường, va bóng) engine tự lo — AI không thể và không cần can thiệp vào phần đó.

AI đuổi theo (mẫu mặc định, dùng cho bóng cận chiến)

const dx = enemy.x - self.x, dy = enemy.y - self.y;
const dist = Math.sqrt(dx*dx + dy*dy) || 1;
if (api.now % 15 === 0) {
  self.vx += (dx / dist) * 0.9 + (Math.random() - 0.5) * 0.6;
  self.vy += (dy / dist) * 0.9 + (Math.random() - 0.5) * 0.6;
}

AI giữ khoảng cách / né tránh (dùng cho bóng tầm xa)

const dx = enemy.x - self.x, dy = enemy.y - self.y;
const dist = Math.sqrt(dx*dx + dy*dy) || 1;
const desiredDist = 180;
if (api.now % 20 === 0) {
  const away = dist < desiredDist ? -1 : 0.3;
  self.vx += (dx / dist) * away * 1.2 + (Math.random() - 0.5) * 1.0;
  self.vy += (dy / dist) * away * 1.2 + (Math.random() - 0.5) * 1.0;
}
Dùng api.now % N === 0 để chỉ ra quyết định mỗi N frame thay vì mỗi frame — tránh AI "rung" liên tục và giúp chuyển động tự nhiên hơn.

6. skill.js — Kỹ năng đặc biệt chạy mỗi frame

Nhận (self, enemy, api). Đây là nơi duy nhất có quyền gây damage đặc biệt, tạo phi tiêu, tạo vùng nổ, đổi trạng thái animation. Cooldown/thời điểm kích hoạt tự quản lý bằng cách lưu vào self.skillState (object tự do, bạn muốn lưu gì cũng được).

Mẫu buff tạm thời (tăng damage vũ khí có thời hạn)

if (!self.skillState) self.skillState = { cooldown: 0, active: 0 };
if (self.skillState.active > 0) {
  self.skillState.active--;
  self.weaponBoost = 2.0;
} else {
  self.weaponBoost = 1.0;
  if (self.skillState.cooldown > 0) {
    self.skillState.cooldown--;
  } else {
    self.skillState.active = 60;   // buff kéo dài 60 frame
    self.skillState.cooldown = 300; // cooldown 300 frame
  }
}

Mẫu phóng phi tiêu thẳng

if (!self.skillState) self.skillState = { cooldown: 0 };
if (self.skillState.cooldown > 0) {
  self.skillState.cooldown--;
} else {
  const dx = enemy.x - self.x, dy = enemy.y - self.y;
  const dist = Math.sqrt(dx*dx + dy*dy) || 1;
  const spawned = api.spawnProjectile({
    vx: (dx / dist) * 8,
    vy: (dy / dist) * 8,
    damage: 6,
    radius: 8,
    lifespan: 90
  });
  if (spawned) self.skillState.cooldown = 100;
}

Mẫu bắn CHÍNH vũ khí đang quay quanh thân mới

Khác với phi tiêu tự tạo ở trên (bay 1 vật thể mới), cách này bắn đi đúng cây vũ khí đang quay quanh thân bóng — thân bóng sẽ mất khả năng cận chiến cho tới khi vũ khí bay đủ 3 lần nảy tường và tự quay về. Cần phối hợp 2 file cùng lúc:

skill.js — bắn vũ khí chính đi (kind: "mainWeapon")
if (!self.skillState) self.skillState = { cooldown: 0 };
if (self.mainWeaponFlying) {
  // Vũ khí đang bay -> không bắn thêm, đợi nó tự về (đúng luật:
  // "đợi vũ khí chính bật 3 lần và quay về thì mới xài tiếp")
} else if (self.skillState.cooldown > 0) {
  self.skillState.cooldown--;
} else {
  const dx = enemy.x - self.x, dy = enemy.y - self.y;
  const dist = Math.sqrt(dx*dx + dy*dy) || 1;
  const spawned = api.spawnProjectile({
    vx: (dx / dist) * 7,
    vy: (dy / dist) * 7,
    damage: 15,        // damage riêng cho lúc bay (không nhất thiết = cận chiến)
    radius: 10,
    kind: "mainWeapon"  // <- bắn chính vũ khí, không phải vật thể mới
  });
  if (spawned) self.skillState.cooldown = 250;
}
weapon.js — PHẢI kiểm tra self.mainWeaponFlying ở đầu
// Khi vũ khí đang bay làm phi tiêu, thân bóng không còn gì quay quanh nữa
if (self.mainWeaponFlying) return null;

// Code vẽ vũ khí bình thường — dùng LẠI CHÍNH đoạn code này để vẽ hình
// dạng lúc bay (engine tự gọi lại weapon.js với self giả đặt tại vị trí
// phi tiêu để hình dạng lúc bay giống hệt lúc quay quanh thân)
self.weaponAngle = (self.weaponAngle || 0) + 0.12 * self.weaponBoost;
const len = 46, width = 10;
const tipX = self.x + Math.cos(self.weaponAngle) * len;
const tipY = self.y + Math.sin(self.weaponAngle) * len;
api.drawLine(self.x, self.y, tipX, tipY, "#c9d6e3", width);
api.drawCircle(tipX, tipY, width * 0.55, "#e8eef5");
return { tipX, tipY, hitRadius: width / 2 + 14 };
Vì sao cần dòng if (self.mainWeaponFlying) return null;? Khi vũ khí đang bay, engine vẫn gọi weapon.js mỗi frame như bình thường để kiểm tra xem thân bóng có nên vẽ/gây damage cận chiến không. Nếu code không tự kiểm tra cờ này, vũ khí sẽ vừa bay đi vừa "mọc lại" ngay trên thân bóng — sai với ý đồ "chỉ có 1 vũ khí duy nhất, đang bay thì thân trống tay".

Mẫu vùng nổ hẹn giờ (xem thêm ví dụ đầy đủ ở mục 9)

if (!self.skillState) self.skillState = { cooldown: 0 };
if (self.skillState.cooldown > 0) {
  self.skillState.cooldown--;
} else {
  api.setAnimTag("cast");
  api.spawnZoneEffect({
    x: enemy.x, y: enemy.y,   // đánh dấu ngay vị trí địch lúc kích hoạt
    radius: 50,
    delay: 180,                // đợi 180 frame (~3 giây @ 60fps) rồi nổ
    damage: 35,
    damageMode: "once"
  });
  self.skillState.cooldown = 400;
}

7. anim.js — Animation chạy mỗi frame

Nhận (self, api). Trả về {col, row} — tọa độ frame trong lưới sprite muốn hiển thị ngay lúc này.

Animation chạy vòng lặp đơn giản (idle/di chuyển)

if (!self.animState) self.animState = { tick: 0 };
self.animState.tick++;
const cols = self.sprite ? self.sprite.cols : 1;
const totalFrames = cols * (self.sprite ? self.sprite.rows : 1);
const frameIndex = Math.floor(self.animState.tick / 8) % totalFrames;
return { col: frameIndex % cols, row: Math.floor(frameIndex / cols) };

Animation đổi theo trạng thái skill (đọc self.animTag)

// self.animTag tự động là "idle" bình thường, tự thành "hurt" khi vừa
// trúng đòn (12 frame), hoặc bất kỳ chuỗi nào skill.js đặt qua setAnimTag.
if (self.animTag === "hurt") {
  return { col: 3, row: 0 }; // frame nhăn mặt
}
if (self.animTag === "raise_hand") {
  return { col: 2, row: 0 }; // frame giơ tay ra hiệu
}
// Mặc định: animation idle chạy vòng lặp
if (!self.animState) self.animState = { tick: 0 };
self.animState.tick++;
const frameIndex = Math.floor(self.animState.tick / 10) % 2;
return { col: frameIndex, row: 0 };
self.animTag được engine tự quản lý một phần: mặc định "idle", tự chuyển sang "hurt" trong 12 khung hình ngay sau khi bóng nhận damage (từ bất kỳ nguồn nào — vũ khí, phi tiêu, hay vùng nổ), rồi tự trả lại giá trị trước đó. Bạn không cần tự viết logic này — chỉ cần đọc self.animTag trong anim.js và xử lý theo ý muốn.

8. Bảng tra cứu API đầy đủ

Hàm / BiếnDùng trongMô tả
api.nowCả 4 loạiSố khung hình hiện tại từ đầu trận
api.drawLine(x1,y1,x2,y2,color,width)weaponVẽ 1 đoạn thẳng
api.drawCircle(x,y,r,color)weaponVẽ 1 hình tròn đặc
api.drawTriangleTip(x,y,angle,len,color)weaponVẽ đầu mũi tam giác (kiểu mũi giáo)
api.applyDamage(target, amount)skillGây damage trực tiếp lên 1 bóng (target là self hoặc enemy)
api.heal(target, amount)skillHồi máu, không vượt quá maxHP
api.setAnimTag(tag)skillĐổi self.animTag sang chuỗi tùy ý
api.spawnProjectile(opts)skillPhóng phi tiêu (tối đa 1 cái đang bay/bóng)
api.spawnZoneEffect(opts)skillTạo vùng hiệu ứng hẹn giờ tại tọa độ cố định

Tham số spawnProjectile(opts)

FieldMặc địnhÝ nghĩa
vx, vy0, 0Vận tốc bay ban đầu
damagebaseDamage của bóngSát thương khi trúng
radius6Bán kính va chạm của phi tiêu
colorvàng nhạtMàu vẽ (chỉ áp dụng khi kind:"custom")
lifespan600Số frame tối đa trước khi tự bay về chủ
returnTokhông"owner" = bay về sớm sau khi bay quá nửa lifespan (kiểu boomerang)
kind"custom""custom" = vật thể tự tạo hình tròn; "mainWeapon" = bắn chính vũ khí đang quay quanh thân — xem mục 6 để biết cách phối hợp với weapon.js

Phi tiêu tự động nảy tường như bi-a; sau đúng 3 lần nảy, tự bẻ hướng bay thẳng về chủ dù không set returnTo. Với kind:"mainWeapon": dù có bay trúng đối thủ giữa đường, vũ khí không biến mất — vẫn tiếp tục hành trình cho tới khi đủ 3 lần nảy tường rồi mới tự quay về; trong suốt thời gian đó thân bóng không có vũ khí nào để đánh cận chiến.

Tham số spawnZoneEffect(opts)

FieldMặc địnhÝ nghĩa
x, yvị trí bóng chủTọa độ cố định của vùng hiệu ứng (không di chuyển)
radius40Bán kính vùng ảnh hưởng
delay0Số frame đợi trước khi kích hoạt (vd 180 ≈ 3 giây)
damagebaseDamage của bóngSát thương mỗi lần gây
damageMode"once""once" = nổ 1 phát khi hết delay; "tick" = gây damage đều đặn
duration0(chỉ dùng với "tick") Số frame tồn tại sau khi kích hoạt
tickInterval30(chỉ dùng với "tick") Số frame giữa mỗi lần gây damage
friendlyFirefalsetrue nếu bóng chủ cũng có thể bị chính vùng này gây hại
colorcam đỏMàu vẽ vùng hiệu ứng

9. Ví dụ đầy đủ: "Lính Tên Lửa"

Minh họa cách kết hợp setAnimTag + spawnZoneEffect để tạo skill: khi kích hoạt, đổi hình sang tư thế giơ tay, đánh dấu vị trí đối thủ, đợi 3 giây rồi một quả tên lửa rơi xuống gây nổ diện rộng. Đây chỉ là ví dụ tham khảo — không có sẵn trong game, bạn cần tự tạo folder và sprite riêng nếu muốn dùng.

weapon.js — không có cận chiến, chỉ tấn công bằng tên lửa
return null;
ai.js — đứng giữ khoảng cách vừa phải để ngắm bắn
const dx = enemy.x - self.x, dy = enemy.y - self.y;
const dist = Math.sqrt(dx*dx + dy*dy) || 1;
const desiredDist = 200;
if (api.now % 20 === 0) {
  const away = dist < desiredDist ? -1 : 0.4;
  self.vx += (dx / dist) * away * 1.0 + (Math.random() - 0.5) * 0.8;
  self.vy += (dy / dist) * away * 1.0 + (Math.random() - 0.5) * 0.8;
}
skill.js — giơ tay, đánh dấu, đợi 3 giây, tên lửa rơi nổ
if (!self.skillState) self.skillState = { cooldown: 0, raising: 0 };

if (self.skillState.raising > 0) {
  // Đang trong tư thế giơ tay (30 frame) trước khi thực sự đánh dấu mục tiêu
  api.setAnimTag("raise_hand");
  self.skillState.raising--;
  if (self.skillState.raising === 0) {
    api.spawnZoneEffect({
      x: enemy.x, y: enemy.y,     // chốt vị trí đối thủ NGAY lúc này
      radius: 55,
      delay: 180,                  // 180 frame ~ 3 giây (@ 60fps)
      damage: 35,
      damageMode: "once",
      color: "#ff5533"
    });
  }
} else if (self.skillState.cooldown > 0) {
  self.skillState.cooldown--;
} else {
  self.skillState.raising = 30;   // bắt đầu tư thế giơ tay
  self.skillState.cooldown = 500; // cooldown dài vì damage cao
}
anim.js — đổi frame theo animTag
if (self.animTag === "raise_hand") {
  return { col: 2, row: 0 }; // frame giơ tay trong sprite sheet
}
if (self.animTag === "hurt") {
  return { col: 3, row: 0 }; // frame trúng đòn (engine tự set, không cần gọi)
}
// idle mặc định: đi bộ tại chỗ, lặp giữa frame 0 và 1
if (!self.animState) self.animState = { tick: 0 };
self.animState.tick++;
const frameIndex = Math.floor(self.animState.tick / 12) % 2;
return { col: frameIndex, row: 0 };
config.json — sprite 4 cột: idle1, idle2, raise_hand, hurt
{
  "cols": 4,
  "rows": 1,
  "maxHP": 180,
  "maxSpeed": 4.5,
  "baseDamage": 35,
  "color": "#5a7a4a",
  "skillName": "Không Kích",
  "skillDesc": "Giơ tay đánh dấu mục tiêu, 3 giây sau tên lửa rơi xuống gây 35 sát thương diện rộng."
}

10. Những gì KHÔNG thể thay đổi

Đây là phần cố định trong engine — áp dụng cho mọi bóng, kể cả bóng mới tạo, không code nào (weapon/ai/skill/anim) có thể ghi đè hay tắt được:

Vật lý va chạm kiểu bi-a: bóng luôn nảy lại khi chạm tường (bảo toàn tốc độ), và va chạm giữa 2 bóng luôn tính theo công thức elastic collision thật (bảo toàn động lượng + động năng). AI code chỉ có quyền tác động lực (chỉnh self.vx/vy) — không có cách nào trong 4 loại code khiến bóng xuyên tường, dịch chuyển tức thời, hay bỏ qua va chạm với bóng đối thủ.

Điều này đảm bảo mọi bóng — dù skill có sáng tạo đến đâu — vẫn tuân theo cùng 1 luật chơi vật lý nền tảng, giữ cho trận đấu luôn công bằng và có thể đoán trước được ở cấp độ cơ bản nhất.