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:
Paul 2026-04-05 12:27:35 +03:00
parent e58f9b4b40
commit b4989dc11f
53 changed files with 5110 additions and 2099 deletions

View file

@ -0,0 +1,339 @@
#include "rendering/animation/activity_fsm.hpp"
#include "rendering/animation/animation_ids.hpp"
namespace wowee {
namespace rendering {
// ── One-shot completion helper ───────────────────────────────────────────────
bool ActivityFSM::oneShotComplete(const Input& in, uint32_t expectedAnimId) const {
if (!in.haveAnimState) return false;
return in.currentAnimId != expectedAnimId ||
(in.currentAnimDuration > 0.1f && in.currentAnimTime >= in.currentAnimDuration - 0.05f);
}
// ── Event handling ───────────────────────────────────────────────────────────
void ActivityFSM::onEvent(AnimEvent event) {
switch (event) {
case AnimEvent::EMOTE_START:
// Handled by startEmote() with animId
break;
case AnimEvent::EMOTE_STOP:
cancelEmote();
break;
case AnimEvent::LOOT_START:
startLooting();
break;
case AnimEvent::LOOT_STOP:
stopLooting();
break;
case AnimEvent::SIT:
// Handled by setStandState()
break;
case AnimEvent::STAND_UP:
if (state_ == State::SITTING || state_ == State::SIT_DOWN) {
if (sitUpAnim_ != 0)
state_ = State::SIT_UP;
else
state_ = State::NONE;
}
break;
case AnimEvent::MOVE_START:
case AnimEvent::JUMP:
case AnimEvent::SWIM_ENTER:
// Movement cancels all activities
if (state_ == State::EMOTE) cancelEmote();
if (state_ == State::LOOTING || state_ == State::LOOT_KNEELING || state_ == State::LOOT_END)
state_ = State::NONE;
if (state_ == State::SIT_UP || state_ == State::SIT_DOWN) {
state_ = State::NONE;
standState_ = 0;
}
break;
default:
break;
}
}
// ── Emote management ─────────────────────────────────────────────────────────
void ActivityFSM::startEmote(uint32_t animId, bool loop) {
emoteActive_ = true;
emoteAnimId_ = animId;
emoteLoop_ = loop;
state_ = State::EMOTE;
}
void ActivityFSM::cancelEmote() {
emoteActive_ = false;
emoteAnimId_ = 0;
emoteLoop_ = false;
if (state_ == State::EMOTE) state_ = State::NONE;
}
// ── Sit/sleep/kneel ──────────────────────────────────────────────────────────
void ActivityFSM::setStandState(uint8_t standState) {
if (standState == standState_) return;
standState_ = standState;
if (standState == STAND_STATE_STAND) {
// Standing up — exit via SIT_UP if we have an exit animation
return;
}
if (standState == STAND_STATE_SIT) {
sitDownAnim_ = anim::SIT_GROUND_DOWN;
sitLoopAnim_ = anim::SITTING;
sitUpAnim_ = anim::SIT_GROUND_UP;
sitDownAnimSeen_ = false;
sitDownFrames_ = 0;
state_ = State::SIT_DOWN;
} else if (standState == STAND_STATE_SLEEP) {
sitDownAnim_ = anim::SLEEP_DOWN;
sitLoopAnim_ = anim::SLEEP;
sitUpAnim_ = anim::SLEEP_UP;
sitDownAnimSeen_ = false;
sitDownFrames_ = 0;
state_ = State::SIT_DOWN;
} else if (standState == STAND_STATE_KNEEL) {
sitDownAnim_ = anim::KNEEL_START;
sitLoopAnim_ = anim::KNEEL_LOOP;
sitUpAnim_ = anim::KNEEL_END;
sitDownAnimSeen_ = false;
sitDownFrames_ = 0;
state_ = State::SIT_DOWN;
} else if (standState >= STAND_STATE_SIT_CHAIR && standState <= STAND_STATE_SIT_HIGH) {
// Chair variants — no transition animation, go directly to loop
sitDownAnim_ = 0;
sitUpAnim_ = 0;
if (standState == STAND_STATE_SIT_LOW) {
sitLoopAnim_ = anim::SIT_CHAIR_LOW;
} else if (standState == STAND_STATE_SIT_HIGH) {
sitLoopAnim_ = anim::SIT_CHAIR_HIGH;
} else {
sitLoopAnim_ = anim::SIT_CHAIR_MED;
}
state_ = State::SITTING;
} else if (standState == STAND_STATE_DEAD) {
sitDownAnim_ = 0;
sitLoopAnim_ = 0;
sitUpAnim_ = 0;
return;
}
}
// ── Loot management ──────────────────────────────────────────────────────────
void ActivityFSM::startLooting() {
state_ = State::LOOTING;
lootAnimSeen_ = false;
lootFrames_ = 0;
}
void ActivityFSM::stopLooting() {
if (state_ == State::LOOTING || state_ == State::LOOT_KNEELING) {
state_ = State::LOOT_END;
lootEndAnimSeen_ = false;
lootEndFrames_ = 0;
}
}
// ── State transitions ────────────────────────────────────────────────────────
void ActivityFSM::updateTransitions(const Input& in) {
switch (state_) {
case State::NONE:
break;
case State::EMOTE:
if (in.swimming || (in.jumping && !in.grounded) || in.moving || in.sitting) {
cancelEmote();
} else if (!emoteLoop_ && in.haveAnimState) {
if (oneShotComplete(in, emoteAnimId_)) {
cancelEmote();
}
}
break;
case State::LOOTING:
if (in.swimming || (in.jumping && !in.grounded) || in.moving) {
state_ = State::NONE;
} else if (in.haveAnimState) {
if (in.currentAnimId == anim::LOOT) lootAnimSeen_ = true;
if (lootAnimSeen_ && oneShotComplete(in, anim::LOOT)) {
state_ = State::LOOT_KNEELING;
}
// Safety: if anim never seen (model lacks it), advance after a timeout
if (!lootAnimSeen_ && ++lootFrames_ > 10) {
state_ = State::LOOT_KNEELING;
}
}
break;
case State::LOOT_KNEELING:
if (in.swimming || (in.jumping && !in.grounded) || in.moving) {
state_ = State::NONE;
}
// Stays in LOOT_KNEELING until stopLooting() transitions to LOOT_END
break;
case State::LOOT_END:
if (in.swimming || (in.jumping && !in.grounded) || in.moving) {
state_ = State::NONE;
} else if (in.haveAnimState) {
if (in.currentAnimId == anim::KNEEL_END) lootEndAnimSeen_ = true;
if (lootEndAnimSeen_ && oneShotComplete(in, anim::KNEEL_END)) {
state_ = State::NONE;
}
// Safety timeout
if (!lootEndAnimSeen_ && ++lootEndFrames_ > 10) {
state_ = State::NONE;
}
}
break;
case State::SIT_DOWN:
if (in.swimming) {
state_ = State::NONE;
standState_ = 0;
} else if (!in.sitting) {
// Stand up requested
if (sitUpAnim_ != 0 && !in.moving) {
sitUpAnimSeen_ = false;
sitUpFrames_ = 0;
state_ = State::SIT_UP;
} else {
state_ = State::NONE;
standState_ = 0;
}
} else if (sitDownAnim_ != 0 && in.haveAnimState) {
// Track whether the sit-down anim has started playing
if (in.currentAnimId == sitDownAnim_) sitDownAnimSeen_ = true;
// Only detect completion after the anim has been seen at least once
if (sitDownAnimSeen_ && oneShotComplete(in, sitDownAnim_)) {
state_ = State::SITTING;
}
// Safety: if animation was never seen after enough frames (model
// may lack it), fall through to the sitting loop.
if (!sitDownAnimSeen_ && ++sitDownFrames_ > 10) {
state_ = State::SITTING;
}
}
break;
case State::SITTING:
if (in.swimming) {
state_ = State::NONE;
standState_ = 0;
} else if (!in.sitting) {
if (sitUpAnim_ != 0 && !in.moving) {
sitUpAnimSeen_ = false;
sitUpFrames_ = 0;
state_ = State::SIT_UP;
} else {
state_ = State::NONE;
standState_ = 0;
}
}
break;
case State::SIT_UP:
if (in.swimming || in.moving) {
state_ = State::NONE;
standState_ = 0;
} else if (in.haveAnimState) {
uint32_t expected = sitUpAnim_ ? sitUpAnim_ : anim::SIT_GROUND_UP;
// Track whether the sit-up anim has started playing
if (in.currentAnimId == expected) sitUpAnimSeen_ = true;
// Only detect completion after the anim has been seen at least once
if (sitUpAnimSeen_ && oneShotComplete(in, expected)) {
state_ = State::NONE;
standState_ = 0;
}
// Safety: if animation was never seen after enough frames, finish
if (!sitUpAnimSeen_ && ++sitUpFrames_ > 10) {
state_ = State::NONE;
standState_ = 0;
}
}
break;
}
}
// ── Animation resolution ─────────────────────────────────────────────────────
AnimOutput ActivityFSM::resolve(const Input& in, const AnimCapabilitySet& /*caps*/) {
updateTransitions(in);
if (state_ == State::NONE) return AnimOutput::stay();
uint32_t animId = 0;
bool loop = true;
switch (state_) {
case State::NONE:
return AnimOutput::stay();
case State::EMOTE:
animId = emoteAnimId_;
loop = emoteLoop_;
break;
case State::LOOTING:
animId = anim::LOOT;
loop = false;
break;
case State::LOOT_KNEELING:
animId = anim::KNEEL_LOOP;
loop = true;
break;
case State::LOOT_END:
animId = anim::KNEEL_END;
loop = false;
break;
case State::SIT_DOWN:
animId = sitDownAnim_ ? sitDownAnim_ : anim::SIT_GROUND_DOWN;
loop = false;
break;
case State::SITTING:
animId = sitLoopAnim_ ? sitLoopAnim_ : anim::SITTING;
loop = true;
break;
case State::SIT_UP:
animId = sitUpAnim_ ? sitUpAnim_ : anim::SIT_GROUND_UP;
loop = false;
break;
}
if (animId == 0) return AnimOutput::stay();
return AnimOutput::ok(animId, loop);
}
// ── Reset ────────────────────────────────────────────────────────────────────
void ActivityFSM::reset() {
state_ = State::NONE;
cancelEmote();
standState_ = 0;
sitDownAnim_ = 0;
sitLoopAnim_ = 0;
sitUpAnim_ = 0;
sitDownAnimSeen_ = false;
sitUpAnimSeen_ = false;
sitDownFrames_ = 0;
sitUpFrames_ = 0;
lootAnimSeen_ = false;
lootFrames_ = 0;
lootEndAnimSeen_ = false;
lootEndFrames_ = 0;
}
} // namespace rendering
} // namespace wowee

View 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

View file

