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.
This commit is contained in:
Kelsi 2026-03-12 11:44:30 -07:00
parent 9c276d1072
commit a4c23b7fa2
3 changed files with 62 additions and 0 deletions

View file

@ -705,6 +705,11 @@ public:
// Auras
const std::vector<AuraSlot>& getPlayerAuras() const { return playerAuras; }
const std::vector<AuraSlot>& getTargetAuras() const { return targetAuras; }
// Per-unit aura cache (populated for party members and any unit we receive updates for)
const std::vector<AuraSlot>* 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<ActionBarSlot, ACTION_BAR_SLOTS> actionBar{};
std::vector<AuraSlot> playerAuras;
std::vector<AuraSlot> targetAuras;
std::unordered_map<uint64_t, std::vector<AuraSlot>> 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

View file

@ -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) {

View file

@ -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<game::AuraSlot>* 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)