mirror of
https://github.com/Kelsidavis/WoWee.git
synced 2026-04-16 01:03: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
|
|
@ -34,19 +34,19 @@ static float mergeCooldownSeconds(float current, float incoming) {
|
|||
|
||||
static CombatTextEntry::Type combatTextTypeFromSpellMissInfo(uint8_t missInfo) {
|
||||
switch (missInfo) {
|
||||
case 0: return CombatTextEntry::MISS;
|
||||
case 1: return CombatTextEntry::DODGE;
|
||||
case 2: return CombatTextEntry::PARRY;
|
||||
case 3: return CombatTextEntry::BLOCK;
|
||||
case 4: return CombatTextEntry::EVADE;
|
||||
case 5: return CombatTextEntry::IMMUNE;
|
||||
case 6: return CombatTextEntry::DEFLECT;
|
||||
case 7: return CombatTextEntry::ABSORB;
|
||||
case 8: return CombatTextEntry::RESIST;
|
||||
case 9:
|
||||
case 10:
|
||||
case SpellMissInfo::MISS: return CombatTextEntry::MISS;
|
||||
case SpellMissInfo::DODGE: return CombatTextEntry::DODGE;
|
||||
case SpellMissInfo::PARRY: return CombatTextEntry::PARRY;
|
||||
case SpellMissInfo::BLOCK: return CombatTextEntry::BLOCK;
|
||||
case SpellMissInfo::EVADE: return CombatTextEntry::EVADE;
|
||||
case SpellMissInfo::IMMUNE: return CombatTextEntry::IMMUNE;
|
||||
case SpellMissInfo::DEFLECT: return CombatTextEntry::DEFLECT;
|
||||
case SpellMissInfo::ABSORB: return CombatTextEntry::ABSORB;
|
||||
case SpellMissInfo::RESIST: return CombatTextEntry::RESIST;
|
||||
case SpellMissInfo::IMMUNE2:
|
||||
case SpellMissInfo::IMMUNE3:
|
||||
return CombatTextEntry::IMMUNE;
|
||||
case 11: return CombatTextEntry::REFLECT;
|
||||
case SpellMissInfo::REFLECT: return CombatTextEntry::REFLECT;
|
||||
default: return CombatTextEntry::MISS;
|
||||
}
|
||||
}
|
||||
|
|
@ -939,7 +939,7 @@ void SpellHandler::handleSpellGo(network::Packet& packet) {
|
|||
}
|
||||
}
|
||||
if (isMeleeAbility) {
|
||||
if (owner_.meleeSwingCallback_) owner_.meleeSwingCallback_();
|
||||
if (owner_.meleeSwingCallback_) owner_.meleeSwingCallback_(sid);
|
||||
if (auto* ac = owner_.services().audioCoordinator) {
|
||||
if (auto* csm = ac->getCombatSoundManager()) {
|
||||
csm->playWeaponSwing(audio::CombatSoundManager::WeaponSize::MEDIUM, false);
|
||||
|
|
@ -951,6 +951,14 @@ void SpellHandler::handleSpellGo(network::Packet& packet) {
|
|||
|
||||
const bool wasInTimedCast = casting_ && (data.spellId == currentCastSpellId_);
|
||||
|
||||
// Instant spell cast animation — if this wasn't a timed cast and isn't a
|
||||
// melee ability, play a brief spell cast animation (one-shot)
|
||||
if (!wasInTimedCast && !isMeleeAbility && !owner_.isProfessionSpell(data.spellId)) {
|
||||
if (owner_.spellCastAnimCallback_) {
|
||||
owner_.spellCastAnimCallback_(owner_.playerGuid, true, false);
|
||||
}
|
||||
}
|
||||
|
||||
LOG_WARNING("[GO-DIAG] SPELL_GO: spellId=", data.spellId,
|
||||
" casting=", casting_, " currentCast=", currentCastSpellId_,
|
||||
" wasInTimedCast=", wasInTimedCast,
|
||||
|
|
@ -991,6 +999,13 @@ void SpellHandler::handleSpellGo(network::Packet& packet) {
|
|||
castSpell(nextSpell, nextTarget);
|
||||
}
|
||||
} else {
|
||||
// For non-player casters: if no tracked cast state exists, this was an
|
||||
// instant cast — play a brief one-shot spell animation before stopping
|
||||
auto castIt = unitCastStates_.find(data.casterUnit);
|
||||
bool wasTrackedCast = (castIt != unitCastStates_.end());
|
||||
if (!wasTrackedCast && owner_.spellCastAnimCallback_) {
|
||||
owner_.spellCastAnimCallback_(data.casterUnit, true, false);
|
||||
}
|
||||
if (owner_.spellCastAnimCallback_) {
|
||||
owner_.spellCastAnimCallback_(data.casterUnit, false, false);
|
||||
}
|
||||
|
|
@ -1181,6 +1196,26 @@ void SpellHandler::handleAuraUpdate(network::Packet& packet, bool isAll) {
|
|||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Sprint aura detection — check if any sprint/dash speed buff is active
|
||||
if (data.guid == owner_.playerGuid && owner_.sprintAuraCallback_) {
|
||||
static const uint32_t sprintSpells[] = {
|
||||
2983, 8696, 11305, // Rogue Sprint (ranks 1-3)
|
||||
1850, 9821, 33357, // Druid Dash (ranks 1-3)
|
||||
36554, // Shadowstep (speed component)
|
||||
68992, 68991, // Darkflight (worgen racial)
|
||||
58984, // Aspect of the Pack speed
|
||||
};
|
||||
bool hasSprint = false;
|
||||
for (const auto& a : playerAuras_) {
|
||||
if (a.isEmpty()) continue;
|
||||
for (uint32_t sid : sprintSpells) {
|
||||
if (a.spellId == sid) { hasSprint = true; break; }
|
||||
}
|
||||
if (hasSprint) break;
|
||||
}
|
||||
owner_.sprintAuraCallback_(hasSprint);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -2222,7 +2257,7 @@ void SpellHandler::handleSpellLogMiss(network::Packet& packet) {
|
|||
// TBC: spellId(4) + uint64 caster + uint8 unk + uint32 count
|
||||
// + count × (uint64 victim + uint8 missInfo)
|
||||
// All expansions append uint32 reflectSpellId + uint8 reflectResult when
|
||||
// missInfo==11 (REFLECT).
|
||||
// missInfo==REFLECT (11).
|
||||
const bool spellMissUsesFullGuid = isActiveExpansion("tbc");
|
||||
auto readSpellMissGuid = [&]() -> uint64_t {
|
||||
if (spellMissUsesFullGuid)
|
||||
|
|
@ -2248,7 +2283,7 @@ void SpellHandler::handleSpellLogMiss(network::Packet& packet) {
|
|||
struct SpellMissLogEntry {
|
||||
uint64_t victimGuid = 0;
|
||||
uint8_t missInfo = 0;
|
||||
uint32_t reflectSpellId = 0; // Only valid when missInfo==11 (REFLECT)
|
||||
uint32_t reflectSpellId = 0; // Only valid when missInfo==REFLECT
|
||||
};
|
||||
std::vector<SpellMissLogEntry> parsedMisses;
|
||||
parsedMisses.reserve(storedLimit);
|
||||
|
|
@ -2266,9 +2301,9 @@ void SpellHandler::handleSpellLogMiss(network::Packet& packet) {
|
|||
return;
|
||||
}
|
||||
const uint8_t missInfo = packet.readUInt8();
|
||||
// REFLECT (11): extra uint32 reflectSpellId + uint8 reflectResult
|
||||
// REFLECT: extra uint32 reflectSpellId + uint8 reflectResult
|
||||
uint32_t reflectSpellId = 0;
|
||||
if (missInfo == 11) {
|
||||
if (missInfo == SpellMissInfo::REFLECT) {
|
||||
if (packet.hasRemaining(5)) {
|
||||
reflectSpellId = packet.readUInt32();
|
||||
/*uint8_t reflectResult =*/ packet.readUInt8();
|
||||
|
|
@ -2912,7 +2947,7 @@ void SpellHandler::handleSpellLogExecute(network::Packet& packet) {
|
|||
uint8_t effectType = packet.readUInt8();
|
||||
uint32_t effectLogCount = packet.readUInt32();
|
||||
effectLogCount = std::min(effectLogCount, 64u); // sanity
|
||||
if (effectType == 10) {
|
||||
if (effectType == SpellEffect::POWER_DRAIN) {
|
||||
// SPELL_EFFECT_POWER_DRAIN: packed_guid target + uint32 amount + uint32 powerType + float multiplier
|
||||
for (uint32_t li = 0; li < effectLogCount; ++li) {
|
||||
if (!packet.hasRemaining(exeUsesFullGuid ? 8u : 1u)
|
||||
|
|
@ -2950,7 +2985,7 @@ void SpellHandler::handleSpellLogExecute(network::Packet& packet) {
|
|||
" power=", drainPower, " amount=", drainAmount,
|
||||
" multiplier=", drainMult);
|
||||
}
|
||||
} else if (effectType == 11) {
|
||||
} else if (effectType == SpellEffect::HEALTH_LEECH) {
|
||||
// SPELL_EFFECT_HEALTH_LEECH: packed_guid target + uint32 amount + float multiplier
|
||||
for (uint32_t li = 0; li < effectLogCount; ++li) {
|
||||
if (!packet.hasRemaining(exeUsesFullGuid ? 8u : 1u)
|
||||
|
|
@ -2983,7 +3018,7 @@ void SpellHandler::handleSpellLogExecute(network::Packet& packet) {
|
|||
LOG_DEBUG("SMSG_SPELLLOGEXECUTE HEALTH_LEECH: spell=", exeSpellId,
|
||||
" amount=", leechAmount, " multiplier=", leechMult);
|
||||
}
|
||||
} else if (effectType == 24 || effectType == 114) {
|
||||
} else if (effectType == SpellEffect::CREATE_ITEM || effectType == SpellEffect::CREATE_ITEM2) {
|
||||
// SPELL_EFFECT_CREATE_ITEM / CREATE_ITEM2: uint32 itemEntry per log entry
|
||||
for (uint32_t li = 0; li < effectLogCount; ++li) {
|
||||
if (!packet.hasRemaining(4)) break;
|
||||
|
|
@ -3012,7 +3047,7 @@ void SpellHandler::handleSpellLogExecute(network::Packet& packet) {
|
|||
}
|
||||
}
|
||||
}
|
||||
} else if (effectType == 26) {
|
||||
} else if (effectType == SpellEffect::INTERRUPT_CAST) {
|
||||
// SPELL_EFFECT_INTERRUPT_CAST: packed_guid target + uint32 interrupted_spell_id
|
||||
for (uint32_t li = 0; li < effectLogCount; ++li) {
|
||||
if (!packet.hasRemaining(exeUsesFullGuid ? 8u : 1u)
|
||||
|
|
@ -3033,7 +3068,7 @@ void SpellHandler::handleSpellLogExecute(network::Packet& packet) {
|
|||
LOG_DEBUG("SMSG_SPELLLOGEXECUTE INTERRUPT_CAST: spell=", exeSpellId,
|
||||
" interrupted=", icSpellId, " target=0x", std::hex, icTarget, std::dec);
|
||||
}
|
||||
} else if (effectType == 49) {
|
||||
} else if (effectType == SpellEffect::FEED_PET) {
|
||||
// SPELL_EFFECT_FEED_PET: uint32 itemEntry per log entry
|
||||
for (uint32_t li = 0; li < effectLogCount; ++li) {
|
||||
if (!packet.hasRemaining(4)) break;
|
||||
|
|
@ -3182,6 +3217,12 @@ void SpellHandler::handleChannelStart(network::Packet& packet) {
|
|||
}
|
||||
LOG_DEBUG("MSG_CHANNEL_START: caster=0x", std::hex, chanCaster, std::dec,
|
||||
" spell=", chanSpellId, " total=", chanTotalMs, "ms");
|
||||
|
||||
// Play channeling animation (looping)
|
||||
if (owner_.spellCastAnimCallback_) {
|
||||
owner_.spellCastAnimCallback_(chanCaster, true, true);
|
||||
}
|
||||
|
||||
// Fire UNIT_SPELLCAST_CHANNEL_START for Lua addons
|
||||
if (owner_.addonEventCallback_) {
|
||||
auto unitId = owner_.guidToUnitId(chanCaster);
|
||||
|
|
@ -3217,6 +3258,10 @@ void SpellHandler::handleChannelUpdate(network::Packet& packet) {
|
|||
" remaining=", chanRemainMs, "ms");
|
||||
// Fire UNIT_SPELLCAST_CHANNEL_STOP when channel ends
|
||||
if (chanRemainMs == 0) {
|
||||
// Stop channeling animation — return to idle
|
||||
if (owner_.spellCastAnimCallback_) {
|
||||
owner_.spellCastAnimCallback_(chanCaster2, false, true);
|
||||
}
|
||||
auto unitId = owner_.guidToUnitId(chanCaster2);
|
||||
if (!unitId.empty())
|
||||
owner_.fireAddonEvent("UNIT_SPELLCAST_CHANNEL_STOP", {unitId});
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue