mirror of
https://github.com/Kelsidavis/WoWee.git
synced 2026-04-17 09: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
|
|
@ -123,6 +123,9 @@ void CombatHandler::registerOpcodes(DispatchTable& table) {
|
|||
addCombatText(CombatTextEntry::ABSORB, static_cast<int32_t>(envAbs), 0, false, 0, 0, victimGuid);
|
||||
if (envRes > 0)
|
||||
addCombatText(CombatTextEntry::RESIST, static_cast<int32_t>(envRes), 0, false, 0, 0, victimGuid);
|
||||
// Drowning damage → play DROWN one-shot on player
|
||||
if (envType == 1 && dmg > 0 && owner_.emoteAnimCallback_)
|
||||
owner_.emoteAnimCallback_(victimGuid, 131); // anim::DROWN
|
||||
}
|
||||
packet.skipAll();
|
||||
};
|
||||
|
|
@ -440,7 +443,7 @@ void CombatHandler::handleAttackerStateUpdate(network::Packet& packet) {
|
|||
lastMeleeSwingMs_ = static_cast<uint64_t>(
|
||||
std::chrono::duration_cast<std::chrono::milliseconds>(
|
||||
std::chrono::system_clock::now().time_since_epoch()).count());
|
||||
if (owner_.meleeSwingCallback_) owner_.meleeSwingCallback_();
|
||||
if (owner_.meleeSwingCallback_) owner_.meleeSwingCallback_(0);
|
||||
}
|
||||
if (!isPlayerAttacker && owner_.npcSwingCallback_) {
|
||||
owner_.npcSwingCallback_(data.attackerGuid);
|
||||
|
|
@ -520,6 +523,17 @@ void CombatHandler::handleAttackerStateUpdate(network::Packet& packet) {
|
|||
addCombatText(CombatTextEntry::RESIST, static_cast<int32_t>(totalResisted), 0, isPlayerAttacker, 0, data.attackerGuid, data.targetGuid);
|
||||
}
|
||||
|
||||
// Fire hit reaction animation on the victim
|
||||
if (owner_.hitReactionCallback_ && !data.isMiss()) {
|
||||
using HR = GameHandler::HitReaction;
|
||||
HR reaction = HR::WOUND;
|
||||
if (data.victimState == 1) reaction = HR::DODGE;
|
||||
else if (data.victimState == 2) reaction = HR::PARRY;
|
||||
else if (data.victimState == 4) reaction = HR::BLOCK;
|
||||
else if (data.isCrit()) reaction = HR::CRIT_WOUND;
|
||||
owner_.hitReactionCallback_(data.targetGuid, reaction);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
void CombatHandler::handleSpellDamageLog(network::Packet& packet) {
|
||||
|
|
|
|||
|
|
@ -542,6 +542,7 @@ EntityController::UnitFieldIndices EntityController::UnitFieldIndices::resolve()
|
|||
fieldIndex(UF::UNIT_FIELD_DISPLAYID),
|
||||
fieldIndex(UF::UNIT_FIELD_MOUNTDISPLAYID),
|
||||
fieldIndex(UF::UNIT_NPC_FLAGS),
|
||||
fieldIndex(UF::UNIT_NPC_EMOTESTATE),
|
||||
fieldIndex(UF::UNIT_FIELD_BYTES_0),
|
||||
fieldIndex(UF::UNIT_FIELD_BYTES_1)
|
||||
};
|
||||
|
|
@ -697,6 +698,7 @@ bool EntityController::applyUnitFieldsOnCreate(const UpdateBlock& block,
|
|||
}
|
||||
}
|
||||
else if (key == ufi.npcFlags) { unit->setNpcFlags(val); }
|
||||
else if (key == ufi.npcEmoteState) { unit->setNpcEmoteState(val); }
|
||||
else if (key == ufi.dynFlags) {
|
||||
unit->setDynamicFlags(val);
|
||||
if (block.objectType == ObjectType::UNIT &&
|
||||
|
|
@ -795,7 +797,28 @@ EntityController::UnitFieldUpdateResult EntityController::applyUnitFieldsOnUpdat
|
|||
if (!uid.empty())
|
||||
pendingEvents_.emit("UNIT_DISPLAYPOWER", {uid});
|
||||
}
|
||||
} else if (key == ufi.flags) { unit->setUnitFlags(val); }
|
||||
} else if (key == ufi.flags) {
|
||||
uint32_t oldFlags = unit->getUnitFlags();
|
||||
unit->setUnitFlags(val);
|
||||
// Detect stun state change on local player
|
||||
constexpr uint32_t UNIT_FLAG_STUNNED = 0x00040000;
|
||||
if (block.guid == owner_.playerGuid && owner_.stunStateCallback_) {
|
||||
bool wasStunned = (oldFlags & UNIT_FLAG_STUNNED) != 0;
|
||||
bool nowStunned = (val & UNIT_FLAG_STUNNED) != 0;
|
||||
if (wasStunned != nowStunned) {
|
||||
owner_.stunStateCallback_(nowStunned);
|
||||
}
|
||||
}
|
||||
// Detect stealth state change on local player
|
||||
constexpr uint32_t UNIT_FLAG_SNEAKING = 0x02000000;
|
||||
if (block.guid == owner_.playerGuid && owner_.stealthStateCallback_) {
|
||||
bool wasStealth = (oldFlags & UNIT_FLAG_SNEAKING) != 0;
|
||||
bool nowStealth = (val & UNIT_FLAG_SNEAKING) != 0;
|
||||
if (wasStealth != nowStealth) {
|
||||
owner_.stealthStateCallback_(nowStealth);
|
||||
}
|
||||
}
|
||||
}
|
||||
else if (ufi.bytes1 != 0xFFFF && key == ufi.bytes1 && block.guid == owner_.playerGuid) {
|
||||
uint8_t newForm = static_cast<uint8_t>((val >> 24) & 0xFF);
|
||||
if (newForm != owner_.shapeshiftFormId_) {
|
||||
|
|
@ -863,6 +886,14 @@ EntityController::UnitFieldUpdateResult EntityController::applyUnitFieldsOnUpdat
|
|||
}
|
||||
unit->setMountDisplayId(val);
|
||||
} else if (key == ufi.npcFlags) { unit->setNpcFlags(val); }
|
||||
else if (key == ufi.npcEmoteState) {
|
||||
uint32_t oldEmote = unit->getNpcEmoteState();
|
||||
unit->setNpcEmoteState(val);
|
||||
// Fire emote animation callback so entity_spawner can update the NPC's idle anim
|
||||
if (val != oldEmote && owner_.emoteAnimCallback_) {
|
||||
owner_.emoteAnimCallback_(block.guid, val);
|
||||
}
|
||||
}
|
||||
// Power/maxpower range checks AFTER all specific fields
|
||||
else if (key >= ufi.powerBase && key < ufi.powerBase + 7) {
|
||||
unit->setPowerByType(static_cast<uint8_t>(key - ufi.powerBase), val);
|
||||
|
|
@ -889,6 +920,11 @@ EntityController::UnitFieldUpdateResult EntityController::applyUnitFieldsOnUpdat
|
|||
}
|
||||
}
|
||||
|
||||
// Fire player health callback for wounded-idle animation
|
||||
if (result.healthChanged && block.guid == owner_.playerGuid && owner_.playerHealthCallback_) {
|
||||
owner_.playerHealthCallback_(unit->getHealth(), unit->getMaxHealth());
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
|
|
@ -1632,6 +1668,17 @@ void EntityController::onValuesUpdateGameObject(const UpdateBlock& block, std::s
|
|||
entity->getZ(), entity->getOrientation());
|
||||
}
|
||||
}
|
||||
|
||||
// Detect GO state changes from GAMEOBJECT_BYTES_1 (packed: byte0=state, byte1=type, byte2=artKit, byte3=animProgress)
|
||||
const uint16_t ufGoBytes1 = fieldIndex(UF::GAMEOBJECT_BYTES_1);
|
||||
if (ufGoBytes1 != 0xFFFF) {
|
||||
auto itB = block.fields.find(ufGoBytes1);
|
||||
if (itB != block.fields.end()) {
|
||||
uint8_t goState = static_cast<uint8_t>(itB->second & 0xFF);
|
||||
if (owner_.gameObjectStateCallback_)
|
||||
owner_.gameObjectStateCallback_(block.guid, goState);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ============================================================
|
||||
|
|
|
|||
|
|
@ -31,6 +31,7 @@
|
|||
#include "pipeline/asset_manager.hpp"
|
||||
#include "pipeline/dbc_loader.hpp"
|
||||
#include "core/logger.hpp"
|
||||
#include "rendering/animation_ids.hpp"
|
||||
#include <glm/gtx/quaternion.hpp>
|
||||
#include <algorithm>
|
||||
#include <cmath>
|
||||
|
|
@ -1275,12 +1276,25 @@ void GameHandler::registerOpcodeHandlers() {
|
|||
};
|
||||
// Consume silently — opcodes we receive but don't need to act on
|
||||
for (auto op : {
|
||||
Opcode::SMSG_GAMEOBJECT_DESPAWN_ANIM, Opcode::SMSG_GAMEOBJECT_RESET_STATE,
|
||||
Opcode::SMSG_FLIGHT_SPLINE_SYNC, Opcode::SMSG_FORCE_DISPLAY_UPDATE,
|
||||
Opcode::SMSG_FORCE_SEND_QUEUED_PACKETS, Opcode::SMSG_FORCE_SET_VEHICLE_REC_ID,
|
||||
Opcode::SMSG_CORPSE_MAP_POSITION_QUERY_RESPONSE, Opcode::SMSG_DAMAGE_CALC_LOG,
|
||||
Opcode::SMSG_DYNAMIC_DROP_ROLL_RESULT, Opcode::SMSG_DESTRUCTIBLE_BUILDING_DAMAGE,
|
||||
}) { registerSkipHandler(op); }
|
||||
|
||||
// Game object despawn animation — reset state to closed before actual despawn
|
||||
dispatchTable_[Opcode::SMSG_GAMEOBJECT_DESPAWN_ANIM] = [this](network::Packet& packet) {
|
||||
if (!packet.hasRemaining(8)) return;
|
||||
uint64_t guid = packet.readUInt64();
|
||||
// Trigger a CLOSE animation / freeze before the object is removed
|
||||
if (gameObjectStateCallback_) gameObjectStateCallback_(guid, 0);
|
||||
};
|
||||
// Game object reset state — return to READY(closed) state
|
||||
dispatchTable_[Opcode::SMSG_GAMEOBJECT_RESET_STATE] = [this](network::Packet& packet) {
|
||||
if (!packet.hasRemaining(8)) return;
|
||||
uint64_t guid = packet.readUInt64();
|
||||
if (gameObjectStateCallback_) gameObjectStateCallback_(guid, 0);
|
||||
};
|
||||
dispatchTable_[Opcode::SMSG_FORCED_DEATH_UPDATE] = [this](network::Packet& packet) {
|
||||
playerDead_ = true;
|
||||
if (ghostStateCallback_) ghostStateCallback_(false);
|
||||
|
|
@ -2124,10 +2138,15 @@ void GameHandler::registerOpcodeHandlers() {
|
|||
if (packet.hasRemaining(1)) {
|
||||
(void)packet.readPackedGuid(); // player guid (unused)
|
||||
}
|
||||
uint32_t newVehicleId = 0;
|
||||
if (packet.hasRemaining(4)) {
|
||||
vehicleId_ = packet.readUInt32();
|
||||
} else {
|
||||
vehicleId_ = 0;
|
||||
newVehicleId = packet.readUInt32();
|
||||
}
|
||||
bool wasInVehicle = vehicleId_ != 0;
|
||||
bool nowInVehicle = newVehicleId != 0;
|
||||
vehicleId_ = newVehicleId;
|
||||
if (wasInVehicle != nowInVehicle && vehicleStateCallback_) {
|
||||
vehicleStateCallback_(nowInVehicle, newVehicleId);
|
||||
}
|
||||
};
|
||||
// guid(8) + status(1): status 1 = NPC has available/new routes for this player
|
||||
|
|
@ -2842,6 +2861,9 @@ void GameHandler::registerOpcodeHandlers() {
|
|||
};
|
||||
dispatchTable_[Opcode::SMSG_ON_CANCEL_EXPECTED_RIDE_VEHICLE_AURA] = [this](network::Packet& packet) {
|
||||
vehicleId_ = 0; // Vehicle ride cancelled; clear UI
|
||||
if (vehicleStateCallback_) {
|
||||
vehicleStateCallback_(false, 0);
|
||||
}
|
||||
packet.skipAll();
|
||||
};
|
||||
// uint32 type (0=normal, 1=heavy, 2=tired/restricted) + uint32 minutes played
|
||||
|
|
@ -6048,6 +6070,12 @@ void GameHandler::preloadDBCCaches() const {
|
|||
loadMapNameCache(); // Map.dbc
|
||||
loadLfgDungeonDbc(); // LFGDungeons.dbc
|
||||
|
||||
// Validate animation constants against AnimationData.dbc
|
||||
if (auto* am = services_.assetManager) {
|
||||
auto animDbc = am->loadDBC("AnimationData.dbc");
|
||||
rendering::anim::validateAgainstDBC(animDbc);
|
||||
}
|
||||
|
||||
auto elapsed = std::chrono::duration_cast<std::chrono::milliseconds>(
|
||||
std::chrono::steady_clock::now() - t0).count();
|
||||
LOG_INFO("DBC cache pre-load complete in ", elapsed, " ms");
|
||||
|
|
|
|||
|
|
@ -679,6 +679,7 @@ void InventoryHandler::closeLoot() {
|
|||
owner_.socket->send(packet);
|
||||
}
|
||||
lootWindowOpen_ = false;
|
||||
if (owner_.lootWindowCallback_) owner_.lootWindowCallback_(false);
|
||||
if (owner_.addonEventCallback_) owner_.addonEventCallback_("LOOT_CLOSED", {});
|
||||
currentLoot_ = LootResponseData{};
|
||||
}
|
||||
|
|
@ -704,6 +705,7 @@ void InventoryHandler::handleLootResponse(network::Packet& packet) {
|
|||
return;
|
||||
}
|
||||
lootWindowOpen_ = true;
|
||||
if (owner_.lootWindowCallback_) owner_.lootWindowCallback_(true);
|
||||
if (owner_.addonEventCallback_) {
|
||||
owner_.addonEventCallback_("LOOT_OPENED", {});
|
||||
owner_.addonEventCallback_("LOOT_READY", {});
|
||||
|
|
@ -749,6 +751,7 @@ void InventoryHandler::handleLootReleaseResponse(network::Packet& packet) {
|
|||
(void)packet;
|
||||
localLootState_.erase(currentLoot_.lootGuid);
|
||||
lootWindowOpen_ = false;
|
||||
if (owner_.lootWindowCallback_) owner_.lootWindowCallback_(false);
|
||||
if (owner_.addonEventCallback_) owner_.addonEventCallback_("LOOT_CLOSED", {});
|
||||
currentLoot_ = LootResponseData{};
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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});
|
||||
|
|
|
|||
|
|
@ -36,6 +36,7 @@ static const UFNameEntry kUFNames[] = {
|
|||
{"UNIT_FIELD_AURAS", UF::UNIT_FIELD_AURAS},
|
||||
{"UNIT_FIELD_AURAFLAGS", UF::UNIT_FIELD_AURAFLAGS},
|
||||
{"UNIT_NPC_FLAGS", UF::UNIT_NPC_FLAGS},
|
||||
{"UNIT_NPC_EMOTESTATE", UF::UNIT_NPC_EMOTESTATE},
|
||||
{"UNIT_DYNAMIC_FLAGS", UF::UNIT_DYNAMIC_FLAGS},
|
||||
{"UNIT_FIELD_RESISTANCES", UF::UNIT_FIELD_RESISTANCES},
|
||||
{"UNIT_FIELD_STAT0", UF::UNIT_FIELD_STAT0},
|
||||
|
|
@ -61,6 +62,7 @@ static const UFNameEntry kUFNames[] = {
|
|||
{"PLAYER_SKILL_INFO_START", UF::PLAYER_SKILL_INFO_START},
|
||||
{"PLAYER_EXPLORED_ZONES_START", UF::PLAYER_EXPLORED_ZONES_START},
|
||||
{"GAMEOBJECT_DISPLAYID", UF::GAMEOBJECT_DISPLAYID},
|
||||
{"GAMEOBJECT_BYTES_1", UF::GAMEOBJECT_BYTES_1},
|
||||
{"ITEM_FIELD_STACK_COUNT", UF::ITEM_FIELD_STACK_COUNT},
|
||||
{"ITEM_FIELD_DURABILITY", UF::ITEM_FIELD_DURABILITY},
|
||||
{"ITEM_FIELD_MAXDURABILITY", UF::ITEM_FIELD_MAXDURABILITY},
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue