Architecture & Core Concept
The player and enemies share a unified combat entity model: - Coordinates: (x, y, vx, vy) - Health: hp, maxHp - Combat state: attack ('punch'|'kick'), attackTime, cool, face (1 or -1) When the player executes an attack, we scan for foes within range in the facing direction: const ahead = (foe.x - hero.x) * hero.face; If ahead > 0 and ahead < attackRange, apply damage, trigger enemy hit-stun (e.hit = 0.2), apply knockback velocity, and increment the combo multiplier.
Key Game Mechanics Taught:
- Combat state machine (idle, punch, kick, hit-stun, cooldown)
- Directional forward melee hitboxes scaled by character facing
- Combo multipliers and hit counter decay timers
- Enemy patrol and dynamic attack range approach AI
- Boss encounter mechanics with boss health bar UI
Step-by-Step Implementation
Define Combat State & Directional Hitboxes
Melee attacks only hit enemies in front of the warrior. By multiplying (foe.x - hero.x) by hero.face (1 for right, -1 for left), positive values guarantee the enemy is in front.
function strike(type) {
if (hero.cool > 0 || hero.attack) return;
hero.attack = type;
hero.attackTime = type === 'kick' ? 0.31 : 0.20;
hero.cool = type === 'kick' ? 0.43 : 0.27;
const range = type === 'kick' ? 75 : 58;
const damage = type === 'kick' ? 2 : 1;
enemies.forEach(e => {
if (e.dead || Math.abs(e.y - hero.y) > 65) return;
const ahead = (e.x - hero.x) * hero.face;
if (ahead > 0 && ahead < range) {
e.hp -= damage;
e.hit = 0.2; // Hit-stun duration
e.vx = hero.face * (type === 'kick' ? 250 : 150); // Knockback
hero.combo++;
hero.comboTimer = 1.5;
hero.score += 100 * hero.combo;
spawnHitSpark(e.x, e.y - 30);
}
});
}
Design Enemy Guard AI
Guards have vision range. If the player is within 500px, the guard faces the hero and walks forward until reaching striking distance (55px), where it executes periodic attacks.
function updateEnemyAI(e, dt) {
if (e.dead) return;
e.hit = Math.max(0, e.hit - dt);
e.wait -= dt;
e.face = e.x > hero.x ? -1 : 1;
const dist = Math.abs(e.x - hero.x);
if (e.hit <= 0 && dist > 50) {
e.vx += e.face * 360 * dt;
e.vx = Math.max(-90, Math.min(90, e.vx));
} else {
e.vx *= 0.7; // Stop walking when in melee range
}
e.x += e.vx * dt;
// Attack player if close enough and attack timer ready
if (dist < 58 && e.wait <= 0) {
e.wait = 1.2;
damagePlayer(e.boss ? 15 : 8);
}
}
Combo Counter Decay and Multipliers
Each successive strike raises the combo counter. A 1.5-second timer decays the counter back to zero if no hit lands in time, rewarding rhythmic aggressiveness.
function updateCombos(dt) {
hero.comboTimer -= dt;
if (hero.comboTimer <= 0) {
hero.combo = 0;
}
}
Complete Standalone Source Code
Here is the full, self-contained implementation. Save this as an index.html file on your computer and open it in any web browser to run your game immediately:
<!DOCTYPE html>
<html><head><meta charset="utf-8"><title>Martial Arts Brawler Demo</title>
<style>body{margin:0;background:#1e1b2e;display:flex;justify-content:center;align-items:center;height:100vh;}canvas{background:#a78bd0;}</style>
</head><body><canvas id="c" width="960" height="540"></canvas>
<script>
const c = document.querySelector('#c'), x = c.getContext('2d');
const K = { left:0, right:0, punch:0, kick:0 };
const H = { x:100, y:400, vx:0, face:1, hp:100, score:0, combo:0, cool:0 };
const foes = [{ x:500, y:400, hp:3, face:-1, vx:0, dead:0 }];
let last = performance.now();
window.onkeydown = e => {
if(['ArrowLeft','a'].includes(e.key)) K.left = 1;
if(['ArrowRight','d'].includes(e.key)) K.right = 1;
if(['j','z'].includes(e.key.toLowerCase())) attack('punch');
if(['k','x'].includes(e.key.toLowerCase())) attack('kick');
};
window.onkeyup = e => {
if(['ArrowLeft','a'].includes(e.key)) K.left = 0;
if(['ArrowRight','d'].includes(e.key)) K.right = 0;
};
function attack(type) {
if(H.cool > 0) return;
H.cool = 0.3;
const range = type==='kick'?70:50;
foes.forEach(e => {
if(e.dead) return;
const ahead = (e.x - H.x) * H.face;
if(ahead > 0 && ahead < range) {
e.hp -= (type==='kick'?2:1);
e.x += H.face * 30;
H.combo++; H.score += 100 * H.combo;
if(e.hp <= 0) e.dead = 1;
}
});
}
function loop(t) {
const dt = Math.min((t-last)/1000, 0.03); last = t;
H.cool = Math.max(0, H.cool - dt);
H.vx += (K.right - K.left) * 1200 * dt;
if(K.right) H.face = 1; if(K.left) H.face = -1;
H.vx *= Math.pow(0.01, dt);
H.x += H.vx * dt; H.x = Math.max(40, Math.min(920, H.x));
foes.forEach(e => {
if(e.dead) return;
e.face = e.x > H.x ? -1 : 1;
const dist = Math.abs(e.x - H.x);
if(dist > 50) e.x += e.face * 80 * dt;
});
x.fillStyle = '#a78bd0'; x.fillRect(0,0,960,540);
x.fillStyle = '#f9edc8'; x.fillRect(0,440,960,100);
// Draw Hero
x.fillStyle = '#e84046'; x.fillRect(H.x - 16, H.y, 32, 40);
x.fillStyle = '#d99b6b'; x.fillRect(H.x - 10, H.y - 20, 20, 20);
// Draw Foes
foes.filter(e => !e.dead).forEach(e => {
x.fillStyle = '#206b70'; x.fillRect(e.x - 16, e.y, 32, 40);
x.fillStyle = '#d99b6b'; x.fillRect(e.x - 10, e.y - 20, 20, 20);
});
x.fillStyle = '#fff'; x.font = 'bold 16px sans-serif';
x.fillText('SCORE: ' + H.score + ' | COMBO: ' + H.combo, 20, 30);
x.fillText('Controls: A/D (Move), J (Punch), K (Kick)', 20, 60);
requestAnimationFrame(loop);
}
requestAnimationFrame(loop);
</script>
</body></html>
Ideas to Expand Your Game
Now that you have the core mechanics running, consider adding:
- Local High Scores: Use
localStorage.setItem()andlocalStorage.getItem()to persist personal bests. - Visual Polish: Add screen shake on impacts by displacing
ctx.translate(shakeX, shakeY). - Mobile Touch Controls: Bind on-screen directional buttons using
onpointerdownandonpointerup.