feat: add Shaman totem frame with element-colored duration bars

- Renders below pet frame, visible only for Shaman (class 7)
- Shows each active totem (Earth/Fire/Water/Air) with its spell name
  and a colored countdown progress bar
- Colored element dot (brown/red/blue/light-blue) identifies element
- Only rendered when at least one totem is active
This commit is contained in:
Kelsi 2026-03-12 10:14:44 -07:00
parent ef08366efc
commit 1f7f1076ca
2 changed files with 87 additions and 0 deletions

View file

@ -283,6 +283,7 @@ private:
* Render pet frame (below player frame when player has an active pet)
*/
void renderPetFrame(game::GameHandler& gameHandler);
void renderTotemFrame(game::GameHandler& gameHandler);
/**
* Process targeting input (Tab, Escape, click)

View file

@ -516,6 +516,11 @@ void GameScreen::render(game::GameHandler& gameHandler) {
renderPetFrame(gameHandler);
}
// Totem frame (Shaman only, when any totem is active)
if (gameHandler.getPlayerClass() == 7) {
renderTotemFrame(gameHandler);
}
// Target frame (only when we have a target)
if (gameHandler.hasTarget()) {
renderTargetFrame(gameHandler);
@ -3032,6 +3037,87 @@ void GameScreen::renderPetFrame(game::GameHandler& gameHandler) {
ImGui::PopStyleVar();
}
// ============================================================
// Totem Frame (Shaman — below pet frame / player frame)
// ============================================================
void GameScreen::renderTotemFrame(game::GameHandler& gameHandler) {
// Only show if at least one totem is active
bool anyActive = false;
for (int i = 0; i < game::GameHandler::NUM_TOTEM_SLOTS; ++i) {
if (gameHandler.getTotemSlot(i).active()) { anyActive = true; break; }
}
if (!anyActive) return;
static const struct { const char* name; ImU32 color; } kTotemInfo[4] = {
{ "Earth", IM_COL32(139, 90, 43, 255) }, // brown
{ "Fire", IM_COL32(220, 80, 30, 255) }, // red-orange
{ "Water", IM_COL32( 30,120, 220, 255) }, // blue
{ "Air", IM_COL32(180,220, 255, 255) }, // light blue
};
// Position: below pet frame / player frame, left side
// Pet frame is at ~y=200 if active, player frame is at y=20; totem frame near y=300
// We anchor relative to screen left edge like pet frame
ImGui::SetNextWindowPos(ImVec2(8.0f, 300.0f), ImGuiCond_FirstUseEver);
ImGui::SetNextWindowSize(ImVec2(130.0f, 0.0f), ImGuiCond_Always);
ImGuiWindowFlags flags = ImGuiWindowFlags_NoResize | ImGuiWindowFlags_NoCollapse |
ImGuiWindowFlags_NoScrollbar | ImGuiWindowFlags_AlwaysAutoResize |
ImGuiWindowFlags_NoTitleBar;
ImGui::PushStyleVar(ImGuiStyleVar_WindowRounding, 4.0f);
ImGui::PushStyleColor(ImGuiCol_WindowBg, ImVec4(0.1f, 0.08f, 0.06f, 0.88f));
if (ImGui::Begin("##TotemFrame", nullptr, flags)) {
ImGui::TextColored(ImVec4(0.9f, 0.75f, 0.3f, 1.0f), "Totems");
ImGui::Separator();
for (int i = 0; i < game::GameHandler::NUM_TOTEM_SLOTS; ++i) {
const auto& slot = gameHandler.getTotemSlot(i);
if (!slot.active()) continue;
ImGui::PushID(i);
// Colored element dot
ImVec2 dotPos = ImGui::GetCursorScreenPos();
dotPos.x += 4.0f; dotPos.y += 6.0f;
ImGui::GetWindowDrawList()->AddCircleFilled(
ImVec2(dotPos.x + 4.0f, dotPos.y + 4.0f), 4.0f, kTotemInfo[i].color);
ImGui::SetCursorPosX(ImGui::GetCursorPosX() + 14.0f);
// Totem name or spell name
const std::string& spellName = gameHandler.getSpellName(slot.spellId);
const char* displayName = spellName.empty() ? kTotemInfo[i].name : spellName.c_str();
ImGui::Text("%s", displayName);
// Duration countdown bar
float remMs = slot.remainingMs();
float totMs = static_cast<float>(slot.durationMs);
float frac = (totMs > 0.0f) ? std::min(remMs / totMs, 1.0f) : 0.0f;
float remSec = remMs / 1000.0f;
// Color bar with totem element tint
ImVec4 barCol(
static_cast<float>((kTotemInfo[i].color >> IM_COL32_R_SHIFT) & 0xFF) / 255.0f,
static_cast<float>((kTotemInfo[i].color >> IM_COL32_G_SHIFT) & 0xFF) / 255.0f,
static_cast<float>((kTotemInfo[i].color >> IM_COL32_B_SHIFT) & 0xFF) / 255.0f,
0.9f);
ImGui::PushStyleColor(ImGuiCol_PlotHistogram, barCol);
char timeBuf[16];
snprintf(timeBuf, sizeof(timeBuf), "%.0fs", remSec);
ImGui::ProgressBar(frac, ImVec2(-1, 8), timeBuf);
ImGui::PopStyleColor();
ImGui::PopID();
}
}
ImGui::End();
ImGui::PopStyleColor();
ImGui::PopStyleVar();
}
void GameScreen::renderTargetFrame(game::GameHandler& gameHandler) {
auto target = gameHandler.getTarget();
if (!target) return;