Kelsidavis-WoWee/docs/ANIMATION_SYSTEM.md
Paul b4989dc11f feat(animation): decompose AnimationController into FSM-based architecture
Replace the 2,200-line monolithic AnimationController (goto-driven,
single class, untestable) with a composed FSM architecture per
refactor.md.

New subsystem (src/rendering/animation/ — 16 headers, 10 sources):
- CharacterAnimator: FSM composer implementing ICharacterAnimator
- LocomotionFSM: idle/walk/run/sprint/jump/swim/strafe
- CombatFSM: melee/ranged/spell cast/stun/hit reaction/charge
- ActivityFSM: emote/loot/sit-down/sitting/sit-up
- MountFSM: idle/run/flight/taxi/fidget/rear-up (per-instance RNG)
- AnimCapabilitySet + AnimCapabilityProbe: probe once at model load,
  eliminate per-frame hasAnimation() linear search
- AnimationManager: registry of CharacterAnimator by GUID
- EmoteRegistry: DBC-backed emote command → animId singleton
- FootstepDriver, SfxStateDriver: extracted from AnimationController

animation_ids.hpp/.cpp moved to animation/ subdirectory (452 named
constants); all include paths updated.

AnimationController retained as thin adapter (~400 LOC): collects
FrameInput, delegates to CharacterAnimator, applies AnimOutput.

Priority order: Mount > Stun > HitReaction > Spell > Charge >
Melee/Ranged > CombatIdle > Emote > Loot > Sit > Locomotion.
STAY_IN_STATE policy when all FSMs return valid=false.

Bugs fixed:
- Remove static mt19937 in mount fidget (shared state across all
  mounted units) — replaced with per-instance seeded RNG
- Remove goto from mounted animation branch (skipped init)
- Remove per-frame hasAnimation() calls (now one probe at load)
- Fix VK_INDEX_TYPE_UINT16 → UINT32 in shadow pass

Tests (4 new suites, all ASAN+UBSan clean):
- test_locomotion_fsm: 167 assertions
- test_combat_fsm: 125 cases
- test_activity_fsm: 112 cases
- test_anim_capability: 56 cases

docs/ANIMATION_SYSTEM.md added (architecture reference).
2026-04-05 12:27:35 +03:00

110 lines
4.9 KiB
Markdown

# Animation System
Unified, FSM-based animation system for all characters (players, NPCs, companions).
Every character uses the same `CharacterAnimator` — there is no separate NPC/Mob animator.
## Architecture
```
AnimationController (thin adapter — bridges Renderer ↔ CharacterAnimator)
└─ CharacterAnimator (FSM composer — implements ICharacterAnimator)
├─ CombatFSM (stun, hit reaction, spell cast, melee, ranged, charge)
├─ ActivityFSM (emote, loot, sit/stand/kneel/sleep)
├─ LocomotionFSM (idle, walk, run, sprint, jump, swim, strafe)
└─ MountFSM (mount idle, mount run, flight)
AnimationManager (registry of CharacterAnimator instances by ID)
AnimCapabilitySet (probed once per model — cached resolved anim IDs)
AnimCapabilityProbe (queries which animations a model supports)
```
### Priority Resolution
`CharacterAnimator::resolveAnimation()` runs every frame. The first FSM to
return a valid `AnimOutput` wins:
1. **Mount** — if mounted, return `MOUNT` (overrides everything)
2. **Combat** — stun > hit reaction > spell > charge > melee/ranged > combat idle
3. **Activity** — emote > loot > sit/stand transitions
4. **Locomotion** — run/walk/sprint/jump/swim/strafe/idle
If no FSM produces a valid output, the last animation continues (STAY policy).
### Overlay Layer
After resolution, `applyOverlays()` substitutes stealth animation variants
(stealth idle, stealth walk, stealth run) without changing sub-FSM state.
## File Map
### Headers (`include/rendering/animation/`)
| File | Purpose |
|---|---|
| `i_animator.hpp` | Base interface: `onEvent()`, `update()` |
| `i_character_animator.hpp` | 20 virtual methods (combat, spells, emotes, mounts, etc.) |
| `character_animator.hpp` | FSM composer — the single animator class |
| `locomotion_fsm.hpp` | Movement states: idle, walk, run, sprint, jump, swim |
| `combat_fsm.hpp` | Combat states: melee, ranged, spell cast, stun, hit reaction |
| `activity_fsm.hpp` | Activity states: emote, loot, sit/stand/kneel |
| `mount_fsm.hpp` | Mount states: idle, run, flight, taxi |
| `anim_capability_set.hpp` | Probed capability flags + resolved animation IDs |
| `anim_capability_probe.hpp` | Probes a model for available animations |
| `anim_event.hpp` | `AnimEvent` enum (MOVE_START, MOVE_STOP, JUMP, etc.) |
| `animation_manager.hpp` | Central registry of CharacterAnimator instances |
| `weapon_type.hpp` | WeaponLoadout, RangedWeaponType enums |
| `emote_registry.hpp` | Emote name → animation ID lookup |
| `footstep_driver.hpp` | Footstep sound event driver |
| `sfx_state_driver.hpp` | State-transition SFX (jump, land, swim enter/exit) |
| `i_anim_renderer.hpp` | Interface for renderer animation queries |
### Sources (`src/rendering/animation/`)
| File | Purpose |
|---|---|
| `character_animator.cpp` | ICharacterAnimator implementation + priority resolver |
| `locomotion_fsm.cpp` | Locomotion state transitions + resolve logic |
| `combat_fsm.cpp` | Combat state transitions + resolve logic |
| `activity_fsm.cpp` | Activity state transitions + resolve logic |
| `mount_fsm.cpp` | Mount state transitions + resolve logic |
| `anim_capability_probe.cpp` | Model animation probing |
| `animation_manager.cpp` | Registry CRUD + bulk update |
| `emote_registry.cpp` | Emote database |
| `footstep_driver.cpp` | Footstep timing logic |
| `sfx_state_driver.cpp` | SFX transition detection |
### Controller (`include/rendering/animation_controller.hpp` + `src/rendering/animation_controller.cpp`)
Thin adapter that:
- Collects per-frame input from camera/renderer → `CharacterAnimator::FrameInput`
- Forwards state changes (combat, emote, spell, mount, etc.) → `CharacterAnimator`
- Reads `AnimOutput` → applies via `CharacterRenderer`
- Owns footstep and SFX drivers
## Key Types
- **`AnimEvent`** — discrete events: `MOVE_START`, `MOVE_STOP`, `JUMP`, `LAND`, `MOUNT`, `DISMOUNT`, etc.
- **`AnimOutput`** — result of FSM resolution: `{animId, loop, valid}`. `valid=false` means STAY.
- **`AnimCapabilitySet`** — probed once per model load. Caches resolved IDs and capability flags.
- **`CharacterAnimator::FrameInput`** — per-frame input struct (movement flags, timers, animation state queries).
## Adding a New Animation State
1. Decide which FSM owns the state (combat, activity, locomotion, or mount).
2. Add the state enum to the FSM's `State` enum.
3. Add transitions in the FSM's `resolve()` method.
4. Add resolved ID fields to `AnimCapabilitySet` if the animation needs model probing.
5. If the state needs external triggering, add a method to `ICharacterAnimator` and implement in `CharacterAnimator`.
## Tests
Each FSM has its own test file in `tests/`:
- `test_locomotion_fsm.cpp`
- `test_combat_fsm.cpp`
- `test_activity_fsm.cpp`
- `test_anim_capability.cpp`
Run all tests:
```bash
cd build && ctest --output-on-failure
```