feat: add Shaman totem bar in player frame

Store active totem state (slot, spellId, duration, placedAt) from
SMSG_TOTEM_CREATED. Render 4 element slots (Earth/Fire/Water/Air) as
color-coded duration bars in the player frame for Shamans (class 7).
Shows countdown seconds, element letter when inactive, and tooltip
with spell name + remaining time on hover.
This commit is contained in:
Kelsi 2026-03-12 05:16:43 -07:00
parent 8efdaed7e4
commit 5827a8fcdd
3 changed files with 107 additions and 0 deletions

View file

@ -1277,6 +1277,26 @@ public:
}; };
const std::vector<FactionStandingInit>& getInitialFactions() const { return initialFactions_; } const std::vector<FactionStandingInit>& getInitialFactions() const { return initialFactions_; }
const std::unordered_map<uint32_t, int32_t>& getFactionStandings() const { return factionStandings_; } const std::unordered_map<uint32_t, int32_t>& getFactionStandings() const { return factionStandings_; }
// Shaman totems (4 slots: 0=Earth, 1=Fire, 2=Water, 3=Air)
struct TotemSlot {
uint32_t spellId = 0;
uint32_t durationMs = 0;
std::chrono::steady_clock::time_point placedAt{};
bool active() const { return spellId != 0 && remainingMs() > 0; }
float remainingMs() const {
if (spellId == 0 || durationMs == 0) return 0.0f;
auto elapsed = std::chrono::duration_cast<std::chrono::milliseconds>(
std::chrono::steady_clock::now() - placedAt).count();
float rem = static_cast<float>(durationMs) - static_cast<float>(elapsed);
return rem > 0.0f ? rem : 0.0f;
}
};
static constexpr int NUM_TOTEM_SLOTS = 4;
const TotemSlot& getTotemSlot(int slot) const {
static TotemSlot empty;
return (slot >= 0 && slot < NUM_TOTEM_SLOTS) ? activeTotemSlots_[slot] : empty;
}
const std::string& getFactionNamePublic(uint32_t factionId) const; const std::string& getFactionNamePublic(uint32_t factionId) const;
uint32_t getWatchedFactionId() const { return watchedFactionId_; } uint32_t getWatchedFactionId() const { return watchedFactionId_; }
void setWatchedFactionId(uint32_t id) { watchedFactionId_ = id; } void setWatchedFactionId(uint32_t id) { watchedFactionId_ = id; }
@ -2254,6 +2274,9 @@ private:
uint64_t myTradeGold_ = 0; uint64_t myTradeGold_ = 0;
uint64_t peerTradeGold_ = 0; uint64_t peerTradeGold_ = 0;
// Shaman totem state
TotemSlot activeTotemSlots_[NUM_TOTEM_SLOTS];
// Duel state // Duel state
bool pendingDuelRequest_ = false; bool pendingDuelRequest_ = false;
uint64_t duelChallengerGuid_= 0; uint64_t duelChallengerGuid_= 0;

View file