@ -0,0 +1,567 @@
// ============================================================================
// animation_ids.cpp — Inverse lookup & DBC validation
// Generated from animation_ids.hpp (452 constants, IDs 0451)
// ============================================================================
#include "rendering/animation/animation_ids.hpp"
#include "pipeline/dbc_loader.hpp"
#include "core/logger.hpp"
#include <unordered_set>
namespace wowee {
namespace rendering {
namespace anim {
const char* nameFromId(uint32_t id) {
static const char* const names[ANIM_COUNT] = {
/* 0 */ "STAND",
/* 1 */ "DEATH",
/* 2 */ "SPELL",
/* 3 */ "STOP",
/* 4 */ "WALK",
/* 5 */ "RUN",
/* 6 */ "DEAD",
/* 7 */ "RISE",
/* 8 */ "STAND_WOUND",
/* 9 */ "COMBAT_WOUND",
/* 10 */ "COMBAT_CRITICAL",
/* 11 */ "SHUFFLE_LEFT",
/* 12 */ "SHUFFLE_RIGHT",
/* 13 */ "WALK_BACKWARDS",
/* 14 */ "STUN",
/* 15 */ "HANDS_CLOSED",
/* 16 */ "ATTACK_UNARMED",
/* 17 */ "ATTACK_1H",
/* 18 */ "ATTACK_2H",
/* 19 */ "ATTACK_2H_LOOSE",
/* 20 */ "PARRY_UNARMED",
/* 21 */ "PARRY_1H",
/* 22 */ "PARRY_2H",
/* 23 */ "PARRY_2H_LOOSE",
/* 24 */ "SHIELD_BLOCK",
/* 25 */ "READY_UNARMED",
/* 26 */ "READY_1H",
/* 27 */ "READY_2H",
/* 28 */ "READY_2H_LOOSE",
/* 29 */ "READY_BOW",
/* 30 */ "DODGE",
/* 31 */ "SPELL_PRECAST",
/* 32 */ "SPELL_CAST",
/* 33 */ "SPELL_CAST_AREA",
/* 34 */ "NPC_WELCOME",
/* 35 */ "NPC_GOODBYE",
/* 36 */ "BLOCK",
/* 37 */ "JUMP_START",
/* 38 */ "JUMP",
/* 39 */ "JUMP_END",
/* 40 */ "FALL",
/* 41 */ "SWIM_IDLE",
/* 42 */ "SWIM",
/* 43 */ "SWIM_LEFT",
/* 44 */ "SWIM_RIGHT",
/* 45 */ "SWIM_BACKWARDS",
/* 46 */ "ATTACK_BOW",
/* 47 */ "FIRE_BOW",
/* 48 */ "READY_RIFLE",
/* 49 */ "ATTACK_RIFLE",
/* 50 */ "LOOT",
/* 51 */ "READY_SPELL_DIRECTED",
/* 52 */ "READY_SPELL_OMNI",
/* 53 */ "SPELL_CAST_DIRECTED",
/* 54 */ "SPELL_CAST_OMNI",
/* 55 */ "BATTLE_ROAR",
/* 56 */ "READY_ABILITY",
/* 57 */ "SPECIAL_1H",
/* 58 */ "SPECIAL_2H",
/* 59 */ "SHIELD_BASH",
/* 60 */ "EMOTE_TALK",
/* 61 */ "EMOTE_EAT",
/* 62 */ "EMOTE_WORK",
/* 63 */ "EMOTE_USE_STANDING",
/* 64 */ "EMOTE_EXCLAMATION",
/* 65 */ "EMOTE_QUESTION",
/* 66 */ "EMOTE_BOW",
/* 67 */ "EMOTE_WAVE",
/* 68 */ "EMOTE_CHEER",
/* 69 */ "EMOTE_DANCE",
/* 70 */ "EMOTE_LAUGH",
/* 71 */ "EMOTE_SLEEP",
/* 72 */ "EMOTE_SIT_GROUND",
/* 73 */ "EMOTE_RUDE",
/* 74 */ "EMOTE_ROAR",
/* 75 */ "EMOTE_KNEEL",
/* 76 */ "EMOTE_KISS",
/* 77 */ "EMOTE_CRY",
/* 78 */ "EMOTE_CHICKEN",
/* 79 */ "EMOTE_BEG",
/* 80 */ "EMOTE_APPLAUD",
/* 81 */ "EMOTE_SHOUT",
/* 82 */ "EMOTE_FLEX",
/* 83 */ "EMOTE_SHY",
/* 84 */ "EMOTE_POINT",
/* 85 */ "ATTACK_1H_PIERCE",
/* 86 */ "ATTACK_2H_LOOSE_PIERCE",
/* 87 */ "ATTACK_OFF",
/* 88 */ "ATTACK_OFF_PIERCE",
/* 89 */ "SHEATHE",
/* 90 */ "HIP_SHEATHE",
/* 91 */ "MOUNT",
/* 92 */ "RUN_RIGHT",
/* 93 */ "RUN_LEFT",
/* 94 */ "MOUNT_SPECIAL",
/* 95 */ "KICK",
/* 96 */ "SIT_GROUND_DOWN",
/* 97 */ "SITTING",
/* 98 */ "SIT_GROUND_UP",
/* 99 */ "SLEEP_DOWN",
/* 100 */ "SLEEP",
/* 101 */ "SLEEP_UP",
/* 102 */ "SIT_CHAIR_LOW",
/* 103 */ "SIT_CHAIR_MED",
/* 104 */ "SIT_CHAIR_HIGH",
/* 105 */ "LOAD_BOW",
/* 106 */ "LOAD_RIFLE",
/* 107 */ "ATTACK_THROWN",
/* 108 */ "READY_THROWN",
/* 109 */ "HOLD_BOW",
/* 110 */ "HOLD_RIFLE",
/* 111 */ "HOLD_THROWN",
/* 112 */ "LOAD_THROWN",
/* 113 */ "EMOTE_SALUTE",
/* 114 */ "KNEEL_START",
/* 115 */ "KNEEL_LOOP",
/* 116 */ "KNEEL_END",
/* 117 */ "ATTACK_UNARMED_OFF",
/* 118 */ "SPECIAL_UNARMED",
/* 119 */ "STEALTH_WALK",
/* 120 */ "STEALTH_STAND",
/* 121 */ "KNOCKDOWN",
/* 122 */ "EATING_LOOP",
/* 123 */ "USE_STANDING_LOOP",
/* 124 */ "CHANNEL_CAST_DIRECTED",
/* 125 */ "CHANNEL_CAST_OMNI",
/* 126 */ "WHIRLWIND",
/* 127 */ "BIRTH",
/* 128 */ "USE_STANDING_START",
/* 129 */ "USE_STANDING_END",
/* 130 */ "CREATURE_SPECIAL",
/* 131 */ "DROWN",
/* 132 */ "DROWNED",
/* 133 */ "FISHING_CAST",
/* 134 */ "FISHING_LOOP",
/* 135 */ "FLY",
/* 136 */ "EMOTE_WORK_NO_SHEATHE",
/* 137 */ "EMOTE_STUN_NO_SHEATHE",
/* 138 */ "EMOTE_USE_STANDING_NO_SHEATHE",
/* 139 */ "SPELL_SLEEP_DOWN",
/* 140 */ "SPELL_KNEEL_START",
/* 141 */ "SPELL_KNEEL_LOOP",
/* 142 */ "SPELL_KNEEL_END",
/* 143 */ "SPRINT",
/* 144 */ "IN_FLIGHT",
/* 145 */ "SPAWN",
/* 146 */ "CLOSE",
/* 147 */ "CLOSED",
/* 148 */ "OPEN",
/* 149 */ "DESTROY",
/* 150 */ "DESTROYED",
/* 151 */ "UNSHEATHE",
/* 152 */ "SHEATHE_ALT",
/* 153 */ "ATTACK_UNARMED_NO_SHEATHE",
/* 154 */ "STEALTH_RUN",
/* 155 */ "READY_CROSSBOW",
/* 156 */ "ATTACK_CROSSBOW",
/* 157 */ "EMOTE_TALK_EXCLAMATION",
/* 158 */ "FLY_IDLE",
/* 159 */ "FLY_FORWARD",
/* 160 */ "FLY_BACKWARDS",
/* 161 */ "FLY_LEFT",
/* 162 */ "FLY_RIGHT",
/* 163 */ "FLY_UP",
/* 164 */ "FLY_DOWN",
/* 165 */ "FLY_LAND_START",
/* 166 */ "FLY_LAND_RUN",
/* 167 */ "FLY_LAND_END",
/* 168 */ "EMOTE_TALK_QUESTION",
/* 169 */ "EMOTE_READ",
/* 170 */ "EMOTE_SHIELDBLOCK",
/* 171 */ "EMOTE_CHOP",
/* 172 */ "EMOTE_HOLDRIFLE",
/* 173 */ "EMOTE_HOLDBOW",
/* 174 */ "EMOTE_HOLDTHROWN",
/* 175 */ "CUSTOM_SPELL_02",
/* 176 */ "CUSTOM_SPELL_03",
/* 177 */ "CUSTOM_SPELL_04",
/* 178 */ "CUSTOM_SPELL_05",
/* 179 */ "CUSTOM_SPELL_06",
/* 180 */ "CUSTOM_SPELL_07",
/* 181 */ "CUSTOM_SPELL_08",
/* 182 */ "CUSTOM_SPELL_09",
/* 183 */ "CUSTOM_SPELL_10",
/* 184 */ "EMOTE_STATE_DANCE",
/* 185 */ "FLY_STAND",
/* 186 */ "EMOTE_STATE_LAUGH",
/* 187 */ "EMOTE_STATE_POINT",
/* 188 */ "EMOTE_STATE_EAT",
/* 189 */ "EMOTE_STATE_WORK",
/* 190 */ "EMOTE_STATE_SIT_GROUND",
/* 191 */ "EMOTE_STATE_HOLD_BOW",
/* 192 */ "EMOTE_STATE_HOLD_RIFLE",
/* 193 */ "EMOTE_STATE_HOLD_THROWN",
/* 194 */ "FLY_COMBAT_WOUND",
/* 195 */ "FLY_COMBAT_CRITICAL",
/* 196 */ "RECLINED",
/* 197 */ "EMOTE_STATE_ROAR",
/* 198 */ "EMOTE_USE_STANDING_LOOP_2",
/* 199 */ "EMOTE_STATE_APPLAUD",
/* 200 */ "READY_FIST",
/* 201 */ "SPELL_CHANNEL_DIRECTED_OMNI",
/* 202 */ "SPECIAL_ATTACK_1H_OFF",
/* 203 */ "ATTACK_FIST_1H",
/* 204 */ "ATTACK_FIST_1H_OFF",
/* 205 */ "PARRY_FIST_1H",
/* 206 */ "READY_FIST_1H",
/* 207 */ "EMOTE_STATE_READ_AND_TALK",
/* 208 */ "EMOTE_STATE_WORK_NO_SHEATHE",
/* 209 */ "FLY_RUN",
/* 210 */ "EMOTE_STATE_KNEEL_2",
/* 211 */ "EMOTE_STATE_SPELL_KNEEL",
/* 212 */ "EMOTE_STATE_USE_STANDING",
/* 213 */ "EMOTE_STATE_STUN",
/* 214 */ "EMOTE_STATE_STUN_NO_SHEATHE",
/* 215 */ "EMOTE_TRAIN",
/* 216 */ "EMOTE_DEAD",
/* 217 */ "EMOTE_STATE_DANCE_ONCE",
/* 218 */ "FLY_DEATH",
/* 219 */ "FLY_STAND_WOUND",
/* 220 */ "FLY_SHUFFLE_LEFT",
/* 221 */ "FLY_SHUFFLE_RIGHT",
/* 222 */ "FLY_WALK_BACKWARDS",
/* 223 */ "FLY_STUN",
/* 224 */ "FLY_HANDS_CLOSED",
/* 225 */ "FLY_ATTACK_UNARMED",
/* 226 */ "FLY_ATTACK_1H",
/* 227 */ "FLY_ATTACK_2H",
/* 228 */ "FLY_ATTACK_2H_LOOSE",
/* 229 */ "FLY_SPELL",
/* 230 */ "FLY_STOP",
/* 231 */ "FLY_WALK",
/* 232 */ "FLY_DEAD",
/* 233 */ "FLY_RISE",
/* 234 */ "FLY_RUN_2",
/* 235 */ "FLY_FALL",
/* 236 */ "FLY_SWIM_IDLE",
/* 237 */ "FLY_SWIM",
/* 238 */ "FLY_SWIM_LEFT",
/* 239 */ "FLY_SWIM_RIGHT",
/* 240 */ "FLY_SWIM_BACKWARDS",
/* 241 */ "FLY_ATTACK_BOW",
/* 242 */ "FLY_FIRE_BOW",
/* 243 */ "FLY_READY_RIFLE",
/* 244 */ "FLY_ATTACK_RIFLE",
/* 245 */ "TOTEM_SMALL",
/* 246 */ "TOTEM_MEDIUM",
/* 247 */ "TOTEM_LARGE",
/* 248 */ "FLY_LOOT",
/* 249 */ "FLY_READY_SPELL_DIRECTED",
/* 250 */ "FLY_READY_SPELL_OMNI",
/* 251 */ "FLY_SPELL_CAST_DIRECTED",
/* 252 */ "FLY_SPELL_CAST_OMNI",
/* 253 */ "FLY_BATTLE_ROAR",
/* 254 */ "FLY_READY_ABILITY",
/* 255 */ "FLY_SPECIAL_1H",
/* 256 */ "FLY_SPECIAL_2H",
/* 257 */ "FLY_SHIELD_BASH",
/* 258 */ "FLY_EMOTE_TALK",
/* 259 */ "FLY_EMOTE_EAT",
/* 260 */ "FLY_EMOTE_WORK",
/* 261 */ "FLY_EMOTE_USE_STANDING",
/* 262 */ "FLY_EMOTE_BOW",
/* 263 */ "FLY_EMOTE_WAVE",
/* 264 */ "FLY_EMOTE_CHEER",
/* 265 */ "FLY_EMOTE_DANCE",
/* 266 */ "FLY_EMOTE_LAUGH",
/* 267 */ "FLY_EMOTE_SLEEP",
/* 268 */ "FLY_EMOTE_SIT_GROUND",
/* 269 */ "FLY_EMOTE_RUDE",
/* 270 */ "FLY_EMOTE_ROAR",
/* 271 */ "FLY_EMOTE_KNEEL",
/* 272 */ "FLY_EMOTE_KISS",
/* 273 */ "FLY_EMOTE_CRY",
/* 274 */ "FLY_EMOTE_CHICKEN",
/* 275 */ "FLY_EMOTE_BEG",
/* 276 */ "FLY_EMOTE_APPLAUD",
/* 277 */ "FLY_EMOTE_SHOUT",
/* 278 */ "FLY_EMOTE_FLEX",
/* 279 */ "FLY_EMOTE_SHY",
/* 280 */ "FLY_EMOTE_POINT",
/* 281 */ "FLY_ATTACK_1H_PIERCE",
/* 282 */ "FLY_ATTACK_2H_LOOSE_PIERCE",
/* 283 */ "FLY_ATTACK_OFF",
/* 284 */ "FLY_ATTACK_OFF_PIERCE",
/* 285 */ "FLY_SHEATHE",
/* 286 */ "FLY_HIP_SHEATHE",
/* 287 */ "FLY_MOUNT",
/* 288 */ "FLY_RUN_RIGHT",
/* 289 */ "FLY_RUN_LEFT",
/* 290 */ "FLY_MOUNT_SPECIAL",
/* 291 */ "FLY_KICK",
/* 292 */ "FLY_SIT_GROUND_DOWN",
/* 293 */ "FLY_SITTING",
/* 294 */ "FLY_SIT_GROUND_UP",
/* 295 */ "FLY_SLEEP_DOWN",
/* 296 */ "FLY_SLEEP",
/* 297 */ "FLY_SLEEP_UP",
/* 298 */ "FLY_SIT_CHAIR_LOW",
/* 299 */ "FLY_SIT_CHAIR_MED",
/* 300 */ "FLY_SIT_CHAIR_HIGH",
/* 301 */ "FLY_LOAD_BOW",
/* 302 */ "FLY_LOAD_RIFLE",
/* 303 */ "FLY_ATTACK_THROWN",
/* 304 */ "FLY_READY_THROWN",
/* 305 */ "FLY_HOLD_BOW",
/* 306 */ "FLY_HOLD_RIFLE",
/* 307 */ "FLY_HOLD_THROWN",
/* 308 */ "FLY_LOAD_THROWN",
/* 309 */ "FLY_EMOTE_SALUTE",
/* 310 */ "FLY_KNEEL_START",
/* 311 */ "FLY_KNEEL_LOOP",
/* 312 */ "FLY_KNEEL_END",
/* 313 */ "FLY_ATTACK_UNARMED_OFF",
/* 314 */ "FLY_SPECIAL_UNARMED",
/* 315 */ "FLY_STEALTH_WALK",
/* 316 */ "FLY_STEALTH_STAND",
/* 317 */ "FLY_KNOCKDOWN",
/* 318 */ "FLY_EATING_LOOP",
/* 319 */ "FLY_USE_STANDING_LOOP",
/* 320 */ "FLY_CHANNEL_CAST_DIRECTED",
/* 321 */ "FLY_CHANNEL_CAST_OMNI",
/* 322 */ "FLY_WHIRLWIND",
/* 323 */ "FLY_BIRTH",
/* 324 */ "FLY_USE_STANDING_START",
/* 325 */ "FLY_USE_STANDING_END",
/* 326 */ "FLY_CREATURE_SPECIAL",
/* 327 */ "FLY_DROWN",
/* 328 */ "FLY_DROWNED",
/* 329 */ "FLY_FISHING_CAST",
/* 330 */ "FLY_FISHING_LOOP",
/* 331 */ "FLY_FLY",
/* 332 */ "FLY_EMOTE_WORK_NO_SHEATHE",
/* 333 */ "FLY_EMOTE_STUN_NO_SHEATHE",
/* 334 */ "FLY_EMOTE_USE_STANDING_NO_SHEATHE",
/* 335 */ "FLY_SPELL_SLEEP_DOWN",
/* 336 */ "FLY_SPELL_KNEEL_START",
/* 337 */ "FLY_SPELL_KNEEL_LOOP",
/* 338 */ "FLY_SPELL_KNEEL_END",
/* 339 */ "FLY_SPRINT",
/* 340 */ "FLY_IN_FLIGHT",
/* 341 */ "FLY_SPAWN",
/* 342 */ "FLY_CLOSE",
/* 343 */ "FLY_CLOSED",
/* 344 */ "FLY_OPEN",
/* 345 */ "FLY_DESTROY",
/* 346 */ "FLY_DESTROYED",
/* 347 */ "FLY_UNSHEATHE",
/* 348 */ "FLY_SHEATHE_ALT",
/* 349 */ "FLY_ATTACK_UNARMED_NO_SHEATHE",
/* 350 */ "FLY_STEALTH_RUN",
/* 351 */ "FLY_READY_CROSSBOW",
/* 352 */ "FLY_ATTACK_CROSSBOW",
/* 353 */ "FLY_EMOTE_TALK_EXCLAMATION",
/* 354 */ "FLY_EMOTE_TALK_QUESTION",
/* 355 */ "FLY_EMOTE_READ",
/* 356 */ "EMOTE_HOLD_CROSSBOW",
/* 357 */ "FLY_EMOTE_HOLD_BOW",
/* 358 */ "FLY_EMOTE_HOLD_RIFLE",
/* 359 */ "FLY_EMOTE_HOLD_THROWN",
/* 360 */ "FLY_EMOTE_HOLD_CROSSBOW",
/* 361 */ "FLY_CUSTOM_SPELL_02",
/* 362 */ "FLY_CUSTOM_SPELL_03",
/* 363 */ "FLY_CUSTOM_SPELL_04",
/* 364 */ "FLY_CUSTOM_SPELL_05",
/* 365 */ "FLY_CUSTOM_SPELL_06",
/* 366 */ "FLY_CUSTOM_SPELL_07",
/* 367 */ "FLY_CUSTOM_SPELL_08",
/* 368 */ "FLY_CUSTOM_SPELL_09",
/* 369 */ "FLY_CUSTOM_SPELL_10",
/* 370 */ "FLY_EMOTE_STATE_DANCE",
/* 371 */ "EMOTE_EAT_NO_SHEATHE",
/* 372 */ "MOUNT_RUN_RIGHT",
/* 373 */ "MOUNT_RUN_LEFT",
/* 374 */ "MOUNT_WALK_BACKWARDS",
/* 375 */ "MOUNT_SWIM_IDLE",
/* 376 */ "MOUNT_SWIM",
/* 377 */ "MOUNT_SWIM_LEFT",
/* 378 */ "MOUNT_SWIM_RIGHT",
/* 379 */ "MOUNT_SWIM_BACKWARDS",
/* 380 */ "MOUNT_FLIGHT_IDLE",
/* 381 */ "MOUNT_FLIGHT_FORWARD",
/* 382 */ "MOUNT_FLIGHT_BACKWARDS",
/* 383 */ "MOUNT_FLIGHT_LEFT",
/* 384 */ "MOUNT_FLIGHT_RIGHT",
/* 385 */ "MOUNT_FLIGHT_UP",
/* 386 */ "MOUNT_FLIGHT_DOWN",
/* 387 */ "MOUNT_FLIGHT_LAND_START",
/* 388 */ "MOUNT_FLIGHT_LAND_RUN",
/* 389 */ "MOUNT_FLIGHT_LAND_END",
/* 390 */ "FLY_EMOTE_STATE_LAUGH",
/* 391 */ "FLY_EMOTE_STATE_POINT",
/* 392 */ "FLY_EMOTE_STATE_EAT",
/* 393 */ "FLY_EMOTE_STATE_WORK",
/* 394 */ "FLY_EMOTE_STATE_SIT_GROUND",
/* 395 */ "FLY_EMOTE_STATE_HOLD_BOW",
/* 396 */ "FLY_EMOTE_STATE_HOLD_RIFLE",
/* 397 */ "FLY_EMOTE_STATE_HOLD_THROWN",
/* 398 */ "FLY_EMOTE_STATE_ROAR",
/* 399 */ "FLY_RECLINED",
/* 400 */ "EMOTE_TRAIN_2",
/* 401 */ "EMOTE_DEAD_2",
/* 402 */ "FLY_EMOTE_USE_STANDING_LOOP_2",
/* 403 */ "FLY_EMOTE_STATE_APPLAUD",
/* 404 */ "FLY_READY_FIST",
/* 405 */ "FLY_SPELL_CHANNEL_DIRECTED_OMNI",
/* 406 */ "FLY_SPECIAL_ATTACK_1H_OFF",
/* 407 */ "FLY_ATTACK_FIST_1H",
/* 408 */ "FLY_ATTACK_FIST_1H_OFF",
/* 409 */ "FLY_PARRY_FIST_1H",
/* 410 */ "FLY_READY_FIST_1H",
/* 411 */ "FLY_EMOTE_STATE_READ_AND_TALK",
/* 412 */ "FLY_EMOTE_STATE_WORK_NO_SHEATHE",
/* 413 */ "FLY_EMOTE_STATE_KNEEL_2",
/* 414 */ "FLY_EMOTE_STATE_SPELL_KNEEL",
/* 415 */ "FLY_EMOTE_STATE_USE_STANDING",
/* 416 */ "FLY_EMOTE_STATE_STUN",
/* 417 */ "FLY_EMOTE_STATE_STUN_NO_SHEATHE",
/* 418 */ "FLY_EMOTE_TRAIN",
/* 419 */ "FLY_EMOTE_DEAD",
/* 420 */ "FLY_EMOTE_STATE_DANCE_ONCE",
/* 421 */ "FLY_EMOTE_EAT_NO_SHEATHE",
/* 422 */ "FLY_MOUNT_RUN_RIGHT",
/* 423 */ "FLY_MOUNT_RUN_LEFT",
/* 424 */ "FLY_MOUNT_WALK_BACKWARDS",
/* 425 */ "FLY_MOUNT_SWIM_IDLE",
/* 426 */ "FLY_MOUNT_SWIM",
/* 427 */ "FLY_MOUNT_SWIM_LEFT",
/* 428 */ "FLY_MOUNT_SWIM_RIGHT",
/* 429 */ "FLY_MOUNT_SWIM_BACKWARDS",
/* 430 */ "FLY_MOUNT_FLIGHT_IDLE",
/* 431 */ "FLY_MOUNT_FLIGHT_FORWARD",
/* 432 */ "FLY_MOUNT_FLIGHT_BACKWARDS",
/* 433 */ "FLY_MOUNT_FLIGHT_LEFT",
/* 434 */ "FLY_MOUNT_FLIGHT_RIGHT",
/* 435 */ "FLY_MOUNT_FLIGHT_UP",
/* 436 */ "FLY_MOUNT_FLIGHT_DOWN",
/* 437 */ "FLY_MOUNT_FLIGHT_LAND_START",
/* 438 */ "FLY_MOUNT_FLIGHT_LAND_RUN",
/* 439 */ "FLY_MOUNT_FLIGHT_LAND_END",
/* 440 */ "FLY_TOTEM_SMALL",
/* 441 */ "FLY_TOTEM_MEDIUM",
/* 442 */ "FLY_TOTEM_LARGE",
/* 443 */ "FLY_EMOTE_HOLD_CROSSBOW_2",
/* 444 */ "VEHICLE_GRAB",
/* 445 */ "VEHICLE_THROW",
/* 446 */ "FLY_VEHICLE_GRAB",
/* 447 */ "FLY_VEHICLE_THROW",
/* 448 */ "GUILD_CHAMPION_1",
/* 449 */ "GUILD_CHAMPION_2",
/* 450 */ "FLY_GUILD_CHAMPION_1",
/* 451 */ "FLY_GUILD_CHAMPION_2",
};
if (id < ANIM_COUNT) return names[id];
return "UNKNOWN";
}
uint32_t flyVariant(uint32_t groundId) {
// Compact lookup: ground animation ID (0451) → FLY_* variant, or 0 if none.
// Built from the 155 ground→fly pairs in animation_ids.hpp.
static const uint16_t table[] = {
// 0-9
185, 218, 229, 230, 231, 209, 232, 233, 219, 194,
// 10-19
195, 220, 221, 222, 223, 224, 225, 226, 227, 228,
// 20-29 (PARRY/READY/DODGE — no fly variants)
0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
// 30-39 (BLOCK/SPELL_PRECAST/NPC — no fly variants)
0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
// 40-49
235, 236, 237, 238, 239, 240, 241, 242, 243, 244,
// 50-59
248, 249, 250, 251, 252, 253, 254, 255, 256, 257,
// 60-69
258, 259, 260, 261, 0, 0, 262, 263, 264, 265,
// 70-79
266, 267, 268, 269, 270, 271, 272, 273, 274, 275,
// 80-89
276, 277, 278, 279, 280, 281, 282, 283, 284, 285,
// 90-99
286, 287, 288, 289, 290, 291, 292, 293, 294, 295,
// 100-109
296, 297, 298, 299, 300, 301, 302, 303, 304, 305,
// 110-119
306, 307, 308, 309, 310, 311, 312, 313, 314, 315,
// 120-129
316, 317, 318, 319, 320, 321, 322, 323, 324, 325,
// 130-139
326, 327, 328, 329, 330, 331, 332, 333, 334, 335,
// 140-149
336, 337, 338, 339, 340, 341, 342, 343, 344, 345,
// 150-159
346, 347, 348, 349, 350, 351, 352, 353, 0, 0,
// 160-169 (FLY_BACKWARDS..FLY_LAND_END are already FLY_ themselves: 0)
0, 0, 0, 0, 0, 0, 0, 0, 354, 355,
// 170-179
0, 0, 0, 0, 0, 361, 362, 363, 364, 365,
// 180-189
366, 367, 368, 369, 370, 0, 390, 391, 392, 393,
// 190-199
394, 395, 396, 397, 0, 0, 399, 398, 402, 403,
// 200-209
404, 405, 406, 407, 408, 409, 410, 411, 412, 0,
// 210-217
413, 414, 415, 416, 417, 418, 419, 420,
};
constexpr uint32_t tableSize = sizeof(table) / sizeof(table[0]);
if (groundId >= tableSize) return 0;
return table[groundId];
}
void validateAgainstDBC(const std::shared_ptr<wowee::pipeline::DBCFile>& dbc) {
if (!dbc || !dbc->isLoaded()) {
LOG_WARNING("AnimationData.dbc not available — skipping animation ID validation");
return;
}
// Collect all IDs present in the DBC (first field is the animation ID)
std::unordered_set<uint32_t> dbcIds;
for (uint32_t i = 0; i < dbc->getRecordCount(); ++i) {
uint32_t id = dbc->getUInt32(i, 0);
dbcIds.insert(id);
}
// Check: constants we define that are missing from DBC
uint32_t missingInDbc = 0;
for (uint32_t id = 0; id < ANIM_COUNT; ++id) {
if (dbcIds.find(id) == dbcIds.end()) {
LOG_WARNING("Animation ID ", id, " (", nameFromId(id),
") defined in constants but missing from AnimationData.dbc");
++missingInDbc;
}
}
// Check: DBC IDs beyond our constant range
uint32_t extraInDbc = 0;
for (uint32_t dbcId : dbcIds) {
if (dbcId >= ANIM_COUNT) {
++extraInDbc;
}
}
LOG_INFO("AnimationData.dbc validation: ", dbc->getRecordCount(), " DBC records, ",
ANIM_COUNT, " constants, ",
missingInDbc, " missing from DBC, ",
extraInDbc, " DBC-only IDs beyond constant range");
}
} // namespace anim
} // namespace rendering
} // namespace wowee

View file

@ -0,0 +1,36 @@
// Renamed from PlayerAnimator/NpcAnimator dual-map → unified CharacterAnimator registry.
// NpcAnimator methods removed — all characters use CharacterAnimator.
#include "rendering/animation/animation_manager.hpp"
namespace wowee {
namespace rendering {
// ── Character animators ──────────────────────────────────────────────────────
CharacterAnimator& AnimationManager::getOrCreate(uint32_t instanceId) {
auto it = animators_.find(instanceId);
if (it != animators_.end()) return *it->second;
auto [inserted, _] = animators_.emplace(instanceId, std::make_unique<CharacterAnimator>());
return *inserted->second;
}
CharacterAnimator* AnimationManager::get(uint32_t instanceId) {
auto it = animators_.find(instanceId);
return it != animators_.end() ? it->second.get() : nullptr;
}
void AnimationManager::remove(uint32_t instanceId) {
animators_.erase(instanceId);
}
// ── Update all ───────────────────────────────────────────────────────────────
void AnimationManager::updateAll(float dt) {
for (auto& [id, animator] : animators_) {
animator->update(dt);
}
}
} // namespace rendering
} // namespace wowee

View file

@ -0,0 +1,216 @@
// Renamed from player_animator.cpp → character_animator.cpp
// Class renamed: PlayerAnimator → CharacterAnimator
// All animations are now generic (character-based, not player-specific).
#include "rendering/animation/character_animator.hpp"
#include "rendering/animation/animation_ids.hpp"
#include "game/inventory.hpp"
namespace wowee {
namespace rendering {
CharacterAnimator::CharacterAnimator() = default;
// ── IAnimator ────────────────────────────────────────────────────────────────
void CharacterAnimator::onEvent(AnimEvent event) {
locomotion_.onEvent(event);
combat_.onEvent(event);
activity_.onEvent(event);
mount_.onEvent(event);
}
void CharacterAnimator::update(float dt) {
lastDt_ = dt;
lastOutput_ = resolveAnimation();
}
// ── ICharacterAnimator ──────────────────────────────────────────────────────
void CharacterAnimator::startSpellCast(uint32_t precast, uint32_t cast, bool loop, uint32_t finalize) {
combat_.startSpellCast(precast, cast, loop, finalize);
}
void CharacterAnimator::stopSpellCast() {
combat_.stopSpellCast();
}
void CharacterAnimator::triggerMeleeSwing() {
// Melee is handled via timer in FrameInput — CombatFSM transitions automatically
}
void CharacterAnimator::triggerRangedShot() {
// Ranged is handled via timer in FrameInput — CombatFSM transitions automatically
}
void CharacterAnimator::triggerHitReaction(uint32_t animId) {
combat_.triggerHitReaction(animId);
}
void CharacterAnimator::triggerSpecialAttack(uint32_t /*spellId*/) {
// Special attack animation is injected via FrameInput::specialAttackAnimId
}
void CharacterAnimator::setEquippedWeaponType(const WeaponLoadout& loadout) {
loadout_ = loadout;
}
void CharacterAnimator::setEquippedRangedType(RangedWeaponType type) {
loadout_.rangedType = type;
}
void CharacterAnimator::playEmote(uint32_t animId, bool loop) {
activity_.startEmote(animId, loop);
}
void CharacterAnimator::cancelEmote() {
activity_.cancelEmote();
}
void CharacterAnimator::startLooting() {
activity_.startLooting();
}
void CharacterAnimator::stopLooting() {
activity_.stopLooting();
}
void CharacterAnimator::setStunned(bool stunned) {
combat_.setStunned(stunned);
}
void CharacterAnimator::setCharging(bool charging) {
combat_.setCharging(charging);
}
void CharacterAnimator::setStandState(uint8_t state) {
activity_.setStandState(state);
}
void CharacterAnimator::setStealthed(bool stealth) {
stealthed_ = stealth;
}
void CharacterAnimator::setInCombat(bool combat) {
inCombat_ = combat;
}
void CharacterAnimator::setLowHealth(bool low) {
lowHealth_ = low;
}
void CharacterAnimator::setSprintAuraActive(bool active) {
sprintAura_ = active;
}
// ── Mount ────────────────────────────────────────────────────────────────────
void CharacterAnimator::configureMountFSM(const MountFSM::MountAnimSet& anims, bool taxiFlight) {
mount_.configure(anims, taxiFlight);
}
void CharacterAnimator::clearMountFSM() {
mount_.clear();
}
// ── Priority resolver ────────────────────────────────────────────────────────
AnimOutput CharacterAnimator::resolveAnimation() {
const auto& fi = frameInput_;
// ── Mount takes over everything ─────────────────────────────────────
if (mount_.isActive()) {
// MountFSM returns mount-specific output; rider anim is separate
// For the main character animation, we return MOUNT (or flight variant)
uint32_t riderAnim = caps_.resolvedMount ? caps_.resolvedMount : anim::MOUNT;
return AnimOutput::ok(riderAnim, true);
}
// ── Build combat input ──────────────────────────────────────────────
CombatFSM::Input combatIn;
combatIn.inCombat = inCombat_;
combatIn.grounded = fi.grounded;
combatIn.jumping = fi.jumping;
combatIn.swimming = fi.swimming;
combatIn.moving = fi.moving;
combatIn.sprinting = fi.sprinting;
combatIn.lowHealth = lowHealth_;
combatIn.meleeSwingTimer = fi.meleeSwingTimer;
combatIn.rangedShootTimer = fi.rangedShootTimer;
combatIn.specialAttackAnimId = fi.specialAttackAnimId;
combatIn.rangedAnimId = fi.rangedAnimId;
combatIn.currentAnimId = fi.currentAnimId;
combatIn.currentAnimTime = fi.currentAnimTime;
combatIn.currentAnimDuration = fi.currentAnimDuration;
combatIn.haveAnimState = fi.haveAnimState;
combatIn.hasUnsheathe = caps_.resolvedUnsheathe != 0;
combatIn.hasSheathe = caps_.resolvedSheathe != 0;
// ── Combat FSM (highest priority for non-mount) ─────────────────────
auto combatOut = combat_.resolve(combatIn, caps_, loadout_);
if (combatOut.valid) return applyOverlays(combatOut);
// ── Activity FSM (emote, loot, sit) ─────────────────────────────────
ActivityFSM::Input actIn;
actIn.moving = fi.moving;
actIn.sprinting = fi.sprinting;
actIn.jumping = fi.jumping;
actIn.grounded = fi.grounded;
actIn.swimming = fi.swimming;
actIn.sitting = fi.sitting;
actIn.stunned = combat_.isStunned();
actIn.currentAnimId = fi.currentAnimId;
actIn.currentAnimTime = fi.currentAnimTime;
actIn.currentAnimDuration = fi.currentAnimDuration;
actIn.haveAnimState = fi.haveAnimState;
auto actOut = activity_.resolve(actIn, caps_);
if (actOut.valid) return actOut;
// ── Locomotion FSM (lowest priority) ────────────────────────────────
LocomotionFSM::Input locoIn;
locoIn.moving = fi.moving;
locoIn.movingForward = fi.movingForward;
locoIn.sprinting = fi.sprinting;
locoIn.movingBackward = fi.movingBackward;
locoIn.strafeLeft = fi.strafeLeft;
locoIn.strafeRight = fi.strafeRight;
locoIn.grounded = fi.grounded;
locoIn.jumping = fi.jumping;
locoIn.swimming = fi.swimming;
locoIn.sitting = fi.sitting;
locoIn.sprintAura = sprintAura_;
locoIn.deltaTime = lastDt_;
// Animation state for one-shot completion detection (jump start/end)
locoIn.currentAnimId = fi.currentAnimId;
locoIn.currentAnimTime = fi.currentAnimTime;
locoIn.currentAnimDuration = fi.currentAnimDuration;
locoIn.haveAnimState = fi.haveAnimState;
auto locoOut = locomotion_.resolve(locoIn, caps_);
if (locoOut.valid) return applyOverlays(locoOut);
// All FSMs returned invalid → STAY (keep last animation)
return AnimOutput::stay();
}
// ── Overlay application ──────────────────────────────────────────────────────
AnimOutput CharacterAnimator::applyOverlays(AnimOutput base) const {
if (!stealthed_) return base;
// Stealth substitution based on locomotion state
auto locoState = locomotion_.getState();
if (locoState == LocomotionFSM::State::IDLE) {
if (caps_.resolvedStealthIdle) base.animId = caps_.resolvedStealthIdle;
} else if (locoState == LocomotionFSM::State::WALK) {
if (caps_.resolvedStealthWalk) base.animId = caps_.resolvedStealthWalk;
} else if (locoState == LocomotionFSM::State::RUN) {
if (caps_.resolvedStealthRun) base.animId = caps_.resolvedStealthRun;
else if (caps_.resolvedStealthWalk) base.animId = caps_.resolvedStealthWalk;
}
return base;
}
} // namespace rendering
} // namespace wowee

View file

@ -0,0 +1,459 @@
#include "rendering/animation/combat_fsm.hpp"
#include "rendering/animation/animation_ids.hpp"
#include "game/inventory.hpp"
namespace wowee {
namespace rendering {
// ── One-shot completion helper ───────────────────────────────────────────────
bool CombatFSM::oneShotComplete(const Input& in, uint32_t expectedAnimId) const {
if (!in.haveAnimState) return false;
// Renderer auto-returns one-shots to STAND; detect that OR normal completion
return in.currentAnimId != expectedAnimId ||
(in.currentAnimDuration > 0.1f && in.currentAnimTime >= in.currentAnimDuration - 0.05f);
}
// ── Event handling ───────────────────────────────────────────────────────────
void CombatFSM::onEvent(AnimEvent event) {
switch (event) {
case AnimEvent::COMBAT_ENTER:
if (state_ == State::INACTIVE)
state_ = State::UNSHEATHE;
break;
case AnimEvent::COMBAT_EXIT:
if (state_ == State::COMBAT_IDLE)
state_ = State::SHEATHE;
break;
case AnimEvent::STUN_ENTER:
clearSpellState();
hitReactionAnimId_ = 0;
stunned_ = true;
state_ = State::STUNNED;
break;
case AnimEvent::STUN_EXIT:
stunned_ = false;
if (state_ == State::STUNNED)
state_ = State::INACTIVE;
break;
case AnimEvent::HIT_REACT:
// Handled by triggerHitReaction() with animId
break;
case AnimEvent::CHARGE_START:
charging_ = true;
clearSpellState();
state_ = State::CHARGE;
break;
case AnimEvent::CHARGE_END:
charging_ = false;
if (state_ == State::CHARGE)
state_ = State::INACTIVE;
break;
case AnimEvent::SWIM_ENTER:
clearSpellState();
hitReactionAnimId_ = 0;
stunned_ = false;
state_ = State::INACTIVE;
break;
default:
break;
}
}
// ── Spell cast management ────────────────────────────────────────────────────
void CombatFSM::startSpellCast(uint32_t precast, uint32_t cast, bool castLoop, uint32_t finalize) {
spellPrecastAnimId_ = precast;
spellCastAnimId_ = cast;
spellCastLoop_ = castLoop;
spellFinalizeAnimId_ = finalize;
spellPrecastAnimSeen_ = false;
spellPrecastFrames_ = 0;
spellFinalizeAnimSeen_ = false;
spellFinalizeFrames_ = 0;
state_ = (precast != 0) ? State::SPELL_PRECAST : State::SPELL_CASTING;
}
void CombatFSM::stopSpellCast() {
if (state_ != State::SPELL_PRECAST && state_ != State::SPELL_CASTING) return;
spellFinalizeAnimSeen_ = false;
spellFinalizeFrames_ = 0;
state_ = State::SPELL_FINALIZE;
}
void CombatFSM::clearSpellState() {
spellPrecastAnimId_ = 0;
spellCastAnimId_ = 0;
spellCastLoop_ = false;
spellFinalizeAnimId_ = 0;
spellPrecastAnimSeen_ = false;
spellPrecastFrames_ = 0;
spellFinalizeAnimSeen_ = false;
spellFinalizeFrames_ = 0;
}
// ── Hit/stun ─────────────────────────────────────────────────────────────────
void CombatFSM::triggerHitReaction(uint32_t animId) {
// Don't interrupt swim/jump/stun states
if (state_ == State::STUNNED) return;
// Interrupt spell casting
if (state_ == State::SPELL_PRECAST || state_ == State::SPELL_CASTING || state_ == State::SPELL_FINALIZE) {
clearSpellState();
}
hitReactionAnimId_ = animId;
state_ = State::HIT_REACTION;
}
void CombatFSM::setStunned(bool stunned) {
stunned_ = stunned;
if (stunned) {
if (state_ == State::SPELL_PRECAST || state_ == State::SPELL_CASTING || state_ == State::SPELL_FINALIZE) {
clearSpellState();
}
hitReactionAnimId_ = 0;
state_ = State::STUNNED;
} else {
if (state_ == State::STUNNED)
state_ = State::INACTIVE;
}
}
void CombatFSM::setCharging(bool charging) {
charging_ = charging;
if (charging) {
clearSpellState();
hitReactionAnimId_ = 0;
state_ = State::CHARGE;
} else if (state_ == State::CHARGE) {
state_ = State::INACTIVE;
}
}
// ── State transitions ────────────────────────────────────────────────────────
void CombatFSM::updateTransitions(const Input& in) {
// Stun override: can't act while stunned
if (stunned_ && state_ != State::STUNNED) {
state_ = State::STUNNED;
return;
}
// Force melee/ranged overrides
if (in.meleeSwingTimer > 0.0f && !stunned_ && in.grounded && !in.swimming) {
if (state_ != State::MELEE_SWING) {
clearSpellState();
hitReactionAnimId_ = 0;
}
state_ = State::MELEE_SWING;
return;
}
if (in.rangedShootTimer > 0.0f && !stunned_ && in.meleeSwingTimer <= 0.0f && in.grounded && !in.swimming) {
if (state_ != State::RANGED_SHOOT) {
clearSpellState();
hitReactionAnimId_ = 0;
}
state_ = State::RANGED_SHOOT;
return;
}
if (charging_ && !stunned_) {
if (state_ != State::CHARGE) {
clearSpellState();
hitReactionAnimId_ = 0;
}
state_ = State::CHARGE;
return;
}
switch (state_) {
case State::INACTIVE:
if (in.inCombat && in.grounded && !in.swimming && !in.moving) {
state_ = in.hasUnsheathe ? State::UNSHEATHE : State::COMBAT_IDLE;
}
break;
case State::COMBAT_IDLE:
if (in.swimming || in.jumping || !in.grounded || in.moving) {
state_ = State::INACTIVE;
} else if (!in.inCombat) {
state_ = in.hasSheathe ? State::SHEATHE : State::INACTIVE;
}
break;
case State::MELEE_SWING:
if (in.meleeSwingTimer <= 0.0f) {
if (in.swimming) {
state_ = State::INACTIVE;
} else if (in.inCombat && in.grounded) {
state_ = State::COMBAT_IDLE;
} else {
state_ = State::INACTIVE;
}
}
break;
case State::RANGED_SHOOT:
if (in.rangedShootTimer <= 0.0f) {
if (in.swimming) {
state_ = State::INACTIVE;
} else if (in.inCombat && in.grounded) {
state_ = State::RANGED_LOAD;
} else {
state_ = State::INACTIVE;
}
}
break;
case State::RANGED_LOAD:
if (in.swimming || in.jumping || !in.grounded || in.moving) {
state_ = State::INACTIVE;
} else if (in.inCombat) {
state_ = State::COMBAT_IDLE;
} else {
state_ = State::INACTIVE;
}
break;
case State::SPELL_PRECAST:
if (in.swimming || (in.jumping && !in.grounded) || (!in.grounded && !in.jumping)) {
clearSpellState();
state_ = State::INACTIVE;
} else if (in.haveAnimState) {
uint32_t expectedAnim = spellPrecastAnimId_ ? spellPrecastAnimId_ : anim::SPELL_PRECAST;
if (in.currentAnimId == expectedAnim) spellPrecastAnimSeen_ = true;
if (spellPrecastAnimSeen_ && oneShotComplete(in, expectedAnim)) {
state_ = State::SPELL_CASTING;
}
if (!spellPrecastAnimSeen_ && ++spellPrecastFrames_ > 10) {
state_ = State::SPELL_CASTING;
}
}
break;
case State::SPELL_CASTING:
if (in.swimming || (in.jumping && !in.grounded) || (!in.grounded && !in.jumping)) {
clearSpellState();
state_ = State::INACTIVE;
} else if (in.moving) {
clearSpellState();
state_ = State::INACTIVE;
}
// Stays in SPELL_CASTING until stopSpellCast() is called externally
break;
case State::SPELL_FINALIZE: {
if (in.swimming || (in.jumping && !in.grounded)) {
clearSpellState();
state_ = State::INACTIVE;
} else if (in.haveAnimState) {
uint32_t expectedAnim = spellFinalizeAnimId_ ? spellFinalizeAnimId_
: (spellCastAnimId_ ? spellCastAnimId_ : anim::SPELL);
if (in.currentAnimId == expectedAnim) spellFinalizeAnimSeen_ = true;
if (spellFinalizeAnimSeen_ && oneShotComplete(in, expectedAnim)) {
clearSpellState();
state_ = in.inCombat ? State::COMBAT_IDLE : State::INACTIVE;
}
// Safety: if finalize anim never seen (model lacks it), finish after timeout
if (!spellFinalizeAnimSeen_ && ++spellFinalizeFrames_ > 10) {
clearSpellState();
state_ = in.inCombat ? State::COMBAT_IDLE : State::INACTIVE;
}
}
break;
}
case State::HIT_REACTION:
if (in.swimming || in.moving) {
hitReactionAnimId_ = 0;
state_ = State::INACTIVE;
} else if (in.haveAnimState) {
uint32_t expectedAnim = hitReactionAnimId_ ? hitReactionAnimId_ : anim::COMBAT_WOUND;
if (oneShotComplete(in, expectedAnim)) {
hitReactionAnimId_ = 0;
state_ = in.inCombat ? State::COMBAT_IDLE : State::INACTIVE;
}
}
break;
case State::STUNNED:
if (!stunned_) {
state_ = in.inCombat ? State::COMBAT_IDLE : State::INACTIVE;
} else if (in.swimming) {
stunned_ = false;
state_ = State::INACTIVE;
}
break;
case State::CHARGE:
if (!charging_) {
state_ = State::INACTIVE;
}
break;
case State::UNSHEATHE:
if (in.swimming || in.moving) {
state_ = State::INACTIVE;
} else if (in.haveAnimState && oneShotComplete(in, anim::UNSHEATHE)) {
state_ = State::COMBAT_IDLE;
}
break;
case State::SHEATHE:
if (in.swimming || in.moving) {
state_ = State::INACTIVE;
} else if (in.inCombat) {
state_ = State::COMBAT_IDLE;
} else if (in.haveAnimState && oneShotComplete(in, anim::SHEATHE)) {
state_ = State::INACTIVE;
}
break;
}
}
// ── Animation resolution ─────────────────────────────────────────────────────
AnimOutput CombatFSM::resolve(const Input& in, const AnimCapabilitySet& caps,
const WeaponLoadout& loadout) {
updateTransitions(in);
if (state_ == State::INACTIVE) return AnimOutput::stay();
uint32_t animId = 0;
bool loop = true;
switch (state_) {
case State::INACTIVE:
return AnimOutput::stay();
case State::COMBAT_IDLE:
if (in.lowHealth && caps.resolvedStandWound) {
animId = caps.resolvedStandWound;
} else if (loadout.rangedType == RangedWeaponType::BOW) {
animId = caps.resolvedReadyBow;
} else if (loadout.rangedType == RangedWeaponType::GUN) {
animId = caps.resolvedReadyRifle;
} else if (loadout.rangedType == RangedWeaponType::CROSSBOW) {
animId = caps.resolvedReadyCrossbow;
} else if (loadout.rangedType == RangedWeaponType::THROWN) {
animId = caps.resolvedReadyThrown;
} else if (loadout.is2HLoose) {
animId = caps.resolvedReady2HLoose;
} else if (loadout.inventoryType == game::InvType::TWO_HAND) {
animId = caps.resolvedReady2H;
} else if (loadout.isFist) {
animId = caps.resolvedReadyFist;
} else if (loadout.inventoryType == game::InvType::NON_EQUIP) {
animId = caps.resolvedReadyUnarmed;
} else {
animId = caps.resolvedReady1H;
}
loop = true;
break;
case State::MELEE_SWING:
if (in.specialAttackAnimId != 0) {
animId = in.specialAttackAnimId;
} else {
// Resolve melee animation using probed capabilities + weapon loadout
bool useOffHand = loadout.hasOffHand && offHandTurn_;
offHandTurn_ = loadout.hasOffHand ? !offHandTurn_ : false;
if (useOffHand) {
if (loadout.isFist) animId = caps.resolvedMeleeOffHandFist;
else if (loadout.isDagger) animId = caps.resolvedMeleeOffHandPierce;
else if (loadout.inventoryType == game::InvType::NON_EQUIP) animId = caps.resolvedMeleeOffHandUnarmed;
else animId = caps.resolvedMeleeOffHand;
} else if (loadout.isFist) {
animId = caps.resolvedMeleeFist;
} else if (loadout.isDagger) {
animId = caps.resolvedMeleePierce;
} else if (loadout.is2HLoose) {
animId = caps.resolvedMelee2HLoose;
} else if (loadout.inventoryType == game::InvType::TWO_HAND) {
animId = caps.resolvedMelee2H;
} else if (loadout.inventoryType == game::InvType::NON_EQUIP) {
animId = caps.resolvedMeleeUnarmed;
} else {
animId = caps.resolvedMelee1H;
}
}
if (animId == 0) animId = anim::STAND; // Melee must play something
loop = false;
break;
case State::RANGED_SHOOT:
animId = in.rangedAnimId ? in.rangedAnimId : anim::ATTACK_BOW;
loop = false;
break;
case State::RANGED_LOAD:
switch (loadout.rangedType) {
case RangedWeaponType::BOW: animId = caps.resolvedLoadBow; break;
case RangedWeaponType::GUN: animId = caps.resolvedLoadRifle; break;
case RangedWeaponType::CROSSBOW: animId = caps.resolvedLoadBow; break;
default: break;
}
loop = false;
break;
case State::SPELL_PRECAST:
animId = spellPrecastAnimId_ ? spellPrecastAnimId_ : anim::SPELL_PRECAST;
loop = false;
break;
case State::SPELL_CASTING:
animId = spellCastAnimId_ ? spellCastAnimId_ : anim::SPELL;
loop = spellCastLoop_;
break;
case State::SPELL_FINALIZE:
animId = spellFinalizeAnimId_ ? spellFinalizeAnimId_
: (spellCastAnimId_ ? spellCastAnimId_ : anim::SPELL);
loop = false;
break;
case State::HIT_REACTION:
animId = hitReactionAnimId_ ? hitReactionAnimId_
: (caps.resolvedCombatWound ? caps.resolvedCombatWound : anim::COMBAT_WOUND);
loop = false;
break;
case State::STUNNED:
animId = caps.resolvedStun ? caps.resolvedStun : anim::STUN;
loop = true;
break;
case State::CHARGE:
animId = caps.resolvedRun ? caps.resolvedRun : anim::RUN;
loop = true;
break;
case State::UNSHEATHE:
animId = caps.resolvedUnsheathe ? caps.resolvedUnsheathe : anim::UNSHEATHE;
loop = false;
break;
case State::SHEATHE:
animId = caps.resolvedSheathe ? caps.resolvedSheathe : anim::SHEATHE;
loop = false;
break;
}
if (animId == 0) return AnimOutput::stay();
return AnimOutput::ok(animId, loop);
}
// ── Reset ────────────────────────────────────────────────────────────────────
void CombatFSM::reset() {
state_ = State::INACTIVE;
clearSpellState();
hitReactionAnimId_ = 0;
stunned_ = false;
charging_ = false;
offHandTurn_ = false;
}
} // namespace rendering
} // namespace wowee

View file

@ -0,0 +1,299 @@
#include "rendering/animation/emote_registry.hpp"
#include "rendering/animation/animation_ids.hpp"
#include "pipeline/asset_manager.hpp"
#include "pipeline/dbc_loader.hpp"
#include "pipeline/dbc_layout.hpp"
#include "core/application.hpp"
#include "core/logger.hpp"
#include <algorithm>
#include <cctype>
#include <unordered_set>
namespace wowee {
namespace rendering {
// ── Helper functions (moved from animation_controller.cpp) ───────────────────
static std::vector<std::string> parseEmoteCommands(const std::string& raw) {
std::vector<std::string> out;
std::string cur;
for (char c : raw) {
if (std::isalnum(static_cast<unsigned char>(c)) || c == '_') {
cur.push_back(static_cast<char>(std::tolower(static_cast<unsigned char>(c))));
} else if (!cur.empty()) {
out.push_back(cur);
cur.clear();
}
}
if (!cur.empty()) out.push_back(cur);
return out;
}
static bool isLoopingEmote(const std::string& command) {
static const std::unordered_set<std::string> kLooping = {
"dance", "train", "dead", "eat", "work",
};
return kLooping.find(command) != kLooping.end();
}
// Map one-shot emote animation IDs to their persistent EMOTE_STATE_* looping variants.
// When a looping emote is played, we prefer the STATE variant if the model has it.
static uint32_t getEmoteStateVariantStatic(uint32_t oneShotAnimId) {
static const std::unordered_map<uint32_t, uint32_t> kStateMap = {
{anim::EMOTE_DANCE, anim::EMOTE_STATE_DANCE},
{anim::EMOTE_LAUGH, anim::EMOTE_STATE_LAUGH},
{anim::EMOTE_POINT, anim::EMOTE_STATE_POINT},
{anim::EMOTE_EAT, anim::EMOTE_STATE_EAT},
{anim::EMOTE_ROAR, anim::EMOTE_STATE_ROAR},
{anim::EMOTE_APPLAUD, anim::EMOTE_STATE_APPLAUD},
{anim::EMOTE_WORK, anim::EMOTE_STATE_WORK},
{anim::EMOTE_USE_STANDING, anim::EMOTE_STATE_USE_STANDING},
{anim::EATING_LOOP, anim::EMOTE_STATE_EAT},
};
auto it = kStateMap.find(oneShotAnimId);
return it != kStateMap.end() ? it->second : 0;
}
static std::string replacePlaceholders(const std::string& text, const std::string* targetName) {
if (text.empty()) return text;
std::string out;
out.reserve(text.size() + 16);
for (size_t i = 0; i < text.size(); ++i) {
if (text[i] == '%' && i + 1 < text.size() && text[i + 1] == 's') {
if (targetName && !targetName->empty()) out += *targetName;
i++;
} else {
out.push_back(text[i]);
}
}
return out;
}
// ── EmoteRegistry implementation ─────────────────────────────────────────────
EmoteRegistry& EmoteRegistry::instance() {
static EmoteRegistry inst;
return inst;
}
void EmoteRegistry::loadFromDbc() {
if (loaded_) return;
loaded_ = true;
auto* assetManager = core::Application::getInstance().getAssetManager();
if (!assetManager) {
LOG_WARNING("Emotes: no AssetManager");
loadFallbackEmotes();
return;
}
auto emotesTextDbc = assetManager->loadDBC("EmotesText.dbc");
auto emotesTextDataDbc = assetManager->loadDBC("EmotesTextData.dbc");
if (!emotesTextDbc || !emotesTextDataDbc || !emotesTextDbc->isLoaded() || !emotesTextDataDbc->isLoaded()) {
LOG_WARNING("Emotes: DBCs not available (EmotesText/EmotesTextData)");
loadFallbackEmotes();
return;
}
const auto* activeLayout = pipeline::getActiveDBCLayout();
const auto* etdL = activeLayout ? activeLayout->getLayout("EmotesTextData") : nullptr;
const auto* emL = activeLayout ? activeLayout->getLayout("Emotes") : nullptr;
const auto* etL = activeLayout ? activeLayout->getLayout("EmotesText") : nullptr;
std::unordered_map<uint32_t, std::string> textData;
textData.reserve(emotesTextDataDbc->getRecordCount());
for (uint32_t r = 0; r < emotesTextDataDbc->getRecordCount(); ++r) {
uint32_t id = emotesTextDataDbc->getUInt32(r, etdL ? (*etdL)["ID"] : 0);
std::string text = emotesTextDataDbc->getString(r, etdL ? (*etdL)["Text"] : 1);
if (!text.empty()) textData.emplace(id, std::move(text));
}
std::unordered_map<uint32_t, uint32_t> emoteIdToAnim;
if (auto emotesDbc = assetManager->loadDBC("Emotes.dbc"); emotesDbc && emotesDbc->isLoaded()) {
emoteIdToAnim.reserve(emotesDbc->getRecordCount());
for (uint32_t r = 0; r < emotesDbc->getRecordCount(); ++r) {
uint32_t emoteId = emotesDbc->getUInt32(r, emL ? (*emL)["ID"] : 0);
uint32_t animId = emotesDbc->getUInt32(r, emL ? (*emL)["AnimID"] : 2);
if (animId != 0) emoteIdToAnim[emoteId] = animId;
}
}
emoteTable_.clear();
emoteTable_.reserve(emotesTextDbc->getRecordCount());
for (uint32_t r = 0; r < emotesTextDbc->getRecordCount(); ++r) {
uint32_t recordId = emotesTextDbc->getUInt32(r, etL ? (*etL)["ID"] : 0);
std::string cmdRaw = emotesTextDbc->getString(r, etL ? (*etL)["Command"] : 1);
if (cmdRaw.empty()) continue;
uint32_t emoteRef = emotesTextDbc->getUInt32(r, etL ? (*etL)["EmoteRef"] : 2);
uint32_t animId = 0;
auto animIt = emoteIdToAnim.find(emoteRef);
if (animIt != emoteIdToAnim.end()) {
animId = animIt->second;
} else {
animId = emoteRef;
}
uint32_t senderTargetTextId = emotesTextDbc->getUInt32(r, etL ? (*etL)["SenderTargetTextID"] : 5);
uint32_t senderNoTargetTextId = emotesTextDbc->getUInt32(r, etL ? (*etL)["SenderNoTargetTextID"] : 9);
uint32_t othersTargetTextId = emotesTextDbc->getUInt32(r, etL ? (*etL)["OthersTargetTextID"] : 3);
uint32_t othersNoTargetTextId = emotesTextDbc->getUInt32(r, etL ? (*etL)["OthersNoTargetTextID"] : 7);
std::string textTarget, textNoTarget, oTarget, oNoTarget;
if (auto it = textData.find(senderTargetTextId); it != textData.end()) textTarget = it->second;
if (auto it = textData.find(senderNoTargetTextId); it != textData.end()) textNoTarget = it->second;
if (auto it = textData.find(othersTargetTextId); it != textData.end()) oTarget = it->second;
if (auto it = textData.find(othersNoTargetTextId); it != textData.end()) oNoTarget = it->second;
for (const std::string& cmd : parseEmoteCommands(cmdRaw)) {
if (cmd.empty()) continue;
EmoteInfo info;
info.animId = animId;
info.dbcId = recordId;
info.loop = isLoopingEmote(cmd);
info.textNoTarget = textNoTarget;
info.textTarget = textTarget;
info.othersNoTarget = oNoTarget;
info.othersTarget = oTarget;
info.command = cmd;
emoteTable_.emplace(cmd, std::move(info));
}
}
if (emoteTable_.empty()) {
LOG_WARNING("Emotes: DBC loaded but no commands parsed, using fallback list");
loadFallbackEmotes();
} else {
LOG_INFO("Emotes: loaded ", emoteTable_.size(), " commands from DBC");
}
buildDbcIdIndex();
}
void EmoteRegistry::loadFallbackEmotes() {
if (!emoteTable_.empty()) return;
emoteTable_ = {
{"wave", {anim::EMOTE_WAVE, 0, false, "You wave.", "You wave at %s.", "%s waves.", "%s waves at %s.", "wave"}},
{"bow", {anim::EMOTE_BOW, 0, false, "You bow down graciously.", "You bow down before %s.", "%s bows down graciously.", "%s bows down before %s.", "bow"}},
{"laugh", {anim::EMOTE_LAUGH, 0, false, "You laugh.", "You laugh at %s.", "%s laughs.", "%s laughs at %s.", "laugh"}},
{"point", {anim::EMOTE_POINT, 0, false, "You point over yonder.", "You point at %s.", "%s points over yonder.", "%s points at %s.", "point"}},
{"cheer", {anim::EMOTE_CHEER, 0, false, "You cheer!", "You cheer at %s.", "%s cheers!", "%s cheers at %s.", "cheer"}},
{"dance", {anim::EMOTE_DANCE, 0, true, "You burst into dance.", "You dance with %s.", "%s bursts into dance.", "%s dances with %s.", "dance"}},
{"kneel", {anim::EMOTE_KNEEL, 0, false, "You kneel down.", "You kneel before %s.", "%s kneels down.", "%s kneels before %s.", "kneel"}},
{"applaud", {anim::EMOTE_APPLAUD, 0, false, "You applaud. Bravo!", "You applaud at %s. Bravo!", "%s applauds. Bravo!", "%s applauds at %s. Bravo!", "applaud"}},
{"shout", {anim::EMOTE_SHOUT, 0, false, "You shout.", "You shout at %s.", "%s shouts.", "%s shouts at %s.", "shout"}},
{"chicken", {anim::EMOTE_CHICKEN, 0, false, "With arms flapping, you strut around. Cluck, Cluck, Chicken!",
"With arms flapping, you strut around %s. Cluck, Cluck, Chicken!",
"%s struts around. Cluck, Cluck, Chicken!", "%s struts around %s. Cluck, Cluck, Chicken!", "chicken"}},
{"cry", {anim::EMOTE_CRY, 0, false, "You cry.", "You cry on %s's shoulder.", "%s cries.", "%s cries on %s's shoulder.", "cry"}},
{"kiss", {anim::EMOTE_KISS, 0, false, "You blow a kiss into the wind.", "You blow a kiss to %s.", "%s blows a kiss into the wind.", "%s blows a kiss to %s.", "kiss"}},
{"roar", {anim::EMOTE_ROAR, 0, false, "You roar with bestial vigor. So fierce!", "You roar with bestial vigor at %s. So fierce!", "%s roars with bestial vigor. So fierce!", "%s roars with bestial vigor at %s. So fierce!", "roar"}},
{"salute", {anim::EMOTE_SALUTE, 0, false, "You salute.", "You salute %s with respect.", "%s salutes.", "%s salutes %s with respect.", "salute"}},
{"rude", {anim::EMOTE_RUDE, 0, false, "You make a rude gesture.", "You make a rude gesture at %s.", "%s makes a rude gesture.", "%s makes a rude gesture at %s.", "rude"}},
{"flex", {anim::EMOTE_FLEX, 0, false, "You flex your muscles. Oooooh so strong!", "You flex at %s. Oooooh so strong!", "%s flexes. Oooooh so strong!", "%s flexes at %s. Oooooh so strong!", "flex"}},
{"shy", {anim::EMOTE_SHY, 0, false, "You smile shyly.", "You smile shyly at %s.", "%s smiles shyly.", "%s smiles shyly at %s.", "shy"}},
{"beg", {anim::EMOTE_BEG, 0, false, "You beg everyone around you. How pathetic.", "You beg %s. How pathetic.", "%s begs everyone around. How pathetic.", "%s begs %s. How pathetic.", "beg"}},
{"eat", {anim::EMOTE_EAT, 0, true, "You begin to eat.", "You begin to eat in front of %s.", "%s begins to eat.", "%s begins to eat in front of %s.", "eat"}},
{"talk", {anim::EMOTE_TALK, 0, false, "You talk.", "You talk to %s.", "%s talks.", "%s talks to %s.", "talk"}},
{"work", {anim::EMOTE_WORK, 0, true, "You begin to work.", "You begin to work near %s.", "%s begins to work.", "%s begins to work near %s.", "work"}},
{"train", {anim::EMOTE_TRAIN, 0, true, "You let off a train whistle. Choo Choo!", "You let off a train whistle at %s. Choo Choo!", "%s lets off a train whistle. Choo Choo!", "%s lets off a train whistle at %s. Choo Choo!", "train"}},
{"dead", {anim::EMOTE_DEAD, 0, true, "You play dead.", "You play dead in front of %s.", "%s plays dead.", "%s plays dead in front of %s.", "dead"}},
};
buildDbcIdIndex();
}
void EmoteRegistry::buildDbcIdIndex() {
emoteByDbcId_.clear();
for (auto& [cmd, info] : emoteTable_) {
if (info.dbcId != 0) {
emoteByDbcId_.emplace(info.dbcId, &info);
}
}
}
std::optional<EmoteRegistry::EmoteResult> EmoteRegistry::findEmote(const std::string& command) const {
auto it = emoteTable_.find(command);
if (it == emoteTable_.end()) return std::nullopt;
const auto& info = it->second;
if (info.animId == 0) return std::nullopt;
return EmoteResult{info.animId, info.loop};
}
uint32_t EmoteRegistry::animByDbcId(uint32_t dbcId) const {
auto it = emoteByDbcId_.find(dbcId);
if (it != emoteByDbcId_.end()) {
return it->second->animId;
}
return 0;
}
uint32_t EmoteRegistry::getStateVariant(uint32_t oneShotAnimId) const {
return getEmoteStateVariantStatic(oneShotAnimId);
}
const EmoteInfo* EmoteRegistry::findInfo(const std::string& command) const {
auto it = emoteTable_.find(command);
return it != emoteTable_.end() ? &it->second : nullptr;
}
std::string EmoteRegistry::textFor(const std::string& emoteName,
const std::string* targetName) const {
auto it = emoteTable_.find(emoteName);
if (it != emoteTable_.end()) {
const auto& info = it->second;
const std::string& base = (targetName ? info.textTarget : info.textNoTarget);
if (!base.empty()) {
return replacePlaceholders(base, targetName);
}
if (targetName && !targetName->empty()) {
return "You " + info.command + " at " + *targetName + ".";
}
return "You " + info.command + ".";
}
return "";
}
uint32_t EmoteRegistry::dbcIdFor(const std::string& emoteName) const {
auto it = emoteTable_.find(emoteName);
if (it != emoteTable_.end()) {
return it->second.dbcId;
}
return 0;
}
std::string EmoteRegistry::textByDbcId(uint32_t dbcId,
const std::string& senderName,
const std::string* targetName) const {
auto it = emoteByDbcId_.find(dbcId);
if (it == emoteByDbcId_.end()) return "";
const EmoteInfo& info = *it->second;
if (targetName && !targetName->empty()) {
if (!info.othersTarget.empty()) {
std::string out;
out.reserve(info.othersTarget.size() + senderName.size() + targetName->size());
bool firstReplaced = false;
for (size_t i = 0; i < info.othersTarget.size(); ++i) {
if (info.othersTarget[i] == '%' && i + 1 < info.othersTarget.size() && info.othersTarget[i + 1] == 's') {
out += firstReplaced ? *targetName : senderName;
firstReplaced = true;
++i;
} else {
out.push_back(info.othersTarget[i]);
}
}
return out;
}
return senderName + " " + info.command + "s at " + *targetName + ".";
} else {
if (!info.othersNoTarget.empty()) {
return replacePlaceholders(info.othersNoTarget, &senderName);
}
return senderName + " " + info.command + "s.";
}
}
} // namespace rendering
} // namespace wowee

View file

@ -0,0 +1,207 @@
#include "rendering/animation/footstep_driver.hpp"
#include "rendering/renderer.hpp"
#include "rendering/camera_controller.hpp"
#include "rendering/character_renderer.hpp"
#include "rendering/terrain_manager.hpp"
#include "rendering/wmo_renderer.hpp"
#include "rendering/water_renderer.hpp"
#include "rendering/swim_effects.hpp"
#include "audio/audio_coordinator.hpp"
#include "audio/footstep_manager.hpp"
#include "audio/movement_sound_manager.hpp"
#include <algorithm>
#include <cctype>
#include <cmath>
namespace wowee {
namespace rendering {
// ── Footstep event detection (moved from AnimationController) ────────────────
bool FootstepDriver::shouldTriggerFootstepEvent(uint32_t animationId, float animationTimeMs, float animationDurationMs) {
if (animationDurationMs <= 1.0f) {
footstepNormInitialized_ = false;
return false;
}
float wrappedTime = animationTimeMs;
while (wrappedTime >= animationDurationMs) {
wrappedTime -= animationDurationMs;
}
if (wrappedTime < 0.0f) wrappedTime += animationDurationMs;
float norm = wrappedTime / animationDurationMs;
if (animationId != footstepLastAnimationId_) {
footstepLastAnimationId_ = animationId;
footstepLastNormTime_ = norm;
footstepNormInitialized_ = true;
return false;
}
if (!footstepNormInitialized_) {
footstepNormInitialized_ = true;
footstepLastNormTime_ = norm;
return false;
}
auto crossed = [&](float eventNorm) {
if (footstepLastNormTime_ <= norm) {
return footstepLastNormTime_ < eventNorm && eventNorm <= norm;
}
return footstepLastNormTime_ < eventNorm || eventNorm <= norm;
};
bool trigger = crossed(0.22f) || crossed(0.72f);
footstepLastNormTime_ = norm;
return trigger;
}
audio::FootstepSurface FootstepDriver::resolveFootstepSurface(Renderer* renderer) const {
auto* cameraController = renderer->getCameraController();
if (!cameraController || !cameraController->isThirdPerson()) {
return audio::FootstepSurface::STONE;
}
const glm::vec3& p = renderer->getCharacterPosition();
float distSq = glm::dot(p - cachedFootstepPosition_, p - cachedFootstepPosition_);
if (distSq < 2.25f && cachedFootstepUpdateTimer_ < 0.5f) {
return cachedFootstepSurface_;
}
cachedFootstepPosition_ = p;
cachedFootstepUpdateTimer_ = 0.0f;
if (cameraController->isSwimming()) {
cachedFootstepSurface_ = audio::FootstepSurface::WATER;
return audio::FootstepSurface::WATER;
}
auto* waterRenderer = renderer->getWaterRenderer();
if (waterRenderer) {
auto waterH = waterRenderer->getWaterHeightAt(p.x, p.y);
if (waterH && p.z < (*waterH + 0.25f)) {
cachedFootstepSurface_ = audio::FootstepSurface::WATER;
return audio::FootstepSurface::WATER;
}
}
auto* wmoRenderer = renderer->getWMORenderer();
auto* terrainManager = renderer->getTerrainManager();
if (wmoRenderer) {
auto wmoFloor = wmoRenderer->getFloorHeight(p.x, p.y, p.z + 1.5f);
auto terrainFloor = terrainManager ? terrainManager->getHeightAt(p.x, p.y) : std::nullopt;
if (wmoFloor && (!terrainFloor || *wmoFloor >= *terrainFloor - 0.1f)) {
cachedFootstepSurface_ = audio::FootstepSurface::STONE;
return audio::FootstepSurface::STONE;
}
}
audio::FootstepSurface surface = audio::FootstepSurface::STONE;
if (terrainManager) {
auto texture = terrainManager->getDominantTextureAt(p.x, p.y);
if (texture) {
std::string t = *texture;
for (char& c : t) c = static_cast<char>(std::tolower(static_cast<unsigned char>(c)));
if (t.find("snow") != std::string::npos || t.find("ice") != std::string::npos) surface = audio::FootstepSurface::SNOW;
else if (t.find("grass") != std::string::npos || t.find("moss") != std::string::npos || t.find("leaf") != std::string::npos) surface = audio::FootstepSurface::GRASS;
else if (t.find("sand") != std::string::npos || t.find("dirt") != std::string::npos || t.find("mud") != std::string::npos) surface = audio::FootstepSurface::DIRT;
else if (t.find("wood") != std::string::npos || t.find("timber") != std::string::npos) surface = audio::FootstepSurface::WOOD;
else if (t.find("metal") != std::string::npos || t.find("iron") != std::string::npos) surface = audio::FootstepSurface::METAL;
else if (t.find("stone") != std::string::npos || t.find("rock") != std::string::npos || t.find("cobble") != std::string::npos || t.find("brick") != std::string::npos) surface = audio::FootstepSurface::STONE;
}
}
cachedFootstepSurface_ = surface;
return surface;
}
// ── Footstep update (moved from AnimationController::updateFootsteps) ────────
void FootstepDriver::update(float deltaTime, Renderer* renderer,
bool mounted, uint32_t mountInstanceId, bool taxiFlight,
bool isFootstepState) {
auto* footstepManager = renderer->getAudioCoordinator()->getFootstepManager();
if (!footstepManager) return;
auto* characterRenderer = renderer->getCharacterRenderer();
auto* cameraController = renderer->getCameraController();
uint32_t characterInstanceId = renderer->getCharacterInstanceId();
footstepManager->update(deltaTime);
cachedFootstepUpdateTimer_ += deltaTime;
bool canPlayFootsteps = characterRenderer && characterInstanceId > 0 &&
cameraController && cameraController->isThirdPerson() &&
cameraController->isGrounded() && !cameraController->isSwimming();
if (canPlayFootsteps && mounted && mountInstanceId > 0 && !taxiFlight) {
// Mount footsteps: use mount's animation for timing
uint32_t animId = 0;
float animTimeMs = 0.0f, animDurationMs = 0.0f;
if (characterRenderer->getAnimationState(mountInstanceId, animId, animTimeMs, animDurationMs) &&
animDurationMs > 1.0f && cameraController->isMoving()) {
float wrappedTime = animTimeMs;
while (wrappedTime >= animDurationMs) {
wrappedTime -= animDurationMs;
}
if (wrappedTime < 0.0f) wrappedTime += animDurationMs;
float norm = wrappedTime / animDurationMs;
if (animId != mountFootstepLastAnimId_) {
mountFootstepLastAnimId_ = animId;
mountFootstepLastNormTime_ = norm;
mountFootstepNormInitialized_ = true;
} else if (!mountFootstepNormInitialized_) {
mountFootstepNormInitialized_ = true;
mountFootstepLastNormTime_ = norm;
} else {
auto crossed = [&](float eventNorm) {
if (mountFootstepLastNormTime_ <= norm) {
return mountFootstepLastNormTime_ < eventNorm && eventNorm <= norm;
}
return mountFootstepLastNormTime_ < eventNorm || eventNorm <= norm;
};
if (crossed(0.25f) || crossed(0.75f)) {
footstepManager->playFootstep(resolveFootstepSurface(renderer), true);
}
mountFootstepLastNormTime_ = norm;
}
} else {
mountFootstepNormInitialized_ = false;
}
footstepNormInitialized_ = false;
} else if (canPlayFootsteps && isFootstepState) {
uint32_t animId = 0;
float animTimeMs = 0.0f;
float animDurationMs = 0.0f;
if (characterRenderer->getAnimationState(characterInstanceId, animId, animTimeMs, animDurationMs) &&
shouldTriggerFootstepEvent(animId, animTimeMs, animDurationMs)) {
auto surface = resolveFootstepSurface(renderer);
footstepManager->playFootstep(surface, cameraController->isSprinting());
if (surface == audio::FootstepSurface::WATER) {
if (renderer->getAudioCoordinator()->getMovementSoundManager()) {
renderer->getAudioCoordinator()->getMovementSoundManager()->playWaterFootstep(audio::MovementSoundManager::CharacterSize::MEDIUM);
}
auto* swimEffects = renderer->getSwimEffects();
auto* waterRenderer = renderer->getWaterRenderer();
if (swimEffects && waterRenderer) {
const glm::vec3& characterPosition = renderer->getCharacterPosition();
auto wh = waterRenderer->getWaterHeightAt(characterPosition.x, characterPosition.y);
if (wh) {
swimEffects->spawnFootSplash(characterPosition, *wh);
}
}
}
}
mountFootstepNormInitialized_ = false;
} else {
footstepNormInitialized_ = false;
mountFootstepNormInitialized_ = false;
}
}
} // namespace rendering
} // namespace wowee

View file

@ -0,0 +1,283 @@
#include "rendering/animation/locomotion_fsm.hpp"
#include "rendering/animation/animation_ids.hpp"
#include <algorithm>
namespace wowee {
namespace rendering {
// ── One-shot completion helper ─────────────────────────────────────────────────────────
bool LocomotionFSM::oneShotComplete(const Input& in, uint32_t expectedAnimId) const {
if (!in.haveAnimState) return false;
return in.currentAnimId != expectedAnimId ||
(in.currentAnimDuration > 0.1f && in.currentAnimTime >= in.currentAnimDuration - 0.05f);
}
// ── Event handling ───────────────────────────────────────────────────────────
void LocomotionFSM::onEvent(AnimEvent event) {
switch (event) {
case AnimEvent::SWIM_ENTER:
state_ = State::SWIM_IDLE;
break;
case AnimEvent::SWIM_EXIT:
state_ = State::IDLE;
break;
case AnimEvent::JUMP:
if (state_ != State::SWIM_IDLE && state_ != State::SWIM) {
jumpStartSeen_ = false;
state_ = State::JUMP_START;
}
break;
case AnimEvent::LANDED:
if (state_ == State::JUMP_MID || state_ == State::JUMP_START) {
jumpEndSeen_ = false;
state_ = State::JUMP_END;
}
break;
case AnimEvent::MOVE_START:
graceTimer_ = kGraceSec;
break;
case AnimEvent::MOVE_STOP:
// Grace timer handles the delay in updateTransitions
break;
default:
break;
}
}
// ── State transitions ────────────────────────────────────────────────────────
void LocomotionFSM::updateTransitions(const Input& in, const AnimCapabilitySet& caps) {
// Update grace timer
if (in.moving) {
graceTimer_ = kGraceSec;
wasSprinting_ = in.sprinting;
} else {
graceTimer_ = std::max(0.0f, graceTimer_ - in.deltaTime);
}
const bool effectiveMoving = in.moving || graceTimer_ > 0.0f;
const bool effectiveSprinting = in.sprinting || (!in.moving && effectiveMoving && wasSprinting_);
switch (state_) {
case State::IDLE:
if (in.swimming) {
state_ = effectiveMoving ? State::SWIM : State::SWIM_IDLE;
} else if (!in.grounded && in.jumping) {
jumpStartSeen_ = false;
state_ = State::JUMP_START;
} else if (!in.grounded) {
state_ = State::JUMP_MID;
} else if (effectiveMoving && effectiveSprinting) {
state_ = State::RUN;
} else if (effectiveMoving) {
state_ = State::WALK;
}
break;
case State::WALK:
if (in.swimming) {
state_ = effectiveMoving ? State::SWIM : State::SWIM_IDLE;
} else if (!in.grounded && in.jumping) {
jumpStartSeen_ = false;
state_ = State::JUMP_START;
} else if (!in.grounded) {
state_ = State::JUMP_MID;
} else if (!effectiveMoving) {
state_ = State::IDLE;
} else if (effectiveSprinting) {
state_ = State::RUN;
}
break;
case State::RUN:
if (in.swimming) {
state_ = effectiveMoving ? State::SWIM : State::SWIM_IDLE;
} else if (!in.grounded && in.jumping) {
jumpStartSeen_ = false;
state_ = State::JUMP_START;
} else if (!in.grounded) {
state_ = State::JUMP_MID;
} else if (!effectiveMoving) {
state_ = State::IDLE;
} else if (!effectiveSprinting) {
state_ = State::WALK;
}
break;
case State::JUMP_START:
if (in.swimming) {
state_ = State::SWIM_IDLE;
} else if (in.grounded) {
state_ = State::JUMP_END;
jumpEndSeen_ = false;
} else if (caps.resolvedJumpStart == 0) {
// Model doesn't have JUMP_START animation — skip to mid-air
state_ = State::JUMP_MID;
} else if (in.haveAnimState) {
// Use the same resolved ID that resolve() outputs
uint32_t expected = caps.resolvedJumpStart;
if (in.currentAnimId == expected) jumpStartSeen_ = true;
// Also detect completion via renderer's auto-STAND reset:
// once the animation was seen and currentAnimId changed, it completed.
if (jumpStartSeen_ && oneShotComplete(in, expected)) {
state_ = State::JUMP_MID;
}
} else {
// No animation state available — fall through after 1 frame
state_ = State::JUMP_MID;
}
break;
case State::JUMP_MID:
if (in.swimming) {
state_ = State::SWIM_IDLE;
} else if (in.grounded) {
state_ = State::JUMP_END;
}
break;
case State::JUMP_END:
if (in.swimming) {
state_ = effectiveMoving ? State::SWIM : State::SWIM_IDLE;
} else if (effectiveMoving) {
// Movement overrides landing animation
state_ = effectiveSprinting ? State::RUN : State::WALK;
} else if (caps.resolvedJumpEnd == 0) {
// Model doesn't have JUMP_END animation — go straight to IDLE
state_ = State::IDLE;
} else if (in.haveAnimState) {
uint32_t expected = caps.resolvedJumpEnd;
if (in.currentAnimId == expected) jumpEndSeen_ = true;
// Only transition to IDLE after landing animation completes
if (jumpEndSeen_ && oneShotComplete(in, expected)) {
state_ = State::IDLE;
}
} else {
state_ = State::IDLE;
}
break;
case State::SWIM_IDLE:
if (!in.swimming) {
state_ = effectiveMoving ? State::WALK : State::IDLE;
} else if (effectiveMoving) {
state_ = State::SWIM;
}
break;
case State::SWIM:
if (!in.swimming) {
state_ = effectiveMoving ? State::WALK : State::IDLE;
} else if (!effectiveMoving) {
state_ = State::SWIM_IDLE;
}
break;
}
}
// ── Animation resolution ─────────────────────────────────────────────────────
AnimOutput LocomotionFSM::resolve(const Input& in, const AnimCapabilitySet& caps) {
updateTransitions(in, caps);
const bool pureStrafe = !in.movingForward && !in.movingBackward; // strafe without forward/back
const bool anyStrafeLeft = in.strafeLeft && !in.strafeRight && pureStrafe;
const bool anyStrafeRight = in.strafeRight && !in.strafeLeft && pureStrafe;
uint32_t animId = anim::STAND;
bool animSelected = true;
bool loop = true;
switch (state_) {
case State::IDLE:
animId = anim::STAND;
break;
case State::WALK:
if (in.movingBackward) {
animId = caps.resolvedWalkBackwards ? caps.resolvedWalkBackwards
: caps.resolvedWalk ? caps.resolvedWalk
: anim::WALK_BACKWARDS;
} else if (anyStrafeLeft) {
animId = caps.resolvedStrafeLeft ? caps.resolvedStrafeLeft : anim::SHUFFLE_LEFT;
} else if (anyStrafeRight) {
animId = caps.resolvedStrafeRight ? caps.resolvedStrafeRight : anim::SHUFFLE_RIGHT;
} else {
animId = caps.resolvedWalk ? caps.resolvedWalk : anim::WALK;
}
break;
case State::RUN:
if (in.movingBackward) {
animId = caps.resolvedWalkBackwards ? caps.resolvedWalkBackwards
: caps.resolvedWalk ? caps.resolvedWalk
: anim::WALK_BACKWARDS;
} else if (anyStrafeLeft) {
animId = caps.resolvedRunLeft ? caps.resolvedRunLeft
: caps.resolvedRun ? caps.resolvedRun
: anim::RUN;
} else if (anyStrafeRight) {
animId = caps.resolvedRunRight ? caps.resolvedRunRight
: caps.resolvedRun ? caps.resolvedRun
: anim::RUN;
} else if (in.sprintAura) {
animId = caps.resolvedSprint ? caps.resolvedSprint : anim::RUN;
} else {
animId = caps.resolvedRun ? caps.resolvedRun : anim::RUN;
}
break;
case State::JUMP_START:
animId = caps.resolvedJumpStart ? caps.resolvedJumpStart : anim::JUMP_START;
loop = false;
break;
case State::JUMP_MID:
animId = caps.resolvedJump ? caps.resolvedJump : anim::JUMP;
loop = true; // Must loop — long falls outlast a single play cycle
break;
case State::JUMP_END:
animId = caps.resolvedJumpEnd ? caps.resolvedJumpEnd : anim::JUMP_END;
loop = false;
break;
case State::SWIM_IDLE:
animId = caps.resolvedSwimIdle ? caps.resolvedSwimIdle : anim::SWIM_IDLE;
break;
case State::SWIM:
if (in.movingBackward) {
animId = caps.resolvedSwimBackwards ? caps.resolvedSwimBackwards
: caps.resolvedSwim ? caps.resolvedSwim
: anim::SWIM;
} else if (anyStrafeLeft) {
animId = caps.resolvedSwimLeft ? caps.resolvedSwimLeft
: caps.resolvedSwim ? caps.resolvedSwim
: anim::SWIM;
} else if (anyStrafeRight) {
animId = caps.resolvedSwimRight ? caps.resolvedSwimRight
: caps.resolvedSwim ? caps.resolvedSwim
: anim::SWIM;
} else {
animId = caps.resolvedSwim ? caps.resolvedSwim : anim::SWIM;
}
break;
}
if (!animSelected) return AnimOutput::stay();
return AnimOutput::ok(animId, loop);
}
// ── Reset ────────────────────────────────────────────────────────────────────
void LocomotionFSM::reset() {
state_ = State::IDLE;
graceTimer_ = 0.0f;
wasSprinting_ = false;
jumpStartSeen_ = false;
jumpEndSeen_ = false;
}
} // namespace rendering
} // namespace wowee

View file

