feat(animation): 452 named constants, 30-phase character animation state machine

Add animation_ids.hpp/cpp with all 452 WoW animation ID constants (anim::STAND,
anim::RUN, anim::FIRE_BOW, ... anim::FLY_BACKWARDS, etc.), nameFromId() O(1)
lookup, and flyVariant() compact 218-element ground→FLY_* resolver.

Expand AnimationController into a full state machine with 20+ named states:
spell cast (directed→omni→cast fallback chain, instant one-shot release),
hit reactions (WOUND/CRIT/DODGE/BLOCK/SHIELD_BLOCK), stun, wounded idle,
stealth animation substitution, loot, fishing channel, sit/sleep/kneel
down→loop→up transitions, sheathe/unsheathe combat enter/exit, ranged weapons
(BOW/GUN/CROSSBOW/THROWN with reload states), game object OPEN/CLOSE/DESTROY,
vehicle enter/exit, mount flight directionals (FLY_LEFT/RIGHT/UP/DOWN/BACKWARDS),
emote state variants, off-hand/pierce/dual-wield alternation, NPC
birth/spawn/drown/rise, sprint aura override, totem idle, NPC greeting/farewell.

Add spell_defines.hpp with SpellEffect (~45 constants) and SpellMissInfo
(12 constants) namespaces; replace all magic numbers in spell_handler.cpp.

Add GAMEOBJECT_BYTES_1 to update field table (all 4 expansion JSONs) and wire
GameObjectStateCallback. Add DBC cross-validation on world entry.

Expand tools/_ANIM_NAMES from ~35 to 452 entries in m2_viewer.py and
asset_pipeline_gui.py. Add tests/test_animation_ids.cpp.

Bug fixes included:
- Stand state 1 was animating READY_2H(27) — fixed to SITTING(97)
- Spell casts ended freeze-frame — add one-shot release animation
- NPC 2H swing probe chain missing ATTACK_2H_LOOSE (polearm/staff)
- Chair sits (states 2/4/5/6) incorrectly played floor-sit transition
- STOP(3) used for all spell casts — replaced with model-aware chain
This commit is contained in:
Paul 2026-04-04 23:02:53 +03:00
parent d54e262048
commit e58f9b4b40
59 changed files with 3903 additions and 483 deletions

View file

