mirror of
https://github.com/Kelsidavis/WoWee.git
synced 2026-04-15 00:43:52 +00:00
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).
This commit is contained in:
parent
e58f9b4b40
commit
b4989dc11f
53 changed files with 5110 additions and 2099 deletions
287
src/rendering/animation/anim_capability_probe.cpp
Normal file
287
src/rendering/animation/anim_capability_probe.cpp
Normal file
|
|
@ -0,0 +1,287 @@
|
|||
// ============================================================================
|
||||
// AnimCapabilityProbe
|
||||
//
|
||||
// Scans a model's animation capabilities once and returns an
|
||||
// AnimCapabilitySet with resolved IDs (after fallback chains).
|
||||
// Extracted from the scattered hasAnimation/pickFirstAvailable calls
|
||||
// in AnimationController.
|
||||
// ============================================================================
|
||||
|
||||
#include "rendering/animation/anim_capability_probe.hpp"
|
||||
#include "rendering/animation/animation_ids.hpp"
|
||||
#include "rendering/renderer.hpp"
|
||||
#include "rendering/character_renderer.hpp"
|
||||
#include "rendering/m2_renderer.hpp"
|
||||
|
||||
namespace wowee {
|
||||
namespace rendering {
|
||||
|
||||
uint32_t AnimCapabilityProbe::pickFirst(Renderer* renderer, uint32_t instanceId,
|
||||
const uint32_t* candidates, size_t count) {
|
||||
auto* charRenderer = renderer->getCharacterRenderer();
|
||||
if (!charRenderer) return 0;
|
||||
for (size_t i = 0; i < count; ++i) {
|
||||
if (charRenderer->hasAnimation(instanceId, candidates[i])) {
|
||||
return candidates[i];
|
||||
}
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
AnimCapabilitySet AnimCapabilityProbe::probe(Renderer* renderer, uint32_t instanceId) {
|
||||
AnimCapabilitySet caps;
|
||||
if (!renderer || instanceId == 0) return caps;
|
||||
|
||||
auto* cr = renderer->getCharacterRenderer();
|
||||
if (!cr) return caps;
|
||||
|
||||
auto has = [&](uint32_t id) -> bool {
|
||||
return cr->hasAnimation(instanceId, id);
|
||||
};
|
||||
|
||||
// Helper: pick first available from static array
|
||||
auto pick = [&](const uint32_t* candidates, size_t count) -> uint32_t {
|
||||
return pickFirst(renderer, instanceId, candidates, count);
|
||||
};
|
||||
|
||||
// ── Locomotion ──────────────────────────────────────────────────────
|
||||
// STAND is animation ID 0. Every M2 model has sequence 0 as its base idle.
|
||||
// Cannot use ternary `has(0) ? 0 : 0` — both branches are 0.
|
||||
caps.resolvedStand = anim::STAND;
|
||||
caps.hasStand = has(anim::STAND);
|
||||
|
||||
{
|
||||
static const uint32_t walkCands[] = {anim::WALK, anim::RUN};
|
||||
caps.resolvedWalk = pick(walkCands, 2);
|
||||
caps.hasWalk = (caps.resolvedWalk != 0);
|
||||
}
|
||||
{
|
||||
static const uint32_t runCands[] = {anim::RUN, anim::WALK};
|
||||
caps.resolvedRun = pick(runCands, 2);
|
||||
caps.hasRun = (caps.resolvedRun != 0);
|
||||
}
|
||||
{
|
||||
static const uint32_t sprintCands[] = {anim::SPRINT, anim::RUN, anim::WALK};
|
||||
caps.resolvedSprint = pick(sprintCands, 3);
|
||||
caps.hasSprint = (caps.resolvedSprint != 0);
|
||||
}
|
||||
{
|
||||
static const uint32_t walkBackCands[] = {anim::WALK_BACKWARDS, anim::WALK};
|
||||
caps.resolvedWalkBackwards = pick(walkBackCands, 2);
|
||||
caps.hasWalkBackwards = (caps.resolvedWalkBackwards != 0);
|
||||
}
|
||||
{
|
||||
static const uint32_t strafeLeftCands[] = {anim::SHUFFLE_LEFT, anim::RUN_LEFT, anim::WALK};
|
||||
caps.resolvedStrafeLeft = pick(strafeLeftCands, 3);
|
||||
}
|
||||
{
|
||||
static const uint32_t strafeRightCands[] = {anim::SHUFFLE_RIGHT, anim::RUN_RIGHT, anim::WALK};
|
||||
caps.resolvedStrafeRight = pick(strafeRightCands, 3);
|
||||
}
|
||||
{
|
||||
static const uint32_t runLeftCands[] = {anim::RUN_LEFT, anim::RUN};
|
||||
caps.resolvedRunLeft = pick(runLeftCands, 2);
|
||||
}
|
||||
{
|
||||
static const uint32_t runRightCands[] = {anim::RUN_RIGHT, anim::RUN};
|
||||
caps.resolvedRunRight = pick(runRightCands, 2);
|
||||
}
|
||||
|
||||
// ── Jump ────────────────────────────────────────────────────────────
|
||||
caps.resolvedJumpStart = has(anim::JUMP_START) ? anim::JUMP_START : 0;
|
||||
caps.resolvedJump = has(anim::JUMP) ? anim::JUMP : 0;
|
||||
caps.resolvedJumpEnd = has(anim::JUMP_END) ? anim::JUMP_END : 0;
|
||||
caps.hasJump = (caps.resolvedJumpStart != 0);
|
||||
|
||||
// ── Swim ────────────────────────────────────────────────────────────
|
||||
caps.resolvedSwimIdle = has(anim::SWIM_IDLE) ? anim::SWIM_IDLE : 0;
|
||||
caps.resolvedSwim = has(anim::SWIM) ? anim::SWIM : 0;
|
||||
caps.hasSwim = (caps.resolvedSwimIdle != 0 || caps.resolvedSwim != 0);
|
||||
{
|
||||
static const uint32_t swimBackCands[] = {anim::SWIM_BACKWARDS, anim::SWIM};
|
||||
caps.resolvedSwimBackwards = pick(swimBackCands, 2);
|
||||
}
|
||||
{
|
||||
static const uint32_t swimLeftCands[] = {anim::SWIM_LEFT, anim::SWIM};
|
||||
caps.resolvedSwimLeft = pick(swimLeftCands, 2);
|
||||
}
|
||||
{
|
||||
static const uint32_t swimRightCands[] = {anim::SWIM_RIGHT, anim::SWIM};
|
||||
caps.resolvedSwimRight = pick(swimRightCands, 2);
|
||||
}
|
||||
|
||||
// ── Melee combat (fallback chains match resolveMeleeAnimId) ─────────
|
||||
{
|
||||
static const uint32_t melee1HCands[] = {
|
||||
anim::ATTACK_1H, anim::ATTACK_2H, anim::ATTACK_UNARMED,
|
||||
anim::ATTACK_2H_LOOSE, anim::PARRY_UNARMED, anim::PARRY_1H};
|
||||
caps.resolvedMelee1H = pick(melee1HCands, 6);
|
||||
}
|
||||
{
|
||||
static const uint32_t melee2HCands[] = {
|
||||
anim::ATTACK_2H, anim::ATTACK_1H, anim::ATTACK_UNARMED,
|
||||
anim::ATTACK_2H_LOOSE, anim::PARRY_UNARMED, anim::PARRY_1H};
|
||||
caps.resolvedMelee2H = pick(melee2HCands, 6);
|
||||
}
|
||||
{
|
||||
static const uint32_t melee2HLooseCands[] = {
|
||||
anim::ATTACK_2H_LOOSE_PIERCE, anim::ATTACK_2H_LOOSE,
|
||||
anim::ATTACK_2H, anim::ATTACK_1H, anim::ATTACK_UNARMED};
|
||||
caps.resolvedMelee2HLoose = pick(melee2HLooseCands, 5);
|
||||
}
|
||||
{
|
||||
static const uint32_t meleeUnarmedCands[] = {
|
||||
anim::ATTACK_UNARMED, anim::ATTACK_1H, anim::ATTACK_2H,
|
||||
anim::ATTACK_2H_LOOSE, anim::PARRY_UNARMED, anim::PARRY_1H};
|
||||
caps.resolvedMeleeUnarmed = pick(meleeUnarmedCands, 6);
|
||||
}
|
||||
{
|
||||
static const uint32_t meleeFistCands[] = {
|
||||
anim::ATTACK_FIST_1H, anim::ATTACK_FIST_1H_OFF,
|
||||
anim::ATTACK_1H, anim::ATTACK_UNARMED,
|
||||
anim::PARRY_FIST_1H, anim::PARRY_1H};
|
||||
caps.resolvedMeleeFist = pick(meleeFistCands, 6);
|
||||
}
|
||||
{
|
||||
static const uint32_t meleePierceCands[] = {
|
||||
anim::ATTACK_1H_PIERCE, anim::ATTACK_1H, anim::ATTACK_UNARMED};
|
||||
caps.resolvedMeleePierce = pick(meleePierceCands, 3);
|
||||
}
|
||||
{
|
||||
static const uint32_t meleeOffCands[] = {
|
||||
anim::ATTACK_OFF, anim::ATTACK_1H, anim::ATTACK_UNARMED};
|
||||
caps.resolvedMeleeOffHand = pick(meleeOffCands, 3);
|
||||
}
|
||||
{
|
||||
static const uint32_t meleeOffFistCands[] = {
|
||||
anim::ATTACK_FIST_1H_OFF, anim::ATTACK_OFF,
|
||||
anim::ATTACK_FIST_1H, anim::ATTACK_1H};
|
||||
caps.resolvedMeleeOffHandFist = pick(meleeOffFistCands, 4);
|
||||
}
|
||||
{
|
||||
static const uint32_t meleeOffPierceCands[] = {
|
||||
anim::ATTACK_OFF_PIERCE, anim::ATTACK_OFF,
|
||||
anim::ATTACK_1H_PIERCE, anim::ATTACK_1H};
|
||||
caps.resolvedMeleeOffHandPierce = pick(meleeOffPierceCands, 4);
|
||||
}
|
||||
{
|
||||
static const uint32_t meleeOffUnarmedCands[] = {
|
||||
anim::ATTACK_UNARMED_OFF, anim::ATTACK_UNARMED,
|
||||
anim::ATTACK_OFF, anim::ATTACK_1H};
|
||||
caps.resolvedMeleeOffHandUnarmed = pick(meleeOffUnarmedCands, 4);
|
||||
}
|
||||
caps.hasMelee = (caps.resolvedMelee1H != 0 || caps.resolvedMeleeUnarmed != 0);
|
||||
|
||||
// ── Ready stances ───────────────────────────────────────────────────
|
||||
{
|
||||
static const uint32_t ready1HCands[] = {
|
||||
anim::READY_1H, anim::READY_2H, anim::READY_UNARMED};
|
||||
caps.resolvedReady1H = pick(ready1HCands, 3);
|
||||
}
|
||||
{
|
||||
static const uint32_t ready2HCands[] = {
|
||||
anim::READY_2H, anim::READY_2H_LOOSE, anim::READY_1H, anim::READY_UNARMED};
|
||||
caps.resolvedReady2H = pick(ready2HCands, 4);
|
||||
}
|
||||
{
|
||||
static const uint32_t ready2HLooseCands[] = {
|
||||
anim::READY_2H_LOOSE, anim::READY_2H, anim::READY_1H, anim::READY_UNARMED};
|
||||
caps.resolvedReady2HLoose = pick(ready2HLooseCands, 4);
|
||||
}
|
||||
{
|
||||
static const uint32_t readyUnarmedCands[] = {
|
||||
anim::READY_UNARMED, anim::READY_1H, anim::READY_FIST};
|
||||
caps.resolvedReadyUnarmed = pick(readyUnarmedCands, 3);
|
||||
}
|
||||
{
|
||||
static const uint32_t readyFistCands[] = {
|
||||
anim::READY_FIST_1H, anim::READY_FIST, anim::READY_1H, anim::READY_UNARMED};
|
||||
caps.resolvedReadyFist = pick(readyFistCands, 4);
|
||||
}
|
||||
{
|
||||
static const uint32_t readyBowCands[] = {
|
||||
anim::READY_BOW, anim::READY_1H, anim::READY_UNARMED};
|
||||
caps.resolvedReadyBow = pick(readyBowCands, 3);
|
||||
}
|
||||
{
|
||||
static const uint32_t readyRifleCands[] = {
|
||||
anim::READY_RIFLE, anim::READY_BOW, anim::READY_1H, anim::READY_UNARMED};
|
||||
caps.resolvedReadyRifle = pick(readyRifleCands, 4);
|
||||
}
|
||||
{
|
||||
static const uint32_t readyCrossbowCands[] = {
|
||||
anim::READY_CROSSBOW, anim::READY_BOW, anim::READY_1H, anim::READY_UNARMED};
|
||||
caps.resolvedReadyCrossbow = pick(readyCrossbowCands, 4);
|
||||
}
|
||||
{
|
||||
static const uint32_t readyThrownCands[] = {
|
||||
anim::READY_THROWN, anim::READY_1H, anim::READY_UNARMED};
|
||||
caps.resolvedReadyThrown = pick(readyThrownCands, 3);
|
||||
}
|
||||
|
||||
// ── Ranged attacks ──────────────────────────────────────────────────
|
||||
{
|
||||
static const uint32_t fireBowCands[] = {anim::FIRE_BOW, anim::ATTACK_BOW};
|
||||
caps.resolvedFireBow = pick(fireBowCands, 2);
|
||||
}
|
||||
caps.resolvedAttackRifle = has(anim::ATTACK_RIFLE) ? anim::ATTACK_RIFLE : 0;
|
||||
{
|
||||
static const uint32_t attackCrossbowCands[] = {anim::ATTACK_CROSSBOW, anim::ATTACK_BOW};
|
||||
caps.resolvedAttackCrossbow = pick(attackCrossbowCands, 2);
|
||||
}
|
||||
caps.resolvedAttackThrown = has(anim::ATTACK_THROWN) ? anim::ATTACK_THROWN : 0;
|
||||
caps.resolvedLoadBow = has(anim::LOAD_BOW) ? anim::LOAD_BOW : 0;
|
||||
caps.resolvedLoadRifle = has(anim::LOAD_RIFLE) ? anim::LOAD_RIFLE : 0;
|
||||
|
||||
// ── Special attacks ─────────────────────────────────────────────────
|
||||
caps.resolvedSpecial1H = has(anim::SPECIAL_1H) ? anim::SPECIAL_1H : 0;
|
||||
caps.resolvedSpecial2H = has(anim::SPECIAL_2H) ? anim::SPECIAL_2H : 0;
|
||||
caps.resolvedSpecialUnarmed = has(anim::SPECIAL_UNARMED) ? anim::SPECIAL_UNARMED : 0;
|
||||
caps.resolvedShieldBash = has(anim::SHIELD_BASH) ? anim::SHIELD_BASH : 0;
|
||||
|
||||
// ── Combat idle ─────────────────────────────────────────────────────
|
||||
// Base combat idle — weapon-specific stances are resolved per-frame
|
||||
// using ready stance fields above
|
||||
caps.resolvedCombatIdle = has(anim::READY_1H) ? anim::READY_1H
|
||||
: (has(anim::READY_UNARMED) ? anim::READY_UNARMED : 0);
|
||||
|
||||
// ── Activity animations ─────────────────────────────────────────────
|
||||
caps.resolvedStandWound = has(anim::STAND_WOUND) ? anim::STAND_WOUND : 0;
|
||||
caps.resolvedDeath = has(anim::DEATH) ? anim::DEATH : 0;
|
||||
caps.hasDeath = (caps.resolvedDeath != 0);
|
||||
caps.resolvedLoot = has(anim::LOOT) ? anim::LOOT : 0;
|
||||
caps.resolvedSitDown = has(anim::SIT_GROUND_DOWN) ? anim::SIT_GROUND_DOWN : 0;
|
||||
caps.resolvedSitLoop = has(anim::SITTING) ? anim::SITTING : 0;
|
||||
caps.resolvedSitUp = has(anim::SIT_GROUND_UP) ? anim::SIT_GROUND_UP : 0;
|
||||
caps.resolvedKneel = has(anim::KNEEL_LOOP) ? anim::KNEEL_LOOP : 0;
|
||||
|
||||
// ── Stealth ─────────────────────────────────────────────────────────
|
||||
caps.resolvedStealthIdle = has(anim::STEALTH_STAND) ? anim::STEALTH_STAND : 0;
|
||||
caps.resolvedStealthWalk = has(anim::STEALTH_WALK) ? anim::STEALTH_WALK : 0;
|
||||
caps.resolvedStealthRun = has(anim::STEALTH_RUN) ? anim::STEALTH_RUN : 0;
|
||||
caps.hasStealth = (caps.resolvedStealthIdle != 0);
|
||||
|
||||
// ── Misc ────────────────────────────────────────────────────────────
|
||||
caps.resolvedMount = has(anim::MOUNT) ? anim::MOUNT : 0;
|
||||
caps.hasMount = (caps.resolvedMount != 0);
|
||||
caps.resolvedUnsheathe = has(anim::UNSHEATHE) ? anim::UNSHEATHE : 0;
|
||||
{
|
||||
static const uint32_t sheatheCands[] = {anim::SHEATHE, anim::HIP_SHEATHE};
|
||||
caps.resolvedSheathe = pick(sheatheCands, 2);
|
||||
}
|
||||
caps.resolvedStun = has(anim::STUN) ? anim::STUN : 0;
|
||||
caps.resolvedCombatWound = has(anim::COMBAT_WOUND) ? anim::COMBAT_WOUND : 0;
|
||||
|
||||
return caps;
|
||||
}
|
||||
|
||||
AnimCapabilitySet AnimCapabilityProbe::probeMountModel(Renderer* /*renderer*/, uint32_t /*mountInstanceId*/) {
|
||||
// Mount models use M2Renderer, not CharacterRenderer
|
||||
// For now, mount capabilities are handled separately via MountAnimSet discovery
|
||||
// This stub returns an empty set — mount animations are discovered in setMounted()
|
||||
AnimCapabilitySet caps;
|
||||
return caps;
|
||||
}
|
||||
|
||||
} // namespace rendering
|
||||
} // namespace wowee
|
||||
Loading…
Add table
Add a link
Reference in a new issue