From 39634f442bd782f88ba2a7ceefa4fb855a6d3b30 Mon Sep 17 00:00:00 2001 From: Kelsi Date: Thu, 12 Mar 2026 06:01:42 -0700 Subject: [PATCH] feat: add insufficient-power tint to action bar spell slots Spell icons now render with a purple desaturated tint when the player lacks enough mana/rage/energy/runic power to cast them. Power cost and type are read from Spell.dbc via the spellbook's DBC cache. The spell tooltip also shows "Not enough power" in purple when applicable. Priority: cooldown > GCD > out-of-range > insufficient-power so states don't conflict. Adds SpellbookScreen::getSpellPowerInfo() as a public DBC accessor. --- include/ui/spellbook_screen.hpp | 6 ++++++ src/ui/game_screen.cpp | 33 +++++++++++++++++++++++++++++---- src/ui/spellbook_screen.cpp | 14 ++++++++++++++ 3 files changed, 49 insertions(+), 4 deletions(-) diff --git a/include/ui/spellbook_screen.hpp b/include/ui/spellbook_screen.hpp index 8059ddf2..2bc0f866 100644 --- a/include/ui/spellbook_screen.hpp +++ b/include/ui/spellbook_screen.hpp @@ -58,6 +58,12 @@ public: /// Triggers DBC load if needed. Used by the action bar for out-of-range tinting. uint32_t getSpellMaxRange(uint32_t spellId, pipeline::AssetManager* assetManager); + /// Returns the power cost and type for a spell (cost=0 if unknown/free). + /// powerType: 0=mana, 1=rage, 2=focus, 3=energy, 6=runic power. + /// Triggers DBC load if needed. Used by the action bar for insufficient-power tinting. + void getSpellPowerInfo(uint32_t spellId, pipeline::AssetManager* assetManager, + uint32_t& outCost, uint32_t& outPowerType); + /// Returns a WoW spell link string if the user shift-clicked a spell, then clears it. std::string getAndClearPendingChatLink() { std::string out = std::move(pendingChatSpellLink_); diff --git a/src/ui/game_screen.cpp b/src/ui/game_screen.cpp index bd3a7921..17b9a518 100644 --- a/src/ui/game_screen.cpp +++ b/src/ui/game_screen.cpp @@ -4990,6 +4990,26 @@ void GameScreen::renderActionBar(game::GameHandler& gameHandler) { } } + // Insufficient-power check: orange tint when player doesn't have enough power to cast. + // Only applies to SPELL slots with a known power cost and when not already on cooldown. + bool insufficientPower = false; + if (!slot.isEmpty() && slot.type == game::ActionBarSlot::SPELL && slot.id != 0 + && !onCooldown) { + uint32_t spellCost = 0, spellPowerType = 0; + spellbookScreen.getSpellPowerInfo(slot.id, assetMgr, spellCost, spellPowerType); + if (spellCost > 0) { + auto playerEnt = gameHandler.getEntityManager().getEntity(gameHandler.getPlayerGuid()); + if (playerEnt && (playerEnt->getType() == game::ObjectType::PLAYER || + playerEnt->getType() == game::ObjectType::UNIT)) { + auto unit = std::static_pointer_cast(playerEnt); + if (unit->getPowerType() == static_cast(spellPowerType)) { + if (unit->getPower() < spellCost) + insufficientPower = true; + } + } + } + } + auto getSpellName = [&](uint32_t spellId) -> std::string { std::string name = spellbookScreen.lookupSpellName(spellId, assetMgr); if (!name.empty()) return name; @@ -5043,16 +5063,18 @@ void GameScreen::renderActionBar(game::GameHandler& gameHandler) { if (onCooldown) { tintColor = ImVec4(0.4f, 0.4f, 0.4f, 0.8f); } else if (onGCD) { tintColor = ImVec4(0.6f, 0.6f, 0.6f, 0.85f); } else if (outOfRange) { tintColor = ImVec4(0.85f, 0.35f, 0.35f, 0.9f); } + else if (insufficientPower) { tintColor = ImVec4(0.6f, 0.5f, 0.9f, 0.85f); } clicked = ImGui::ImageButton("##icon", (ImTextureID)(uintptr_t)iconTex, ImVec2(slotSize, slotSize), ImVec2(0, 0), ImVec2(1, 1), bgColor, tintColor); } else { - if (onCooldown) ImGui::PushStyleColor(ImGuiCol_Button, ImVec4(0.2f, 0.2f, 0.2f, 0.8f)); - else if (outOfRange) ImGui::PushStyleColor(ImGuiCol_Button, ImVec4(0.45f, 0.15f, 0.15f, 0.9f)); - else if (slot.isEmpty())ImGui::PushStyleColor(ImGuiCol_Button, ImVec4(0.15f, 0.15f, 0.15f, 0.8f)); - else ImGui::PushStyleColor(ImGuiCol_Button, ImVec4(0.3f, 0.3f, 0.5f, 0.9f)); + if (onCooldown) ImGui::PushStyleColor(ImGuiCol_Button, ImVec4(0.2f, 0.2f, 0.2f, 0.8f)); + else if (outOfRange) ImGui::PushStyleColor(ImGuiCol_Button, ImVec4(0.45f, 0.15f, 0.15f, 0.9f)); + else if (insufficientPower)ImGui::PushStyleColor(ImGuiCol_Button, ImVec4(0.2f, 0.15f, 0.4f, 0.9f)); + else if (slot.isEmpty()) ImGui::PushStyleColor(ImGuiCol_Button, ImVec4(0.15f, 0.15f, 0.15f, 0.8f)); + else ImGui::PushStyleColor(ImGuiCol_Button, ImVec4(0.3f, 0.3f, 0.5f, 0.9f)); char label[32]; if (slot.type == game::ActionBarSlot::SPELL) { @@ -5163,6 +5185,9 @@ void GameScreen::renderActionBar(game::GameHandler& gameHandler) { if (outOfRange) { ImGui::TextColored(ImVec4(1.0f, 0.35f, 0.35f, 1.0f), "Out of range"); } + if (insufficientPower) { + ImGui::TextColored(ImVec4(0.75f, 0.55f, 1.0f, 1.0f), "Not enough power"); + } if (onCooldown) { float cd = slot.cooldownRemaining; if (cd >= 60.0f) diff --git a/src/ui/spellbook_screen.cpp b/src/ui/spellbook_screen.cpp index 60211f3f..8c78ab7d 100644 --- a/src/ui/spellbook_screen.cpp +++ b/src/ui/spellbook_screen.cpp @@ -212,6 +212,20 @@ uint32_t SpellbookScreen::getSpellMaxRange(uint32_t spellId, pipeline::AssetMana return 0; } +void SpellbookScreen::getSpellPowerInfo(uint32_t spellId, pipeline::AssetManager* assetManager, + uint32_t& outCost, uint32_t& outPowerType) { + outCost = 0; + outPowerType = 0; + if (!dbcLoadAttempted) { + loadSpellDBC(assetManager); + } + auto it = spellData.find(spellId); + if (it != spellData.end()) { + outCost = it->second.manaCost; + outPowerType = it->second.powerType; + } +} + void SpellbookScreen::loadSpellIconDBC(pipeline::AssetManager* assetManager) { if (iconDbLoaded) return; iconDbLoaded = true;