@ -3061,6 +3061,11 @@ void GameHandler::handlePacket(network::Packet& packet) {
uint32_t spellId = packet.readUInt32(); uint32_t spellId = packet.readUInt32();
LOG_DEBUG("SMSG_TOTEM_CREATED: slot=", (int)slot, LOG_DEBUG("SMSG_TOTEM_CREATED: slot=", (int)slot,
" spellId=", spellId, " duration=", duration, "ms"); " spellId=", spellId, " duration=", duration, "ms");
if (slot < NUM_TOTEM_SLOTS) {
activeTotemSlots_[slot].spellId = spellId;
activeTotemSlots_[slot].durationMs = duration;
activeTotemSlots_[slot].placedAt = std::chrono::steady_clock::now();
}
break; break;
} }
case Opcode::SMSG_AREA_SPIRIT_HEALER_TIME: { case Opcode::SMSG_AREA_SPIRIT_HEALER_TIME: {

View file

@ -2348,6 +2348,85 @@ void GameScreen::renderPlayerFrame(game::GameHandler& gameHandler) {
} }
} }
} }
// Shaman totem bar (class 7) — 4 slots: Earth, Fire, Water, Air
if (gameHandler.getPlayerClass() == 7) {
static const ImVec4 kTotemColors[] = {
ImVec4(0.80f, 0.55f, 0.25f, 1.0f), // Earth — brown
ImVec4(1.00f, 0.35f, 0.10f, 1.0f), // Fire — orange-red
ImVec4(0.20f, 0.55f, 0.90f, 1.0f), // Water — blue
ImVec4(0.70f, 0.90f, 1.00f, 1.0f), // Air — pale sky
};
static const char* kTotemNames[] = { "Earth", "Fire", "Water", "Air" };
ImGui::Spacing();
ImVec2 cursor = ImGui::GetCursorScreenPos();
float totalW = ImGui::GetContentRegionAvail().x;
float spacing = 3.0f;
float slotW = (totalW - spacing * 3.0f) / 4.0f;
float slotH = 14.0f;
ImDrawList* tdl = ImGui::GetWindowDrawList();
for (int i = 0; i < game::GameHandler::NUM_TOTEM_SLOTS; i++) {
const auto& ts = gameHandler.getTotemSlot(i);
float x0 = cursor.x + i * (slotW + spacing);
float y0 = cursor.y;
float x1 = x0 + slotW;
float y1 = y0 + slotH;
// Background
tdl->AddRectFilled(ImVec2(x0, y0), ImVec2(x1, y1), IM_COL32(20, 20, 20, 200), 2.0f);
if (ts.active()) {
float rem = ts.remainingMs();
float frac = rem / static_cast<float>(ts.durationMs);
float fillX = x0 + (x1 - x0) * frac;
tdl->AddRectFilled(ImVec2(x0, y0), ImVec2(fillX, y1),
ImGui::ColorConvertFloat4ToU32(kTotemColors[i]), 2.0f);
// Remaining seconds label
char secBuf[8];
snprintf(secBuf, sizeof(secBuf), "%.0f", rem / 1000.0f);
ImVec2 tsz = ImGui::CalcTextSize(secBuf);
float lx = x0 + (slotW - tsz.x) * 0.5f;
float ly = y0 + (slotH - tsz.y) * 0.5f;
tdl->AddText(ImVec2(lx + 1, ly + 1), IM_COL32(0, 0, 0, 180), secBuf);
tdl->AddText(ImVec2(lx, ly), IM_COL32(255, 255, 255, 230), secBuf);
} else {
// Inactive — show element letter
const char* letter = kTotemNames[i];
char single[2] = { letter[0], '\0' };
ImVec2 tsz = ImGui::CalcTextSize(single);
float lx = x0 + (slotW - tsz.x) * 0.5f;
float ly = y0 + (slotH - tsz.y) * 0.5f;
tdl->AddText(ImVec2(lx, ly), IM_COL32(80, 80, 80, 200), single);
}
// Border
ImU32 borderCol = ts.active()
? ImGui::ColorConvertFloat4ToU32(kTotemColors[i])
: IM_COL32(60, 60, 60, 160);
tdl->AddRect(ImVec2(x0, y0), ImVec2(x1, y1), borderCol, 2.0f);
// Tooltip on hover
ImGui::SetCursorScreenPos(ImVec2(x0, y0));
ImGui::InvisibleButton(("##totem" + std::to_string(i)).c_str(), ImVec2(slotW, slotH));
if (ImGui::IsItemHovered()) {
ImGui::BeginTooltip();
if (ts.active()) {
const std::string& spellNm = gameHandler.getSpellName(ts.spellId);
ImGui::TextColored(ImVec4(kTotemColors[i].x, kTotemColors[i].y,
kTotemColors[i].z, 1.0f),
"%s Totem", kTotemNames[i]);
if (!spellNm.empty()) ImGui::Text("%s", spellNm.c_str());
ImGui::Text("%.1fs remaining", ts.remainingMs() / 1000.0f);
} else {
ImGui::TextDisabled("%s Totem (empty)", kTotemNames[i]);
}
ImGui::EndTooltip();
}
}
ImGui::SetCursorScreenPos(ImVec2(cursor.x, cursor.y + slotH + 2.0f));
}
} }
ImGui::End(); ImGui::End();