mirror of
https://github.com/Kelsidavis/WoWee.git
synced 2026-04-14 16:33: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).
287 lines
13 KiB
C++
287 lines
13 KiB
C++
// ============================================================================
|
|
// 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
|