From 59c50e3beb52bc38d75957f3039c47f0912fa8da Mon Sep 17 00:00:00 2001 From: Kelsi Date: Tue, 10 Mar 2026 09:42:17 -0700 Subject: [PATCH] game/rendering: play SpellCast animation during SMSG_SPELL_START Add SpellCastAnimCallback to GameHandler, triggered on SMSG_SPELL_START (start=true) and cleared on SMSG_SPELL_GO / SMSG_SPELL_FAILURE (start=false) for both the player and other units. Connect the callback in Application to play animation 3 (SpellCast) on the player character, NPCs, and other players when they begin a cast. The cast animation is one-shot (loop=false) so it auto-returns to Stand when complete via the existing return-to-idle logic. Also fire stop-cast on spell failure to cancel any stuck cast pose. --- include/game/game_handler.hpp | 6 ++++++ src/core/application.cpp | 32 ++++++++++++++++++++++++++++++++ src/game/game_handler.cpp | 23 +++++++++++++++++++++++ 3 files changed, 61 insertions(+) diff --git a/include/game/game_handler.hpp b/include/game/game_handler.hpp index 4633249c..d9e69992 100644 --- a/include/game/game_handler.hpp +++ b/include/game/game_handler.hpp @@ -623,6 +623,11 @@ public: using MeleeSwingCallback = std::function; void setMeleeSwingCallback(MeleeSwingCallback cb) { meleeSwingCallback_ = std::move(cb); } + // Spell cast animation callbacks — true=start cast/channel, false=finish/cancel + // guid: caster (may be player or another unit), isChannel: channel vs regular cast + using SpellCastAnimCallback = std::function; + void setSpellCastAnimCallback(SpellCastAnimCallback cb) { spellCastAnimCallback_ = std::move(cb); } + // NPC swing callback (plays attack animation on NPC) using NpcSwingCallback = std::function; void setNpcSwingCallback(NpcSwingCallback cb) { npcSwingCallback_ = std::move(cb); } @@ -2246,6 +2251,7 @@ private: NpcAggroCallback npcAggroCallback_; NpcRespawnCallback npcRespawnCallback_; MeleeSwingCallback meleeSwingCallback_; + SpellCastAnimCallback spellCastAnimCallback_; NpcSwingCallback npcSwingCallback_; NpcGreetingCallback npcGreetingCallback_; NpcFarewellCallback npcFarewellCallback_; diff --git a/src/core/application.cpp b/src/core/application.cpp index 2493d4a4..a710d7e8 100644 --- a/src/core/application.cpp +++ b/src/core/application.cpp @@ -2762,6 +2762,38 @@ 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*/) { + 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 + { + 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; + } + } + // Check creatures and other online players + { + auto it = creatureInstances_.find(guid); + if (it != creatureInstances_.end()) { + if (start) cr->playAnimation(it->second, castAnim, false); + return; + } + } + { + auto it = playerInstances_.find(guid); + if (it != playerInstances_.end()) { + if (start) cr->playAnimation(it->second, castAnim, false); + } + } + }); + // NPC greeting callback - play voice line gameHandler->setNpcGreetingCallback([this](uint64_t guid, const glm::vec3& position) { if (renderer && renderer->getNpcVoiceManager()) { diff --git a/src/game/game_handler.cpp b/src/game/game_handler.cpp index 5eda7143..5aaeb021 100644 --- a/src/game/game_handler.cpp +++ b/src/game/game_handler.cpp @@ -2605,9 +2605,15 @@ void GameHandler::handlePacket(network::Packet& packet) { ssm->stopPrecast(); } } + if (spellCastAnimCallback_) { + spellCastAnimCallback_(playerGuid, false, false); + } } else { // Another unit's cast failed — clear their tracked cast bar unitCastStates_.erase(failGuid); + if (spellCastAnimCallback_) { + spellCastAnimCallback_(failGuid, false, false); + } } break; } @@ -12949,6 +12955,10 @@ void GameHandler::handleSpellStart(network::Packet& packet) { s.spellId = data.spellId; s.timeTotal = data.castTime / 1000.0f; s.timeRemaining = s.timeTotal; + // Trigger cast animation on the casting unit + if (spellCastAnimCallback_) { + spellCastAnimCallback_(data.casterUnit, true, false); + } } // If this is the player's own cast, start cast bar @@ -12970,6 +12980,11 @@ void GameHandler::handleSpellStart(network::Packet& packet) { } } + // Trigger cast animation on player character + if (spellCastAnimCallback_) { + spellCastAnimCallback_(playerGuid, true, false); + } + // Hearthstone cast: begin pre-loading terrain at bind point during cast time // so tiles are ready when the teleport fires (avoids falling through un-loaded terrain). // Spell IDs: 6948 = Vanilla Hearthstone (rank 1), 8690 = TBC/WotLK Hearthstone @@ -13021,6 +13036,14 @@ void GameHandler::handleSpellGo(network::Packet& packet) { casting = false; currentCastSpellId = 0; castTimeRemaining = 0.0f; + + // End cast animation on player character + if (spellCastAnimCallback_) { + spellCastAnimCallback_(playerGuid, false, false); + } + } else if (spellCastAnimCallback_) { + // End cast animation on other unit + spellCastAnimCallback_(data.casterUnit, false, false); } // Clear unit cast bar when the spell lands (for any tracked unit)