@ -1,6 +1,8 @@
#include "core/application.hpp"
#include "core/coordinates.hpp"
#include "core/profiler.hpp"
#include "rendering/animation_ids.hpp"
#include "rendering/animation_controller.hpp"
#include <unordered_set>
#include <cmath>
#include <chrono>
@ -941,7 +943,7 @@ void Application::setState(AppState newState) {
});
cc->setStandUpCallback([this]() {
if (gameHandler) {
gameHandler->setStandState(0); // CMSG_STAND_STATE_CHANGE(STAND)
gameHandler->setStandState(rendering::AnimationController::STAND_STATE_STAND);
}
});
cc->setAutoFollowCancelCallback([this]() {
@ -952,9 +954,16 @@ void Application::setState(AppState newState) {
cc->setUseWoWSpeed(true);
}
if (gameHandler) {
gameHandler->setMeleeSwingCallback([this]() {
gameHandler->setMeleeSwingCallback([this](uint32_t spellId) {
if (renderer) {
renderer->triggerMeleeSwing();
// Ranged auto-attack spells: Auto Shot (75), Shoot (5019), Throw (2764)
if (spellId == 75 || spellId == 5019 || spellId == 2764) {
renderer->triggerRangedShot();
} else if (spellId != 0) {
renderer->triggerSpecialAttack(spellId);
} else {
renderer->triggerMeleeSwing();
}
}
});
gameHandler->setKnockBackCallback([this](float vcos, float vsin, float hspeed, float vspeed) {
@ -1924,17 +1933,17 @@ void Application::update(float deltaTime) {
_creatureWasWalking[guid] = isWalkingNow;
uint32_t curAnimId = 0; float curT = 0.0f, curDur = 0.0f;
bool gotState = charRenderer->getAnimationState(instanceId, curAnimId, curT, curDur);
if (!gotState || curAnimId != 1 /*Death*/) {
if (!gotState || curAnimId != rendering::anim::DEATH) {
uint32_t targetAnim;
if (isMovingNow) {
if (isFlyingNow) targetAnim = 159u; // FlyForward
else if (isSwimmingNow) targetAnim = 42u; // Swim
else if (isWalkingNow) targetAnim = 4u; // Walk
else targetAnim = 5u; // Run
if (isFlyingNow) targetAnim = rendering::anim::FLY_FORWARD;
else if (isSwimmingNow) targetAnim = rendering::anim::SWIM;
else if (isWalkingNow) targetAnim = rendering::anim::WALK;
else targetAnim = rendering::anim::RUN;
} else {
if (isFlyingNow) targetAnim = 158u; // FlyIdle (hover)
else if (isSwimmingNow) targetAnim = 41u; // SwimIdle
else targetAnim = 0u; // Stand
if (isFlyingNow) targetAnim = rendering::anim::FLY_IDLE;
else if (isSwimmingNow) targetAnim = rendering::anim::SWIM_IDLE;
else targetAnim = rendering::anim::STAND;
}
charRenderer->playAnimation(instanceId, targetAnim, /*loop=*/true);
}
@ -2038,17 +2047,17 @@ void Application::update(float deltaTime) {
_pCreatureWasWalking[guid] = isWalkingNow;
uint32_t curAnimId = 0; float curT = 0.0f, curDur = 0.0f;
bool gotState = charRenderer->getAnimationState(instanceId, curAnimId, curT, curDur);
if (!gotState || curAnimId != 1 /*Death*/) {
if (!gotState || curAnimId != rendering::anim::DEATH) {
uint32_t targetAnim;
if (isMovingNow) {
if (isFlyingNow) targetAnim = 159u; // FlyForward
else if (isSwimmingNow) targetAnim = 42u; // Swim
else if (isWalkingNow) targetAnim = 4u; // Walk
else targetAnim = 5u; // Run
if (isFlyingNow) targetAnim = rendering::anim::FLY_FORWARD;
else if (isSwimmingNow) targetAnim = rendering::anim::SWIM;
else if (isWalkingNow) targetAnim = rendering::anim::WALK;
else targetAnim = rendering::anim::RUN;
} else {
if (isFlyingNow) targetAnim = 158u; // FlyIdle (hover)
else if (isSwimmingNow) targetAnim = 41u; // SwimIdle
else targetAnim = 0u; // Stand
if (isFlyingNow) targetAnim = rendering::anim::FLY_IDLE;
else if (isSwimmingNow) targetAnim = rendering::anim::SWIM_IDLE;
else targetAnim = rendering::anim::STAND;
}
charRenderer->playAnimation(instanceId, targetAnim, /*loop=*/true);
}
@ -2748,19 +2757,70 @@ void Application::setupUICallbacks() {
});
// GameObject custom animation callback (e.g. chest opening)
gameHandler->setGameObjectCustomAnimCallback([this](uint64_t guid, uint32_t /*animId*/) {
if (!entitySpawner_) return;
gameHandler->setGameObjectCustomAnimCallback([this](uint64_t guid, uint32_t animId) {
if (!entitySpawner_ || !renderer) return;
auto& goInstances = entitySpawner_->getGameObjectInstances();
auto it = goInstances.find(guid);
if (it == goInstances.end() || !renderer) return;
if (it == goInstances.end()) return;
auto& info = it->second;
if (!info.isWmo) {
if (auto* m2r = renderer->getM2Renderer()) {
m2r->setInstanceAnimationFrozen(info.instanceId, false);
// Play the custom animation as a one-shot if model supports it
if (m2r->hasAnimation(info.instanceId, animId))
m2r->setInstanceAnimation(info.instanceId, animId, false);
else
m2r->setInstanceAnimationFrozen(info.instanceId, false);
}
}
});
// GameObject state change callback — animate doors/chests opening/closing/destroying
gameHandler->setGameObjectStateCallback([this](uint64_t guid, uint8_t goState) {
if (!entitySpawner_ || !renderer) return;
auto& goInstances = entitySpawner_->getGameObjectInstances();
auto it = goInstances.find(guid);
if (it == goInstances.end()) return;
auto& info = it->second;
if (info.isWmo) return; // WMOs don't have M2 animation sequences
auto* m2r = renderer->getM2Renderer();
if (!m2r) return;
uint32_t instId = info.instanceId;
// GO states: 0=READY(closed), 1=OPEN, 2=DESTROYED/ACTIVE
if (goState == 1) {
// Opening: play OPEN(148) one-shot, fall back to unfreezing
if (m2r->hasAnimation(instId, 148))
m2r->setInstanceAnimation(instId, 148, false);
else
m2r->setInstanceAnimationFrozen(instId, false);
} else if (goState == 2) {
// Destroyed: play DESTROY(149) one-shot
if (m2r->hasAnimation(instId, 149))
m2r->setInstanceAnimation(instId, 149, false);
} else {
// Closed: play CLOSE(146) one-shot, else freeze
if (m2r->hasAnimation(instId, 146))
m2r->setInstanceAnimation(instId, 146, false);
else
m2r->setInstanceAnimationFrozen(instId, true);
}
});
// Sprint aura callback — use SPRINT(143) animation when sprint-type buff is active
gameHandler->setSprintAuraCallback([this](bool active) {
if (!renderer) return;
auto* ac = renderer->getAnimationController();
if (ac) ac->setSprintAuraActive(active);
});
// Vehicle state callback — hide player character when inside a vehicle
gameHandler->setVehicleStateCallback([this](bool entered, uint32_t /*vehicleId*/) {
if (!renderer) return;
auto* cr = renderer->getCharacterRenderer();
uint32_t instId = renderer->getCharacterInstanceId();
if (!cr || instId == 0) return;
cr->setInstanceVisible(instId, !entered);
});
// Charge callback — warrior rushes toward target
gameHandler->setChargeCallback([this](uint64_t targetGuid, float tx, float ty, float tz) {
if (!renderer || !renderer->getCameraController() || !gameHandler) return;
@ -3059,8 +3119,8 @@ void Application::setupUICallbacks() {
auto* cr = renderer->getCharacterRenderer();
bool gotState = cr->getAnimationState(instanceId, curAnimId, curT, curDur);
// Only start Run if not already running and not in Death animation.
if (!gotState || (curAnimId != 1 /*Death*/ && curAnimId != 5u /*Run*/)) {
cr->playAnimation(instanceId, 5u, /*loop=*/true);
if (!gotState || (curAnimId != rendering::anim::DEATH && curAnimId != rendering::anim::RUN)) {
cr->playAnimation(instanceId, rendering::anim::RUN, /*loop=*/true);
}
entitySpawner_->getCreatureWasMoving()[guid] = true;
}
@ -3256,11 +3316,11 @@ void Application::setupUICallbacks() {
uint32_t instanceId = entitySpawner_->getCreatureInstanceId(guid);
if (instanceId == 0) instanceId = entitySpawner_->getPlayerInstanceId(guid);
if (instanceId != 0) {
renderer->getCharacterRenderer()->playAnimation(instanceId, 1, false); // Death
renderer->getCharacterRenderer()->playAnimation(instanceId, rendering::anim::DEATH, false);
}
});
// NPC/player respawn callback (online mode) - reset to idle animation
// NPC/player respawn callback (online mode) - play rise animation then idle
gameHandler->setNpcRespawnCallback([this](uint64_t guid) {
if (!entitySpawner_) return;
entitySpawner_->unmarkCreatureDead(guid);
@ -3268,11 +3328,18 @@ void Application::setupUICallbacks() {
uint32_t instanceId = entitySpawner_->getCreatureInstanceId(guid);
if (instanceId == 0) instanceId = entitySpawner_->getPlayerInstanceId(guid);
if (instanceId != 0) {
renderer->getCharacterRenderer()->playAnimation(instanceId, 0, true); // Idle
auto* cr = renderer->getCharacterRenderer();
// Play RISE one-shot (auto-returns to STAND when finished), fall back to STAND
if (cr->hasAnimation(instanceId, rendering::anim::RISE))
cr->playAnimation(instanceId, rendering::anim::RISE, false);
else
cr->playAnimation(instanceId, rendering::anim::STAND, true);
}
});
// NPC/player swing callback (online mode) - play attack animation
// Probes the model for the best available attack animation:
// ATTACK_1H(17) → ATTACK_2H(18) → ATTACK_2H_LOOSE(19) → ATTACK_UNARMED(16)
gameHandler->setNpcSwingCallback([this](uint64_t guid) {
if (!entitySpawner_) return;
if (!renderer || !renderer->getCharacterRenderer()) return;
@ -3280,8 +3347,12 @@ void Application::setupUICallbacks() {
if (instanceId == 0) instanceId = entitySpawner_->getPlayerInstanceId(guid);
if (instanceId != 0) {
auto* cr = renderer->getCharacterRenderer();
// Try weapon-appropriate attack anim: 17=1H, 18=2H, 16=unarmed fallback
static const uint32_t attackAnims[] = {17, 18, 16};
static const uint32_t attackAnims[] = {
rendering::anim::ATTACK_1H,
rendering::anim::ATTACK_2H,
rendering::anim::ATTACK_2H_LOOSE,
rendering::anim::ATTACK_UNARMED
};
bool played = false;
for (uint32_t anim : attackAnims) {
if (cr->hasAnimation(instanceId, anim)) {
@ -3290,10 +3361,70 @@ void Application::setupUICallbacks() {
break;
}
}
if (!played) cr->playAnimation(instanceId, 16, false);
if (!played) cr->playAnimation(instanceId, rendering::anim::ATTACK_UNARMED, false);
}
});
// Hit reaction callback — plays one-shot dodge/block/wound animation on the victim
gameHandler->setHitReactionCallback([this](uint64_t victimGuid, game::GameHandler::HitReaction reaction) {
if (!renderer) return;
auto* cr = renderer->getCharacterRenderer();
if (!cr) return;
// Determine animation based on reaction type
uint32_t animId = rendering::anim::COMBAT_WOUND;
switch (reaction) {
case game::GameHandler::HitReaction::DODGE: animId = rendering::anim::DODGE; break;
case game::GameHandler::HitReaction::PARRY: break; // Parry already handled by existing system
case game::GameHandler::HitReaction::BLOCK: animId = rendering::anim::BLOCK; break;
case game::GameHandler::HitReaction::SHIELD_BLOCK: animId = rendering::anim::SHIELD_BLOCK; break;
case game::GameHandler::HitReaction::CRIT_WOUND: animId = rendering::anim::COMBAT_CRITICAL; break;
case game::GameHandler::HitReaction::WOUND: animId = rendering::anim::COMBAT_WOUND; break;
}
// For local player: use AnimationController state
bool isLocalPlayer = (victimGuid == gameHandler->getPlayerGuid());
if (isLocalPlayer) {
auto* ac = renderer->getAnimationController();
if (ac) {
uint32_t charInstId = renderer->getCharacterInstanceId();
if (charInstId && cr->hasAnimation(charInstId, animId))
ac->triggerHitReaction(animId);
}
return;
}
// For NPCs/other players: direct playAnimation
if (!entitySpawner_) return;
uint32_t instanceId = entitySpawner_->getCreatureInstanceId(victimGuid);
if (instanceId == 0) instanceId = entitySpawner_->getPlayerInstanceId(victimGuid);
if (instanceId != 0 && cr->hasAnimation(instanceId, animId))
cr->playAnimation(instanceId, animId, false);
});
// Stun state callback — enters/exits STUNNED animation on local player
gameHandler->setStunStateCallback([this](bool stunned) {
if (!renderer) return;
auto* ac = renderer->getAnimationController();
if (ac) ac->setStunned(stunned);
});
// Stealth state callback — switches to stealth animation variants
gameHandler->setStealthStateCallback([this](bool stealthed) {
if (!renderer) return;
auto* ac = renderer->getAnimationController();
if (ac) ac->setStealthed(stealthed);
});
// Player health callback — switches to wounded idle when HP < 20%
gameHandler->setPlayerHealthCallback([this](uint32_t health, uint32_t maxHealth) {
if (!renderer) return;
auto* ac = renderer->getAnimationController();
if (!ac) return;
bool lowHp = (maxHealth > 0) && (health > 0) && (health * 5 <= maxHealth);
ac->setLowHealth(lowHp);
});
// Unit animation hint callback — plays jump (38=JumpMid) animation on other players/NPCs.
// Swim/walking state is now authoritative from the move-flags callback below.
// animId=38 (JumpMid): airborne jump animation; land detection is via per-frame sync.
@ -3305,9 +3436,9 @@ void Application::setupUICallbacks() {
uint32_t instanceId = entitySpawner_->getPlayerInstanceId(guid);
if (instanceId == 0) instanceId = entitySpawner_->getCreatureInstanceId(guid);
if (instanceId == 0) return;
// Don't override Death animation (1)
// Don't override Death animation
uint32_t curAnim = 0; float curT = 0.0f, curDur = 0.0f;
if (cr->getAnimationState(instanceId, curAnim, curT, curDur) && curAnim == 1) return;
if (cr->getAnimationState(instanceId, curAnim, curT, curDur) && curAnim == rendering::anim::DEATH) return;
cr->playAnimation(instanceId, animId, /*loop=*/true);
});
@ -3331,16 +3462,23 @@ void Application::setupUICallbacks() {
else flyState.erase(guid);
});
// Emote animation callback — play server-driven emote animations on NPCs and other players
// Emote animation callback — play server-driven emote animations on NPCs and other players.
// When emoteAnim is 0, the NPC's emote state was cleared → revert to STAND.
// Non-zero values from UNIT_NPC_EMOTESTATE updates are persistent (played looping).
gameHandler->setEmoteAnimCallback([this](uint64_t guid, uint32_t emoteAnim) {
if (!entitySpawner_) return;
if (!renderer || emoteAnim == 0) return;
if (!renderer) return;
auto* cr = renderer->getCharacterRenderer();
if (!cr) return;
// Look up creature instance first, then online players
uint32_t emoteInstanceId = entitySpawner_->getCreatureInstanceId(guid);
if (emoteInstanceId != 0) {
cr->playAnimation(emoteInstanceId, emoteAnim, false);
if (emoteAnim == 0) {
// Emote state cleared → return to idle
cr->playAnimation(emoteInstanceId, rendering::anim::STAND, true);
} else {
cr->playAnimation(emoteInstanceId, emoteAnim, false);
}
return;
}
emoteInstanceId = entitySpawner_->getPlayerInstanceId(guid);
@ -3350,34 +3488,134 @@ void Application::setupUICallbacks() {
});
// Spell cast animation callback — play cast animation on caster (player or NPC/other player)
gameHandler->setSpellCastAnimCallback([this](uint64_t guid, bool start, bool /*isChannel*/) {
// Probes the model for the best available spell animation with fallback chain:
// Regular cast: SPELL_CAST_DIRECTED(53) → SPELL_CAST_OMNI(54) → SPELL_CAST(32) → SPELL(2)
// Channel: CHANNEL_CAST_DIRECTED(124) → CHANNEL_CAST_OMNI(125) → SPELL_CAST_DIRECTED(53) → SPELL(2)
// For the local player, uses AnimationController state machine to prevent
// COMBAT_IDLE from overriding the spell animation. For NPCs/other players,
// calls playAnimation directly (they don't share the player state machine).
gameHandler->setSpellCastAnimCallback([this](uint64_t guid, bool start, bool isChannel) {
if (!entitySpawner_) return;
if (!renderer) return;
auto* cr = renderer->getCharacterRenderer();
if (!cr) return;
// Animation 3 = SpellCast (one-shot; return-to-idle handled by character_renderer)
const uint32_t castAnim = 3;
// Check player character
// Determine if this is the local player
bool isLocalPlayer = false;
uint32_t instanceId = 0;
{
uint32_t charInstId = renderer->getCharacterInstanceId();
if (charInstId != 0 && guid == gameHandler->getPlayerGuid()) {
if (start) cr->playAnimation(charInstId, castAnim, false);
// On finish: playAnimation(castAnim, loop=false) will auto-return to Stand
return;
instanceId = charInstId;
isLocalPlayer = true;
}
}
// Check creatures and other online players
{
uint32_t cInst = entitySpawner_->getCreatureInstanceId(guid);
if (cInst != 0) {
if (start) cr->playAnimation(cInst, castAnim, false);
return;
if (instanceId == 0) instanceId = entitySpawner_->getCreatureInstanceId(guid);
if (instanceId == 0) instanceId = entitySpawner_->getPlayerInstanceId(guid);
if (instanceId == 0) return;
if (start) {
// Detect fishing spells (channeled) — use FISHING_LOOP instead of generic cast
auto isFishingSpell = [](uint32_t spellId) {
return spellId == 7620 || spellId == 7731 || spellId == 7732 ||
spellId == 18248 || spellId == 33095 || spellId == 51294;
};
uint32_t currentSpell = isLocalPlayer ? gameHandler->getCurrentCastSpellId() : 0;
bool isFishing = isChannel && isFishingSpell(currentSpell);
if (isFishing && cr->hasAnimation(instanceId, rendering::anim::FISHING_LOOP)) {
// Fishing: use FISHING_LOOP (looping idle) for the channel duration
if (isLocalPlayer) {
auto* ac = renderer->getAnimationController();
if (ac) ac->startSpellCast(0, rendering::anim::FISHING_LOOP, true, 0);
} else {
cr->playAnimation(instanceId, rendering::anim::FISHING_LOOP, true);
}
} else {
// Spell animation sequence: PRECAST (one-shot) → CAST (loop) → FINALIZE (one-shot) → idle
// Probe model for best available animations with fallback chains:
// Regular cast: SPELL_CAST_DIRECTED → SPELL_CAST_OMNI → SPELL_CAST → SPELL
// Channel: CHANNEL_CAST_DIRECTED → CHANNEL_CAST_OMNI → SPELL_CAST_DIRECTED → SPELL
bool hasTarget = gameHandler->hasTarget();
// Phase 1: Precast wind-up (one-shot, non-channels only)
uint32_t precastAnim = 0;
if (!isChannel && cr->hasAnimation(instanceId, rendering::anim::SPELL_PRECAST)) {
precastAnim = rendering::anim::SPELL_PRECAST;
}
}
{
uint32_t pInst = entitySpawner_->getPlayerInstanceId(guid);
if (pInst != 0) {
if (start) cr->playAnimation(pInst, castAnim, false);
// Phase 2: Cast hold (looping until stopSpellCast)
static const uint32_t castDirected[] = {
rendering::anim::SPELL_CAST_DIRECTED,
rendering::anim::SPELL_CAST_OMNI,
rendering::anim::SPELL_CAST,
rendering::anim::SPELL
};
static const uint32_t castOmni[] = {
rendering::anim::SPELL_CAST_OMNI,
rendering::anim::SPELL_CAST_DIRECTED,
rendering::anim::SPELL_CAST,
rendering::anim::SPELL
};
static const uint32_t channelDirected[] = {
rendering::anim::CHANNEL_CAST_DIRECTED,
rendering::anim::CHANNEL_CAST_OMNI,
rendering::anim::SPELL_CAST_DIRECTED,
rendering::anim::SPELL
};
static const uint32_t channelOmni[] = {
rendering::anim::CHANNEL_CAST_OMNI,
rendering::anim::CHANNEL_CAST_DIRECTED,
rendering::anim::SPELL_CAST_DIRECTED,
rendering::anim::SPELL
};
const uint32_t* chain;
if (isChannel) {
chain = hasTarget ? channelDirected : channelOmni;
} else {
chain = hasTarget ? castDirected : castOmni;
}
uint32_t castAnim = rendering::anim::SPELL;
for (size_t i = 0; i < 4; ++i) {
if (cr->hasAnimation(instanceId, chain[i])) {
castAnim = chain[i];
break;
}
}
// Phase 3: Finalization release (one-shot after cast ends)
// Pick a different animation from the cast loop for visual variety
static const uint32_t finalizeChain[] = {
rendering::anim::SPELL_CAST_OMNI,
rendering::anim::SPELL_CAST,
rendering::anim::SPELL
};
uint32_t finalizeAnim = 0;
if (isLocalPlayer && !isChannel) {
for (uint32_t fa : finalizeChain) {
if (fa != castAnim && cr->hasAnimation(instanceId, fa)) {
finalizeAnim = fa;
break;
}
}
if (finalizeAnim == 0 && cr->hasAnimation(instanceId, rendering::anim::SPELL))
finalizeAnim = rendering::anim::SPELL;
}
if (isLocalPlayer) {
auto* ac = renderer->getAnimationController();
if (ac) ac->startSpellCast(precastAnim, castAnim, true, finalizeAnim);
} else {
cr->playAnimation(instanceId, castAnim, true);
}
} // end !isFishing
} else {
// Cast/channel ended — plays finalization anim completely then returns to idle
if (isLocalPlayer) {
auto* ac = renderer->getAnimationController();
if (ac) ac->stopSpellCast();
} else if (isChannel) {
cr->playAnimation(instanceId, rendering::anim::STAND, true);
}
}
});
@ -3392,41 +3630,54 @@ void Application::setupUICallbacks() {
cr->setInstanceOpacity(charInstId, isGhost ? 0.5f : 1.0f);
});
// Stand state animation callback — map server stand state to M2 animation on player
// and sync camera sit flag so movement is blocked while sitting
// Stand state animation callback — route through AnimationController state machine
// for proper sit/sleep/kneel transition animations (down → loop → up)
gameHandler->setStandStateCallback([this](uint8_t standState) {
if (!renderer) return;
using AC = rendering::AnimationController;
// Sync camera controller sitting flag: block movement while sitting/kneeling
if (auto* cc = renderer->getCameraController()) {
cc->setSitting(standState >= 1 && standState <= 8 && standState != 7);
cc->setSitting(standState >= AC::STAND_STATE_SIT &&
standState <= AC::STAND_STATE_KNEEL &&
standState != AC::STAND_STATE_DEAD);
}
auto* cr = renderer->getCharacterRenderer();
if (!cr) return;
uint32_t charInstId = renderer->getCharacterInstanceId();
if (charInstId == 0) return;
// WoW stand state → M2 animation ID mapping
// 0=Stand→0, 1-6=Sit variants→27 (SitGround), 7=Dead→1, 8=Kneel→72
// Do not force Stand(0) here: locomotion state machine already owns standing/running.
// Forcing Stand on packet timing causes visible run-cycle hitching while steering.
uint32_t animId = 0;
if (standState == 0) {
auto* ac = renderer->getAnimationController();
if (!ac) return;
// Death is special — play directly, not through sit state machine
if (standState == AC::STAND_STATE_DEAD) {
auto* cr = renderer->getCharacterRenderer();
if (!cr) return;
uint32_t charInstId = renderer->getCharacterInstanceId();
if (charInstId == 0) return;
cr->playAnimation(charInstId, rendering::anim::DEATH, false);
return;
} else if (standState >= 1 && standState <= 6) {
animId = 27; // SitGround (covers sit-chair too; correct visual differs by chair height)
} else if (standState == 7) {
animId = 1; // Death
} else if (standState == 8) {
animId = 72; // Kneel
}
// Loop sit/kneel (not death) so the held-pose frame stays visible
const bool loop = (animId != 1);
cr->playAnimation(charInstId, animId, loop);
ac->setStandState(standState);
});
// Loot window callback — play kneel/loot animation while looting
gameHandler->setLootWindowCallback([this](bool open) {
if (!renderer) return;
auto* ac = renderer->getAnimationController();
if (!ac) return;
if (open) ac->startLooting();
else ac->stopLooting();
});
// NPC greeting callback - play voice line
gameHandler->setNpcGreetingCallback([this](uint64_t guid, const glm::vec3& position) {
// Play NPC_WELCOME animation on the NPC
if (entitySpawner_ && renderer) {
auto* cr = renderer->getCharacterRenderer();
if (cr) {
uint32_t instanceId = entitySpawner_->getCreatureInstanceId(guid);
if (instanceId != 0) cr->playAnimation(instanceId, rendering::anim::NPC_WELCOME, false);
}
}
if (audioCoordinator_ && audioCoordinator_->getNpcVoiceManager()) {
// Convert canonical to render coords for 3D audio
glm::vec3 renderPos = core::coords::canonicalToRender(position);
@ -3722,8 +3973,8 @@ void Application::spawnPlayerCharacter() {
: std::unordered_set<uint16_t>{};
charRenderer->setActiveGeosets(instanceId, activeGeosets);
// Play idle animation (Stand = animation ID 0)
charRenderer->playAnimation(instanceId, 0, true);
// Play idle animation
charRenderer->playAnimation(instanceId, rendering::anim::STAND, true);
LOG_INFO("Spawned player character at (",
static_cast<int>(spawnPos.x), ", ",
static_cast<int>(spawnPos.y), ", ",

View file

@ -9,6 +9,7 @@
#include "audio/npc_voice_manager.hpp"
#include "pipeline/m2_loader.hpp"
#include "pipeline/wmo_loader.hpp"
#include "rendering/animation_ids.hpp"
#include "pipeline/dbc_loader.hpp"
#include "pipeline/asset_manager.hpp"
#include "pipeline/dbc_layout.hpp"
@ -2214,9 +2215,26 @@ void EntitySpawner::spawnOnlineCreature(uint64_t guid, uint32_t displayId, float
// Spawn in the correct pose. If the server marked this creature dead before
// the queued spawn was processed, start directly in death animation.
if (deadCreatureGuids_.count(guid)) {
charRenderer->playAnimation(instanceId, 1, false); // Death
charRenderer->playAnimation(instanceId, rendering::anim::DEATH, false);
} else {
charRenderer->playAnimation(instanceId, 0, true); // Idle
// Check if this NPC has a persistent emote state (e.g. working, eating, dancing)
uint32_t npcEmote = 0;
if (gameHandler_) {
auto entity = gameHandler_->getEntityManager().getEntity(guid);
if (entity && entity->getType() == game::ObjectType::UNIT) {
npcEmote = std::static_pointer_cast<game::Unit>(entity)->getNpcEmoteState();
}
}
if (npcEmote != 0 && charRenderer->hasAnimation(instanceId, npcEmote)) {
charRenderer->playAnimation(instanceId, npcEmote, true);
} else if (charRenderer->hasAnimation(instanceId, rendering::anim::BIRTH)) {
// Play birth animation (one-shot) — will return to STAND after
charRenderer->playAnimation(instanceId, rendering::anim::BIRTH, false);
} else if (charRenderer->hasAnimation(instanceId, rendering::anim::SPAWN)) {
charRenderer->playAnimation(instanceId, rendering::anim::SPAWN, false);
} else {
charRenderer->playAnimation(instanceId, rendering::anim::STAND, true);
}
}
charRenderer->startFadeIn(instanceId, 0.5f);
@ -2316,7 +2334,7 @@ void EntitySpawner::spawnOnlinePlayer(uint64_t guid,
for (uint32_t si = 0; si < model.sequences.size(); si++) {
if (!(model.sequences[si].flags & 0x20)) {
uint32_t animId = model.sequences[si].id;
if (animId != 0 && animId != 4 && animId != 5) continue;
if (animId != rendering::anim::STAND && animId != rendering::anim::WALK && animId != rendering::anim::RUN) continue;
char animFileName[256];
snprintf(animFileName, sizeof(animFileName),
"%s%s%04u-%02u.anim",
@ -2488,7 +2506,7 @@ void EntitySpawner::spawnOnlinePlayer(uint64_t guid,
activeGeosets.insert(kGeosetBareFeet);
charRenderer->setActiveGeosets(instanceId, activeGeosets);
charRenderer->playAnimation(instanceId, 0, true);
charRenderer->playAnimation(instanceId, rendering::anim::STAND, true);
playerInstances_[guid] = instanceId;
OnlinePlayerAppearanceState st;
@ -3373,7 +3391,21 @@ void EntitySpawner::spawnOnlineGameObject(uint64_t guid, uint32_t entry, uint32_
lowerPath.find("portalfx") != std::string::npos ||
lowerPath.find("spellportal") != std::string::npos);
if (!isAnimatedEffect && !isTransportGO) {
m2Renderer->setInstanceAnimationFrozen(instanceId, true);
// Check for totem idle animations — totems should animate, not freeze
bool isTotem = false;
if (m2Renderer->hasAnimation(instanceId, 245)) { // TOTEM_SMALL
m2Renderer->setInstanceAnimation(instanceId, 245, true);
isTotem = true;
} else if (m2Renderer->hasAnimation(instanceId, 246)) { // TOTEM_MEDIUM
m2Renderer->setInstanceAnimation(instanceId, 246, true);
isTotem = true;
} else if (m2Renderer->hasAnimation(instanceId, 247)) { // TOTEM_LARGE
m2Renderer->setInstanceAnimation(instanceId, 247, true);
isTotem = true;
}
if (!isTotem) {
m2Renderer->setInstanceAnimationFrozen(instanceId, true);
}
}
gameObjectInstances_[guid] = {modelId, instanceId, false};
@ -4601,8 +4633,8 @@ void EntitySpawner::processPendingMount() {
for (uint32_t si = 0; si < model.sequences.size(); si++) {
if (!(model.sequences[si].flags & 0x20)) {
uint32_t animId = model.sequences[si].id;
// Only load stand(0), walk(4), run(5) anims to avoid hang
if (animId != 0 && animId != 4 && animId != 5) continue;
// Only load stand, walk, run anims to avoid hang
if (animId != rendering::anim::STAND && animId != rendering::anim::WALK && animId != rendering::anim::RUN) continue;
char animFileName[256];
snprintf(animFileName, sizeof(animFileName), "%s%04u-%02u.anim",
basePath.c_str(), animId, model.sequences[si].variationIndex);
@ -4854,10 +4886,11 @@ void EntitySpawner::processPendingMount() {
// For taxi mounts, start with flying animation; for ground mounts, start with stand
bool isTaxi = gameHandler_ && gameHandler_->isOnTaxiFlight();
uint32_t startAnim = 0; // ANIM_STAND
uint32_t startAnim = rendering::anim::STAND;
if (isTaxi) {
// Try WotLK fly anims first, then Vanilla-friendly fallbacks
uint32_t taxiCandidates[] = {159, 158, 234, 229, 233, 141, 369, 6, 5}; // FlyForward, FlyIdle, FlyRun(234), FlyStand(229), FlyWalk(233), FlyMounted, FlyRun, Fly, Run
using namespace rendering::anim;
uint32_t taxiCandidates[] = {FLY_FORWARD, FLY_IDLE, FLY_RUN_2, FLY_SPELL, FLY_RISE, SPELL_KNEEL_LOOP, FLY_CUSTOM_SPELL_10, DEAD, RUN};
for (uint32_t anim : taxiCandidates) {
if (charRenderer->hasAnimation(instanceId, anim)) {
startAnim = anim;

View file

@ -3,6 +3,7 @@
#include "core/world_loader.hpp"
#include "core/application.hpp"
#include "rendering/animation_ids.hpp"
#include "core/entity_spawner.hpp"
#include "core/appearance_composer.hpp"
#include "core/window.hpp"
@ -876,7 +877,7 @@ void WorldLoader::loadOnlineWorldTerrain(uint32_t mapId, float x, float y, float
uint32_t instanceId = spawner->getCreatureInstanceId(guid);
if (instanceId == 0) instanceId = spawner->getPlayerInstanceId(guid);
if (instanceId != 0 && cr) {
cr->playAnimation(instanceId, 1, false); // animation ID 1 = Death
cr->playAnimation(instanceId, rendering::anim::DEATH, false);
}
});
@ -885,15 +886,30 @@ void WorldLoader::loadOnlineWorldTerrain(uint32_t mapId, float x, float y, float
uint32_t instanceId = spawner->getCreatureInstanceId(guid);
if (instanceId == 0) instanceId = spawner->getPlayerInstanceId(guid);
if (instanceId != 0 && cr) {
cr->playAnimation(instanceId, 0, true); // animation ID 0 = Idle
cr->playAnimation(instanceId, rendering::anim::STAND, true);
}
});
// Probe the creature model for the best available attack animation
gameHandler_->setNpcSwingCallback([cr, spawner](uint64_t guid) {
uint32_t instanceId = spawner->getCreatureInstanceId(guid);
if (instanceId == 0) instanceId = spawner->getPlayerInstanceId(guid);
if (instanceId != 0 && cr) {
cr->playAnimation(instanceId, 16, false); // animation ID 16 = Attack1
static const uint32_t attackAnims[] = {
rendering::anim::ATTACK_1H,
rendering::anim::ATTACK_2H,
rendering::anim::ATTACK_2H_LOOSE,
rendering::anim::ATTACK_UNARMED
};
bool played = false;
for (uint32_t anim : attackAnims) {
if (cr->hasAnimation(instanceId, anim)) {
cr->playAnimation(instanceId, anim, false);
played = true;
break;
}
}
if (!played) cr->playAnimation(instanceId, rendering::anim::ATTACK_UNARMED, false);
}
});
}