From 78ad20f95dda9455d47583fb05488f4e29fab1b4 Mon Sep 17 00:00:00 2001 From: Kelsi Date: Thu, 12 Mar 2026 15:25:07 -0700 Subject: [PATCH] feat: add cooldown tracker panel showing all active spell cooldowns A new opt-in panel (Settings > Interface > Show Cooldown Tracker) lists all spells currently on cooldown, sorted longest-to-shortest, with spell icons and color-coded remaining time (red>30s, orange>10s, yellow>5s, green<5s). Adds getSpellCooldowns() accessor to GameHandler. Setting persists to ~/.wowee/settings.cfg. --- include/game/game_handler.hpp | 1 + include/ui/game_screen.hpp | 4 ++ src/ui/game_screen.cpp | 102 ++++++++++++++++++++++++++++++++++ 3 files changed, 107 insertions(+) diff --git a/include/game/game_handler.hpp b/include/game/game_handler.hpp index fefedffc..81b02737 100644 --- a/include/game/game_handler.hpp +++ b/include/game/game_handler.hpp @@ -983,6 +983,7 @@ public: // Cooldowns float getSpellCooldown(uint32_t spellId) const; + const std::unordered_map& getSpellCooldowns() const { return spellCooldowns; } // Player GUID uint64_t getPlayerGuid() const { return playerGuid; } diff --git a/include/ui/game_screen.hpp b/include/ui/game_screen.hpp index d01b3638..5f75350c 100644 --- a/include/ui/game_screen.hpp +++ b/include/ui/game_screen.hpp @@ -314,6 +314,7 @@ private: void renderRepBar(game::GameHandler& gameHandler); void renderCastBar(game::GameHandler& gameHandler); void renderMirrorTimers(game::GameHandler& gameHandler); + void renderCooldownTracker(game::GameHandler& gameHandler); void renderCombatText(game::GameHandler& gameHandler); void renderRaidWarningOverlay(game::GameHandler& gameHandler); void renderPartyFrames(game::GameHandler& gameHandler); @@ -525,6 +526,9 @@ private: std::string lastKnownZoneName_; void renderZoneText(); + // Cooldown tracker + bool showCooldownTracker_ = false; + // DPS / HPS meter bool showDPSMeter_ = false; float dpsCombatAge_ = 0.0f; // seconds in current combat (for accurate early-combat DPS) diff --git a/src/ui/game_screen.cpp b/src/ui/game_screen.cpp index f7c2f8f8..4ba9fd94 100644 --- a/src/ui/game_screen.cpp +++ b/src/ui/game_screen.cpp @@ -559,6 +559,7 @@ void GameScreen::render(game::GameHandler& gameHandler) { renderRepBar(gameHandler); renderCastBar(gameHandler); renderMirrorTimers(gameHandler); + renderCooldownTracker(gameHandler); renderQuestObjectiveTracker(gameHandler); renderNameplates(gameHandler); // player names always shown; NPC plates gated by showNameplates_ renderBattlegroundScore(gameHandler); @@ -7532,6 +7533,98 @@ void GameScreen::renderMirrorTimers(game::GameHandler& gameHandler) { } } +// ============================================================ +// Cooldown Tracker — floating panel showing all active spell CDs +// ============================================================ + +void GameScreen::renderCooldownTracker(game::GameHandler& gameHandler) { + if (!showCooldownTracker_) return; + + const auto& cooldowns = gameHandler.getSpellCooldowns(); + if (cooldowns.empty()) return; + + // Collect spells with remaining cooldown > 0.5s (skip GCD noise) + struct CDEntry { uint32_t spellId; float remaining; }; + std::vector active; + active.reserve(16); + for (const auto& [sid, rem] : cooldowns) { + if (rem > 0.5f) active.push_back({sid, rem}); + } + if (active.empty()) return; + + // Sort: longest remaining first + std::sort(active.begin(), active.end(), [](const CDEntry& a, const CDEntry& b) { + return a.remaining > b.remaining; + }); + + auto* assetMgr = core::Application::getInstance().getAssetManager(); + auto* window = core::Application::getInstance().getWindow(); + float screenW = window ? static_cast(window->getWidth()) : 1280.0f; + float screenH = window ? static_cast(window->getHeight()) : 720.0f; + + constexpr float TRACKER_W = 200.0f; + constexpr int MAX_SHOWN = 12; + float posX = screenW - TRACKER_W - 10.0f; + float posY = screenH - 220.0f; // above the action bar area + + ImGui::SetNextWindowPos(ImVec2(posX, posY), ImGuiCond_Always, ImVec2(1.0f, 1.0f)); + ImGui::SetNextWindowSize(ImVec2(TRACKER_W, 0.0f), ImGuiCond_Always); + ImGui::SetNextWindowBgAlpha(0.75f); + + ImGuiWindowFlags flags = ImGuiWindowFlags_NoDecoration | ImGuiWindowFlags_NoNav | + ImGuiWindowFlags_NoMove | ImGuiWindowFlags_AlwaysAutoResize | + ImGuiWindowFlags_NoBringToFrontOnFocus; + + ImGui::PushStyleVar(ImGuiStyleVar_WindowRounding, 4.0f); + ImGui::PushStyleVar(ImGuiStyleVar_WindowPadding, ImVec2(4.0f, 4.0f)); + ImGui::PushStyleVar(ImGuiStyleVar_ItemSpacing, ImVec2(4.0f, 2.0f)); + + if (ImGui::Begin("##CooldownTracker", nullptr, flags)) { + ImGui::TextDisabled("Cooldowns"); + ImGui::Separator(); + + int shown = 0; + for (const auto& cd : active) { + if (shown >= MAX_SHOWN) break; + + const std::string& name = gameHandler.getSpellName(cd.spellId); + if (name.empty()) continue; // skip unnamed spells (internal/passive) + + // Small icon if available + VkDescriptorSet icon = assetMgr ? getSpellIcon(cd.spellId, assetMgr) : VK_NULL_HANDLE; + if (icon) { + ImGui::Image((ImTextureID)(uintptr_t)icon, ImVec2(14, 14)); + ImGui::SameLine(0, 3); + } + + // Name (truncated) + remaining time + char timeStr[16]; + if (cd.remaining >= 60.0f) + snprintf(timeStr, sizeof(timeStr), "%dm%ds", (int)cd.remaining / 60, (int)cd.remaining % 60); + else + snprintf(timeStr, sizeof(timeStr), "%.0fs", cd.remaining); + + // Color: red > 30s, orange > 10s, yellow > 5s, green otherwise + ImVec4 cdColor = cd.remaining > 30.0f ? ImVec4(1.0f, 0.3f, 0.3f, 1.0f) : + cd.remaining > 10.0f ? ImVec4(1.0f, 0.6f, 0.2f, 1.0f) : + cd.remaining > 5.0f ? ImVec4(1.0f, 1.0f, 0.3f, 1.0f) : + ImVec4(0.5f, 1.0f, 0.5f, 1.0f); + + // Truncate name to fit + std::string displayName = name; + if (displayName.size() > 16) displayName = displayName.substr(0, 15) + "\xe2\x80\xa6"; // ellipsis + + ImGui::TextColored(ImVec4(0.9f, 0.9f, 0.9f, 1.0f), "%s", displayName.c_str()); + ImGui::SameLine(TRACKER_W - 48.0f); + ImGui::TextColored(cdColor, "%s", timeStr); + + ++shown; + } + } + ImGui::End(); + ImGui::PopStyleVar(3); +} + // ============================================================ // Quest Objective Tracker (right-side HUD) // ============================================================ @@ -14030,6 +14123,12 @@ void GameScreen::renderSettingsWindow() { ImGui::SameLine(); ImGui::TextDisabled("(damage/healing per second above action bar)"); + if (ImGui::Checkbox("Show Cooldown Tracker", &showCooldownTracker_)) { + saveSettings(); + } + ImGui::SameLine(); + ImGui::TextDisabled("(active spell cooldowns near action bar)"); + ImGui::Spacing(); ImGui::SeparatorText("Screen Effects"); ImGui::Spacing(); @@ -16109,6 +16208,7 @@ void GameScreen::saveSettings() { out << "minimap_npc_dots=" << (pendingMinimapNpcDots ? 1 : 0) << "\n"; out << "show_latency_meter=" << (pendingShowLatencyMeter ? 1 : 0) << "\n"; out << "show_dps_meter=" << (showDPSMeter_ ? 1 : 0) << "\n"; + out << "show_cooldown_tracker=" << (showCooldownTracker_ ? 1 : 0) << "\n"; out << "separate_bags=" << (pendingSeparateBags ? 1 : 0) << "\n"; out << "action_bar_scale=" << pendingActionBarScale << "\n"; out << "nameplate_scale=" << nameplateScale_ << "\n"; @@ -16219,6 +16319,8 @@ void GameScreen::loadSettings() { pendingShowLatencyMeter = showLatencyMeter_; } else if (key == "show_dps_meter") { showDPSMeter_ = (std::stoi(val) != 0); + } else if (key == "show_cooldown_tracker") { + showCooldownTracker_ = (std::stoi(val) != 0); } else if (key == "separate_bags") { pendingSeparateBags = (std::stoi(val) != 0); inventoryScreen.setSeparateBags(pendingSeparateBags);