# Melee combat

# Melee combat (`combat`)

`@ferret/combat` runs a fight between two armed bodies by rules that come from their clips. The player and the AI give the same
commands to the same `Fighter`, so the AI fights with exactly the animations and timings the player has, and a role can
pass from one to the other at any moment. The rules work on plain numbers (no Babylon), so a whole fight runs in Node in a test; the
Babylon side reads the hit shapes from the real bones and poses the bodies.

The sample is `examples/fight`: a Viking with an axe against a Knight with a sword, both on the AI.

## Fighter

A `Fighter` is one body's state machine. Its `MoveSet` names the clips it uses and their timings
(`ClipTiming`: length, loop, notifies, additive); the timings come from the library's clip set items, so the rules
always match the clips played.

Commands (`FighterCommand`), the same for the player and the AI:

| Command | Effect |
|---|---|
| `move: [right, forward]` | walk, back off or strafe; the locomotion clip is picked by the velocity |
| `run` | run instead of walk |
| `face: [x, z]` | turn toward a point at `speed.turn` |
| `attack` | the light attack; a second press during it chains the next one (`chain`, `chainFrom`) |
| `heavy` | the heavy attack |
| `block` (held) | raise the guard |
| `dodge` | a quick step back (the dodge clip, its distance and time) |

States (`FighterStateKind`): `free`, `attack`, `block`, `blockImpact`, `parry`, `react`, `stagger`, `dodge`,
`dead`. Each plays a clip of the move set; how long it lasts is the clip's length. An attack (`AttackDef`) can hit only
between its clip's `hit-start` and `hit-end` notifies (or its own `window`), it turns toward the opponent only while it winds up
(`track`), and it costs stamina. Stamina is also spent on dodges and blocked blows and comes back after a short rest
(`STAMINA_REGEN`, `STAMINA_DELAY`).

## Duel

A `Duel` holds two fighters on a round field. `step` takes one command per fighter and returns the
events of the tick (`DuelEvent`). In each tick the weapon of a fighter inside its hit window is swept from its place in
the frame before to its place now against the opponent's capsules (`sweepHit`), so a fast blade does not pass through a
thin arm between two frames. A defender who guards (blocks facing the blow, or is inside his parry window) meets the blade `guardReach`
(0.25 m) in front of his body, so the blade stops on the guard, not inside him. A blow that meets the body or the guard is:

- **parried** when the defender began his block at most `parryWindow` (0.15 s) before it, or is inside a parry clip's window: the
  attacker staggers;
- **blocked** when the defender blocks and the attacker stands in front of him (within 75 degrees): the defender plays the block impact
  and pays stamina for the damage, the attacker's blade bounces back to his guard; with no stamina left the guard breaks and he staggers;
- a **hit** otherwise: damage by the part struck (head more, hands and forearms less), a reaction by the side it came from, and at no
  health the death clip that falls away from the blow.

Something outside the duel can hurt a fighter too: `duel.scorch(fighter, damage)` (the demo's Dragon fire) takes health and staggers him (event `scorched`), leaves him alone while he is reeling, and the last of his health ends the fight for the other one (`death` with `by: 'fire'`, then `over`).

Then the fight is `over`; with `restartAfter` (or by `restart()`, called when the game decides, as the demo does after its ending) both go back to their places with full health. The duel keeps the two bodies apart (their
trunk and head capsules only touch; `bodiesOverlap()` measures it).

## Hit geometry

`BodyGeometry` gives a fighter's capsules and the striking part of his weapon as he is posed now. Two sources:

- `BoneGeometry` reads them from the body's bones in the scene, after the frame's animations and IK;
- `ClipPoseGeometry` computes them from clip JSON on the body's rest pose, for Node.

A body is described by a `BodySpec`: its capsules (`CapsuleSpec`: from one bone to another, with a
radius) and its weapon (`WeaponSpec`: the bone that holds it, the grip, and the striking part as a range along the
grip axis). `tools/fight-clips/capsules.mjs` measures the capsules from the skinned mesh, so they follow the real silhouette.
`attackReach` measures how far an attack lands from its clip; the AI keeps the distance its weapon wants by it.

Combat's frame is right-handed, y up (glTF's). A Babylon scene is left-handed and its glTF loader mirrors models into it, so a scene point
is `(-X, Y, Z)` in combat (`fromScene`, `toScene`) and a yaw is the negative of `rotation.y`.

## Bodies in the scene

`FighterBody` moves a body with its fighter each frame: the root where the fighter stands, turned as he faces, and the
bones posed by the clips of his state through the animation backend (`GroupBackend` of `animation`). Clips are sampled at the time the
rules give, never started on their own, with short crossfades between states. A clip marked `additive` (Unreal's additive hit reactions)
lies on top of the guard instead of replacing it.

Call order in a frame: `FighterBody.update` before the animations, then the animations and IK, then `Duel.step` reading the bones.

## AI

`FighterAI` is a behaviour tree of `ai` over the same commands. Levels (`AI_LEVELS`: `easy`,
`normal`, `hard`, or your own `AiLevel`) set the reaction time, the chances to parry, block and dodge a seen attack,
aggression, combos, heavy attacks, when it tires and how often it strikes back. It closes in, circles, keeps its own weapon's distance,
answers a seen attack after its reaction time, chains and backs off when tired. Its random choices come from
`seededRandom`: the same seed gives the same fight.

```ts
const duel = new Duel([viking, knight], geometry, { starts, restartAfter: 3 });
const ais = [new FighterAI(viking, duel, AI_LEVELS.normal, seededRandom(1), reach),
             new FighterAI(knight, duel, AI_LEVELS.hard, seededRandom(2), reach)];
scene.onAfterAnimationsObservable.add(() => {
  const dt = scene.getEngine().getDeltaTime() / 1000;
  for (const e of duel.step(dt, [ais[0].think(dt), ais[1].think(dt)])) if (e.type === 'over') console.log(`${e.winner} wins`);
});
```

A player's fighter takes its command from input instead of `think`; nothing else changes.

## Testing a fight

Because the rules are plain numbers, a fight can run unattended in Node with `ClipPoseGeometry`: `packages/combat/test/demo-duel.test.ts`
runs sixteen AI-against-AI fights of the demo and checks that every fight ends and restarts, that blows land only inside the hit windows,
that the bodies never overlap, that both sides win and that a hard AI beats an easy one.