@ -0,0 +1,342 @@
#include "rendering/animation/mount_fsm.hpp"
#include "rendering/animation/animation_ids.hpp"
#include <algorithm>
#include <cmath>
namespace wowee {
namespace rendering {
// ── Configure / Clear ────────────────────────────────────────────────────────
void MountFSM::configure(const MountAnimSet& anims, bool taxiFlight) {
anims_ = anims;
taxiFlight_ = taxiFlight;
active_ = true;
state_ = MountState::IDLE;
action_ = MountAction::None;
actionPhase_ = 0;
fidgetTimer_ = 0.0f;
activeFidget_ = 0;
idleSoundTimer_ = 0.0f;
prevYaw_ = 0.0f;
roll_ = 0.0f;
lastMountAnim_ = 0;
// Seed per-instance RNG
std::random_device rd;
rng_.seed(rd());
nextFidgetTime_ = std::uniform_real_distribution<float>(6.0f, 12.0f)(rng_);
nextIdleSoundTime_ = std::uniform_real_distribution<float>(45.0f, 90.0f)(rng_);
}
void MountFSM::clear() {
active_ = false;
state_ = MountState::IDLE;
action_ = MountAction::None;
actionPhase_ = 0;
taxiFlight_ = false;
anims_ = {};
fidgetTimer_ = 0.0f;
activeFidget_ = 0;
idleSoundTimer_ = 0.0f;
lastMountAnim_ = 0;
}
// ── Event handling ───────────────────────────────────────────────────────────
void MountFSM::onEvent(AnimEvent event) {
if (!active_) return;
switch (event) {
case AnimEvent::JUMP:
// Jump only triggered via evaluate() input check
break;
case AnimEvent::DISMOUNT:
clear();
break;
default:
break;
}
}
// ── Helpers ──────────────────────────────────────────────────────────────────
bool MountFSM::actionAnimComplete(const Input& in) const {
return in.haveMountState && in.curMountDuration > 0.1f &&
(in.curMountTime >= in.curMountDuration - 0.05f);
}
uint32_t MountFSM::resolveGroundOrFlyAnim(const Input& in) const {
const bool pureStrafe = !in.movingBackward;
const bool anyStrafeLeft = in.strafeLeft && !in.strafeRight && pureStrafe;
const bool anyStrafeRight = in.strafeRight && !in.strafeLeft && pureStrafe;
if (in.moving) {
if (in.flying) {
if (in.ascending) {
return anims_.flyUp ? anims_.flyUp : (anims_.flyForward ? anims_.flyForward : anim::RUN);
} else if (in.descending) {
return anims_.flyDown ? anims_.flyDown : (anims_.flyForward ? anims_.flyForward : anim::RUN);
} else if (anyStrafeLeft) {
return anims_.flyLeft ? anims_.flyLeft : (anims_.flyForward ? anims_.flyForward : anim::RUN);
} else if (anyStrafeRight) {
return anims_.flyRight ? anims_.flyRight : (anims_.flyForward ? anims_.flyForward : anim::RUN);
} else if (in.movingBackward) {
return anims_.flyBackwards ? anims_.flyBackwards : (anims_.flyForward ? anims_.flyForward : anim::RUN);
} else {
return anims_.flyForward ? anims_.flyForward : (anims_.flyIdle ? anims_.flyIdle : anim::RUN);
}
} else if (in.swimming) {
// Mounted swimming — simplified, no per-direction mount swim anims needed here
// (the original code used pickMountAnim with mount-specific swim IDs)
return anims_.run ? anims_.run : anim::RUN;
} else if (anyStrafeLeft) {
return anims_.run ? anims_.run : anim::RUN;
} else if (anyStrafeRight) {
return anims_.run ? anims_.run : anim::RUN;
} else if (in.movingBackward) {
return anims_.run ? anims_.run : anim::RUN;
} else {
return anim::RUN;
}
} else {
// Idle
if (in.swimming) {
return anims_.stand ? anims_.stand : anim::STAND;
} else if (in.flying) {
if (in.ascending) {
return anims_.flyUp ? anims_.flyUp : (anims_.flyIdle ? anims_.flyIdle : anim::STAND);
} else if (in.descending) {
return anims_.flyDown ? anims_.flyDown : (anims_.flyIdle ? anims_.flyIdle : anim::STAND);
} else {
return anims_.flyIdle ? anims_.flyIdle : (anims_.flyForward ? anims_.flyForward : anim::STAND);
}
} else {
return anims_.stand ? anims_.stand : anim::STAND;
}
}
}
// ── Main evaluation ──────────────────────────────────────────────────────────
MountFSM::Output MountFSM::evaluate(const Input& in) {
Output out;
if (!active_) return out;
const float dt = in.deltaTime;
// ── Procedural lean ─────────────────────────────────────────────────
if (!taxiFlight_ && in.moving && dt > 0.0f) {
float turnRate = (in.characterYaw - prevYaw_) / dt;
while (turnRate > 180.0f) turnRate -= 360.0f;
while (turnRate < -180.0f) turnRate += 360.0f;
float targetLean = std::clamp(turnRate * 0.15f, -0.25f, 0.25f);
roll_ = roll_ + (targetLean - roll_) * (1.0f - std::exp(-6.0f * dt));
} else {
roll_ = roll_ + (0.0f - roll_) * (1.0f - std::exp(-8.0f * dt));
}
prevYaw_ = in.characterYaw;
out.mountRoll = roll_;
// ── Rider animation ─────────────────────────────────────────────────
out.riderAnimId = anim::MOUNT;
out.riderAnimLoop = true;
// (Flight rider variants handled by the caller via capability set, not here)
// ── Taxi flight branch ──────────────────────────────────────────────
if (taxiFlight_) {
// Try flight animations in preference order using discovered anims
uint32_t taxiAnim = anim::STAND;
if (anims_.flyForward) taxiAnim = anims_.flyForward;
else if (anims_.flyIdle) taxiAnim = anims_.flyIdle;
else if (anims_.run) taxiAnim = anims_.run;
out.mountAnimId = taxiAnim;
out.mountAnimLoop = true;
out.mountAnimChanged = (!in.haveMountState || in.curMountAnim != taxiAnim);
// Bob calculation for taxi
if (in.moving && in.haveMountState && in.curMountDuration > 1.0f) {
float wrappedTime = in.curMountTime;
while (wrappedTime >= in.curMountDuration) wrappedTime -= in.curMountDuration;
float norm = wrappedTime / in.curMountDuration;
out.mountBob = std::sin(norm * 2.0f * 3.14159f * 2.0f) * 0.12f;
}
lastMountAnim_ = out.mountAnimId;
return out;
}
// ── Jump/rear-up trigger ────────────────────────────────────────────
if (in.jumpKeyPressed && in.grounded && action_ == MountAction::None) {
if (in.moving && anims_.jumpLoop > 0) {
action_ = MountAction::Jump;
actionPhase_ = 1; // Start with loop directly (matching original)
out.mountAnimId = anims_.jumpLoop;
out.mountAnimLoop = true;
out.mountAnimChanged = true;
out.playJumpSound = true;
out.triggerMountJump = true;
lastMountAnim_ = out.mountAnimId;
// Bob calc
if (in.haveMountState && in.curMountDuration > 1.0f) {
float wrappedTime = in.curMountTime;
while (wrappedTime >= in.curMountDuration) wrappedTime -= in.curMountDuration;
float norm = wrappedTime / in.curMountDuration;
out.mountBob = std::sin(norm * 2.0f * 3.14159f) * 0.12f;
}
return out;
} else if (!in.moving && anims_.rearUp > 0) {
action_ = MountAction::RearUp;
actionPhase_ = 0;
out.mountAnimId = anims_.rearUp;
out.mountAnimLoop = false;
out.mountAnimChanged = true;
out.playRearUpSound = true;
lastMountAnim_ = out.mountAnimId;
return out;
}
}
// ── Handle active mount actions (jump chaining or rear-up) ──────────
if (action_ != MountAction::None) {
bool animFinished = actionAnimComplete(in);
if (action_ == MountAction::Jump) {
if (actionPhase_ == 0 && animFinished && anims_.jumpLoop > 0) {
actionPhase_ = 1;
out.mountAnimId = anims_.jumpLoop;
out.mountAnimLoop = true;
out.mountAnimChanged = true;
} else if (actionPhase_ == 0 && animFinished) {
actionPhase_ = 1;
out.mountAnimId = in.curMountAnim;
} else if (actionPhase_ == 1 && in.grounded && anims_.jumpEnd > 0) {
actionPhase_ = 2;
out.mountAnimId = anims_.jumpEnd;
out.mountAnimLoop = false;
out.mountAnimChanged = true;
out.playLandSound = true;
} else if (actionPhase_ == 1 && in.grounded) {
action_ = MountAction::None;
out.mountAnimId = in.moving ? anims_.run : anims_.stand;
out.mountAnimLoop = true;
out.mountAnimChanged = true;
} else if (actionPhase_ == 2 && animFinished) {
action_ = MountAction::None;
out.mountAnimId = in.moving ? anims_.run : anims_.stand;
out.mountAnimLoop = true;
out.mountAnimChanged = true;
} else {
out.mountAnimId = in.curMountAnim;
}
} else if (action_ == MountAction::RearUp) {
if (animFinished) {
action_ = MountAction::None;
out.mountAnimId = in.moving ? anims_.run : anims_.stand;
out.mountAnimLoop = true;
out.mountAnimChanged = true;
} else {
out.mountAnimId = in.curMountAnim;
}
}
// Bob calc
if (in.moving && in.haveMountState && in.curMountDuration > 1.0f) {
float wrappedTime = in.curMountTime;
while (wrappedTime >= in.curMountDuration) wrappedTime -= in.curMountDuration;
float norm = wrappedTime / in.curMountDuration;
out.mountBob = std::sin(norm * 2.0f * 3.14159f) * 0.12f;
}
lastMountAnim_ = out.mountAnimId;
return out;
}
// ── Normal movement animation resolution ────────────────────────────
uint32_t mountAnimId = resolveGroundOrFlyAnim(in);
// ── Cancel active fidget on movement ────────────────────────────────
if (in.moving && activeFidget_ != 0) {
activeFidget_ = 0;
out.mountAnimId = mountAnimId;
out.mountAnimLoop = true;
out.mountAnimChanged = true;
lastMountAnim_ = out.mountAnimId;
// Bob calc
if (in.haveMountState && in.curMountDuration > 1.0f) {
float wrappedTime = in.curMountTime;
while (wrappedTime >= in.curMountDuration) wrappedTime -= in.curMountDuration;
float norm = wrappedTime / in.curMountDuration;
out.mountBob = std::sin(norm * 2.0f * 3.14159f) * 0.12f;
}
return out;
}
// ── Check if active fidget completed ────────────────────────────────
if (!in.moving && activeFidget_ != 0) {
if (in.haveMountState) {
if (in.curMountAnim != activeFidget_ ||
in.curMountTime >= in.curMountDuration * 0.95f) {
activeFidget_ = 0;
}
}
}
// ── Idle fidgets ────────────────────────────────────────────────────
if (!in.moving && action_ == MountAction::None && activeFidget_ == 0 && !anims_.fidgets.empty()) {
fidgetTimer_ += dt;
if (fidgetTimer_ >= nextFidgetTime_) {
std::uniform_int_distribution<size_t> dist(0, anims_.fidgets.size() - 1);
uint32_t fidgetAnim = anims_.fidgets[dist(rng_)];
activeFidget_ = fidgetAnim;
fidgetTimer_ = 0.0f;
nextFidgetTime_ = std::uniform_real_distribution<float>(6.0f, 12.0f)(rng_);
out.mountAnimId = fidgetAnim;
out.mountAnimLoop = false;
out.mountAnimChanged = true;
out.fidgetStarted = true;
lastMountAnim_ = out.mountAnimId;
return out;
}
}
if (in.moving) fidgetTimer_ = 0.0f;
// ── Idle ambient sounds ─────────────────────────────────────────────
if (!in.moving) {
idleSoundTimer_ += dt;
if (idleSoundTimer_ >= nextIdleSoundTime_) {
out.playIdleSound = true;
idleSoundTimer_ = 0.0f;
nextIdleSoundTime_ = std::uniform_real_distribution<float>(45.0f, 90.0f)(rng_);
}
} else {
idleSoundTimer_ = 0.0f;
}
// ── Set output ──────────────────────────────────────────────────────
out.mountAnimId = activeFidget_ != 0 ? activeFidget_ : mountAnimId;
out.mountAnimLoop = (activeFidget_ == 0);
// Only trigger playAnimation if animation actually changed and no action/fidget active
if (action_ == MountAction::None && activeFidget_ == 0 &&
(!in.haveMountState || in.curMountAnim != mountAnimId)) {
out.mountAnimChanged = true;
out.mountAnimId = mountAnimId;
}
// Bob calculation
if (in.moving && in.haveMountState && in.curMountDuration > 1.0f) {
float wrappedTime = in.curMountTime;
while (wrappedTime >= in.curMountDuration) wrappedTime -= in.curMountDuration;
float norm = wrappedTime / in.curMountDuration;
float bobSpeed = taxiFlight_ ? 2.0f : 1.0f;
out.mountBob = std::sin(norm * 2.0f * 3.14159f * bobSpeed) * 0.12f;
}
lastMountAnim_ = out.mountAnimId;
return out;
}
} // namespace rendering
} // namespace wowee

View file

@ -0,0 +1,94 @@
// ============================================================================
// SfxStateDriver — extracted from AnimationController
//
// Tracks state transitions for activity SFX (jump, landing, swim) and
// mount ambient sounds. Moved from AnimationController::updateSfxState().
// ============================================================================
#include "rendering/animation/sfx_state_driver.hpp"
#include "rendering/animation/footstep_driver.hpp"
#include "rendering/renderer.hpp"
#include "audio/audio_coordinator.hpp"
#include "audio/activity_sound_manager.hpp"
#include "audio/mount_sound_manager.hpp"
#include "audio/music_manager.hpp"
#include "rendering/camera_controller.hpp"
namespace wowee {
namespace rendering {
void SfxStateDriver::update(float deltaTime, Renderer* renderer,
bool mounted, bool taxiFlight,
FootstepDriver& footstepDriver) {
auto* activitySoundManager = renderer->getAudioCoordinator()->getActivitySoundManager();
if (!activitySoundManager) return;
auto* cameraController = renderer->getCameraController();
activitySoundManager->update(deltaTime);
if (cameraController && cameraController->isThirdPerson()) {
bool grounded = cameraController->isGrounded();
bool jumping = cameraController->isJumping();
bool falling = cameraController->isFalling();
bool swimming = cameraController->isSwimming();
bool moving = cameraController->isMoving();
if (!initialized_) {
prevGrounded_ = grounded;
prevJumping_ = jumping;
prevFalling_ = falling;
prevSwimming_ = swimming;
initialized_ = true;
}
// Jump detection
if (jumping && !prevJumping_ && !swimming) {
activitySoundManager->playJump();
}
// Landing detection
if (grounded && !prevGrounded_) {
bool hardLanding = prevFalling_;
activitySoundManager->playLanding(
footstepDriver.resolveFootstepSurface(renderer), hardLanding);
}
// Water transitions
if (swimming && !prevSwimming_) {
activitySoundManager->playWaterEnter();
} else if (!swimming && prevSwimming_) {
activitySoundManager->playWaterExit();
}
activitySoundManager->setSwimmingState(swimming, moving);
if (renderer->getAudioCoordinator()->getMusicManager()) {
renderer->getAudioCoordinator()->getMusicManager()->setUnderwaterMode(swimming);
}
prevGrounded_ = grounded;
prevJumping_ = jumping;
prevFalling_ = falling;
prevSwimming_ = swimming;
} else {
activitySoundManager->setSwimmingState(false, false);
if (renderer->getAudioCoordinator()->getMusicManager()) {
renderer->getAudioCoordinator()->getMusicManager()->setUnderwaterMode(false);
}
initialized_ = false;
}
// Mount ambient sounds
if (renderer->getAudioCoordinator()->getMountSoundManager()) {
renderer->getAudioCoordinator()->getMountSoundManager()->update(deltaTime);
if (cameraController && mounted) {
bool isMoving = cameraController->isMoving();
bool flying = taxiFlight || !cameraController->isGrounded();
renderer->getAudioCoordinator()->getMountSoundManager()->setMoving(isMoving);
renderer->getAudioCoordinator()->getMountSoundManager()->setFlying(flying);
}
}
}
} // namespace rendering
} // namespace wowee