G GAMESRICH
Retro Games · Advanced

How to Build a Martial Arts Brawler: Hang Jebat

Craft punch and kick combo chains, melee hitboxes, aggressive guard AI, and multi-phase boss duels.

By GamesRich Engineering 9 min read Zero External Libraries

Play the Live Game First

Experience the mechanics firsthand to understand what you are building.

PLAY LIVE GAME ↗

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:

Step-by-Step Implementation

1

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.

Step 1 Implementation
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);
    }
  });
}
2

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.

Step 2 Implementation
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);
  }
}
3

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.

Step 3 Implementation
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:

Complete Runnable Game (index.html)
<!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: