mirror of
https://github.com/Kelsidavis/WoWee.git
synced 2026-05-11 19:43:52 +00:00
Replace the 2,200-line monolithic AnimationController (goto-driven, single class, untestable) with a composed FSM architecture per refactor.md. New subsystem (src/rendering/animation/ — 16 headers, 10 sources): - CharacterAnimator: FSM composer implementing ICharacterAnimator - LocomotionFSM: idle/walk/run/sprint/jump/swim/strafe - CombatFSM: melee/ranged/spell cast/stun/hit reaction/charge - ActivityFSM: emote/loot/sit-down/sitting/sit-up - MountFSM: idle/run/flight/taxi/fidget/rear-up (per-instance RNG) - AnimCapabilitySet + AnimCapabilityProbe: probe once at model load, eliminate per-frame hasAnimation() linear search - AnimationManager: registry of CharacterAnimator by GUID - EmoteRegistry: DBC-backed emote command → animId singleton - FootstepDriver, SfxStateDriver: extracted from AnimationController animation_ids.hpp/.cpp moved to animation/ subdirectory (452 named constants); all include paths updated. AnimationController retained as thin adapter (~400 LOC): collects FrameInput, delegates to CharacterAnimator, applies AnimOutput. Priority order: Mount > Stun > HitReaction > Spell > Charge > Melee/Ranged > CombatIdle > Emote > Loot > Sit > Locomotion. STAY_IN_STATE policy when all FSMs return valid=false. Bugs fixed: - Remove static mt19937 in mount fidget (shared state across all mounted units) — replaced with per-instance seeded RNG - Remove goto from mounted animation branch (skipped init) - Remove per-frame hasAnimation() calls (now one probe at load) - Fix VK_INDEX_TYPE_UINT16 → UINT32 in shadow pass Tests (4 new suites, all ASAN+UBSan clean): - test_locomotion_fsm: 167 assertions - test_combat_fsm: 125 cases - test_activity_fsm: 112 cases - test_anim_capability: 56 cases docs/ANIMATION_SYSTEM.md added (architecture reference).
339 lines
12 KiB
C++
339 lines
12 KiB
C++
#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
|