feat: add out-of-range tint to action bar spell slots

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.
This commit is contained in:
Kelsi 2026-03-12 05:57:45 -07:00
parent bc5a7867a9
commit 8081a43d85
3 changed files with 39 additions and 0 deletions

View file

@ -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_);

View file

@ -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<float>(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)

View file

@ -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;