From 3d1b18798631859b45f3c8cb30f7d616d4af82d0 Mon Sep 17 00:00:00 2001 From: Kelsi Date: Thu, 12 Mar 2026 13:43:12 -0700 Subject: [PATCH] feat: add aura icons to boss frame with DoT tracking and duration overlays --- src/ui/game_screen.cpp | 135 +++++++++++++++++++++++++++++++++++++++++ 1 file changed, 135 insertions(+) diff --git a/src/ui/game_screen.cpp b/src/ui/game_screen.cpp index d062be49..a05bd8ed 100644 --- a/src/ui/game_screen.cpp +++ b/src/ui/game_screen.cpp @@ -9339,6 +9339,141 @@ void GameScreen::renderBossFrames(game::GameHandler& gameHandler) { ImGui::PopStyleColor(); } + // Boss aura row: debuffs first (player DoTs), then boss buffs + { + const std::vector* bossAuras = nullptr; + if (bs.guid == gameHandler.getTargetGuid()) + bossAuras = &gameHandler.getTargetAuras(); + else + bossAuras = gameHandler.getUnitAuras(bs.guid); + + if (bossAuras) { + int bossActive = 0; + for (const auto& a : *bossAuras) if (!a.isEmpty()) bossActive++; + if (bossActive > 0) { + constexpr float BA_ICON = 16.0f; + constexpr int BA_PER_ROW = 10; + + uint64_t baNowMs = static_cast( + std::chrono::duration_cast( + std::chrono::steady_clock::now().time_since_epoch()).count()); + + // Sort: player-applied debuffs first (most relevant), then others + const uint64_t pguid = gameHandler.getPlayerGuid(); + std::vector baIdx; + baIdx.reserve(bossAuras->size()); + for (size_t i = 0; i < bossAuras->size(); ++i) + if (!(*bossAuras)[i].isEmpty()) baIdx.push_back(i); + std::sort(baIdx.begin(), baIdx.end(), [&](size_t a, size_t b) { + const auto& aa = (*bossAuras)[a]; + const auto& ab = (*bossAuras)[b]; + bool aPlayerDot = (aa.flags & 0x80) != 0 && aa.casterGuid == pguid; + bool bPlayerDot = (ab.flags & 0x80) != 0 && ab.casterGuid == pguid; + if (aPlayerDot != bPlayerDot) return aPlayerDot > bPlayerDot; + bool aDebuff = (aa.flags & 0x80) != 0; + bool bDebuff = (ab.flags & 0x80) != 0; + if (aDebuff != bDebuff) return aDebuff > bDebuff; + int32_t ra = aa.getRemainingMs(baNowMs); + int32_t rb = ab.getRemainingMs(baNowMs); + if (ra < 0 && rb < 0) return false; + if (ra < 0) return false; + if (rb < 0) return true; + return ra < rb; + }); + + ImGui::PushStyleVar(ImGuiStyleVar_ItemSpacing, ImVec2(2.0f, 2.0f)); + int baShown = 0; + for (size_t si = 0; si < baIdx.size() && baShown < 20; ++si) { + const auto& aura = (*bossAuras)[baIdx[si]]; + bool isBuff = (aura.flags & 0x80) == 0; + bool isPlayerCast = (aura.casterGuid == pguid); + + if (baShown > 0 && baShown % BA_PER_ROW != 0) ImGui::SameLine(); + ImGui::PushID(static_cast(baIdx[si]) + 7000); + + ImVec4 borderCol; + if (isBuff) { + // Boss buffs: gold for important enrage/shield types + borderCol = ImVec4(0.8f, 0.6f, 0.1f, 0.9f); + } else { + uint8_t dt = gameHandler.getSpellDispelType(aura.spellId); + switch (dt) { + case 1: borderCol = ImVec4(0.15f, 0.50f, 1.00f, 0.9f); break; + case 2: borderCol = ImVec4(0.70f, 0.20f, 0.90f, 0.9f); break; + case 3: borderCol = ImVec4(0.55f, 0.30f, 0.10f, 0.9f); break; + case 4: borderCol = ImVec4(0.10f, 0.70f, 0.10f, 0.9f); break; + default: borderCol = isPlayerCast + ? ImVec4(0.90f, 0.30f, 0.10f, 0.9f) // player DoT: orange-red + : ImVec4(0.60f, 0.20f, 0.20f, 0.9f); // other debuff: dark red + break; + } + } + + VkDescriptorSet baIcon = assetMgr + ? getSpellIcon(aura.spellId, assetMgr) : VK_NULL_HANDLE; + if (baIcon) { + ImGui::PushStyleColor(ImGuiCol_Button, borderCol); + ImGui::PushStyleVar(ImGuiStyleVar_FramePadding, ImVec2(1, 1)); + ImGui::ImageButton("##baura", + (ImTextureID)(uintptr_t)baIcon, + ImVec2(BA_ICON - 2, BA_ICON - 2)); + ImGui::PopStyleVar(); + ImGui::PopStyleColor(); + } else { + ImGui::PushStyleColor(ImGuiCol_Button, borderCol); + char lab[8]; + snprintf(lab, sizeof(lab), "%u", aura.spellId % 10000); + ImGui::Button(lab, ImVec2(BA_ICON, BA_ICON)); + ImGui::PopStyleColor(); + } + + // Duration overlay + int32_t baRemain = aura.getRemainingMs(baNowMs); + if (baRemain > 0) { + ImVec2 imin = ImGui::GetItemRectMin(); + ImVec2 imax = ImGui::GetItemRectMax(); + char ts[12]; + int s = (baRemain + 999) / 1000; + if (s >= 3600) snprintf(ts, sizeof(ts), "%dh", s / 3600); + else if (s >= 60) snprintf(ts, sizeof(ts), "%d:%02d", s / 60, s % 60); + else snprintf(ts, sizeof(ts), "%d", s); + ImVec2 tsz = ImGui::CalcTextSize(ts); + float cx = imin.x + (imax.x - imin.x - tsz.x) * 0.5f; + float cy = imax.y - tsz.y; + ImGui::GetWindowDrawList()->AddText(ImVec2(cx + 1, cy + 1), IM_COL32(0, 0, 0, 180), ts); + ImGui::GetWindowDrawList()->AddText(ImVec2(cx, cy), IM_COL32(255, 255, 255, 220), ts); + } + + // Tooltip + if (ImGui::IsItemHovered()) { + ImGui::BeginTooltip(); + bool richOk = spellbookScreen.renderSpellInfoTooltip( + aura.spellId, gameHandler, assetMgr); + if (!richOk) { + std::string nm = spellbookScreen.lookupSpellName(aura.spellId, assetMgr); + if (nm.empty()) nm = "Spell #" + std::to_string(aura.spellId); + ImGui::Text("%s", nm.c_str()); + } + if (isPlayerCast && !isBuff) + ImGui::TextColored(ImVec4(0.9f, 0.7f, 0.3f, 1.0f), "Your DoT"); + if (baRemain > 0) { + int s = baRemain / 1000; + char db[32]; + if (s < 60) snprintf(db, sizeof(db), "Remaining: %ds", s); + else snprintf(db, sizeof(db), "Remaining: %dm %ds", s / 60, s % 60); + ImGui::TextColored(ImVec4(0.7f, 0.7f, 0.7f, 1.0f), "%s", db); + } + ImGui::EndTooltip(); + } + + ImGui::PopID(); + baShown++; + } + ImGui::PopStyleVar(); + } + } + } + ImGui::PopID(); ImGui::Spacing(); }