mirror of
https://github.com/Kelsidavis/WoWee.git
synced 2026-05-04 16:23: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
|
|
@ -1070,7 +1070,7 @@ void CombatHandler::setTarget(uint64_t guid) {
|
|||
// Clear previous target's cast bar on target change
|
||||
// (the new target's cast state is naturally fetched from spellHandler_->unitCastStates_ by GUID)
|
||||
|
||||
// Inform server of target selection (Phase 1)
|
||||
// Inform server of target selection
|
||||
if (owner_.isInWorld()) {
|
||||
auto packet = SetSelectionPacket::build(guid);
|
||||
owner_.socket->send(packet);
|
||||
|
|
|
|||
|
|
@ -258,7 +258,7 @@ void EntityController::processOutOfRangeObjects(const std::vector<uint64_t>& gui
|
|||
}
|
||||
|
||||
// ============================================================
|
||||
// Phase 1: Extracted helper methods
|
||||
// Extracted helper methods
|
||||
// ============================================================
|
||||
|
||||
bool EntityController::extractPlayerAppearance(const std::map<uint16_t, uint32_t>& fields,
|
||||
|
|
@ -383,7 +383,7 @@ void EntityController::maybeDetectCoinageIndex(const std::map<uint16_t, uint32_t
|
|||
}
|
||||
|
||||
// ============================================================
|
||||
// Phase 2: Update type dispatch
|
||||
// Update type dispatch
|
||||
// ============================================================
|
||||
|
||||
void EntityController::applyUpdateObjectBlock(const UpdateBlock& block, bool& newItemCreated) {
|
||||
|
|
@ -404,10 +404,10 @@ void EntityController::applyUpdateObjectBlock(const UpdateBlock& block, bool& ne
|
|||
}
|
||||
|
||||
// ============================================================
|
||||
// Phase 3: Concern-specific helpers
|
||||
// Concern-specific helpers
|
||||
// ============================================================
|
||||
|
||||
// 3i: Non-player transport child attachment — identical in CREATE/VALUES/MOVEMENT
|
||||
// Non-player transport child attachment — identical in CREATE/VALUES/MOVEMENT
|
||||
void EntityController::updateNonPlayerTransportAttachment(const UpdateBlock& block,
|
||||
const std::shared_ptr<Entity>& entity,
|
||||
ObjectType entityType) {
|
||||
|
|
@ -430,7 +430,7 @@ void EntityController::updateNonPlayerTransportAttachment(const UpdateBlock& blo
|
|||
}
|
||||
}
|
||||
|
||||
// 3f: Rebuild playerAuras from UNIT_FIELD_AURAS (Classic/vanilla only).
|
||||
// Rebuild playerAuras from UNIT_FIELD_AURAS (Classic/vanilla only).
|
||||
// blockFields is used to check if any aura field was updated in this packet.
|
||||
// entity->getFields() is used for reading the full accumulated state.
|
||||
// Normalises Classic harmful bit (0x02) to WotLK debuff bit (0x80) so
|
||||
|
|
@ -481,7 +481,7 @@ void EntityController::syncClassicAurasFromFields(const std::shared_ptr<Entity>&
|
|||
pendingEvents_.emit("UNIT_AURA", {"player"});
|
||||
}
|
||||
|
||||
// 3h: Detect player mount/dismount from UNIT_FIELD_MOUNTDISPLAYID changes
|
||||
// Detect player mount/dismount from UNIT_FIELD_MOUNTDISPLAYID changes
|
||||
void EntityController::detectPlayerMountChange(uint32_t newMountDisplayId,
|
||||
const std::map<uint16_t, uint32_t>& blockFields) {
|
||||
uint32_t old = owner_.currentMountDisplayId_;
|
||||
|
|
@ -528,7 +528,7 @@ void EntityController::detectPlayerMountChange(uint32_t newMountDisplayId,
|
|||
}
|
||||
}
|
||||
|
||||
// Phase 4: Resolve cached field indices once per handler call.
|
||||
// Resolve cached field indices once per handler call.
|
||||
EntityController::UnitFieldIndices EntityController::UnitFieldIndices::resolve() {
|
||||
return UnitFieldIndices{
|
||||
fieldIndex(UF::UNIT_FIELD_HEALTH),
|
||||
|
|
@ -579,7 +579,7 @@ EntityController::PlayerFieldIndices EntityController::PlayerFieldIndices::resol
|
|||
};
|
||||
}
|
||||
|
||||
// 3a: Create the appropriate Entity subclass from the block's object type.
|
||||
// Create the appropriate Entity subclass from the block's object type.
|
||||
std::shared_ptr<Entity> EntityController::createEntityFromBlock(const UpdateBlock& block) {
|
||||
switch (block.objectType) {
|
||||
case ObjectType::PLAYER:
|
||||
|
|
@ -596,7 +596,7 @@ std::shared_ptr<Entity> EntityController::createEntityFromBlock(const UpdateBloc
|
|||
}
|
||||
}
|
||||
|
||||
// 3b: Track player-on-transport state from movement blocks.
|
||||
// Track player-on-transport state from movement blocks.
|
||||
// Consolidates near-identical logic from CREATE and MOVEMENT handlers.
|
||||
// When updateMovementInfoPos is true (MOVEMENT), movementInfo.x/y/z are set
|
||||
// to the raw canonical position when not on a resolved transport.
|
||||
|
|
@ -644,7 +644,7 @@ void EntityController::applyPlayerTransportState(const UpdateBlock& block,
|
|||
}
|
||||
}
|
||||
|
||||
// 3c: Apply unit fields during CREATE — sets health/power/level/flags/displayId/etc.
|
||||
// Apply unit fields during CREATE — sets health/power/level/flags/displayId/etc.
|
||||
// Returns true if the entity is initially dead (health=0 or DYNFLAG_DEAD).
|
||||
bool EntityController::applyUnitFieldsOnCreate(const UpdateBlock& block,
|
||||
std::shared_ptr<Unit>& unit,
|
||||
|
|
@ -928,7 +928,7 @@ EntityController::UnitFieldUpdateResult EntityController::applyUnitFieldsOnUpdat
|
|||
return result;
|
||||
}
|
||||
|
||||
// 3d: Apply player stat fields (XP, coinage, combat stats, etc.).
|
||||
// Apply player stat fields (XP, coinage, combat stats, etc.).
|
||||
// Shared between CREATE and VALUES — isCreate controls event firing differences.
|
||||
bool EntityController::applyPlayerStatFields(const std::map<uint16_t, uint32_t>& fields,
|
||||
const PlayerFieldIndices& pfi,
|
||||
|
|
@ -1081,7 +1081,7 @@ bool EntityController::applyPlayerStatFields(const std::map<uint16_t, uint32_t>&
|
|||
return slotsChanged;
|
||||
}
|
||||
|
||||
// 3e: Dispatch entity spawn callbacks for units/players.
|
||||
// Dispatch entity spawn callbacks for units/players.
|
||||
// Consolidates player/creature spawn callback invocation from CREATE and VALUES handlers.
|
||||
// isDead = unitInitiallyDead (CREATE) or computed isDeadNow && !npcDeathNotified (VALUES).
|
||||
void EntityController::dispatchEntitySpawn(uint64_t guid, ObjectType objectType,
|
||||
|
|
@ -1133,7 +1133,7 @@ void EntityController::dispatchEntitySpawn(uint64_t guid, ObjectType objectType,
|
|||
}
|
||||
}
|
||||
|
||||
// 3g: Track online item/container objects during CREATE.
|
||||
// Track online item/container objects during CREATE.
|
||||
void EntityController::trackItemOnCreate(const UpdateBlock& block, bool& newItemCreated) {
|
||||
auto entryIt = block.fields.find(fieldIndex(UF::OBJECT_FIELD_ENTRY));
|
||||
auto stackIt = block.fields.find(fieldIndex(UF::ITEM_FIELD_STACK_COUNT));
|
||||
|
|
@ -1169,7 +1169,7 @@ void EntityController::trackItemOnCreate(const UpdateBlock& block, bool& newItem
|
|||
}
|
||||
}
|
||||
|
||||
// 3g: Update item stack count / durability / enchants for existing items during VALUES.
|
||||
// Update item stack count / durability / enchants for existing items during VALUES.
|
||||
void EntityController::updateItemOnValuesUpdate(const UpdateBlock& block,
|
||||
const std::shared_ptr<Entity>& entity) {
|
||||
bool inventoryChanged = false;
|
||||
|
|
@ -1275,7 +1275,7 @@ void EntityController::updateItemOnValuesUpdate(const UpdateBlock& block,
|
|||
}
|
||||
|
||||
// ============================================================
|
||||
// Phase 5: Object-type handler struct definitions
|
||||
// Object-type handler struct definitions
|
||||
// ============================================================
|
||||
|
||||
struct EntityController::UnitTypeHandler : EntityController::IObjectTypeHandler {
|
||||
|
|
@ -1313,7 +1313,7 @@ struct EntityController::CorpseTypeHandler : EntityController::IObjectTypeHandle
|
|||
};
|
||||
|
||||
// ============================================================
|
||||
// Phase 5: Handler registry infrastructure
|
||||
// Handler registry infrastructure
|
||||
// ============================================================
|
||||
|
||||
void EntityController::initTypeHandlers() {
|
||||
|
|
@ -1331,7 +1331,7 @@ EntityController::IObjectTypeHandler* EntityController::getTypeHandler(ObjectTyp
|
|||
}
|
||||
|
||||
// ============================================================
|
||||
// Phase 6: Deferred event bus flush
|
||||
// Deferred event bus flush
|
||||
// ============================================================
|
||||
|
||||
void EntityController::flushPendingEvents() {
|
||||
|
|
@ -1342,7 +1342,7 @@ void EntityController::flushPendingEvents() {
|
|||
}
|
||||
|
||||
// ============================================================
|
||||
// Phase 5: Type-specific CREATE handlers
|
||||
// Type-specific CREATE handlers
|
||||
// ============================================================
|
||||
|
||||
void EntityController::onCreateUnit(const UpdateBlock& block, std::shared_ptr<Entity>& entity) {
|
||||
|
|
@ -1432,7 +1432,7 @@ void EntityController::onCreatePlayer(const UpdateBlock& block, std::shared_ptr<
|
|||
}
|
||||
}
|
||||
}
|
||||
// 3f: Classic aura sync on initial object create
|
||||
// Classic aura sync on initial object create
|
||||
if (block.guid == owner_.playerGuid) {
|
||||
syncClassicAurasFromFields(entity);
|
||||
}
|
||||
|
|
@ -1449,7 +1449,7 @@ void EntityController::onCreatePlayer(const UpdateBlock& block, std::shared_ptr<
|
|||
}
|
||||
}
|
||||
|
||||
// 3d: Player stat fields (self only)
|
||||
// Player stat fields (self only)
|
||||
if (block.guid == owner_.playerGuid) {
|
||||
// Auto-detect coinage index using the previous snapshot vs this full snapshot.
|
||||
maybeDetectCoinageIndex(owner_.lastPlayerFields_, block.fields);
|
||||
|
|
@ -1557,7 +1557,7 @@ void EntityController::onCreateCorpse(const UpdateBlock& block) {
|
|||
}
|
||||
|
||||
// ============================================================
|
||||
// Phase 5: Type-specific VALUES UPDATE handlers
|
||||
// Type-specific VALUES UPDATE handlers
|
||||
// ============================================================
|
||||
|
||||
void EntityController::handleDisplayIdChange(const UpdateBlock& block,
|
||||
|
|
@ -1602,15 +1602,15 @@ void EntityController::onValuesUpdatePlayer(const UpdateBlock& block, std::share
|
|||
UnitFieldIndices ufi = UnitFieldIndices::resolve();
|
||||
UnitFieldUpdateResult result = applyUnitFieldsOnUpdate(block, entity, unit, ufi);
|
||||
|
||||
// 3f: Classic aura sync from UNIT_FIELD_AURAS when those fields are updated
|
||||
// Classic aura sync from UNIT_FIELD_AURAS when those fields are updated
|
||||
if (block.guid == owner_.playerGuid) {
|
||||
syncClassicAurasFromFields(entity);
|
||||
}
|
||||
|
||||
// 3e: Display ID changed — re-spawn/model-change
|
||||
// Display ID changed — re-spawn/model-change
|
||||
handleDisplayIdChange(block, entity, unit, result);
|
||||
|
||||
// 3d: Self-player stat/inventory/quest field updates
|
||||
// Self-player stat/inventory/quest field updates
|
||||
if (block.guid == owner_.playerGuid) {
|
||||
const bool needCoinageDetectSnapshot =
|
||||
(owner_.pendingMoneyDelta_ != 0 && owner_.pendingMoneyDeltaTimer_ > 0.0f);
|
||||
|
|
@ -1682,7 +1682,7 @@ void EntityController::onValuesUpdateGameObject(const UpdateBlock& block, std::s
|
|||
}
|
||||
|
||||
// ============================================================
|
||||
// Phase 2: Update type handlers (refactored with Phase 5 dispatch)
|
||||
// Update type handlers
|
||||
// ============================================================
|
||||
|
||||
void EntityController::handleCreateObject(const UpdateBlock& block, bool& newItemCreated) {
|
||||
|
|
@ -1716,7 +1716,7 @@ void EntityController::handleCreateObject(const UpdateBlock& block, bool& newIte
|
|||
// Add to manager
|
||||
entityManager.addEntity(block.guid, entity);
|
||||
|
||||
// Phase 5: Dispatch to type-specific handler
|
||||
// Dispatch to type-specific handler
|
||||
auto* handler = getTypeHandler(block.objectType);
|
||||
if (handler) handler->onCreate(block, entity, newItemCreated);
|
||||
|
||||
|
|
@ -1741,7 +1741,7 @@ void EntityController::handleValuesUpdate(const UpdateBlock& block) {
|
|||
entity->setField(field.first, field.second);
|
||||
}
|
||||
|
||||
// Phase 5: Dispatch to type-specific handler
|
||||
// Dispatch to type-specific handler
|
||||
auto* handler = getTypeHandler(entity->getType());
|
||||
if (handler) handler->onValuesUpdate(block, entity);
|
||||
|
||||
|
|
|
|||
|
|
@ -31,7 +31,7 @@
|
|||
#include "pipeline/asset_manager.hpp"
|
||||
#include "pipeline/dbc_loader.hpp"
|
||||
#include "core/logger.hpp"
|
||||
#include "rendering/animation_ids.hpp"
|
||||
#include "rendering/animation/animation_ids.hpp"
|
||||
#include <glm/gtx/quaternion.hpp>
|
||||
#include <algorithm>
|
||||
#include <cmath>
|
||||
|
|
@ -5145,7 +5145,7 @@ const std::vector<std::string>& GameHandler::getJoinedChannels() const {
|
|||
}
|
||||
|
||||
// ============================================================
|
||||
// Phase 1: Name Queries (delegated to EntityController)
|
||||
// Name Queries (delegated to EntityController)
|
||||
// ============================================================
|
||||
|
||||
void GameHandler::queryPlayerName(uint64_t guid) {
|
||||
|
|
@ -5217,7 +5217,7 @@ void GameHandler::emitAllOtherPlayerEquipment() {
|
|||
}
|
||||
|
||||
// ============================================================
|
||||
// Phase 2: Combat (delegated to CombatHandler)
|
||||
// Combat (delegated to CombatHandler)
|
||||
// ============================================================
|
||||
|
||||
void GameHandler::startAutoAttack(uint64_t targetGuid) {
|
||||
|
|
@ -5372,7 +5372,7 @@ void GameHandler::requestPvpLog() {
|
|||
}
|
||||
|
||||
// ============================================================
|
||||
// Phase 3: Spells
|
||||
// Spells
|
||||
// ============================================================
|
||||
|
||||
void GameHandler::castSpell(uint32_t spellId, uint64_t targetGuid) {
|
||||
|
|
@ -5489,7 +5489,7 @@ void GameHandler::sendAlterAppearance(uint32_t hairStyle, uint32_t hairColor, ui
|
|||
}
|
||||
|
||||
// ============================================================
|
||||
// Phase 4: Group/Party
|
||||
// Group/Party
|
||||
// ============================================================
|
||||
|
||||
void GameHandler::inviteToGroup(const std::string& playerName) {
|
||||
|
|
@ -5606,7 +5606,7 @@ void GameHandler::turnInPetition(uint64_t petitionGuid) {
|
|||
}
|
||||
|
||||
// ============================================================
|
||||
// Phase 5: Loot, Gossip, Vendor
|
||||
// Loot, Gossip, Vendor
|
||||
// ============================================================
|
||||
|
||||
void GameHandler::lootTarget(uint64_t guid) {
|
||||
|
|
|
|||
|
|
@ -845,7 +845,19 @@ void SpellHandler::handleSpellStart(network::Packet& packet) {
|
|||
return;
|
||||
}
|
||||
LOG_DEBUG("SMSG_SPELL_START: caster=0x", std::hex, data.casterUnit, std::dec,
|
||||
" spell=", data.spellId, " castTime=", data.castTime);
|
||||
" spell=", data.spellId, " castTime=", data.castTime,
|
||||
" target=0x", std::hex, data.targetGuid, std::dec);
|
||||
|
||||
// Classify spell targeting for animation selection:
|
||||
// DIRECTED — targets a specific other unit (Frostbolt, Heal)
|
||||
// OMNI — self-cast or no explicit target (Arcane Explosion, buffs)
|
||||
// AREA — ground-targeted AoE with no unit target (Blizzard, Rain of Fire)
|
||||
auto classifyCast = [](uint64_t targetGuid, uint64_t casterGuid) -> SpellCastType {
|
||||
if (targetGuid == 0) return SpellCastType::AREA;
|
||||
if (targetGuid == casterGuid) return SpellCastType::OMNI;
|
||||
return SpellCastType::DIRECTED;
|
||||
};
|
||||
const SpellCastType castType = classifyCast(data.targetGuid, data.casterUnit);
|
||||
|
||||
// Track cast bar for any non-player caster
|
||||
if (data.casterUnit != owner_.playerGuid && data.castTime > 0) {
|
||||
|
|
@ -856,8 +868,9 @@ void SpellHandler::handleSpellStart(network::Packet& packet) {
|
|||
s.timeTotal = data.castTime / 1000.0f;
|
||||
s.timeRemaining = s.timeTotal;
|
||||
s.interruptible = owner_.isSpellInterruptible(data.spellId);
|
||||
s.castType = castType;
|
||||
if (owner_.spellCastAnimCallback_) {
|
||||
owner_.spellCastAnimCallback_(data.casterUnit, true, false);
|
||||
owner_.spellCastAnimCallback_(data.casterUnit, true, false, castType);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -891,7 +904,7 @@ void SpellHandler::handleSpellStart(network::Packet& packet) {
|
|||
}
|
||||
|
||||
if (owner_.spellCastAnimCallback_) {
|
||||
owner_.spellCastAnimCallback_(owner_.playerGuid, true, false);
|
||||
owner_.spellCastAnimCallback_(owner_.playerGuid, true, false, castType);
|
||||
}
|
||||
|
||||
// Hearthstone: pre-load terrain at bind point
|
||||
|
|
@ -954,8 +967,14 @@ void SpellHandler::handleSpellGo(network::Packet& packet) {
|
|||
// 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)) {
|
||||
// Classify instant spell from SPELL_GO packet target info
|
||||
SpellCastType goType = SpellCastType::OMNI;
|
||||
if (data.targetGuid != 0 && data.targetGuid != data.casterUnit)
|
||||
goType = SpellCastType::DIRECTED;
|
||||
else if (data.targetGuid == 0 && data.hitCount > 1)
|
||||
goType = SpellCastType::AREA;
|
||||
if (owner_.spellCastAnimCallback_) {
|
||||
owner_.spellCastAnimCallback_(owner_.playerGuid, true, false);
|
||||
owner_.spellCastAnimCallback_(owner_.playerGuid, true, false, goType);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -983,7 +1002,7 @@ void SpellHandler::handleSpellGo(network::Packet& packet) {
|
|||
owner_.pendingGameObjectInteractGuid_ = 0;
|
||||
|
||||
if (owner_.spellCastAnimCallback_) {
|
||||
owner_.spellCastAnimCallback_(owner_.playerGuid, false, false);
|
||||
owner_.spellCastAnimCallback_(owner_.playerGuid, false, false, SpellCastType::OMNI);
|
||||
}
|
||||
|
||||
if (owner_.addonEventCallback_)
|
||||
|
|
@ -1003,11 +1022,17 @@ void SpellHandler::handleSpellGo(network::Packet& packet) {
|
|||
// instant cast — play a brief one-shot spell animation before stopping
|
||||
auto castIt = unitCastStates_.find(data.casterUnit);
|
||||
bool wasTrackedCast = (castIt != unitCastStates_.end());
|
||||
// Classify NPC instant spell from SPELL_GO target info
|
||||
SpellCastType npcGoType = SpellCastType::OMNI;
|
||||
if (data.targetGuid != 0 && data.targetGuid != data.casterUnit)
|
||||
npcGoType = SpellCastType::DIRECTED;
|
||||
else if (data.targetGuid == 0 && data.hitCount > 1)
|
||||
npcGoType = SpellCastType::AREA;
|
||||
if (!wasTrackedCast && owner_.spellCastAnimCallback_) {
|
||||
owner_.spellCastAnimCallback_(data.casterUnit, true, false);
|
||||
owner_.spellCastAnimCallback_(data.casterUnit, true, false, npcGoType);
|
||||
}
|
||||
if (owner_.spellCastAnimCallback_) {
|
||||
owner_.spellCastAnimCallback_(data.casterUnit, false, false);
|
||||
owner_.spellCastAnimCallback_(data.casterUnit, false, false, SpellCastType::OMNI);
|
||||
}
|
||||
bool targetsPlayer = false;
|
||||
for (const auto& tgt : data.hitTargets) {
|
||||
|
|
@ -2400,13 +2425,13 @@ void SpellHandler::handleSpellFailure(network::Packet& packet) {
|
|||
}
|
||||
}
|
||||
if (owner_.spellCastAnimCallback_) {
|
||||
owner_.spellCastAnimCallback_(owner_.playerGuid, false, false);
|
||||
owner_.spellCastAnimCallback_(owner_.playerGuid, false, false, SpellCastType::OMNI);
|
||||
}
|
||||
} else {
|
||||
// Another unit's cast failed — clear their tracked cast bar
|
||||
unitCastStates_.erase(failGuid);
|
||||
if (owner_.spellCastAnimCallback_) {
|
||||
owner_.spellCastAnimCallback_(failGuid, false, false);
|
||||
owner_.spellCastAnimCallback_(failGuid, false, false, SpellCastType::OMNI);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -3219,8 +3244,12 @@ void SpellHandler::handleChannelStart(network::Packet& packet) {
|
|||
" spell=", chanSpellId, " total=", chanTotalMs, "ms");
|
||||
|
||||
// Play channeling animation (looping)
|
||||
// Channel packets don't carry targetGuid — use player's current target as hint
|
||||
SpellCastType chanType = SpellCastType::OMNI;
|
||||
if (chanCaster == owner_.playerGuid && owner_.targetGuid != 0)
|
||||
chanType = SpellCastType::DIRECTED;
|
||||
if (owner_.spellCastAnimCallback_) {
|
||||
owner_.spellCastAnimCallback_(chanCaster, true, true);
|
||||
owner_.spellCastAnimCallback_(chanCaster, true, true, chanType);
|
||||
}
|
||||
|
||||
// Fire UNIT_SPELLCAST_CHANNEL_START for Lua addons
|
||||
|
|
@ -3260,7 +3289,7 @@ void SpellHandler::handleChannelUpdate(network::Packet& packet) {
|
|||
if (chanRemainMs == 0) {
|
||||
// Stop channeling animation — return to idle
|
||||
if (owner_.spellCastAnimCallback_) {
|
||||
owner_.spellCastAnimCallback_(chanCaster2, false, true);
|
||||
owner_.spellCastAnimCallback_(chanCaster2, false, true, SpellCastType::OMNI);
|
||||
}
|
||||
auto unitId = owner_.guidToUnitId(chanCaster2);
|
||||
if (!unitId.empty())
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue