From a4c23b7fa20e050267a6448d7a1b63af6cf69479 Mon Sep 17 00:00:00 2001 From: Kelsi Date: Thu, 12 Mar 2026 11:44:30 -0700 Subject: [PATCH] feat: add per-unit aura cache and dispellable debuff indicators on party frames Extend the aura tracking system to cache auras for any unit (not just player and current target), so healers can see dispellable debuffs on party members. Colored 8px dots appear below the power bar: Magic=blue, Curse=purple, Disease=brown, Poison=green One dot per dispel type; non-dispellable auras are suppressed. Cache is populated via existing SMSG_AURA_UPDATE/SMSG_AURA_UPDATE_ALL handling and cleared on world exit. --- include/game/game_handler.hpp | 6 +++++ src/game/game_handler.cpp | 5 ++++ src/ui/game_screen.cpp | 51 +++++++++++++++++++++++++++++++++++ 3 files changed, 62 insertions(+) diff --git a/include/game/game_handler.hpp b/include/game/game_handler.hpp index 17b66b75..227a27d2 100644 --- a/include/game/game_handler.hpp +++ b/include/game/game_handler.hpp @@ -705,6 +705,11 @@ public: // Auras const std::vector& getPlayerAuras() const { return playerAuras; } const std::vector& getTargetAuras() const { return targetAuras; } + // Per-unit aura cache (populated for party members and any unit we receive updates for) + const std::vector* getUnitAuras(uint64_t guid) const { + auto it = unitAurasCache_.find(guid); + return (it != unitAurasCache_.end()) ? &it->second : nullptr; + } // Completed quests (populated from SMSG_QUERY_QUESTS_COMPLETED_RESPONSE) bool isQuestCompleted(uint32_t questId) const { return completedQuests_.count(questId) > 0; } @@ -2247,6 +2252,7 @@ private: std::array actionBar{}; std::vector playerAuras; std::vector targetAuras; + std::unordered_map> unitAurasCache_; // per-unit aura cache uint64_t petGuid_ = 0; uint32_t petActionSlots_[10] = {}; // SMSG_PET_SPELLS action bar (10 slots) uint8_t petCommand_ = 1; // 0=stay,1=follow,2=attack,3=dismiss diff --git a/src/game/game_handler.cpp b/src/game/game_handler.cpp index ccc620fc..6f59d00a 100644 --- a/src/game/game_handler.cpp +++ b/src/game/game_handler.cpp @@ -6708,6 +6708,7 @@ void GameHandler::selectCharacter(uint64_t characterGuid) { actionBar = {}; playerAuras.clear(); targetAuras.clear(); + unitAurasCache_.clear(); unitCastStates_.clear(); petGuid_ = 0; playerXp_ = 0; @@ -14595,6 +14596,10 @@ void GameHandler::handleAuraUpdate(network::Packet& packet, bool isAll) { } else if (data.guid == targetGuid) { auraList = &targetAuras; } + // Also maintain a per-unit cache for any unit (party members, etc.) + if (data.guid != 0 && data.guid != playerGuid && data.guid != targetGuid) { + auraList = &unitAurasCache_[data.guid]; + } if (auraList) { if (isAll) { diff --git a/src/ui/game_screen.cpp b/src/ui/game_screen.cpp index c841cd0d..1e106f35 100644 --- a/src/ui/game_screen.cpp +++ b/src/ui/game_screen.cpp @@ -8379,6 +8379,57 @@ void GameScreen::renderPartyFrames(game::GameHandler& gameHandler) { ImGui::PopStyleColor(); } + // Dispellable debuff indicators — small colored dots for party member debuffs + // Only show magic/curse/disease/poison (types 1-4); skip non-dispellable + if (!memberDead && !memberOffline) { + const std::vector* unitAuras = nullptr; + if (member.guid == gameHandler.getPlayerGuid()) + unitAuras = &gameHandler.getPlayerAuras(); + else if (member.guid == gameHandler.getTargetGuid()) + unitAuras = &gameHandler.getTargetAuras(); + else + unitAuras = gameHandler.getUnitAuras(member.guid); + + if (unitAuras) { + bool anyDebuff = false; + for (const auto& aura : *unitAuras) { + if (aura.isEmpty()) continue; + if ((aura.flags & 0x80) == 0) continue; // only debuffs + uint8_t dt = gameHandler.getSpellDispelType(aura.spellId); + if (dt == 0) continue; // skip non-dispellable + anyDebuff = true; + break; + } + if (anyDebuff) { + // Render one dot per unique dispel type present + bool shown[5] = {}; + ImGui::PushStyleVar(ImGuiStyleVar_ItemSpacing, ImVec2(2.0f, 1.0f)); + for (const auto& aura : *unitAuras) { + if (aura.isEmpty()) continue; + if ((aura.flags & 0x80) == 0) continue; + uint8_t dt = gameHandler.getSpellDispelType(aura.spellId); + if (dt == 0 || dt > 4 || shown[dt]) continue; + shown[dt] = true; + ImVec4 dotCol; + switch (dt) { + case 1: dotCol = ImVec4(0.25f, 0.50f, 1.00f, 1.0f); break; // Magic: blue + case 2: dotCol = ImVec4(0.70f, 0.15f, 0.90f, 1.0f); break; // Curse: purple + case 3: dotCol = ImVec4(0.65f, 0.45f, 0.10f, 1.0f); break; // Disease: brown + case 4: dotCol = ImVec4(0.10f, 0.75f, 0.10f, 1.0f); break; // Poison: green + default: break; + } + ImGui::PushStyleColor(ImGuiCol_Button, dotCol); + ImGui::PushStyleColor(ImGuiCol_ButtonHovered, dotCol); + ImGui::Button("##d", ImVec2(8.0f, 8.0f)); + ImGui::PopStyleColor(2); + ImGui::SameLine(); + } + ImGui::NewLine(); + ImGui::PopStyleVar(); + } + } + } + // Party member cast bar — shows when the party member is casting if (auto* cs = gameHandler.getUnitCastState(member.guid)) { float castPct = (cs->timeTotal > 0.0f)