mirror of
https://github.com/Kelsidavis/WoWee.git
synced 2026-05-08 01:53:52 +00:00
feat(animation): decompose AnimationController into FSM-based architecture
Replace the 2,200-line monolithic AnimationController (goto-driven, single class, untestable) with a composed FSM architecture per refactor.md. New subsystem (src/rendering/animation/ — 16 headers, 10 sources): - CharacterAnimator: FSM composer implementing ICharacterAnimator - LocomotionFSM: idle/walk/run/sprint/jump/swim/strafe - CombatFSM: melee/ranged/spell cast/stun/hit reaction/charge - ActivityFSM: emote/loot/sit-down/sitting/sit-up - MountFSM: idle/run/flight/taxi/fidget/rear-up (per-instance RNG) - AnimCapabilitySet + AnimCapabilityProbe: probe once at model load, eliminate per-frame hasAnimation() linear search - AnimationManager: registry of CharacterAnimator by GUID - EmoteRegistry: DBC-backed emote command → animId singleton - FootstepDriver, SfxStateDriver: extracted from AnimationController animation_ids.hpp/.cpp moved to animation/ subdirectory (452 named constants); all include paths updated. AnimationController retained as thin adapter (~400 LOC): collects FrameInput, delegates to CharacterAnimator, applies AnimOutput. Priority order: Mount > Stun > HitReaction > Spell > Charge > Melee/Ranged > CombatIdle > Emote > Loot > Sit > Locomotion. STAY_IN_STATE policy when all FSMs return valid=false. Bugs fixed: - Remove static mt19937 in mount fidget (shared state across all mounted units) — replaced with per-instance seeded RNG - Remove goto from mounted animation branch (skipped init) - Remove per-frame hasAnimation() calls (now one probe at load) - Fix VK_INDEX_TYPE_UINT16 → UINT32 in shadow pass Tests (4 new suites, all ASAN+UBSan clean): - test_locomotion_fsm: 167 assertions - test_combat_fsm: 125 cases - test_activity_fsm: 112 cases - test_anim_capability: 56 cases docs/ANIMATION_SYSTEM.md added (architecture reference).
This commit is contained in:
parent
e58f9b4b40
commit
b4989dc11f
53 changed files with 5110 additions and 2099 deletions
339
src/rendering/animation/activity_fsm.cpp
Normal file
339
src/rendering/animation/activity_fsm.cpp
Normal 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
|
||||
287
src/rendering/animation/anim_capability_probe.cpp
Normal file
287
src/rendering/animation/anim_capability_probe.cpp
Normal file
|
|
@ -0,0 +1,287 @@
|
|||
// ============================================================================
|
||||
// AnimCapabilityProbe
|
||||
//
|
||||
// Scans a model's animation capabilities once and returns an
|
||||
// AnimCapabilitySet with resolved IDs (after fallback chains).
|
||||
// Extracted from the scattered hasAnimation/pickFirstAvailable calls
|
||||
// in AnimationController.
|
||||
// ============================================================================
|
||||
|
||||
#include "rendering/animation/anim_capability_probe.hpp"
|
||||
#include "rendering/animation/animation_ids.hpp"
|
||||
#include "rendering/renderer.hpp"
|
||||
#include "rendering/character_renderer.hpp"
|
||||
#include "rendering/m2_renderer.hpp"
|
||||
|
||||
namespace wowee {
|
||||
namespace rendering {
|
||||
|
||||
uint32_t AnimCapabilityProbe::pickFirst(Renderer* renderer, uint32_t instanceId,
|
||||
const uint32_t* candidates, size_t count) {
|
||||
auto* charRenderer = renderer->getCharacterRenderer();
|
||||
if (!charRenderer) return 0;
|
||||
for (size_t i = 0; i < count; ++i) {
|
||||
if (charRenderer->hasAnimation(instanceId, candidates[i])) {
|
||||
return candidates[i];
|
||||
}
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
AnimCapabilitySet AnimCapabilityProbe::probe(Renderer* renderer, uint32_t instanceId) {
|
||||
AnimCapabilitySet caps;
|
||||
if (!renderer || instanceId == 0) return caps;
|
||||
|
||||
auto* cr = renderer->getCharacterRenderer();
|
||||
if (!cr) return caps;
|
||||
|
||||
auto has = [&](uint32_t id) -> bool {
|
||||
return cr->hasAnimation(instanceId, id);
|
||||
};
|
||||
|
||||
// Helper: pick first available from static array
|
||||
auto pick = [&](const uint32_t* candidates, size_t count) -> uint32_t {
|
||||
return pickFirst(renderer, instanceId, candidates, count);
|
||||
};
|
||||
|
||||
// ── Locomotion ──────────────────────────────────────────────────────
|
||||
// STAND is animation ID 0. Every M2 model has sequence 0 as its base idle.
|
||||
// Cannot use ternary `has(0) ? 0 : 0` — both branches are 0.
|
||||
caps.resolvedStand = anim::STAND;
|
||||
caps.hasStand = has(anim::STAND);
|
||||
|
||||
{
|
||||
static const uint32_t walkCands[] = {anim::WALK, anim::RUN};
|
||||
caps.resolvedWalk = pick(walkCands, 2);
|
||||
caps.hasWalk = (caps.resolvedWalk != 0);
|
||||
}
|
||||
{
|
||||
static const uint32_t runCands[] = {anim::RUN, anim::WALK};
|
||||
caps.resolvedRun = pick(runCands, 2);
|
||||
caps.hasRun = (caps.resolvedRun != 0);
|
||||
}
|
||||
{
|
||||
static const uint32_t sprintCands[] = {anim::SPRINT, anim::RUN, anim::WALK};
|
||||
caps.resolvedSprint = pick(sprintCands, 3);
|
||||
caps.hasSprint = (caps.resolvedSprint != 0);
|
||||
}
|
||||
{
|
||||
static const uint32_t walkBackCands[] = {anim::WALK_BACKWARDS, anim::WALK};
|
||||
caps.resolvedWalkBackwards = pick(walkBackCands, 2);
|
||||
caps.hasWalkBackwards = (caps.resolvedWalkBackwards != 0);
|
||||
}
|
||||
{
|
||||
static const uint32_t strafeLeftCands[] = {anim::SHUFFLE_LEFT, anim::RUN_LEFT, anim::WALK};
|
||||
caps.resolvedStrafeLeft = pick(strafeLeftCands, 3);
|
||||
}
|
||||
{
|
||||
static const uint32_t strafeRightCands[] = {anim::SHUFFLE_RIGHT, anim::RUN_RIGHT, anim::WALK};
|
||||
caps.resolvedStrafeRight = pick(strafeRightCands, 3);
|
||||
}
|
||||
{
|
||||
static const uint32_t runLeftCands[] = {anim::RUN_LEFT, anim::RUN};
|
||||
caps.resolvedRunLeft = pick(runLeftCands, 2);
|
||||
}
|
||||
{
|
||||
static const uint32_t runRightCands[] = {anim::RUN_RIGHT, anim::RUN};
|
||||
caps.resolvedRunRight = pick(runRightCands, 2);
|
||||
}
|
||||
|
||||
// ── Jump ────────────────────────────────────────────────────────────
|
||||
caps.resolvedJumpStart = has(anim::JUMP_START) ? anim::JUMP_START : 0;
|
||||
caps.resolvedJump = has(anim::JUMP) ? anim::JUMP : 0;
|
||||
caps.resolvedJumpEnd = has(anim::JUMP_END) ? anim::JUMP_END : 0;
|
||||
caps.hasJump = (caps.resolvedJumpStart != 0);
|
||||
|
||||
// ── Swim ────────────────────────────────────────────────────────────
|
||||
caps.resolvedSwimIdle = has(anim::SWIM_IDLE) ? anim::SWIM_IDLE : 0;
|
||||
caps.resolvedSwim = has(anim::SWIM) ? anim::SWIM : 0;
|
||||
caps.hasSwim = (caps.resolvedSwimIdle != 0 || caps.resolvedSwim != 0);
|
||||
{
|
||||
static const uint32_t swimBackCands[] = {anim::SWIM_BACKWARDS, anim::SWIM};
|
||||
caps.resolvedSwimBackwards = pick(swimBackCands, 2);
|
||||
}
|
||||
{
|
||||
static const uint32_t swimLeftCands[] = {anim::SWIM_LEFT, anim::SWIM};
|
||||
caps.resolvedSwimLeft = pick(swimLeftCands, 2);
|
||||
}
|
||||
{
|
||||
static const uint32_t swimRightCands[] = {anim::SWIM_RIGHT, anim::SWIM};
|
||||
caps.resolvedSwimRight = pick(swimRightCands, 2);
|
||||
}
|
||||
|
||||
// ── Melee combat (fallback chains match resolveMeleeAnimId) ─────────
|
||||
{
|
||||
static const uint32_t melee1HCands[] = {
|
||||
anim::ATTACK_1H, anim::ATTACK_2H, anim::ATTACK_UNARMED,
|
||||
anim::ATTACK_2H_LOOSE, anim::PARRY_UNARMED, anim::PARRY_1H};
|
||||
caps.resolvedMelee1H = pick(melee1HCands, 6);
|
||||
}
|
||||
{
|
||||
static const uint32_t melee2HCands[] = {
|
||||
anim::ATTACK_2H, anim::ATTACK_1H, anim::ATTACK_UNARMED,
|
||||
anim::ATTACK_2H_LOOSE, anim::PARRY_UNARMED, anim::PARRY_1H};
|
||||
caps.resolvedMelee2H = pick(melee2HCands, 6);
|
||||
}
|
||||
{
|
||||
static const uint32_t melee2HLooseCands[] = {
|
||||
anim::ATTACK_2H_LOOSE_PIERCE, anim::ATTACK_2H_LOOSE,
|
||||
anim::ATTACK_2H, anim::ATTACK_1H, anim::ATTACK_UNARMED};
|
||||
caps.resolvedMelee2HLoose = pick(melee2HLooseCands, 5);
|
||||
}
|
||||
{
|
||||
static const uint32_t meleeUnarmedCands[] = {
|
||||
anim::ATTACK_UNARMED, anim::ATTACK_1H, anim::ATTACK_2H,
|
||||
anim::ATTACK_2H_LOOSE, anim::PARRY_UNARMED, anim::PARRY_1H};
|
||||
caps.resolvedMeleeUnarmed = pick(meleeUnarmedCands, 6);
|
||||
}
|
||||
{
|
||||
static const uint32_t meleeFistCands[] = {
|
||||
anim::ATTACK_FIST_1H, anim::ATTACK_FIST_1H_OFF,
|
||||
anim::ATTACK_1H, anim::ATTACK_UNARMED,
|
||||
anim::PARRY_FIST_1H, anim::PARRY_1H};
|
||||
caps.resolvedMeleeFist = pick(meleeFistCands, 6);
|
||||
}
|
||||
{
|
||||
static const uint32_t meleePierceCands[] = {
|
||||
anim::ATTACK_1H_PIERCE, anim::ATTACK_1H, anim::ATTACK_UNARMED};
|
||||
caps.resolvedMeleePierce = pick(meleePierceCands, 3);
|
||||
}
|
||||
{
|
||||
static const uint32_t meleeOffCands[] = {
|
||||
anim::ATTACK_OFF, anim::ATTACK_1H, anim::ATTACK_UNARMED};
|
||||
caps.resolvedMeleeOffHand = pick(meleeOffCands, 3);
|
||||
}
|
||||
{
|
||||
static const uint32_t meleeOffFistCands[] = {
|
||||
anim::ATTACK_FIST_1H_OFF, anim::ATTACK_OFF,
|
||||
anim::ATTACK_FIST_1H, anim::ATTACK_1H};
|
||||
caps.resolvedMeleeOffHandFist = pick(meleeOffFistCands, 4);
|
||||
}
|
||||
{
|
||||
static const uint32_t meleeOffPierceCands[] = {
|
||||
anim::ATTACK_OFF_PIERCE, anim::ATTACK_OFF,
|
||||
anim::ATTACK_1H_PIERCE, anim::ATTACK_1H};
|
||||
caps.resolvedMeleeOffHandPierce = pick(meleeOffPierceCands, 4);
|
||||
}
|
||||
{
|
||||
static const uint32_t meleeOffUnarmedCands[] = {
|
||||
anim::ATTACK_UNARMED_OFF, anim::ATTACK_UNARMED,
|
||||
anim::ATTACK_OFF, anim::ATTACK_1H};
|
||||
caps.resolvedMeleeOffHandUnarmed = pick(meleeOffUnarmedCands, 4);
|
||||
}
|
||||
caps.hasMelee = (caps.resolvedMelee1H != 0 || caps.resolvedMeleeUnarmed != 0);
|
||||
|
||||
// ── Ready stances ───────────────────────────────────────────────────
|
||||
{
|
||||
static const uint32_t ready1HCands[] = {
|
||||
anim::READY_1H, anim::READY_2H, anim::READY_UNARMED};
|
||||
caps.resolvedReady1H = pick(ready1HCands, 3);
|
||||
}
|
||||
{
|
||||
static const uint32_t ready2HCands[] = {
|
||||
anim::READY_2H, anim::READY_2H_LOOSE, anim::READY_1H, anim::READY_UNARMED};
|
||||
caps.resolvedReady2H = pick(ready2HCands, 4);
|
||||
}
|
||||
{
|
||||
static const uint32_t ready2HLooseCands[] = {
|
||||
anim::READY_2H_LOOSE, anim::READY_2H, anim::READY_1H, anim::READY_UNARMED};
|
||||
caps.resolvedReady2HLoose = pick(ready2HLooseCands, 4);
|
||||
}
|
||||
{
|
||||
static const uint32_t readyUnarmedCands[] = {
|
||||
anim::READY_UNARMED, anim::READY_1H, anim::READY_FIST};
|
||||
caps.resolvedReadyUnarmed = pick(readyUnarmedCands, 3);
|
||||
}
|
||||
{
|
||||
static const uint32_t readyFistCands[] = {
|
||||
anim::READY_FIST_1H, anim::READY_FIST, anim::READY_1H, anim::READY_UNARMED};
|
||||
caps.resolvedReadyFist = pick(readyFistCands, 4);
|
||||
}
|
||||
{
|
||||
static const uint32_t readyBowCands[] = {
|
||||
anim::READY_BOW, anim::READY_1H, anim::READY_UNARMED};
|
||||
caps.resolvedReadyBow = pick(readyBowCands, 3);
|
||||
}
|
||||
{
|
||||
static const uint32_t readyRifleCands[] = {
|
||||
anim::READY_RIFLE, anim::READY_BOW, anim::READY_1H, anim::READY_UNARMED};
|
||||
caps.resolvedReadyRifle = pick(readyRifleCands, 4);
|
||||
}
|
||||
{
|
||||
static const uint32_t readyCrossbowCands[] = {
|
||||
anim::READY_CROSSBOW, anim::READY_BOW, anim::READY_1H, anim::READY_UNARMED};
|
||||
caps.resolvedReadyCrossbow = pick(readyCrossbowCands, 4);
|
||||
}
|
||||
{
|
||||
static const uint32_t readyThrownCands[] = {
|
||||
anim::READY_THROWN, anim::READY_1H, anim::READY_UNARMED};
|
||||
caps.resolvedReadyThrown = pick(readyThrownCands, 3);
|
||||
}
|
||||
|
||||
// ── Ranged attacks ──────────────────────────────────────────────────
|
||||
{
|
||||
static const uint32_t fireBowCands[] = {anim::FIRE_BOW, anim::ATTACK_BOW};
|
||||
caps.resolvedFireBow = pick(fireBowCands, 2);
|
||||
}
|
||||
caps.resolvedAttackRifle = has(anim::ATTACK_RIFLE) ? anim::ATTACK_RIFLE : 0;
|
||||
{
|
||||
static const uint32_t attackCrossbowCands[] = {anim::ATTACK_CROSSBOW, anim::ATTACK_BOW};
|
||||
caps.resolvedAttackCrossbow = pick(attackCrossbowCands, 2);
|
||||
}
|
||||
caps.resolvedAttackThrown = has(anim::ATTACK_THROWN) ? anim::ATTACK_THROWN : 0;
|
||||
caps.resolvedLoadBow = has(anim::LOAD_BOW) ? anim::LOAD_BOW : 0;
|
||||
caps.resolvedLoadRifle = has(anim::LOAD_RIFLE) ? anim::LOAD_RIFLE : 0;
|
||||
|
||||
// ── Special attacks ─────────────────────────────────────────────────
|
||||
caps.resolvedSpecial1H = has(anim::SPECIAL_1H) ? anim::SPECIAL_1H : 0;
|
||||
caps.resolvedSpecial2H = has(anim::SPECIAL_2H) ? anim::SPECIAL_2H : 0;
|
||||
caps.resolvedSpecialUnarmed = has(anim::SPECIAL_UNARMED) ? anim::SPECIAL_UNARMED : 0;
|
||||
caps.resolvedShieldBash = has(anim::SHIELD_BASH) ? anim::SHIELD_BASH : 0;
|
||||
|
||||
// ── Combat idle ─────────────────────────────────────────────────────
|
||||
// Base combat idle — weapon-specific stances are resolved per-frame
|
||||
// using ready stance fields above
|
||||
caps.resolvedCombatIdle = has(anim::READY_1H) ? anim::READY_1H
|
||||
: (has(anim::READY_UNARMED) ? anim::READY_UNARMED : 0);
|
||||
|
||||
// ── Activity animations ─────────────────────────────────────────────
|
||||
caps.resolvedStandWound = has(anim::STAND_WOUND) ? anim::STAND_WOUND : 0;
|
||||
caps.resolvedDeath = has(anim::DEATH) ? anim::DEATH : 0;
|
||||
caps.hasDeath = (caps.resolvedDeath != 0);
|
||||
caps.resolvedLoot = has(anim::LOOT) ? anim::LOOT : 0;
|
||||
caps.resolvedSitDown = has(anim::SIT_GROUND_DOWN) ? anim::SIT_GROUND_DOWN : 0;
|
||||
caps.resolvedSitLoop = has(anim::SITTING) ? anim::SITTING : 0;
|
||||
caps.resolvedSitUp = has(anim::SIT_GROUND_UP) ? anim::SIT_GROUND_UP : 0;
|
||||
caps.resolvedKneel = has(anim::KNEEL_LOOP) ? anim::KNEEL_LOOP : 0;
|
||||
|
||||
// ── Stealth ─────────────────────────────────────────────────────────
|
||||
caps.resolvedStealthIdle = has(anim::STEALTH_STAND) ? anim::STEALTH_STAND : 0;
|
||||
caps.resolvedStealthWalk = has(anim::STEALTH_WALK) ? anim::STEALTH_WALK : 0;
|
||||
caps.resolvedStealthRun = has(anim::STEALTH_RUN) ? anim::STEALTH_RUN : 0;
|
||||
caps.hasStealth = (caps.resolvedStealthIdle != 0);
|
||||
|
||||
// ── Misc ────────────────────────────────────────────────────────────
|
||||
caps.resolvedMount = has(anim::MOUNT) ? anim::MOUNT : 0;
|
||||
caps.hasMount = (caps.resolvedMount != 0);
|
||||
caps.resolvedUnsheathe = has(anim::UNSHEATHE) ? anim::UNSHEATHE : 0;
|
||||
{
|
||||
static const uint32_t sheatheCands[] = {anim::SHEATHE, anim::HIP_SHEATHE};
|
||||
caps.resolvedSheathe = pick(sheatheCands, 2);
|
||||
}
|
||||
caps.resolvedStun = has(anim::STUN) ? anim::STUN : 0;
|
||||
caps.resolvedCombatWound = has(anim::COMBAT_WOUND) ? anim::COMBAT_WOUND : 0;
|
||||
|
||||
return caps;
|
||||
}
|
||||
|
||||
AnimCapabilitySet AnimCapabilityProbe::probeMountModel(Renderer* /*renderer*/, uint32_t /*mountInstanceId*/) {
|
||||
// Mount models use M2Renderer, not CharacterRenderer
|
||||
// For now, mount capabilities are handled separately via MountAnimSet discovery
|
||||
// This stub returns an empty set — mount animations are discovered in setMounted()
|
||||
AnimCapabilitySet caps;
|
||||
return caps;
|
||||
}
|
||||
|
||||
} // namespace rendering
|
||||
} // namespace wowee
|
||||
567
src/rendering/animation/animation_ids.cpp
Normal file
567
src/rendering/animation/animation_ids.cpp
Normal file
|
|
@ -0,0 +1,567 @@
|
|||
// ============================================================================
|
||||
// animation_ids.cpp — Inverse lookup & DBC validation
|
||||
// Generated from animation_ids.hpp (452 constants, IDs 0–451)
|
||||
// ============================================================================
|
||||
#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 (0–451) → 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
|
||||
36
src/rendering/animation/animation_manager.cpp
Normal file
36
src/rendering/animation/animation_manager.cpp
Normal 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
|
||||
216
src/rendering/animation/character_animator.cpp
Normal file
216
src/rendering/animation/character_animator.cpp
Normal 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
|
||||
459
src/rendering/animation/combat_fsm.cpp
Normal file
459
src/rendering/animation/combat_fsm.cpp
Normal 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
|
||||
299
src/rendering/animation/emote_registry.cpp
Normal file
299
src/rendering/animation/emote_registry.cpp
Normal 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
|
||||
207
src/rendering/animation/footstep_driver.cpp
Normal file
207
src/rendering/animation/footstep_driver.cpp
Normal 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
|
||||
283
src/rendering/animation/locomotion_fsm.cpp
Normal file
283
src/rendering/animation/locomotion_fsm.cpp
Normal 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
|
||||
342
src/rendering/animation/mount_fsm.cpp
Normal file
342
src/rendering/animation/mount_fsm.cpp
Normal 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
|
||||
94
src/rendering/animation/sfx_state_driver.cpp
Normal file
94
src/rendering/animation/sfx_state_driver.cpp
Normal 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
|
||||
Loading…
Add table
Add a link
Reference in a new issue