mirror of
https://github.com/Kelsidavis/WoWee.git
synced 2026-04-29 14:33:51 +00:00
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:
parent
d54e262048
commit
e58f9b4b40
59 changed files with 3903 additions and 483 deletions
|
|
@ -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;
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue