FERRETGame engine over Babylon.js
Ferret

Melee combat

Skill ferret-combat · download all skills · SKILL.md

When to use#

A sword, axe or other melee fight between two bodies: a duel mini-game, a boss, sparring NPCs, a player against the AI.

The model#

A Fighter is one body's state machine (free, attack, block, blockImpact, parry, react, stagger, dodge, dead) driven by commands (move, face, run, attack, heavy, block, dodge). The player's input and FighterAI give the same commands. Its MoveSet names the clips; their timings (length, hit-start/hit-end notifies, additive) come from the library's clip set items. A Duel steps both fighters, sweeps the striking part of a weapon inside its window against the opponent's capsules and decides parry, block or hit, then the end and the restart. The rules are plain numbers: the hit shapes come from a BodyGeometry (BoneGeometry in the scene, ClipPoseGeometry in Node).

Steps#

  1. Make the clips: attacks that start and end in the guard, with hit-start/hit-end notifies; a block loop, a block impact, reactions, deaths, walks (ferret-animation, edit.clip.*).
  2. Measure the body: capsules from the skinned mesh (tools/fight-clips/capsules.mjs), the weapon's grip and striking part (tools/fight-clips/grip.mjs), into a BodySpec.
  3. Build each MoveSet from the clip timings of the bound clip sets; one Fighter per body.
  4. new Duel([a, b], geometry, { starts, restartAfter }); measure each attack's reach on the body (attackReach, or on the bones). Damage from outside the duel (fire, a trap): duel.scorch(fighter, damage); without restartAfter the game calls duel.restart() itself.
  5. In the scene: one FighterBody per body (update before the animations), then duel.step(dt, [cmdA, cmdB]) after the animations and IK. A player's command comes from input, the AI's from FighterAI.think(dt).
  6. React to the events (hit, blocked, parried, over, restart): HUD, sparks, sound.

Check afterwards#

Run fights unattended in Node with ClipPoseGeometry (as packages/combat/test/demo-duel.test.ts): every fight ends, blows land only inside the windows, bodiesOverlap() stays 0, both sides win, a harder AI wins more. In the browser draw the shapes (?capsules in examples/fight) and see that the red striking part meets the green capsules where the blade meets the body.

What usually goes wrong#

  • Scene and combat frames mixed: a scene point is (-X, Y, Z) in combat and the yaw is -rotation.y (fromScene, toScene).
  • Unreal additive clips (hit reactions, leans) played as poses fold the body to a point: they must be marked additive in the clip set and layered on the guard.
  • Hit capsules read before the frame's animations and IK: the rules see last frame's pose.
  • A reach guessed instead of measured: the AI stands too far to land or too close to swing.
  • One AI random source for both fighters: give each its own seededRandom so a seed replays the same fight.

Files#

  • reference/combat.md — Fighters driven by the player or the AI by the same rules, weapon sweeps against hit capsules, blocks, parries, stamina, health, reactions and the end of a fight.
  • reference/api-combat.md — every export of @ferret/combat with signature and doc comment