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

4.9 KiB

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:

cd build && ctest --output-on-failure