From 279b4de09a787dc0cc76733ce8cdd3f75b62be76 Mon Sep 17 00:00:00 2001 From: Kelsi Date: Tue, 17 Mar 2026 19:43:19 -0700 Subject: [PATCH] feat: color cast bars green/red by spell interruptibility from Spell.dbc Load AttributesEx from Spell.dbc for all expansions (Classic/TBC/WotLK/ Turtle). Check SPELL_ATTR_EX_NOT_INTERRUPTIBLE (bit 4 = 0x10) to classify each cast as interruptible or not when SMSG_SPELL_START arrives. Target frame and nameplate cast bars now use: - Green: spell can be interrupted by Kick/Counterspell/Pummel etc. - Red: spell is immune to interrupt (boss abilities, instant-cast effects) Both colors pulse faster at >80% completion to signal the closing window. Adds GameHandler::isSpellInterruptible() and UnitCastState::interruptible. --- Data/expansions/classic/dbc_layouts.json | 2 +- Data/expansions/tbc/dbc_layouts.json | 2 +- Data/expansions/turtle/dbc_layouts.json | 2 +- Data/expansions/wotlk/dbc_layouts.json | 2 +- include/game/game_handler.hpp | 10 +++++++- src/game/game_handler.cpp | 31 ++++++++++++++++++++---- src/ui/game_screen.cpp | 23 ++++++++++++------ 7 files changed, 55 insertions(+), 17 deletions(-) diff --git a/Data/expansions/classic/dbc_layouts.json b/Data/expansions/classic/dbc_layouts.json index 4ee5ffb1..e5d0793f 100644 --- a/Data/expansions/classic/dbc_layouts.json +++ b/Data/expansions/classic/dbc_layouts.json @@ -1,6 +1,6 @@ { "Spell": { - "ID": 0, "Attributes": 5, "IconID": 117, + "ID": 0, "Attributes": 5, "AttributesEx": 6, "IconID": 117, "Name": 120, "Tooltip": 147, "Rank": 129, "SchoolEnum": 1, "CastingTimeIndex": 15, "PowerType": 28, "ManaCost": 29, "RangeIndex": 33, "DispelType": 4 diff --git a/Data/expansions/tbc/dbc_layouts.json b/Data/expansions/tbc/dbc_layouts.json index d9ad4351..da2fb9a5 100644 --- a/Data/expansions/tbc/dbc_layouts.json +++ b/Data/expansions/tbc/dbc_layouts.json @@ -1,6 +1,6 @@ { "Spell": { - "ID": 0, "Attributes": 5, "IconID": 124, + "ID": 0, "Attributes": 5, "AttributesEx": 6, "IconID": 124, "Name": 127, "Tooltip": 154, "Rank": 136, "SchoolMask": 215, "CastingTimeIndex": 22, "PowerType": 35, "ManaCost": 36, "RangeIndex": 40, "DispelType": 3 diff --git a/Data/expansions/turtle/dbc_layouts.json b/Data/expansions/turtle/dbc_layouts.json index a76d065c..cb44c54a 100644 --- a/Data/expansions/turtle/dbc_layouts.json +++ b/Data/expansions/turtle/dbc_layouts.json @@ -1,6 +1,6 @@ { "Spell": { - "ID": 0, "Attributes": 5, "IconID": 117, + "ID": 0, "Attributes": 5, "AttributesEx": 6, "IconID": 117, "Name": 120, "Tooltip": 147, "Rank": 129, "SchoolEnum": 1, "CastingTimeIndex": 15, "PowerType": 28, "ManaCost": 29, "RangeIndex": 33, "DispelType": 4 diff --git a/Data/expansions/wotlk/dbc_layouts.json b/Data/expansions/wotlk/dbc_layouts.json index a332f041..4ecbfc32 100644 --- a/Data/expansions/wotlk/dbc_layouts.json +++ b/Data/expansions/wotlk/dbc_layouts.json @@ -1,6 +1,6 @@ { "Spell": { - "ID": 0, "Attributes": 4, "IconID": 133, + "ID": 0, "Attributes": 4, "AttributesEx": 5, "IconID": 133, "Name": 136, "Tooltip": 139, "Rank": 153, "SchoolMask": 225, "PowerType": 14, "ManaCost": 39, "CastingTimeIndex": 47, "RangeIndex": 49, "DispelType": 2 diff --git a/include/game/game_handler.hpp b/include/game/game_handler.hpp index 5abce0ed..58d73e37 100644 --- a/include/game/game_handler.hpp +++ b/include/game/game_handler.hpp @@ -798,6 +798,7 @@ public: uint32_t spellId = 0; float timeRemaining = 0.0f; float timeTotal = 0.0f; + bool interruptible = true; ///< false when SPELL_ATTR_EX_NOT_INTERRUPTIBLE is set }; // Returns cast state for any unit by GUID (empty/non-casting if not found) const UnitCastState* getUnitCastState(uint64_t guid) const { @@ -819,6 +820,10 @@ public: auto* s = getUnitCastState(targetGuid); return s ? s->timeRemaining : 0.0f; } + bool isTargetCastInterruptible() const { + auto* s = getUnitCastState(targetGuid); + return s ? s->interruptible : true; + } // Talents uint8_t getActiveTalentSpec() const { return activeTalentSpec_; } @@ -2056,6 +2061,9 @@ public: const std::string& getSkillLineName(uint32_t spellId) const; /// Returns the DispelType for a spell (0=none,1=magic,2=curse,3=disease,4=poison,5+=other) uint8_t getSpellDispelType(uint32_t spellId) const; + /// Returns true if the spell can be interrupted by abilities like Kick/Counterspell. + /// False for spells with SPELL_ATTR_EX_NOT_INTERRUPTIBLE (attrEx bit 4 = 0x10). + bool isSpellInterruptible(uint32_t spellId) const; struct TrainerTab { std::string name; @@ -3059,7 +3067,7 @@ private: // Trainer bool trainerWindowOpen_ = false; TrainerListData currentTrainerList_; - struct SpellNameEntry { std::string name; std::string rank; std::string description; uint32_t schoolMask = 0; uint8_t dispelType = 0; }; + struct SpellNameEntry { std::string name; std::string rank; std::string description; uint32_t schoolMask = 0; uint8_t dispelType = 0; uint32_t attrEx = 0; }; std::unordered_map spellNameCache_; bool spellNameCacheLoaded_ = false; diff --git a/src/game/game_handler.cpp b/src/game/game_handler.cpp index 2a16211f..27de9b6d 100644 --- a/src/game/game_handler.cpp +++ b/src/game/game_handler.cpp @@ -18279,10 +18279,11 @@ void GameHandler::handleSpellStart(network::Packet& packet) { // Track cast bar for any non-player caster (target frame + boss frames) if (data.casterUnit != playerGuid && data.castTime > 0) { auto& s = unitCastStates_[data.casterUnit]; - s.casting = true; - s.spellId = data.spellId; - s.timeTotal = data.castTime / 1000.0f; - s.timeRemaining = s.timeTotal; + s.casting = true; + s.spellId = data.spellId; + s.timeTotal = data.castTime / 1000.0f; + s.timeRemaining = s.timeTotal; + s.interruptible = isSpellInterruptible(data.spellId); // Trigger cast animation on the casting unit if (spellCastAnimCallback_) { spellCastAnimCallback_(data.casterUnit, true, false); @@ -21320,6 +21321,14 @@ void GameHandler::loadSpellNameCache() { if (f != 0xFFFFFFFF && f < dbc->getFieldCount()) { dispelField = f; hasDispelField = true; } } + // AttributesEx field (bit 4 = SPELL_ATTR_EX_NOT_INTERRUPTIBLE) + uint32_t attrExField = 0xFFFFFFFF; + bool hasAttrExField = false; + if (spellL) { + uint32_t f = spellL->field("AttributesEx"); + if (f != 0xFFFFFFFF && f < dbc->getFieldCount()) { attrExField = f; hasAttrExField = true; } + } + // Tooltip/description field uint32_t tooltipField = 0xFFFFFFFF; if (spellL) { @@ -21334,7 +21343,7 @@ void GameHandler::loadSpellNameCache() { std::string name = dbc->getString(i, spellL ? (*spellL)["Name"] : 136); std::string rank = dbc->getString(i, spellL ? (*spellL)["Rank"] : 153); if (!name.empty()) { - SpellNameEntry entry{std::move(name), std::move(rank), {}, 0, 0}; + SpellNameEntry entry{std::move(name), std::move(rank), {}, 0, 0, 0}; if (tooltipField != 0xFFFFFFFF) { entry.description = dbc->getString(i, tooltipField); } @@ -21349,6 +21358,9 @@ void GameHandler::loadSpellNameCache() { if (hasDispelField) { entry.dispelType = static_cast(dbc->getUInt32(i, dispelField)); } + if (hasAttrExField) { + entry.attrEx = dbc->getUInt32(i, attrExField); + } spellNameCache_[id] = std::move(entry); } } @@ -21554,6 +21566,15 @@ uint8_t GameHandler::getSpellDispelType(uint32_t spellId) const { return (it != spellNameCache_.end()) ? it->second.dispelType : 0; } +bool GameHandler::isSpellInterruptible(uint32_t spellId) const { + if (spellId == 0) return true; + const_cast(this)->loadSpellNameCache(); + auto it = spellNameCache_.find(spellId); + if (it == spellNameCache_.end()) return true; // assume interruptible if unknown + // SPELL_ATTR_EX_NOT_INTERRUPTIBLE = bit 4 of AttributesEx (0x00000010) + return (it->second.attrEx & 0x00000010u) == 0; +} + const std::string& GameHandler::getSkillLineName(uint32_t spellId) const { auto slIt = spellToSkillLine_.find(spellId); if (slIt == spellToSkillLine_.end()) return EMPTY_STRING; diff --git a/src/ui/game_screen.cpp b/src/ui/game_screen.cpp index 03a027dd..9502058f 100644 --- a/src/ui/game_screen.cpp +++ b/src/ui/game_screen.cpp @@ -4288,14 +4288,19 @@ void GameScreen::renderTargetFrame(game::GameHandler& gameHandler) { float castPct = gameHandler.getTargetCastProgress(); float castLeft = gameHandler.getTargetCastTimeRemaining(); uint32_t tspell = gameHandler.getTargetCastSpellId(); + bool interruptible = gameHandler.isTargetCastInterruptible(); const std::string& castName = (tspell != 0) ? gameHandler.getSpellName(tspell) : ""; - // Pulse bright orange when cast is > 80% complete — interrupt window closing + // Color: interruptible = green (can Kick/CS), not interruptible = red, both pulse when >80% ImVec4 castBarColor; if (castPct > 0.8f) { float pulse = 0.7f + 0.3f * std::sin(static_cast(ImGui::GetTime()) * 8.0f); - castBarColor = ImVec4(1.0f * pulse, 0.5f * pulse, 0.0f, 1.0f); + if (interruptible) + castBarColor = ImVec4(0.2f * pulse, 0.9f * pulse, 0.2f * pulse, 1.0f); // green pulse + else + castBarColor = ImVec4(1.0f * pulse, 0.1f * pulse, 0.1f * pulse, 1.0f); // red pulse } else { - castBarColor = ImVec4(0.9f, 0.3f, 0.2f, 1.0f); + castBarColor = interruptible ? ImVec4(0.2f, 0.75f, 0.2f, 1.0f) // green = can interrupt + : ImVec4(0.85f, 0.15f, 0.15f, 1.0f); // red = uninterruptible } ImGui::PushStyleColor(ImGuiCol_PlotHistogram, castBarColor); char castLabel[72]; @@ -9702,14 +9707,18 @@ void GameScreen::renderNameplates(game::GameHandler& gameHandler) { castBarBaseY += snSz.y + 2.0f; } - // Cast bar background + fill (pulse orange when >80% = interrupt window closing) - ImU32 cbBg = IM_COL32(40, 30, 60, A(180)); + // Cast bar: green = interruptible, red = uninterruptible; both pulse when >80% complete + ImU32 cbBg = IM_COL32(30, 25, 40, A(180)); ImU32 cbFill; if (castPct > 0.8f && unit->isHostile()) { float pulse = 0.7f + 0.3f * std::sin(static_cast(ImGui::GetTime()) * 8.0f); - cbFill = IM_COL32(static_cast(255 * pulse), static_cast(130 * pulse), 0, A(220)); + cbFill = cs->interruptible + ? IM_COL32(static_cast(40 * pulse), static_cast(220 * pulse), static_cast(40 * pulse), A(220)) // green pulse + : IM_COL32(static_cast(255 * pulse), static_cast(30 * pulse), static_cast(30 * pulse), A(220)); // red pulse } else { - cbFill = IM_COL32(140, 80, 220, A(200)); // purple cast bar + cbFill = cs->interruptible + ? IM_COL32(50, 190, 50, A(200)) // green = interruptible + : IM_COL32(190, 40, 40, A(200)); // red = uninterruptible } drawList->AddRectFilled(ImVec2(barX, castBarBaseY), ImVec2(barX + barW, castBarBaseY + cbH), cbBg, 2.0f);