diff --git a/include/game/game_handler.hpp b/include/game/game_handler.hpp index 38579ff5..1a40d5d8 100644 --- a/include/game/game_handler.hpp +++ b/include/game/game_handler.hpp @@ -939,6 +939,10 @@ public: using SpellCastAnimCallback = std::function; void setSpellCastAnimCallback(SpellCastAnimCallback cb) { spellCastAnimCallback_ = std::move(cb); } + // Fired when the player's own spell cast fails (spellId of the failed spell). + using SpellCastFailedCallback = std::function; + void setSpellCastFailedCallback(SpellCastFailedCallback cb) { spellCastFailedCallback_ = std::move(cb); } + // Unit animation hint: signal jump (animId=38) for other players/NPCs using UnitAnimHintCallback = std::function; void setUnitAnimHintCallback(UnitAnimHintCallback cb) { unitAnimHintCallback_ = std::move(cb); } @@ -3309,6 +3313,7 @@ private: MeleeSwingCallback meleeSwingCallback_; uint64_t lastMeleeSwingMs_ = 0; // system_clock ms at last player auto-attack swing SpellCastAnimCallback spellCastAnimCallback_; + SpellCastFailedCallback spellCastFailedCallback_; UnitAnimHintCallback unitAnimHintCallback_; UnitMoveFlagsCallback unitMoveFlagsCallback_; NpcSwingCallback npcSwingCallback_; diff --git a/include/ui/game_screen.hpp b/include/ui/game_screen.hpp index e7bc42dc..e76d2e12 100644 --- a/include/ui/game_screen.hpp +++ b/include/ui/game_screen.hpp @@ -58,6 +58,10 @@ private: // Set to true by /stopmacro; checked in executeMacroText to halt remaining commands. bool macroStopped_ = false; + // Action bar error-flash: spellId → wall-clock time (seconds) when the flash ends. + // Populated by the SpellCastFailedCallback; queried during action bar button rendering. + std::unordered_map actionFlashEndTimes_; + // Tab-completion state for slash commands std::string chatTabPrefix_; // prefix captured on first Tab press std::vector chatTabMatches_; // matching command list @@ -109,6 +113,8 @@ private: std::vector uiErrors_; bool uiErrorCallbackSet_ = false; static constexpr float kUIErrorLifetime = 2.5f; + bool castFailedCallbackSet_ = false; + static constexpr float kActionFlashDuration = 0.5f; // seconds for error-red overlay to fade // Reputation change toast: brief colored slide-in below minimap struct RepToastEntry { std::string factionName; int32_t delta = 0; int32_t standing = 0; float age = 0.0f; }; diff --git a/src/game/game_handler.cpp b/src/game/game_handler.cpp index 80a50af0..f0456c5e 100644 --- a/src/game/game_handler.cpp +++ b/src/game/game_handler.cpp @@ -2267,6 +2267,7 @@ void GameHandler::handlePacket(network::Packet& packet) { std::string errMsg = reason ? reason : ("Spell cast failed (error " + std::to_string(castResult) + ")"); addUIError(errMsg); + if (spellCastFailedCallback_) spellCastFailedCallback_(castResultSpellId); MessageChatData msg; msg.type = ChatType::SYSTEM; msg.language = ChatLanguage::UNIVERSAL; diff --git a/src/ui/game_screen.cpp b/src/ui/game_screen.cpp index 7338da94..0e74ab18 100644 --- a/src/ui/game_screen.cpp +++ b/src/ui/game_screen.cpp @@ -414,6 +414,16 @@ void GameScreen::render(game::GameHandler& gameHandler) { uiErrorCallbackSet_ = true; } + // Flash the action bar button whose spell just failed (0.5 s red overlay). + if (!castFailedCallbackSet_) { + gameHandler.setSpellCastFailedCallback([this](uint32_t spellId) { + if (spellId == 0) return; + float now = static_cast(ImGui::GetTime()); + actionFlashEndTimes_[spellId] = now + kActionFlashDuration; + }); + castFailedCallbackSet_ = true; + } + // Set up reputation change toast callback (once) if (!repChangeCallbackSet_) { gameHandler.setRepChangeCallback([this](const std::string& name, int32_t delta, int32_t standing) { @@ -8435,6 +8445,25 @@ void GameScreen::renderActionBar(game::GameHandler& gameHandler) { ImGui::PopStyleColor(); } + // Error-flash overlay: red fade on spell cast failure (~0.5 s). + if (slot.type == game::ActionBarSlot::SPELL && slot.id != 0) { + auto flashIt = actionFlashEndTimes_.find(slot.id); + if (flashIt != actionFlashEndTimes_.end()) { + float now = static_cast(ImGui::GetTime()); + float remaining = flashIt->second - now; + if (remaining > 0.0f) { + float alpha = remaining / kActionFlashDuration; // 1→0 + ImVec2 rMin = ImGui::GetItemRectMin(); + ImVec2 rMax = ImGui::GetItemRectMax(); + ImGui::GetWindowDrawList()->AddRectFilled( + rMin, rMax, + ImGui::ColorConvertFloat4ToU32(ImVec4(1.0f, 0.1f, 0.1f, 0.55f * alpha))); + } else { + actionFlashEndTimes_.erase(flashIt); + } + } + } + bool hoveredOnRelease = ImGui::IsItemHovered(ImGuiHoveredFlags_AllowWhenBlockedByActiveItem) && ImGui::IsMouseReleased(ImGuiMouseButton_Left);