From 8081a43d8562fb6d0597758a23cdf0a13dc5afa4 Mon Sep 17 00:00:00 2001 From: Kelsi Date: Thu, 12 Mar 2026 05:57:45 -0700 Subject: [PATCH] feat: add out-of-range tint to action bar spell slots MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Ranged spell icons dim to a red tint when the current target is farther than the spell's max range (read from SpellRange.dbc via spellbook data). Melee/self spells (max range ≤ 5 yd or unknown) are excluded. The spell tooltip also shows "Out of range" in red when applicable. Adds SpellbookScreen::getSpellMaxRange() as a public accessor so game_screen can query DBC range data without duplicating DBC loading. --- include/ui/spellbook_screen.hpp | 4 ++++ src/ui/game_screen.cpp | 26 ++++++++++++++++++++++++++ src/ui/spellbook_screen.cpp | 9 +++++++++ 3 files changed, 39 insertions(+) diff --git a/include/ui/spellbook_screen.hpp b/include/ui/spellbook_screen.hpp index 6cc13270..8059ddf2 100644 --- a/include/ui/spellbook_screen.hpp +++ b/include/ui/spellbook_screen.hpp @@ -54,6 +54,10 @@ public: uint32_t getDragSpellId() const { return dragSpellId_; } void consumeDragSpell() { draggingSpell_ = false; dragSpellId_ = 0; dragSpellIconTex_ = VK_NULL_HANDLE; } + /// Returns the max range in yards for a spell (0 if self-cast, unknown, or melee). + /// 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 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 80f62e09..bd3a7921 100644 --- a/src/ui/game_screen.cpp +++ b/src/ui/game_screen.cpp @@ -4969,6 +4969,27 @@ void GameScreen::renderActionBar(game::GameHandler& gameHandler) { bool onCooldown = !slot.isReady(); const bool onGCD = gameHandler.isGCDActive() && !onCooldown && !slot.isEmpty(); + // Out-of-range check: red tint when a targeted spell cannot reach the current target. + // Only applies to SPELL slots with a known max range (>5 yd) and an active target. + bool outOfRange = false; + if (!slot.isEmpty() && slot.type == game::ActionBarSlot::SPELL && slot.id != 0 + && !onCooldown && gameHandler.hasTarget()) { + uint32_t maxRange = spellbookScreen.getSpellMaxRange(slot.id, assetMgr); + if (maxRange > 5) { // >5 yd = not melee/self + auto& em = gameHandler.getEntityManager(); + auto playerEnt = em.getEntity(gameHandler.getPlayerGuid()); + auto targetEnt = em.getEntity(gameHandler.getTargetGuid()); + if (playerEnt && targetEnt) { + float dx = playerEnt->getX() - targetEnt->getX(); + float dy = playerEnt->getY() - targetEnt->getY(); + float dz = playerEnt->getZ() - targetEnt->getZ(); + float dist = std::sqrt(dx * dx + dy * dy + dz * dz); + if (dist > static_cast(maxRange)) + outOfRange = true; + } + } + } + auto getSpellName = [&](uint32_t spellId) -> std::string { std::string name = spellbookScreen.lookupSpellName(spellId, assetMgr); if (!name.empty()) return name; @@ -5021,6 +5042,7 @@ void GameScreen::renderActionBar(game::GameHandler& gameHandler) { ImVec4 bgColor(0.1f, 0.1f, 0.1f, 0.9f); 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); } clicked = ImGui::ImageButton("##icon", (ImTextureID)(uintptr_t)iconTex, ImVec2(slotSize, slotSize), @@ -5028,6 +5050,7 @@ void GameScreen::renderActionBar(game::GameHandler& gameHandler) { 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)); @@ -5137,6 +5160,9 @@ void GameScreen::renderActionBar(game::GameHandler& gameHandler) { ImGui::TextColored(ImVec4(0.8f, 0.9f, 1.0f, 1.0f), "Home: %s", mapName); } } + if (outOfRange) { + ImGui::TextColored(ImVec4(1.0f, 0.35f, 0.35f, 1.0f), "Out of range"); + } 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 e2c81756..60211f3f 100644 --- a/src/ui/spellbook_screen.cpp +++ b/src/ui/spellbook_screen.cpp @@ -203,6 +203,15 @@ std::string SpellbookScreen::lookupSpellName(uint32_t spellId, pipeline::AssetMa return {}; } +uint32_t SpellbookScreen::getSpellMaxRange(uint32_t spellId, pipeline::AssetManager* assetManager) { + if (!dbcLoadAttempted) { + loadSpellDBC(assetManager); + } + auto it = spellData.find(spellId); + if (it != spellData.end()) return it->second.rangeIndex; + return 0; +} + void SpellbookScreen::loadSpellIconDBC(pipeline::AssetManager* assetManager) { if (iconDbLoaded) return; iconDbLoaded = true;