mirror of
https://github.com/Kelsidavis/WoWee.git
synced 2026-04-08 06:03:52 +00:00
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).
110 lines
4.9 KiB
Markdown
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
|
|
```
|