mirror of
https://github.com/Kelsidavis/WoWee.git
synced 2026-05-10 02:53:51 +00:00
Some checks are pending
Build / Build (arm64) (push) Waiting to run
Build / Build (x86-64) (push) Waiting to run
Build / Build (macOS arm64) (push) Waiting to run
Build / Build (windows-arm64) (push) Waiting to run
Build / Build (windows-x86-64) (push) Waiting to run
Security / CodeQL (C/C++) (push) Waiting to run
Security / Semgrep (push) Waiting to run
Security / Sanitizer Build (ASan/UBSan) (push) Waiting to run
DRY up renderAuraRemaining, fmtDurationCompact, classColorVec4, classColorU32, entityClassId, classNameStr, kDispelNames, and kRaidMarkNames — duplicated across game_screen, social_panel, and combat_ui after the panel extraction refactors.
2585 lines
123 KiB
C++
2585 lines
123 KiB
C++
// ============================================================
|
|
// SocialPanel — extracted from GameScreen
|
|
// Owns all social/group-related UI rendering: party frames,
|
|
// boss frames, guild roster, social/friends frame, dungeon finder,
|
|
// who window, inspect window.
|
|
// ============================================================
|
|
#include "ui/social_panel.hpp"
|
|
#include "ui/chat_panel.hpp"
|
|
#include "ui/spellbook_screen.hpp"
|
|
#include "ui/inventory_screen.hpp"
|
|
#include "ui/ui_colors.hpp"
|
|
#include "ui/ui_helpers.hpp"
|
|
#include "core/application.hpp"
|
|
#include "core/logger.hpp"
|
|
#include "rendering/renderer.hpp"
|
|
#include "game/game_handler.hpp"
|
|
#include "pipeline/asset_manager.hpp"
|
|
#include "pipeline/dbc_layout.hpp"
|
|
#include "ui/keybinding_manager.hpp"
|
|
#include "game/zone_manager.hpp"
|
|
#include <imgui.h>
|
|
#include <imgui_internal.h>
|
|
#include <algorithm>
|
|
#include <cmath>
|
|
#include <cstdio>
|
|
#include <string>
|
|
|
|
namespace {
|
|
using namespace wowee::ui::colors;
|
|
using namespace wowee::ui::helpers;
|
|
constexpr auto& kColorRed = kRed;
|
|
constexpr auto& kColorGreen = kGreen;
|
|
constexpr auto& kColorBrightGreen = kBrightGreen;
|
|
constexpr auto& kColorYellow = kYellow;
|
|
constexpr auto& kColorGray = kGray;
|
|
constexpr auto& kColorDarkGray = kDarkGray;
|
|
} // anonymous namespace
|
|
|
|
namespace wowee {
|
|
namespace ui {
|
|
|
|
|
|
void SocialPanel::renderPartyFrames(game::GameHandler& gameHandler,
|
|
ChatPanel& chatPanel,
|
|
SpellIconFn getSpellIcon) {
|
|
if (!gameHandler.isInGroup()) return;
|
|
|
|
auto* assetMgr = services_.assetManager;
|
|
const auto& partyData = gameHandler.getPartyData();
|
|
const bool isRaid = (partyData.groupType == 1);
|
|
float frameY = 120.0f;
|
|
|
|
// ---- Raid frame layout ----
|
|
if (isRaid) {
|
|
// Organize members by subgroup (0-7, up to 5 members each)
|
|
constexpr int MAX_SUBGROUPS = 8;
|
|
constexpr int MAX_PER_GROUP = 5;
|
|
std::vector<const game::GroupMember*> subgroups[MAX_SUBGROUPS];
|
|
for (const auto& m : partyData.members) {
|
|
int sg = m.subGroup < MAX_SUBGROUPS ? m.subGroup : 0;
|
|
if (static_cast<int>(subgroups[sg].size()) < MAX_PER_GROUP)
|
|
subgroups[sg].push_back(&m);
|
|
}
|
|
|
|
// Count non-empty subgroups to determine layout
|
|
int activeSgs = 0;
|
|
for (int sg = 0; sg < MAX_SUBGROUPS; sg++)
|
|
if (!subgroups[sg].empty()) activeSgs++;
|
|
|
|
// Compact raid cell: name + 2 narrow bars
|
|
constexpr float CELL_W = 90.0f;
|
|
constexpr float CELL_H = 42.0f;
|
|
constexpr float BAR_H = 7.0f;
|
|
constexpr float CELL_PAD = 3.0f;
|
|
|
|
float winW = activeSgs * (CELL_W + CELL_PAD) + CELL_PAD + 8.0f;
|
|
float winH = MAX_PER_GROUP * (CELL_H + CELL_PAD) + CELL_PAD + 20.0f;
|
|
|
|
auto* window = services_.window;
|
|
float screenW = window ? static_cast<float>(window->getWidth()) : 1280.0f;
|
|
float screenH = window ? static_cast<float>(window->getHeight()) : 720.0f;
|
|
float raidX = (screenW - winW) / 2.0f;
|
|
float raidY = screenH - winH - 120.0f; // above action bar area
|
|
|
|
ImGui::SetNextWindowPos(ImVec2(raidX, raidY), ImGuiCond_Always);
|
|
ImGui::SetNextWindowSize(ImVec2(winW, winH), ImGuiCond_Always);
|
|
|
|
ImGuiWindowFlags raidFlags = ImGuiWindowFlags_NoResize | ImGuiWindowFlags_NoMove |
|
|
ImGuiWindowFlags_NoCollapse | ImGuiWindowFlags_NoTitleBar |
|
|
ImGuiWindowFlags_NoScrollbar;
|
|
ImGui::PushStyleVar(ImGuiStyleVar_WindowRounding, 4.0f);
|
|
ImGui::PushStyleVar(ImGuiStyleVar_ItemSpacing, ImVec2(CELL_PAD, CELL_PAD));
|
|
ImGui::PushStyleColor(ImGuiCol_WindowBg, ImVec4(0.07f, 0.07f, 0.1f, 0.85f));
|
|
|
|
if (ImGui::Begin("##RaidFrames", nullptr, raidFlags)) {
|
|
ImDrawList* draw = ImGui::GetWindowDrawList();
|
|
ImVec2 winPos = ImGui::GetWindowPos();
|
|
|
|
int colIdx = 0;
|
|
for (int sg = 0; sg < MAX_SUBGROUPS; sg++) {
|
|
if (subgroups[sg].empty()) continue;
|
|
|
|
float colX = winPos.x + CELL_PAD + colIdx * (CELL_W + CELL_PAD);
|
|
|
|
for (int row = 0; row < static_cast<int>(subgroups[sg].size()); row++) {
|
|
const auto& m = *subgroups[sg][row];
|
|
float cellY = winPos.y + CELL_PAD + 14.0f + row * (CELL_H + CELL_PAD);
|
|
|
|
ImVec2 cellMin(colX, cellY);
|
|
ImVec2 cellMax(colX + CELL_W, cellY + CELL_H);
|
|
|
|
// Cell background
|
|
bool isTarget = (gameHandler.getTargetGuid() == m.guid);
|
|
ImU32 bg = isTarget ? IM_COL32(60, 80, 120, 200) : IM_COL32(30, 30, 40, 180);
|
|
draw->AddRectFilled(cellMin, cellMax, bg, 3.0f);
|
|
if (isTarget)
|
|
draw->AddRect(cellMin, cellMax, IM_COL32(100, 150, 255, 200), 3.0f);
|
|
|
|
// Dead/ghost overlay
|
|
bool isOnline = (m.onlineStatus & 0x0001) != 0;
|
|
bool isDead = (m.onlineStatus & 0x0020) != 0;
|
|
bool isGhost = (m.onlineStatus & 0x0010) != 0;
|
|
|
|
// Out-of-range check (40 yard threshold)
|
|
bool isOOR = false;
|
|
if (m.hasPartyStats && isOnline && !isDead && !isGhost && m.zoneId != 0) {
|
|
auto playerEnt = gameHandler.getEntityManager().getEntity(gameHandler.getPlayerGuid());
|
|
if (playerEnt) {
|
|
float dx = playerEnt->getX() - static_cast<float>(m.posX);
|
|
float dy = playerEnt->getY() - static_cast<float>(m.posY);
|
|
isOOR = (dx * dx + dy * dy) > (40.0f * 40.0f);
|
|
}
|
|
}
|
|
// Dim cell overlay when out of range
|
|
if (isOOR)
|
|
draw->AddRectFilled(cellMin, cellMax, IM_COL32(0, 0, 0, 80), 3.0f);
|
|
|
|
// Name text (truncated) — class color when alive+online, gray when dead/offline
|
|
char truncName[16];
|
|
snprintf(truncName, sizeof(truncName), "%.12s", m.name.c_str());
|
|
bool isMemberLeader = (m.guid == partyData.leaderGuid);
|
|
ImU32 nameCol;
|
|
if (!isOnline || isDead || isGhost) {
|
|
nameCol = IM_COL32(140, 140, 140, 200); // gray for dead/offline
|
|
} else {
|
|
// Default: gold for leader, light gray for others
|
|
nameCol = isMemberLeader ? IM_COL32(255, 215, 0, 255) : IM_COL32(220, 220, 220, 255);
|
|
// Override with WoW class color if entity is loaded
|
|
auto mEnt = gameHandler.getEntityManager().getEntity(m.guid);
|
|
uint8_t cid = entityClassId(mEnt.get());
|
|
if (cid != 0) nameCol = classColorU32(cid);
|
|
}
|
|
draw->AddText(ImVec2(cellMin.x + 4.0f, cellMin.y + 3.0f), nameCol, truncName);
|
|
|
|
// Leader crown star in top-right of cell
|
|
if (isMemberLeader)
|
|
draw->AddText(ImVec2(cellMax.x - 10.0f, cellMin.y + 2.0f), IM_COL32(255, 215, 0, 255), "*");
|
|
|
|
// Raid mark symbol — small, just to the left of the leader crown
|
|
{
|
|
static constexpr struct { const char* sym; ImU32 col; } kCellMarks[] = {
|
|
{ "\xe2\x98\x85", IM_COL32(255, 220, 50, 255) },
|
|
{ "\xe2\x97\x8f", IM_COL32(255, 140, 0, 255) },
|
|
{ "\xe2\x97\x86", IM_COL32(160, 32, 240, 255) },
|
|
{ "\xe2\x96\xb2", IM_COL32( 50, 200, 50, 255) },
|
|
{ "\xe2\x97\x8c", IM_COL32( 80, 160, 255, 255) },
|
|
{ "\xe2\x96\xa0", IM_COL32( 50, 200, 220, 255) },
|
|
{ "\xe2\x9c\x9d", IM_COL32(255, 80, 80, 255) },
|
|
{ "\xe2\x98\xa0", IM_COL32(255, 255, 255, 255) },
|
|
};
|
|
uint8_t rmk = gameHandler.getEntityRaidMark(m.guid);
|
|
if (rmk < game::GameHandler::kRaidMarkCount) {
|
|
ImFont* rmFont = ImGui::GetFont();
|
|
ImVec2 rmsz = rmFont->CalcTextSizeA(9.0f, FLT_MAX, 0.0f, kCellMarks[rmk].sym);
|
|
float rmX = cellMax.x - 10.0f - 2.0f - rmsz.x;
|
|
draw->AddText(rmFont, 9.0f,
|
|
ImVec2(rmX, cellMin.y + 2.0f),
|
|
kCellMarks[rmk].col, kCellMarks[rmk].sym);
|
|
}
|
|
}
|
|
|
|
// LFG role badge in bottom-right corner of cell
|
|
if (m.roles & 0x02)
|
|
draw->AddText(ImVec2(cellMax.x - 11.0f, cellMax.y - 11.0f), IM_COL32(80, 130, 255, 230), "T");
|
|
else if (m.roles & 0x04)
|
|
draw->AddText(ImVec2(cellMax.x - 11.0f, cellMax.y - 11.0f), IM_COL32(60, 220, 80, 230), "H");
|
|
else if (m.roles & 0x08)
|
|
draw->AddText(ImVec2(cellMax.x - 11.0f, cellMax.y - 11.0f), IM_COL32(220, 80, 80, 230), "D");
|
|
|
|
// Tactical role badge in bottom-left corner (flags from SMSG_GROUP_LIST / SMSG_REAL_GROUP_UPDATE)
|
|
// 0x01=Assistant, 0x02=Main Tank, 0x04=Main Assist
|
|
if (m.flags & 0x02)
|
|
draw->AddText(ImVec2(cellMin.x + 2.0f, cellMax.y - 11.0f), IM_COL32(255, 140, 0, 230), "MT");
|
|
else if (m.flags & 0x04)
|
|
draw->AddText(ImVec2(cellMin.x + 2.0f, cellMax.y - 11.0f), IM_COL32(100, 180, 255, 230), "MA");
|
|
else if (m.flags & 0x01)
|
|
draw->AddText(ImVec2(cellMin.x + 2.0f, cellMax.y - 11.0f), IM_COL32(180, 215, 255, 180), "A");
|
|
|
|
// Health bar
|
|
uint32_t hp = m.hasPartyStats ? m.curHealth : 0;
|
|
uint32_t maxHp = m.hasPartyStats ? m.maxHealth : 0;
|
|
if (maxHp > 0) {
|
|
float pct = static_cast<float>(hp) / static_cast<float>(maxHp);
|
|
float barY = cellMin.y + 16.0f;
|
|
ImVec2 barBg(cellMin.x + 3.0f, barY);
|
|
ImVec2 barBgEnd(cellMax.x - 3.0f, barY + BAR_H);
|
|
draw->AddRectFilled(barBg, barBgEnd, IM_COL32(40, 40, 40, 200), 2.0f);
|
|
ImVec2 barFill(barBg.x, barBg.y);
|
|
ImVec2 barFillEnd(barBg.x + (barBgEnd.x - barBg.x) * pct, barBgEnd.y);
|
|
ImU32 hpCol = isOOR ? IM_COL32(100, 100, 100, 160) :
|
|
pct > 0.5f ? IM_COL32(60, 180, 60, 255) :
|
|
pct > 0.2f ? IM_COL32(200, 180, 50, 255) :
|
|
IM_COL32(200, 60, 60, 255);
|
|
draw->AddRectFilled(barFill, barFillEnd, hpCol, 2.0f);
|
|
// HP percentage or OOR text centered on bar
|
|
char hpPct[8];
|
|
if (isOOR)
|
|
snprintf(hpPct, sizeof(hpPct), "OOR");
|
|
else
|
|
snprintf(hpPct, sizeof(hpPct), "%d%%", static_cast<int>(pct * 100.0f + 0.5f));
|
|
ImVec2 ts = ImGui::CalcTextSize(hpPct);
|
|
float tx = (barBg.x + barBgEnd.x - ts.x) * 0.5f;
|
|
float ty = barBg.y + (BAR_H - ts.y) * 0.5f;
|
|
draw->AddText(ImVec2(tx + 1.0f, ty + 1.0f), IM_COL32(0, 0, 0, 180), hpPct);
|
|
draw->AddText(ImVec2(tx, ty), IM_COL32(255, 255, 255, 230), hpPct);
|
|
}
|
|
|
|
// Power bar
|
|
if (m.hasPartyStats && m.maxPower > 0) {
|
|
float pct = static_cast<float>(m.curPower) / static_cast<float>(m.maxPower);
|
|
float barY = cellMin.y + 16.0f + BAR_H + 2.0f;
|
|
ImVec2 barBg(cellMin.x + 3.0f, barY);
|
|
ImVec2 barBgEnd(cellMax.x - 3.0f, barY + BAR_H - 2.0f);
|
|
draw->AddRectFilled(barBg, barBgEnd, IM_COL32(30, 30, 40, 200), 2.0f);
|
|
ImVec2 barFill(barBg.x, barBg.y);
|
|
ImVec2 barFillEnd(barBg.x + (barBgEnd.x - barBg.x) * pct, barBgEnd.y);
|
|
ImU32 pwrCol;
|
|
switch (m.powerType) {
|
|
case 0: pwrCol = IM_COL32(50, 80, 220, 255); break; // Mana
|
|
case 1: pwrCol = IM_COL32(200, 50, 50, 255); break; // Rage
|
|
case 3: pwrCol = IM_COL32(220, 210, 50, 255); break; // Energy
|
|
case 6: pwrCol = IM_COL32(180, 30, 50, 255); break; // Runic Power
|
|
default: pwrCol = IM_COL32(80, 120, 80, 255); break;
|
|
}
|
|
draw->AddRectFilled(barFill, barFillEnd, pwrCol, 2.0f);
|
|
}
|
|
|
|
// Dispellable debuff dots at the bottom of the raid cell
|
|
// Mirrors party frame debuff indicators for healers in 25/40-man raids
|
|
if (!isDead && !isGhost) {
|
|
const std::vector<game::AuraSlot>* unitAuras = nullptr;
|
|
if (m.guid == gameHandler.getPlayerGuid())
|
|
unitAuras = &gameHandler.getPlayerAuras();
|
|
else if (m.guid == gameHandler.getTargetGuid())
|
|
unitAuras = &gameHandler.getTargetAuras();
|
|
else
|
|
unitAuras = gameHandler.getUnitAuras(m.guid);
|
|
|
|
if (unitAuras) {
|
|
bool shown[5] = {};
|
|
float dotX = cellMin.x + 4.0f;
|
|
const float dotY = cellMax.y - 5.0f;
|
|
const float DOT_R = 3.5f;
|
|
ImVec2 mouse = ImGui::GetMousePos();
|
|
for (const auto& aura : *unitAuras) {
|
|
if (aura.isEmpty()) continue;
|
|
if ((aura.flags & 0x80) == 0) continue; // debuffs only
|
|
uint8_t dt = gameHandler.getSpellDispelType(aura.spellId);
|
|
if (dt == 0 || dt > 4 || shown[dt]) continue;
|
|
shown[dt] = true;
|
|
ImVec4 dc;
|
|
switch (dt) {
|
|
case 1: dc = ImVec4(0.25f, 0.50f, 1.00f, 0.90f); break; // Magic: blue
|
|
case 2: dc = ImVec4(0.70f, 0.15f, 0.90f, 0.90f); break; // Curse: purple
|
|
case 3: dc = ImVec4(0.65f, 0.45f, 0.10f, 0.90f); break; // Disease: brown
|
|
case 4: dc = ImVec4(0.10f, 0.75f, 0.10f, 0.90f); break; // Poison: green
|
|
default: continue;
|
|
}
|
|
ImU32 dotColU = ImGui::ColorConvertFloat4ToU32(dc);
|
|
draw->AddCircleFilled(ImVec2(dotX, dotY), DOT_R, dotColU);
|
|
draw->AddCircle(ImVec2(dotX, dotY), DOT_R + 0.5f, IM_COL32(0, 0, 0, 160), 8, 1.0f);
|
|
|
|
float mdx = mouse.x - dotX, mdy = mouse.y - dotY;
|
|
if (mdx * mdx + mdy * mdy < (DOT_R + 4.0f) * (DOT_R + 4.0f)) {
|
|
ImGui::BeginTooltip();
|
|
ImGui::TextColored(dc, "%s", kDispelNames[dt]);
|
|
for (const auto& da : *unitAuras) {
|
|
if (da.isEmpty() || (da.flags & 0x80) == 0) continue;
|
|
if (gameHandler.getSpellDispelType(da.spellId) != dt) continue;
|
|
const std::string& dName = gameHandler.getSpellName(da.spellId);
|
|
if (!dName.empty())
|
|
ImGui::Text(" %s", dName.c_str());
|
|
}
|
|
ImGui::EndTooltip();
|
|
}
|
|
dotX += 9.0f;
|
|
}
|
|
}
|
|
}
|
|
|
|
// Clickable invisible region over the whole cell
|
|
ImGui::SetCursorScreenPos(cellMin);
|
|
ImGui::PushID(static_cast<int>(m.guid));
|
|
if (ImGui::InvisibleButton("raidCell", ImVec2(CELL_W, CELL_H))) {
|
|
gameHandler.setTarget(m.guid);
|
|
}
|
|
if (ImGui::IsItemHovered()) {
|
|
gameHandler.setMouseoverGuid(m.guid);
|
|
}
|
|
if (ImGui::BeginPopupContextItem("RaidMemberCtx")) {
|
|
ImGui::TextDisabled("%s", m.name.c_str());
|
|
ImGui::Separator();
|
|
if (ImGui::MenuItem("Target"))
|
|
gameHandler.setTarget(m.guid);
|
|
if (ImGui::MenuItem("Set Focus"))
|
|
gameHandler.setFocus(m.guid);
|
|
if (ImGui::MenuItem("Whisper")) {
|
|
chatPanel.setWhisperTarget(m.name);
|
|
}
|
|
if (ImGui::MenuItem("Trade"))
|
|
gameHandler.initiateTrade(m.guid);
|
|
if (ImGui::MenuItem("Inspect")) {
|
|
gameHandler.setTarget(m.guid);
|
|
gameHandler.inspectTarget();
|
|
showInspectWindow_ = true;
|
|
}
|
|
bool isLeader = (partyData.leaderGuid == gameHandler.getPlayerGuid());
|
|
if (isLeader) {
|
|
ImGui::Separator();
|
|
if (ImGui::MenuItem("Kick from Raid"))
|
|
gameHandler.uninvitePlayer(m.name);
|
|
}
|
|
ImGui::Separator();
|
|
if (ImGui::BeginMenu("Set Raid Mark")) {
|
|
for (int mi = 0; mi < 8; ++mi) {
|
|
if (ImGui::MenuItem(kRaidMarkNames[mi]))
|
|
gameHandler.setRaidMark(m.guid, static_cast<uint8_t>(mi));
|
|
}
|
|
ImGui::Separator();
|
|
if (ImGui::MenuItem("Clear Mark"))
|
|
gameHandler.setRaidMark(m.guid, 0xFF);
|
|
ImGui::EndMenu();
|
|
}
|
|
ImGui::EndPopup();
|
|
}
|
|
ImGui::PopID();
|
|
}
|
|
colIdx++;
|
|
}
|
|
|
|
// Subgroup header row
|
|
colIdx = 0;
|
|
for (int sg = 0; sg < MAX_SUBGROUPS; sg++) {
|
|
if (subgroups[sg].empty()) continue;
|
|
float colX = winPos.x + CELL_PAD + colIdx * (CELL_W + CELL_PAD);
|
|
char sgLabel[8];
|
|
snprintf(sgLabel, sizeof(sgLabel), "G%d", sg + 1);
|
|
draw->AddText(ImVec2(colX + CELL_W / 2 - 8.0f, winPos.y + CELL_PAD), IM_COL32(160, 160, 180, 200), sgLabel);
|
|
colIdx++;
|
|
}
|
|
}
|
|
ImGui::End();
|
|
ImGui::PopStyleColor();
|
|
ImGui::PopStyleVar(2);
|
|
return;
|
|
}
|
|
|
|
// ---- Party frame layout (5-man) ----
|
|
ImGui::SetNextWindowPos(ImVec2(10.0f, frameY), ImGuiCond_Always);
|
|
ImGui::SetNextWindowSize(ImVec2(200.0f, 0.0f), ImGuiCond_Always);
|
|
|
|
ImGuiWindowFlags flags = ImGuiWindowFlags_NoResize | ImGuiWindowFlags_NoMove |
|
|
ImGuiWindowFlags_NoCollapse | ImGuiWindowFlags_NoTitleBar |
|
|
ImGuiWindowFlags_AlwaysAutoResize;
|
|
|
|
ImGui::PushStyleVar(ImGuiStyleVar_WindowRounding, 4.0f);
|
|
ImGui::PushStyleColor(ImGuiCol_WindowBg, ImVec4(0.1f, 0.1f, 0.1f, 0.8f));
|
|
|
|
if (ImGui::Begin("##PartyFrames", nullptr, flags)) {
|
|
const uint64_t leaderGuid = partyData.leaderGuid;
|
|
for (const auto& member : partyData.members) {
|
|
ImGui::PushID(static_cast<int>(member.guid));
|
|
|
|
bool isLeader = (member.guid == leaderGuid);
|
|
|
|
// Name with level and status info — leader gets a gold star prefix
|
|
std::string label = (isLeader ? "* " : " ") + member.name;
|
|
if (member.hasPartyStats && member.level > 0) {
|
|
label += " [" + std::to_string(member.level) + "]";
|
|
}
|
|
if (member.hasPartyStats) {
|
|
bool isOnline = (member.onlineStatus & 0x0001) != 0;
|
|
bool isDead = (member.onlineStatus & 0x0020) != 0;
|
|
bool isGhost = (member.onlineStatus & 0x0010) != 0;
|
|
if (!isOnline) label += " (offline)";
|
|
else if (isDead || isGhost) label += " (dead)";
|
|
}
|
|
|
|
// Clickable name to target — use WoW class colors when entity is loaded,
|
|
// fall back to gold for leader / light gray for others
|
|
ImVec4 nameColor = isLeader
|
|
? colors::kBrightGold
|
|
: colors::kVeryLightGray;
|
|
{
|
|
auto memberEntity = gameHandler.getEntityManager().getEntity(member.guid);
|
|
uint8_t cid = entityClassId(memberEntity.get());
|
|
if (cid != 0) nameColor = classColorVec4(cid);
|
|
}
|
|
ImGui::PushStyleColor(ImGuiCol_Text, nameColor);
|
|
if (ImGui::Selectable(label.c_str(), gameHandler.getTargetGuid() == member.guid)) {
|
|
gameHandler.setTarget(member.guid);
|
|
}
|
|
// Set mouseover for [target=mouseover] macro conditionals
|
|
if (ImGui::IsItemHovered()) {
|
|
gameHandler.setMouseoverGuid(member.guid);
|
|
}
|
|
// Zone tooltip on name hover
|
|
if (ImGui::IsItemHovered() && member.hasPartyStats && member.zoneId != 0) {
|
|
std::string zoneName = gameHandler.getWhoAreaName(member.zoneId);
|
|
if (!zoneName.empty())
|
|
ImGui::SetTooltip("%s", zoneName.c_str());
|
|
}
|
|
ImGui::PopStyleColor();
|
|
|
|
// LFG role badge (Tank/Healer/DPS) — shown on same line as name when set
|
|
if (member.roles != 0) {
|
|
ImGui::SameLine();
|
|
if (member.roles & 0x02) ImGui::TextColored(ImVec4(0.3f, 0.5f, 1.0f, 1.0f), "[T]");
|
|
if (member.roles & 0x04) { ImGui::SameLine(); ImGui::TextColored(ImVec4(0.2f, 0.9f, 0.3f, 1.0f), "[H]"); }
|
|
if (member.roles & 0x08) { ImGui::SameLine(); ImGui::TextColored(ImVec4(0.9f, 0.3f, 0.3f, 1.0f), "[D]"); }
|
|
}
|
|
|
|
// Tactical role badge (MT/MA/Asst) from group flags
|
|
if (member.flags & 0x02) {
|
|
ImGui::SameLine();
|
|
ImGui::TextColored(ImVec4(1.0f, 0.55f, 0.0f, 0.9f), "[MT]");
|
|
} else if (member.flags & 0x04) {
|
|
ImGui::SameLine();
|
|
ImGui::TextColored(ImVec4(0.4f, 0.7f, 1.0f, 0.9f), "[MA]");
|
|
} else if (member.flags & 0x01) {
|
|
ImGui::SameLine();
|
|
ImGui::TextColored(ImVec4(0.7f, 0.85f, 1.0f, 0.7f), "[A]");
|
|
}
|
|
|
|
// Raid mark symbol — shown on same line as name when this party member has a mark
|
|
{
|
|
static constexpr struct { const char* sym; ImU32 col; } kPartyMarks[] = {
|
|
{ "\xe2\x98\x85", IM_COL32(255, 220, 50, 255) }, // 0 Star
|
|
{ "\xe2\x97\x8f", IM_COL32(255, 140, 0, 255) }, // 1 Circle
|
|
{ "\xe2\x97\x86", IM_COL32(160, 32, 240, 255) }, // 2 Diamond
|
|
{ "\xe2\x96\xb2", IM_COL32( 50, 200, 50, 255) }, // 3 Triangle
|
|
{ "\xe2\x97\x8c", IM_COL32( 80, 160, 255, 255) }, // 4 Moon
|
|
{ "\xe2\x96\xa0", IM_COL32( 50, 200, 220, 255) }, // 5 Square
|
|
{ "\xe2\x9c\x9d", IM_COL32(255, 80, 80, 255) }, // 6 Cross
|
|
{ "\xe2\x98\xa0", IM_COL32(255, 255, 255, 255) }, // 7 Skull
|
|
};
|
|
uint8_t pmk = gameHandler.getEntityRaidMark(member.guid);
|
|
if (pmk < game::GameHandler::kRaidMarkCount) {
|
|
ImGui::SameLine();
|
|
ImGui::TextColored(
|
|
ImGui::ColorConvertU32ToFloat4(kPartyMarks[pmk].col),
|
|
"%s", kPartyMarks[pmk].sym);
|
|
}
|
|
}
|
|
|
|
// Health bar: prefer party stats, fall back to entity
|
|
uint32_t hp = 0, maxHp = 0;
|
|
if (member.hasPartyStats && member.maxHealth > 0) {
|
|
hp = member.curHealth;
|
|
maxHp = member.maxHealth;
|
|
} else {
|
|
auto entity = gameHandler.getEntityManager().getEntity(member.guid);
|
|
if (entity && (entity->getType() == game::ObjectType::PLAYER || entity->getType() == game::ObjectType::UNIT)) {
|
|
auto unit = std::static_pointer_cast<game::Unit>(entity);
|
|
hp = unit->getHealth();
|
|
maxHp = unit->getMaxHealth();
|
|
}
|
|
}
|
|
// Check dead/ghost state for health bar rendering
|
|
bool memberDead = false;
|
|
bool memberOffline = false;
|
|
if (member.hasPartyStats) {
|
|
bool isOnline2 = (member.onlineStatus & 0x0001) != 0;
|
|
bool isDead2 = (member.onlineStatus & 0x0020) != 0;
|
|
bool isGhost2 = (member.onlineStatus & 0x0010) != 0;
|
|
memberDead = isDead2 || isGhost2;
|
|
memberOffline = !isOnline2;
|
|
}
|
|
|
|
// Out-of-range check: compare player position to member's reported position
|
|
// Range threshold: 40 yards (standard heal/spell range)
|
|
bool memberOutOfRange = false;
|
|
if (member.hasPartyStats && !memberOffline && !memberDead &&
|
|
member.zoneId != 0) {
|
|
// Same map: use 2D Euclidean distance in WoW coordinates (yards)
|
|
auto playerEntity = gameHandler.getEntityManager().getEntity(gameHandler.getPlayerGuid());
|
|
if (playerEntity) {
|
|
float dx = playerEntity->getX() - static_cast<float>(member.posX);
|
|
float dy = playerEntity->getY() - static_cast<float>(member.posY);
|
|
float distSq = dx * dx + dy * dy;
|
|
memberOutOfRange = (distSq > 40.0f * 40.0f);
|
|
}
|
|
}
|
|
|
|
if (memberDead) {
|
|
// Gray "Dead" bar for fallen party members
|
|
ImGui::PushStyleColor(ImGuiCol_PlotHistogram, ImVec4(0.35f, 0.35f, 0.35f, 1.0f));
|
|
ImGui::PushStyleColor(ImGuiCol_FrameBg, ImVec4(0.15f, 0.15f, 0.15f, 1.0f));
|
|
ImGui::ProgressBar(0.0f, ImVec2(-1, 14), "Dead");
|
|
ImGui::PopStyleColor(2);
|
|
} else if (memberOffline) {
|
|
// Dim bar for offline members
|
|
ImGui::PushStyleColor(ImGuiCol_PlotHistogram, ImVec4(0.25f, 0.25f, 0.25f, 0.6f));
|
|
ImGui::PushStyleColor(ImGuiCol_FrameBg, ImVec4(0.1f, 0.1f, 0.1f, 0.6f));
|
|
ImGui::ProgressBar(0.0f, ImVec2(-1, 14), "Offline");
|
|
ImGui::PopStyleColor(2);
|
|
} else if (maxHp > 0) {
|
|
float pct = static_cast<float>(hp) / static_cast<float>(maxHp);
|
|
// Out-of-range: desaturate health bar to gray
|
|
ImVec4 hpBarColor = memberOutOfRange
|
|
? ImVec4(0.45f, 0.45f, 0.45f, 0.7f)
|
|
: (pct > 0.5f ? colors::kHealthGreen :
|
|
pct > 0.2f ? colors::kMidHealthYellow :
|
|
colors::kLowHealthRed);
|
|
ImGui::PushStyleColor(ImGuiCol_PlotHistogram, hpBarColor);
|
|
char hpText[32];
|
|
if (memberOutOfRange) {
|
|
snprintf(hpText, sizeof(hpText), "OOR");
|
|
} else if (maxHp >= 10000) {
|
|
snprintf(hpText, sizeof(hpText), "%dk/%dk",
|
|
static_cast<int>(hp) / 1000, static_cast<int>(maxHp) / 1000);
|
|
} else {
|
|
snprintf(hpText, sizeof(hpText), "%u/%u", hp, maxHp);
|
|
}
|
|
ImGui::ProgressBar(pct, ImVec2(-1, 14), hpText);
|
|
ImGui::PopStyleColor();
|
|
}
|
|
|
|
// Power bar (mana/rage/energy) from party stats — hidden for dead/offline/OOR
|
|
if (!memberDead && !memberOffline && member.hasPartyStats && member.maxPower > 0) {
|
|
float powerPct = static_cast<float>(member.curPower) / static_cast<float>(member.maxPower);
|
|
ImVec4 powerColor;
|
|
switch (member.powerType) {
|
|
case 0: powerColor = colors::kManaBlue; break; // Mana (blue)
|
|
case 1: powerColor = colors::kDarkRed; break; // Rage (red)
|
|
case 2: powerColor = colors::kOrange; break; // Focus (orange)
|
|
case 3: powerColor = colors::kEnergyYellow; break; // Energy (yellow)
|
|
case 4: powerColor = colors::kHappinessGreen; break; // Happiness (green)
|
|
case 6: powerColor = colors::kRunicRed; break; // Runic Power (crimson)
|
|
case 7: powerColor = colors::kSoulShardPurple; break; // Soul Shards (purple)
|
|
default: powerColor = kColorDarkGray; break;
|
|
}
|
|
ImGui::PushStyleColor(ImGuiCol_PlotHistogram, powerColor);
|
|
ImGui::ProgressBar(powerPct, ImVec2(-1, 8), "");
|
|
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);
|
|
if (ImGui::IsItemHovered()) {
|
|
// Find spell name(s) of this dispel type
|
|
ImGui::BeginTooltip();
|
|
ImGui::TextColored(dotCol, "%s", kDispelNames[dt]);
|
|
for (const auto& da : *unitAuras) {
|
|
if (da.isEmpty() || (da.flags & 0x80) == 0) continue;
|
|
if (gameHandler.getSpellDispelType(da.spellId) != dt) continue;
|
|
const std::string& dName = gameHandler.getSpellName(da.spellId);
|
|
if (!dName.empty())
|
|
ImGui::Text(" %s", dName.c_str());
|
|
}
|
|
ImGui::EndTooltip();
|
|
}
|
|
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)
|
|
? (cs->timeTotal - cs->timeRemaining) / cs->timeTotal : 0.0f;
|
|
ImGui::PushStyleColor(ImGuiCol_PlotHistogram, colors::kMidHealthYellow);
|
|
char pcastLabel[48];
|
|
const std::string& spellNm = gameHandler.getSpellName(cs->spellId);
|
|
if (!spellNm.empty())
|
|
snprintf(pcastLabel, sizeof(pcastLabel), "%s (%.1fs)", spellNm.c_str(), cs->timeRemaining);
|
|
else
|
|
snprintf(pcastLabel, sizeof(pcastLabel), "Casting... (%.1fs)", cs->timeRemaining);
|
|
{
|
|
VkDescriptorSet pIcon = (cs->spellId != 0 && assetMgr)
|
|
? getSpellIcon(cs->spellId, assetMgr) : VK_NULL_HANDLE;
|
|
if (pIcon) {
|
|
ImGui::Image((ImTextureID)(uintptr_t)pIcon, ImVec2(10, 10));
|
|
ImGui::SameLine(0, 2);
|
|
ImGui::ProgressBar(castPct, ImVec2(-1, 10), pcastLabel);
|
|
} else {
|
|
ImGui::ProgressBar(castPct, ImVec2(-1, 10), pcastLabel);
|
|
}
|
|
}
|
|
ImGui::PopStyleColor();
|
|
}
|
|
|
|
// Right-click context menu for party member actions
|
|
if (ImGui::BeginPopupContextItem("PartyMemberCtx")) {
|
|
ImGui::TextDisabled("%s", member.name.c_str());
|
|
ImGui::Separator();
|
|
if (ImGui::MenuItem("Target")) {
|
|
gameHandler.setTarget(member.guid);
|
|
}
|
|
if (ImGui::MenuItem("Set Focus")) {
|
|
gameHandler.setFocus(member.guid);
|
|
}
|
|
if (ImGui::MenuItem("Whisper")) {
|
|
chatPanel.setWhisperTarget(member.name);
|
|
}
|
|
if (ImGui::MenuItem("Follow")) {
|
|
gameHandler.setTarget(member.guid);
|
|
gameHandler.followTarget();
|
|
}
|
|
if (ImGui::MenuItem("Trade")) {
|
|
gameHandler.initiateTrade(member.guid);
|
|
}
|
|
if (ImGui::MenuItem("Duel")) {
|
|
gameHandler.proposeDuel(member.guid);
|
|
}
|
|
if (ImGui::MenuItem("Inspect")) {
|
|
gameHandler.setTarget(member.guid);
|
|
gameHandler.inspectTarget();
|
|
showInspectWindow_ = true;
|
|
}
|
|
ImGui::Separator();
|
|
if (!member.name.empty()) {
|
|
if (ImGui::MenuItem("Add Friend")) {
|
|
gameHandler.addFriend(member.name);
|
|
}
|
|
if (ImGui::MenuItem("Ignore")) {
|
|
gameHandler.addIgnore(member.name);
|
|
}
|
|
}
|
|
// Leader-only actions
|
|
bool isLeader = (gameHandler.getPartyData().leaderGuid == gameHandler.getPlayerGuid());
|
|
if (isLeader) {
|
|
ImGui::Separator();
|
|
if (ImGui::MenuItem("Kick from Group")) {
|
|
gameHandler.uninvitePlayer(member.name);
|
|
}
|
|
}
|
|
ImGui::Separator();
|
|
if (ImGui::BeginMenu("Set Raid Mark")) {
|
|
for (int mi = 0; mi < 8; ++mi) {
|
|
if (ImGui::MenuItem(kRaidMarkNames[mi]))
|
|
gameHandler.setRaidMark(member.guid, static_cast<uint8_t>(mi));
|
|
}
|
|
ImGui::Separator();
|
|
if (ImGui::MenuItem("Clear Mark"))
|
|
gameHandler.setRaidMark(member.guid, 0xFF);
|
|
ImGui::EndMenu();
|
|
}
|
|
ImGui::EndPopup();
|
|
}
|
|
|
|
ImGui::Separator();
|
|
ImGui::PopID();
|
|
}
|
|
}
|
|
ImGui::End();
|
|
|
|
ImGui::PopStyleColor();
|
|
ImGui::PopStyleVar();
|
|
}
|
|
|
|
void SocialPanel::renderBossFrames(game::GameHandler& gameHandler,
|
|
SpellbookScreen& spellbookScreen,
|
|
SpellIconFn getSpellIcon) {
|
|
auto* assetMgr = services_.assetManager;
|
|
|
|
// Collect active boss unit slots
|
|
struct BossSlot { uint32_t slot; uint64_t guid; };
|
|
std::vector<BossSlot> active;
|
|
for (uint32_t s = 0; s < game::GameHandler::kMaxEncounterSlots; ++s) {
|
|
uint64_t g = gameHandler.getEncounterUnitGuid(s);
|
|
if (g != 0) active.push_back({s, g});
|
|
}
|
|
if (active.empty()) return;
|
|
|
|
const float frameW = 200.0f;
|
|
const float startX = ImGui::GetIO().DisplaySize.x - frameW - 10.0f;
|
|
float frameY = 120.0f;
|
|
|
|
ImGuiWindowFlags flags = ImGuiWindowFlags_NoResize | ImGuiWindowFlags_NoMove |
|
|
ImGuiWindowFlags_NoCollapse | ImGuiWindowFlags_NoTitleBar |
|
|
ImGuiWindowFlags_AlwaysAutoResize;
|
|
|
|
ImGui::PushStyleVar(ImGuiStyleVar_WindowRounding, 4.0f);
|
|
ImGui::PushStyleColor(ImGuiCol_WindowBg, ImVec4(0.15f, 0.05f, 0.05f, 0.85f));
|
|
|
|
ImGui::SetNextWindowPos(ImVec2(startX, frameY), ImGuiCond_Always);
|
|
ImGui::SetNextWindowSize(ImVec2(frameW, 0.0f), ImGuiCond_Always);
|
|
|
|
if (ImGui::Begin("##BossFrames", nullptr, flags)) {
|
|
for (const auto& bs : active) {
|
|
ImGui::PushID(static_cast<int>(bs.guid));
|
|
|
|
// Try to resolve name, health, and power from entity manager
|
|
std::string name = "Boss";
|
|
uint32_t hp = 0, maxHp = 0;
|
|
uint8_t bossPowerType = 0;
|
|
uint32_t bossPower = 0, bossMaxPower = 0;
|
|
auto entity = gameHandler.getEntityManager().getEntity(bs.guid);
|
|
if (entity && (entity->getType() == game::ObjectType::UNIT ||
|
|
entity->getType() == game::ObjectType::PLAYER)) {
|
|
auto unit = std::static_pointer_cast<game::Unit>(entity);
|
|
const auto& n = unit->getName();
|
|
if (!n.empty()) name = n;
|
|
hp = unit->getHealth();
|
|
maxHp = unit->getMaxHealth();
|
|
bossPowerType = unit->getPowerType();
|
|
bossPower = unit->getPower();
|
|
bossMaxPower = unit->getMaxPower();
|
|
}
|
|
|
|
// Clickable name to target
|
|
if (ImGui::Selectable(name.c_str(), gameHandler.getTargetGuid() == bs.guid)) {
|
|
gameHandler.setTarget(bs.guid);
|
|
}
|
|
|
|
if (maxHp > 0) {
|
|
float pct = static_cast<float>(hp) / static_cast<float>(maxHp);
|
|
// Boss health bar in red shades
|
|
ImGui::PushStyleColor(ImGuiCol_PlotHistogram,
|
|
pct > 0.5f ? colors::kLowHealthRed :
|
|
pct > 0.2f ? ImVec4(0.9f, 0.5f, 0.1f, 1.0f) :
|
|
ImVec4(1.0f, 0.8f, 0.1f, 1.0f));
|
|
char label[32];
|
|
std::snprintf(label, sizeof(label), "%u / %u", hp, maxHp);
|
|
ImGui::ProgressBar(pct, ImVec2(-1, 14), label);
|
|
ImGui::PopStyleColor();
|
|
}
|
|
|
|
// Boss power bar — shown when boss has a non-zero power pool
|
|
// Energy bosses (type 3) are particularly important: full energy signals ability use
|
|
if (bossMaxPower > 0 && bossPower > 0) {
|
|
float bpPct = static_cast<float>(bossPower) / static_cast<float>(bossMaxPower);
|
|
ImVec4 bpColor;
|
|
switch (bossPowerType) {
|
|
case 0: bpColor = ImVec4(0.2f, 0.3f, 0.9f, 1.0f); break; // Mana: blue
|
|
case 1: bpColor = colors::kDarkRed; break; // Rage: red
|
|
case 2: bpColor = colors::kOrange; break; // Focus: orange
|
|
case 3: bpColor = ImVec4(0.9f, 0.9f, 0.1f, 1.0f); break; // Energy: yellow
|
|
default: bpColor = ImVec4(0.4f, 0.8f, 0.4f, 1.0f); break;
|
|
}
|
|
ImGui::PushStyleColor(ImGuiCol_PlotHistogram, bpColor);
|
|
char bpLabel[24];
|
|
std::snprintf(bpLabel, sizeof(bpLabel), "%u", bossPower);
|
|
ImGui::ProgressBar(bpPct, ImVec2(-1, 6), bpLabel);
|
|
ImGui::PopStyleColor();
|
|
}
|
|
|
|
// Boss cast bar — shown when the boss is casting (critical for interrupt)
|
|
if (auto* cs = gameHandler.getUnitCastState(bs.guid)) {
|
|
float castPct = (cs->timeTotal > 0.0f)
|
|
? (cs->timeTotal - cs->timeRemaining) / cs->timeTotal : 0.0f;
|
|
uint32_t bspell = cs->spellId;
|
|
const std::string& bcastName = (bspell != 0)
|
|
? gameHandler.getSpellName(bspell) : "";
|
|
// Green = interruptible, Red = immune; pulse when > 80% complete
|
|
ImVec4 bcastColor;
|
|
if (castPct > 0.8f) {
|
|
float pulse = 0.7f + 0.3f * std::sin(static_cast<float>(ImGui::GetTime()) * 8.0f);
|
|
bcastColor = cs->interruptible
|
|
? ImVec4(0.2f * pulse, 0.9f * pulse, 0.2f * pulse, 1.0f)
|
|
: ImVec4(1.0f * pulse, 0.1f * pulse, 0.1f * pulse, 1.0f);
|
|
} else {
|
|
bcastColor = cs->interruptible
|
|
? colors::kCastGreen
|
|
: ImVec4(0.9f, 0.15f, 0.15f, 1.0f);
|
|
}
|
|
ImGui::PushStyleColor(ImGuiCol_PlotHistogram, bcastColor);
|
|
char bcastLabel[72];
|
|
if (!bcastName.empty())
|
|
snprintf(bcastLabel, sizeof(bcastLabel), "%s (%.1fs)",
|
|
bcastName.c_str(), cs->timeRemaining);
|
|
else
|
|
snprintf(bcastLabel, sizeof(bcastLabel), "Casting... (%.1fs)", cs->timeRemaining);
|
|
{
|
|
VkDescriptorSet bIcon = (bspell != 0 && assetMgr)
|
|
? getSpellIcon(bspell, assetMgr) : VK_NULL_HANDLE;
|
|
if (bIcon) {
|
|
ImGui::Image((ImTextureID)(uintptr_t)bIcon, ImVec2(12, 12));
|
|
ImGui::SameLine(0, 2);
|
|
ImGui::ProgressBar(castPct, ImVec2(-1, 12), bcastLabel);
|
|
} else {
|
|
ImGui::ProgressBar(castPct, ImVec2(-1, 12), bcastLabel);
|
|
}
|
|
}
|
|
ImGui::PopStyleColor();
|
|
}
|
|
|
|
// Boss aura row: debuffs first (player DoTs), then boss buffs
|
|
{
|
|
const std::vector<game::AuraSlot>* 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<uint64_t>(
|
|
std::chrono::duration_cast<std::chrono::milliseconds>(
|
|
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<size_t> 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<int>(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];
|
|
fmtDurationCompact(ts, sizeof(ts), (baRemain + 999) / 1000);
|
|
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);
|
|
}
|
|
|
|
// Stack / charge count — upper-left corner (parity with target/focus frames)
|
|
if (aura.charges > 1) {
|
|
ImVec2 baMin = ImGui::GetItemRectMin();
|
|
char chargeStr[8];
|
|
snprintf(chargeStr, sizeof(chargeStr), "%u", static_cast<unsigned>(aura.charges));
|
|
ImGui::GetWindowDrawList()->AddText(ImVec2(baMin.x + 2, baMin.y + 2),
|
|
IM_COL32(0, 0, 0, 200), chargeStr);
|
|
ImGui::GetWindowDrawList()->AddText(ImVec2(baMin.x + 1, baMin.y + 1),
|
|
IM_COL32(255, 220, 50, 255), chargeStr);
|
|
}
|
|
|
|
// 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");
|
|
renderAuraRemaining(baRemain);
|
|
ImGui::EndTooltip();
|
|
}
|
|
|
|
ImGui::PopID();
|
|
baShown++;
|
|
}
|
|
ImGui::PopStyleVar();
|
|
}
|
|
}
|
|
}
|
|
|
|
ImGui::PopID();
|
|
ImGui::Spacing();
|
|
}
|
|
}
|
|
ImGui::End();
|
|
|
|
ImGui::PopStyleColor();
|
|
ImGui::PopStyleVar();
|
|
}
|
|
|
|
void SocialPanel::renderGuildRoster(game::GameHandler& gameHandler,
|
|
ChatPanel& chatPanel) {
|
|
// Guild Roster toggle (customizable keybind)
|
|
if (!chatPanel.isChatInputActive() && !ImGui::GetIO().WantTextInput &&
|
|
!ImGui::GetIO().WantCaptureKeyboard &&
|
|
KeybindingManager::getInstance().isActionPressed(KeybindingManager::Action::TOGGLE_GUILD_ROSTER)) {
|
|
showGuildRoster_ = !showGuildRoster_;
|
|
if (showGuildRoster_) {
|
|
// Open friends tab directly if not in guild
|
|
if (!gameHandler.isInGuild()) {
|
|
guildRosterTab_ = 2; // Friends tab
|
|
} else {
|
|
// Re-query guild name if we have guildId but no name yet
|
|
if (gameHandler.getGuildName().empty()) {
|
|
const auto* ch = gameHandler.getActiveCharacter();
|
|
if (ch && ch->hasGuild()) {
|
|
gameHandler.queryGuildInfo(ch->guildId);
|
|
}
|
|
}
|
|
gameHandler.requestGuildRoster();
|
|
gameHandler.requestGuildInfo();
|
|
}
|
|
}
|
|
}
|
|
|
|
// Petition creation dialog (shown when NPC sends SMSG_PETITION_SHOWLIST)
|
|
if (gameHandler.hasPetitionShowlist()) {
|
|
ImGui::OpenPopup("CreateGuildPetition");
|
|
gameHandler.clearPetitionDialog();
|
|
}
|
|
if (ImGui::BeginPopupModal("CreateGuildPetition", nullptr, ImGuiWindowFlags_AlwaysAutoResize)) {
|
|
ImGui::Text("Create Guild Charter");
|
|
ImGui::Separator();
|
|
uint32_t cost = gameHandler.getPetitionCost();
|
|
ImGui::TextDisabled("Cost:"); ImGui::SameLine(0, 4);
|
|
renderCoinsFromCopper(cost);
|
|
ImGui::Spacing();
|
|
ImGui::Text("Guild Name:");
|
|
ImGui::InputText("##petitionname", petitionNameBuffer_, sizeof(petitionNameBuffer_));
|
|
ImGui::Spacing();
|
|
if (ImGui::Button("Create", ImVec2(120, 0))) {
|
|
if (petitionNameBuffer_[0] != '\0') {
|
|
gameHandler.buyPetition(gameHandler.getPetitionNpcGuid(), petitionNameBuffer_);
|
|
petitionNameBuffer_[0] = '\0';
|
|
ImGui::CloseCurrentPopup();
|
|
}
|
|
}
|
|
ImGui::SameLine();
|
|
if (ImGui::Button("Cancel", ImVec2(120, 0))) {
|
|
petitionNameBuffer_[0] = '\0';
|
|
ImGui::CloseCurrentPopup();
|
|
}
|
|
ImGui::EndPopup();
|
|
}
|
|
|
|
// Petition signatures window (shown when a petition item is used or offered)
|
|
if (gameHandler.hasPetitionSignaturesUI()) {
|
|
ImGui::OpenPopup("PetitionSignatures");
|
|
gameHandler.clearPetitionSignaturesUI();
|
|
}
|
|
if (ImGui::BeginPopupModal("PetitionSignatures", nullptr, ImGuiWindowFlags_AlwaysAutoResize)) {
|
|
const auto& pInfo = gameHandler.getPetitionInfo();
|
|
if (!pInfo.guildName.empty())
|
|
ImGui::Text("Guild Charter: %s", pInfo.guildName.c_str());
|
|
else
|
|
ImGui::Text("Guild Charter");
|
|
ImGui::Separator();
|
|
|
|
ImGui::Text("Signatures: %u / %u", pInfo.signatureCount, pInfo.signaturesRequired);
|
|
ImGui::Spacing();
|
|
|
|
if (!pInfo.signatures.empty()) {
|
|
for (size_t i = 0; i < pInfo.signatures.size(); ++i) {
|
|
const auto& sig = pInfo.signatures[i];
|
|
// Try to resolve name from entity manager
|
|
std::string sigName;
|
|
if (sig.playerGuid != 0) {
|
|
auto entity = gameHandler.getEntityManager().getEntity(sig.playerGuid);
|
|
if (entity) {
|
|
auto* unit = entity->isUnit() ? static_cast<game::Unit*>(entity.get()) : nullptr;
|
|
if (unit) sigName = unit->getName();
|
|
}
|
|
}
|
|
if (sigName.empty())
|
|
sigName = "Player " + std::to_string(i + 1);
|
|
ImGui::BulletText("%s", sigName.c_str());
|
|
}
|
|
ImGui::Spacing();
|
|
}
|
|
|
|
// If we're not the owner, show Sign button
|
|
bool isOwner = (pInfo.ownerGuid == gameHandler.getPlayerGuid());
|
|
if (!isOwner) {
|
|
if (ImGui::Button("Sign", ImVec2(120, 0))) {
|
|
gameHandler.signPetition(pInfo.petitionGuid);
|
|
ImGui::CloseCurrentPopup();
|
|
}
|
|
ImGui::SameLine();
|
|
} else if (pInfo.signatureCount >= pInfo.signaturesRequired) {
|
|
// Owner with enough sigs — turn in
|
|
if (ImGui::Button("Turn In", ImVec2(120, 0))) {
|
|
gameHandler.turnInPetition(pInfo.petitionGuid);
|
|
ImGui::CloseCurrentPopup();
|
|
}
|
|
ImGui::SameLine();
|
|
}
|
|
if (ImGui::Button("Close", ImVec2(120, 0)))
|
|
ImGui::CloseCurrentPopup();
|
|
ImGui::EndPopup();
|
|
}
|
|
|
|
if (!showGuildRoster_) return;
|
|
|
|
// Get zone manager for name lookup
|
|
game::ZoneManager* zoneManager = nullptr;
|
|
if (auto* renderer = services_.renderer) {
|
|
zoneManager = renderer->getZoneManager();
|
|
}
|
|
|
|
auto* window = services_.window;
|
|
float screenW = window ? static_cast<float>(window->getWidth()) : 1280.0f;
|
|
float screenH = window ? static_cast<float>(window->getHeight()) : 720.0f;
|
|
|
|
ImGui::SetNextWindowPos(ImVec2(screenW / 2 - 375, screenH / 2 - 250), ImGuiCond_Once);
|
|
ImGui::SetNextWindowSize(ImVec2(750, 500), ImGuiCond_Once);
|
|
|
|
std::string title = gameHandler.isInGuild() ? (gameHandler.getGuildName() + " - Social") : "Social";
|
|
bool open = showGuildRoster_;
|
|
if (ImGui::Begin(title.c_str(), &open, ImGuiWindowFlags_NoCollapse)) {
|
|
// Tab bar: Roster | Guild Info
|
|
if (ImGui::BeginTabBar("GuildTabs")) {
|
|
if (ImGui::BeginTabItem("Roster")) {
|
|
guildRosterTab_ = 0;
|
|
if (!gameHandler.hasGuildRoster()) {
|
|
ImGui::Text("Loading roster...");
|
|
} else {
|
|
const auto& roster = gameHandler.getGuildRoster();
|
|
|
|
// MOTD
|
|
if (!roster.motd.empty()) {
|
|
ImGui::TextColored(ImVec4(0.0f, 1.0f, 0.0f, 1.0f), "MOTD: %s", roster.motd.c_str());
|
|
ImGui::Separator();
|
|
}
|
|
|
|
// Count online
|
|
int onlineCount = 0;
|
|
for (const auto& m : roster.members) {
|
|
if (m.online) ++onlineCount;
|
|
}
|
|
ImGui::Text("%d members (%d online)", static_cast<int>(roster.members.size()), onlineCount);
|
|
ImGui::Separator();
|
|
|
|
const auto& rankNames = gameHandler.getGuildRankNames();
|
|
|
|
// Table
|
|
if (ImGui::BeginTable("GuildRoster", 7,
|
|
ImGuiTableFlags_ScrollY | ImGuiTableFlags_RowBg | ImGuiTableFlags_BordersInnerV |
|
|
ImGuiTableFlags_Sortable)) {
|
|
ImGui::TableSetupColumn("Name", ImGuiTableColumnFlags_DefaultSort);
|
|
ImGui::TableSetupColumn("Rank");
|
|
ImGui::TableSetupColumn("Level", ImGuiTableColumnFlags_WidthFixed, 40.0f);
|
|
ImGui::TableSetupColumn("Class", ImGuiTableColumnFlags_WidthFixed, 70.0f);
|
|
ImGui::TableSetupColumn("Zone", ImGuiTableColumnFlags_WidthFixed, 120.0f);
|
|
ImGui::TableSetupColumn("Note");
|
|
ImGui::TableSetupColumn("Officer Note");
|
|
ImGui::TableHeadersRow();
|
|
|
|
// Online members first, then offline
|
|
auto sortedMembers = roster.members;
|
|
std::sort(sortedMembers.begin(), sortedMembers.end(), [](const auto& a, const auto& b) {
|
|
if (a.online != b.online) return a.online > b.online;
|
|
return a.name < b.name;
|
|
});
|
|
|
|
for (const auto& m : sortedMembers) {
|
|
ImGui::TableNextRow();
|
|
ImVec4 textColor = m.online ? ui::colors::kWhite
|
|
: kColorDarkGray;
|
|
ImVec4 nameColor = m.online ? classColorVec4(m.classId) : textColor;
|
|
|
|
ImGui::TableNextColumn();
|
|
ImGui::TextColored(nameColor, "%s", m.name.c_str());
|
|
|
|
// Right-click context menu
|
|
if (ImGui::IsItemClicked(ImGuiMouseButton_Right)) {
|
|
selectedGuildMember_ = m.name;
|
|
ImGui::OpenPopup("GuildMemberContext");
|
|
}
|
|
|
|
ImGui::TableNextColumn();
|
|
// Show rank name instead of index
|
|
if (m.rankIndex < rankNames.size()) {
|
|
ImGui::TextColored(textColor, "%s", rankNames[m.rankIndex].c_str());
|
|
} else {
|
|
ImGui::TextColored(textColor, "Rank %u", m.rankIndex);
|
|
}
|
|
|
|
ImGui::TableNextColumn();
|
|
ImGui::TextColored(textColor, "%u", m.level);
|
|
|
|
ImGui::TableNextColumn();
|
|
const char* className = classNameStr(m.classId);
|
|
ImVec4 classCol = m.online ? classColorVec4(m.classId) : textColor;
|
|
ImGui::TextColored(classCol, "%s", className);
|
|
|
|
ImGui::TableNextColumn();
|
|
// Zone name lookup
|
|
if (zoneManager) {
|
|
const auto* zoneInfo = zoneManager->getZoneInfo(m.zoneId);
|
|
if (zoneInfo && !zoneInfo->name.empty()) {
|
|
ImGui::TextColored(textColor, "%s", zoneInfo->name.c_str());
|
|
} else {
|
|
ImGui::TextColored(textColor, "%u", m.zoneId);
|
|
}
|
|
} else {
|
|
ImGui::TextColored(textColor, "%u", m.zoneId);
|
|
}
|
|
|
|
ImGui::TableNextColumn();
|
|
ImGui::TextColored(textColor, "%s", m.publicNote.c_str());
|
|
|
|
ImGui::TableNextColumn();
|
|
ImGui::TextColored(textColor, "%s", m.officerNote.c_str());
|
|
}
|
|
ImGui::EndTable();
|
|
}
|
|
|
|
// Context menu popup
|
|
if (ImGui::BeginPopup("GuildMemberContext")) {
|
|
ImGui::TextDisabled("%s", selectedGuildMember_.c_str());
|
|
ImGui::Separator();
|
|
// Social actions — only for online members
|
|
bool memberOnline = false;
|
|
for (const auto& mem : roster.members) {
|
|
if (mem.name == selectedGuildMember_) { memberOnline = mem.online; break; }
|
|
}
|
|
if (memberOnline) {
|
|
if (ImGui::MenuItem("Whisper")) {
|
|
chatPanel.setWhisperTarget(selectedGuildMember_);
|
|
}
|
|
if (ImGui::MenuItem("Invite to Group")) {
|
|
gameHandler.inviteToGroup(selectedGuildMember_);
|
|
}
|
|
ImGui::Separator();
|
|
}
|
|
if (!selectedGuildMember_.empty()) {
|
|
if (ImGui::MenuItem("Add Friend"))
|
|
gameHandler.addFriend(selectedGuildMember_);
|
|
if (ImGui::MenuItem("Ignore"))
|
|
gameHandler.addIgnore(selectedGuildMember_);
|
|
ImGui::Separator();
|
|
}
|
|
if (ImGui::MenuItem("Promote")) {
|
|
gameHandler.promoteGuildMember(selectedGuildMember_);
|
|
}
|
|
if (ImGui::MenuItem("Demote")) {
|
|
gameHandler.demoteGuildMember(selectedGuildMember_);
|
|
}
|
|
if (ImGui::MenuItem("Kick")) {
|
|
gameHandler.kickGuildMember(selectedGuildMember_);
|
|
}
|
|
ImGui::Separator();
|
|
if (ImGui::MenuItem("Set Public Note...")) {
|
|
showGuildNoteEdit_ = true;
|
|
editingOfficerNote_ = false;
|
|
guildNoteEditBuffer_[0] = '\0';
|
|
// Pre-fill with existing note
|
|
for (const auto& mem : roster.members) {
|
|
if (mem.name == selectedGuildMember_) {
|
|
snprintf(guildNoteEditBuffer_, sizeof(guildNoteEditBuffer_), "%s", mem.publicNote.c_str());
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
if (ImGui::MenuItem("Set Officer Note...")) {
|
|
showGuildNoteEdit_ = true;
|
|
editingOfficerNote_ = true;
|
|
guildNoteEditBuffer_[0] = '\0';
|
|
for (const auto& mem : roster.members) {
|
|
if (mem.name == selectedGuildMember_) {
|
|
snprintf(guildNoteEditBuffer_, sizeof(guildNoteEditBuffer_), "%s", mem.officerNote.c_str());
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
ImGui::Separator();
|
|
if (ImGui::MenuItem("Set as Leader")) {
|
|
gameHandler.setGuildLeader(selectedGuildMember_);
|
|
}
|
|
ImGui::EndPopup();
|
|
}
|
|
|
|
// Note edit modal
|
|
if (showGuildNoteEdit_) {
|
|
ImGui::OpenPopup("EditGuildNote");
|
|
showGuildNoteEdit_ = false;
|
|
}
|
|
if (ImGui::BeginPopupModal("EditGuildNote", nullptr, ImGuiWindowFlags_AlwaysAutoResize)) {
|
|
ImGui::Text("%s %s for %s:",
|
|
editingOfficerNote_ ? "Officer" : "Public", "Note", selectedGuildMember_.c_str());
|
|
ImGui::InputText("##guildnote", guildNoteEditBuffer_, sizeof(guildNoteEditBuffer_));
|
|
if (ImGui::Button("Save")) {
|
|
if (editingOfficerNote_) {
|
|
gameHandler.setGuildOfficerNote(selectedGuildMember_, guildNoteEditBuffer_);
|
|
} else {
|
|
gameHandler.setGuildPublicNote(selectedGuildMember_, guildNoteEditBuffer_);
|
|
}
|
|
ImGui::CloseCurrentPopup();
|
|
}
|
|
ImGui::SameLine();
|
|
if (ImGui::Button("Cancel")) {
|
|
ImGui::CloseCurrentPopup();
|
|
}
|
|
ImGui::EndPopup();
|
|
}
|
|
}
|
|
ImGui::EndTabItem();
|
|
}
|
|
|
|
if (ImGui::BeginTabItem("Guild Info")) {
|
|
guildRosterTab_ = 1;
|
|
const auto& infoData = gameHandler.getGuildInfoData();
|
|
const auto& queryData = gameHandler.getGuildQueryData();
|
|
const auto& roster = gameHandler.getGuildRoster();
|
|
const auto& rankNames = gameHandler.getGuildRankNames();
|
|
|
|
// Guild name (large, gold)
|
|
ImGui::PushFont(nullptr); // default font
|
|
ImGui::TextColored(ui::colors::kTooltipGold, "<%s>", gameHandler.getGuildName().c_str());
|
|
ImGui::PopFont();
|
|
ImGui::Separator();
|
|
|
|
// Creation date
|
|
if (infoData.isValid()) {
|
|
ImGui::Text("Created: %u/%u/%u", infoData.creationDay, infoData.creationMonth, infoData.creationYear);
|
|
ImGui::Text("Members: %u | Accounts: %u", infoData.numMembers, infoData.numAccounts);
|
|
}
|
|
ImGui::Spacing();
|
|
|
|
// Guild description / info text
|
|
if (!roster.guildInfo.empty()) {
|
|
ImGui::TextColored(colors::kSilver, "Description:");
|
|
ImGui::TextWrapped("%s", roster.guildInfo.c_str());
|
|
}
|
|
ImGui::Spacing();
|
|
|
|
// MOTD with edit button
|
|
ImGui::TextColored(ImVec4(0.0f, 1.0f, 0.0f, 1.0f), "MOTD:");
|
|
ImGui::SameLine();
|
|
if (!roster.motd.empty()) {
|
|
ImGui::TextWrapped("%s", roster.motd.c_str());
|
|
} else {
|
|
ImGui::TextColored(kColorDarkGray, "(not set)");
|
|
}
|
|
if (ImGui::Button("Set MOTD")) {
|
|
showMotdEdit_ = true;
|
|
snprintf(guildMotdEditBuffer_, sizeof(guildMotdEditBuffer_), "%s", roster.motd.c_str());
|
|
}
|
|
ImGui::Spacing();
|
|
|
|
// MOTD edit modal
|
|
if (showMotdEdit_) {
|
|
ImGui::OpenPopup("EditMotd");
|
|
showMotdEdit_ = false;
|
|
}
|
|
if (ImGui::BeginPopupModal("EditMotd", nullptr, ImGuiWindowFlags_AlwaysAutoResize)) {
|
|
ImGui::Text("Set Message of the Day:");
|
|
ImGui::InputText("##motdinput", guildMotdEditBuffer_, sizeof(guildMotdEditBuffer_));
|
|
if (ImGui::Button("Save", ImVec2(120, 0))) {
|
|
gameHandler.setGuildMotd(guildMotdEditBuffer_);
|
|
ImGui::CloseCurrentPopup();
|
|
}
|
|
ImGui::SameLine();
|
|
if (ImGui::Button("Cancel", ImVec2(120, 0))) {
|
|
ImGui::CloseCurrentPopup();
|
|
}
|
|
ImGui::EndPopup();
|
|
}
|
|
|
|
// Emblem info
|
|
if (queryData.isValid()) {
|
|
ImGui::Separator();
|
|
ImGui::Text("Emblem: Style %u, Color %u | Border: Style %u, Color %u | BG: %u",
|
|
queryData.emblemStyle, queryData.emblemColor,
|
|
queryData.borderStyle, queryData.borderColor, queryData.backgroundColor);
|
|
}
|
|
|
|
// Rank list
|
|
ImGui::Separator();
|
|
ImGui::TextColored(ui::colors::kTooltipGold, "Ranks:");
|
|
for (size_t i = 0; i < rankNames.size(); ++i) {
|
|
if (rankNames[i].empty()) continue;
|
|
// Show rank permission summary from roster data
|
|
if (i < roster.ranks.size()) {
|
|
uint32_t rights = roster.ranks[i].rights;
|
|
std::string perms;
|
|
if (rights & 0x01) perms += "Invite ";
|
|
if (rights & 0x02) perms += "Remove ";
|
|
if (rights & 0x40) perms += "Promote ";
|
|
if (rights & 0x80) perms += "Demote ";
|
|
if (rights & 0x04) perms += "OChat ";
|
|
if (rights & 0x10) perms += "MOTD ";
|
|
ImGui::Text(" %zu. %s", i + 1, rankNames[i].c_str());
|
|
if (!perms.empty()) {
|
|
ImGui::SameLine();
|
|
ImGui::TextColored(kColorDarkGray, "[%s]", perms.c_str());
|
|
}
|
|
} else {
|
|
ImGui::Text(" %zu. %s", i + 1, rankNames[i].c_str());
|
|
}
|
|
}
|
|
|
|
// Rank management buttons
|
|
ImGui::Spacing();
|
|
if (ImGui::Button("Add Rank")) {
|
|
showAddRankModal_ = true;
|
|
addRankNameBuffer_[0] = '\0';
|
|
}
|
|
ImGui::SameLine();
|
|
if (ImGui::Button("Delete Last Rank")) {
|
|
gameHandler.deleteGuildRank();
|
|
}
|
|
|
|
// Add rank modal
|
|
if (showAddRankModal_) {
|
|
ImGui::OpenPopup("AddGuildRank");
|
|
showAddRankModal_ = false;
|
|
}
|
|
if (ImGui::BeginPopupModal("AddGuildRank", nullptr, ImGuiWindowFlags_AlwaysAutoResize)) {
|
|
ImGui::Text("New Rank Name:");
|
|
ImGui::InputText("##rankname", addRankNameBuffer_, sizeof(addRankNameBuffer_));
|
|
if (ImGui::Button("Add", ImVec2(120, 0))) {
|
|
if (addRankNameBuffer_[0] != '\0') {
|
|
gameHandler.addGuildRank(addRankNameBuffer_);
|
|
ImGui::CloseCurrentPopup();
|
|
}
|
|
}
|
|
ImGui::SameLine();
|
|
if (ImGui::Button("Cancel", ImVec2(120, 0))) {
|
|
ImGui::CloseCurrentPopup();
|
|
}
|
|
ImGui::EndPopup();
|
|
}
|
|
|
|
ImGui::EndTabItem();
|
|
}
|
|
|
|
// ---- Friends tab ----
|
|
if (ImGui::BeginTabItem("Friends")) {
|
|
guildRosterTab_ = 2;
|
|
const auto& contacts = gameHandler.getContacts();
|
|
|
|
// Add Friend row
|
|
static char addFriendBuf[64] = {};
|
|
ImGui::SetNextItemWidth(180.0f);
|
|
ImGui::InputText("##addfriend", addFriendBuf, sizeof(addFriendBuf));
|
|
ImGui::SameLine();
|
|
if (ImGui::Button("Add Friend") && addFriendBuf[0] != '\0') {
|
|
gameHandler.addFriend(addFriendBuf);
|
|
addFriendBuf[0] = '\0';
|
|
}
|
|
ImGui::Separator();
|
|
|
|
// Note-edit state
|
|
static std::string friendNoteTarget;
|
|
static char friendNoteBuf[256] = {};
|
|
static bool openNotePopup = false;
|
|
|
|
// Filter to friends only
|
|
int friendCount = 0;
|
|
for (size_t ci = 0; ci < contacts.size(); ++ci) {
|
|
const auto& c = contacts[ci];
|
|
if (!c.isFriend()) continue;
|
|
++friendCount;
|
|
|
|
ImGui::PushID(static_cast<int>(ci));
|
|
|
|
// Status dot
|
|
ImU32 dotColor = c.isOnline()
|
|
? IM_COL32(80, 200, 80, 255)
|
|
: IM_COL32(120, 120, 120, 255);
|
|
ImVec2 cursor = ImGui::GetCursorScreenPos();
|
|
ImGui::GetWindowDrawList()->AddCircleFilled(
|
|
ImVec2(cursor.x + 6.0f, cursor.y + 8.0f), 5.0f, dotColor);
|
|
ImGui::Dummy(ImVec2(14.0f, 0.0f));
|
|
ImGui::SameLine();
|
|
|
|
// Name as Selectable for right-click context menu
|
|
const char* displayName = c.name.empty() ? "(unknown)" : c.name.c_str();
|
|
ImVec4 nameCol = c.isOnline()
|
|
? ui::colors::kWhite
|
|
: colors::kInactiveGray;
|
|
ImGui::PushStyleColor(ImGuiCol_Text, nameCol);
|
|
ImGui::Selectable(displayName, false, ImGuiSelectableFlags_AllowOverlap, ImVec2(130.0f, 0.0f));
|
|
ImGui::PopStyleColor();
|
|
|
|
// Double-click to whisper
|
|
if (ImGui::IsItemHovered() && ImGui::IsMouseDoubleClicked(ImGuiMouseButton_Left)
|
|
&& !c.name.empty()) {
|
|
chatPanel.setWhisperTarget(c.name);
|
|
}
|
|
|
|
// Right-click context menu
|
|
if (ImGui::BeginPopupContextItem("FriendCtx")) {
|
|
ImGui::TextDisabled("%s", displayName);
|
|
ImGui::Separator();
|
|
if (ImGui::MenuItem("Whisper") && !c.name.empty()) {
|
|
chatPanel.setWhisperTarget(c.name);
|
|
}
|
|
if (c.isOnline() && ImGui::MenuItem("Invite to Group") && !c.name.empty()) {
|
|
gameHandler.inviteToGroup(c.name);
|
|
}
|
|
if (ImGui::MenuItem("Edit Note")) {
|
|
friendNoteTarget = c.name;
|
|
strncpy(friendNoteBuf, c.note.c_str(), sizeof(friendNoteBuf) - 1);
|
|
friendNoteBuf[sizeof(friendNoteBuf) - 1] = '\0';
|
|
openNotePopup = true;
|
|
}
|
|
ImGui::Separator();
|
|
if (ImGui::MenuItem("Remove Friend")) {
|
|
gameHandler.removeFriend(c.name);
|
|
}
|
|
ImGui::EndPopup();
|
|
}
|
|
|
|
// Note tooltip on hover
|
|
if (ImGui::IsItemHovered() && !c.note.empty()) {
|
|
ImGui::BeginTooltip();
|
|
ImGui::TextDisabled("Note: %s", c.note.c_str());
|
|
ImGui::EndTooltip();
|
|
}
|
|
|
|
// Level, class, and status
|
|
if (c.isOnline()) {
|
|
ImGui::SameLine(150.0f);
|
|
const char* statusLabel =
|
|
(c.status == 2) ? " (AFK)" :
|
|
(c.status == 3) ? " (DND)" : "";
|
|
// Class color for the level/class display
|
|
ImVec4 friendClassCol = classColorVec4(static_cast<uint8_t>(c.classId));
|
|
const char* friendClassName = classNameStr(static_cast<uint8_t>(c.classId));
|
|
if (c.level > 0 && c.classId > 0) {
|
|
ImGui::TextColored(friendClassCol, "Lv%u %s%s", c.level, friendClassName, statusLabel);
|
|
} else if (c.level > 0) {
|
|
ImGui::TextDisabled("Lv %u%s", c.level, statusLabel);
|
|
} else if (*statusLabel) {
|
|
ImGui::TextDisabled("%s", statusLabel + 1);
|
|
}
|
|
|
|
// Tooltip: zone info
|
|
if (ImGui::IsItemHovered() && c.areaId != 0) {
|
|
ImGui::BeginTooltip();
|
|
if (zoneManager) {
|
|
const auto* zi = zoneManager->getZoneInfo(c.areaId);
|
|
if (zi && !zi->name.empty())
|
|
ImGui::Text("Zone: %s", zi->name.c_str());
|
|
else
|
|
ImGui::TextDisabled("Area ID: %u", c.areaId);
|
|
} else {
|
|
ImGui::TextDisabled("Area ID: %u", c.areaId);
|
|
}
|
|
ImGui::EndTooltip();
|
|
}
|
|
}
|
|
|
|
ImGui::PopID();
|
|
}
|
|
|
|
if (friendCount == 0) {
|
|
ImGui::TextDisabled("No friends found.");
|
|
}
|
|
|
|
// Note edit modal
|
|
if (openNotePopup) {
|
|
ImGui::OpenPopup("EditFriendNote");
|
|
openNotePopup = false;
|
|
}
|
|
if (ImGui::BeginPopupModal("EditFriendNote", nullptr, ImGuiWindowFlags_AlwaysAutoResize)) {
|
|
ImGui::Text("Note for %s:", friendNoteTarget.c_str());
|
|
ImGui::SetNextItemWidth(240.0f);
|
|
ImGui::InputText("##fnote", friendNoteBuf, sizeof(friendNoteBuf));
|
|
if (ImGui::Button("Save", ImVec2(110, 0))) {
|
|
gameHandler.setFriendNote(friendNoteTarget, friendNoteBuf);
|
|
ImGui::CloseCurrentPopup();
|
|
}
|
|
ImGui::SameLine();
|
|
if (ImGui::Button("Cancel", ImVec2(110, 0))) {
|
|
ImGui::CloseCurrentPopup();
|
|
}
|
|
ImGui::EndPopup();
|
|
}
|
|
|
|
ImGui::EndTabItem();
|
|
}
|
|
|
|
// ---- Ignore List tab ----
|
|
if (ImGui::BeginTabItem("Ignore")) {
|
|
guildRosterTab_ = 3;
|
|
const auto& contacts = gameHandler.getContacts();
|
|
|
|
// Add Ignore row
|
|
static char addIgnoreBuf[64] = {};
|
|
ImGui::SetNextItemWidth(180.0f);
|
|
ImGui::InputText("##addignore", addIgnoreBuf, sizeof(addIgnoreBuf));
|
|
ImGui::SameLine();
|
|
if (ImGui::Button("Ignore Player") && addIgnoreBuf[0] != '\0') {
|
|
gameHandler.addIgnore(addIgnoreBuf);
|
|
addIgnoreBuf[0] = '\0';
|
|
}
|
|
ImGui::Separator();
|
|
|
|
int ignoreCount = 0;
|
|
for (size_t ci = 0; ci < contacts.size(); ++ci) {
|
|
const auto& c = contacts[ci];
|
|
if (!c.isIgnored()) continue;
|
|
++ignoreCount;
|
|
|
|
ImGui::PushID(static_cast<int>(ci) + 10000);
|
|
const char* displayName = c.name.empty() ? "(unknown)" : c.name.c_str();
|
|
ImGui::Selectable(displayName, false, ImGuiSelectableFlags_AllowOverlap);
|
|
if (ImGui::BeginPopupContextItem("IgnoreCtx")) {
|
|
ImGui::TextDisabled("%s", displayName);
|
|
ImGui::Separator();
|
|
if (ImGui::MenuItem("Remove Ignore")) {
|
|
gameHandler.removeIgnore(c.name);
|
|
}
|
|
ImGui::EndPopup();
|
|
}
|
|
ImGui::PopID();
|
|
}
|
|
|
|
if (ignoreCount == 0) {
|
|
ImGui::TextDisabled("Ignore list is empty.");
|
|
}
|
|
|
|
ImGui::EndTabItem();
|
|
}
|
|
|
|
ImGui::EndTabBar();
|
|
}
|
|
}
|
|
ImGui::End();
|
|
showGuildRoster_ = open;
|
|
}
|
|
|
|
void SocialPanel::renderSocialFrame(game::GameHandler& gameHandler,
|
|
ChatPanel& chatPanel) {
|
|
if (!showSocialFrame_) return;
|
|
|
|
const auto& contacts = gameHandler.getContacts();
|
|
// Count online friends for early-out
|
|
int onlineCount = 0;
|
|
for (const auto& c : contacts)
|
|
if (c.isFriend() && c.isOnline()) ++onlineCount;
|
|
|
|
auto* window = services_.window;
|
|
float screenW = window ? static_cast<float>(window->getWidth()) : 1280.0f;
|
|
|
|
ImGui::SetNextWindowPos(ImVec2(screenW - 230.0f, 240.0f), ImGuiCond_Once);
|
|
ImGui::SetNextWindowSize(ImVec2(220.0f, 0.0f), ImGuiCond_Always);
|
|
|
|
ImGui::PushStyleVar(ImGuiStyleVar_WindowRounding, 4.0f);
|
|
ImGui::PushStyleColor(ImGuiCol_WindowBg, ImVec4(0.1f, 0.1f, 0.1f, 0.92f));
|
|
|
|
// State for "Set Note" inline editing
|
|
static int noteEditContactIdx = -1;
|
|
static char noteEditBuf[128] = {};
|
|
|
|
bool open = showSocialFrame_;
|
|
char socialTitle[32];
|
|
snprintf(socialTitle, sizeof(socialTitle), "Social (%d online)##SocialFrame", onlineCount);
|
|
if (ImGui::Begin(socialTitle, &open,
|
|
ImGuiWindowFlags_NoResize | ImGuiWindowFlags_NoCollapse |
|
|
ImGuiWindowFlags_AlwaysAutoResize | ImGuiWindowFlags_NoScrollbar)) {
|
|
|
|
// Get zone manager for area name lookups
|
|
game::ZoneManager* socialZoneMgr = nullptr;
|
|
if (auto* rend = services_.renderer)
|
|
socialZoneMgr = rend->getZoneManager();
|
|
|
|
if (ImGui::BeginTabBar("##SocialTabs")) {
|
|
// ---- Friends tab ----
|
|
if (ImGui::BeginTabItem("Friends")) {
|
|
ImGui::BeginChild("##FriendsList", ImVec2(200, 200), false);
|
|
|
|
// Online friends first
|
|
int shown = 0;
|
|
for (int pass = 0; pass < 2; ++pass) {
|
|
bool wantOnline = (pass == 0);
|
|
for (size_t ci = 0; ci < contacts.size(); ++ci) {
|
|
const auto& c = contacts[ci];
|
|
if (!c.isFriend()) continue;
|
|
if (c.isOnline() != wantOnline) continue;
|
|
|
|
ImGui::PushID(static_cast<int>(ci));
|
|
|
|
// Status dot
|
|
ImU32 dotColor;
|
|
if (!c.isOnline()) dotColor = IM_COL32(100, 100, 100, 200);
|
|
else if (c.status == 2) dotColor = IM_COL32(255, 200, 50, 255); // AFK
|
|
else if (c.status == 3) dotColor = IM_COL32(255, 120, 50, 255); // DND
|
|
else dotColor = IM_COL32( 50, 220, 50, 255); // online
|
|
|
|
ImVec2 dotMin = ImGui::GetCursorScreenPos();
|
|
dotMin.y += 4.0f;
|
|
ImGui::GetWindowDrawList()->AddCircleFilled(
|
|
ImVec2(dotMin.x + 5.0f, dotMin.y + 5.0f), 4.5f, dotColor);
|
|
ImGui::SetCursorPosX(ImGui::GetCursorPosX() + 14.0f);
|
|
|
|
const char* displayName = c.name.empty() ? "(unknown)" : c.name.c_str();
|
|
ImVec4 nameCol = c.isOnline()
|
|
? classColorVec4(static_cast<uint8_t>(c.classId))
|
|
: kColorDarkGray;
|
|
ImGui::TextColored(nameCol, "%s", displayName);
|
|
|
|
if (c.isOnline() && c.level > 0) {
|
|
ImGui::SameLine();
|
|
// Show level and class name in class color
|
|
ImGui::TextColored(classColorVec4(static_cast<uint8_t>(c.classId)),
|
|
"Lv%u %s", c.level, classNameStr(static_cast<uint8_t>(c.classId)));
|
|
}
|
|
|
|
// Tooltip: zone info and note
|
|
if (ImGui::IsItemHovered() || (c.isOnline() && ImGui::IsItemHovered())) {
|
|
if (c.isOnline() && (c.areaId != 0 || !c.note.empty())) {
|
|
ImGui::BeginTooltip();
|
|
if (c.areaId != 0) {
|
|
const char* zoneName = nullptr;
|
|
if (socialZoneMgr) {
|
|
const auto* zi = socialZoneMgr->getZoneInfo(c.areaId);
|
|
if (zi && !zi->name.empty()) zoneName = zi->name.c_str();
|
|
}
|
|
if (zoneName)
|
|
ImGui::Text("Zone: %s", zoneName);
|
|
else
|
|
ImGui::Text("Area ID: %u", c.areaId);
|
|
}
|
|
if (!c.note.empty())
|
|
ImGui::TextDisabled("Note: %s", c.note.c_str());
|
|
ImGui::EndTooltip();
|
|
}
|
|
}
|
|
|
|
// Right-click context menu
|
|
if (ImGui::BeginPopupContextItem("FriendCtx")) {
|
|
ImGui::TextDisabled("%s", displayName);
|
|
ImGui::Separator();
|
|
if (c.isOnline()) {
|
|
if (ImGui::MenuItem("Whisper")) {
|
|
showSocialFrame_ = false;
|
|
chatPanel.setWhisperTarget(c.name);
|
|
}
|
|
if (ImGui::MenuItem("Invite to Group"))
|
|
gameHandler.inviteToGroup(c.name);
|
|
if (c.guid != 0 && ImGui::MenuItem("Trade"))
|
|
gameHandler.initiateTrade(c.guid);
|
|
}
|
|
if (ImGui::MenuItem("Set Note")) {
|
|
noteEditContactIdx = static_cast<int>(ci);
|
|
strncpy(noteEditBuf, c.note.c_str(), sizeof(noteEditBuf) - 1);
|
|
noteEditBuf[sizeof(noteEditBuf) - 1] = '\0';
|
|
ImGui::OpenPopup("##SetFriendNote");
|
|
}
|
|
if (ImGui::MenuItem("Remove Friend"))
|
|
gameHandler.removeFriend(c.name);
|
|
ImGui::EndPopup();
|
|
}
|
|
|
|
++shown;
|
|
ImGui::PopID();
|
|
}
|
|
// Separator between online and offline if there are both
|
|
if (pass == 0 && shown > 0) {
|
|
ImGui::Separator();
|
|
}
|
|
}
|
|
|
|
if (shown == 0) {
|
|
ImGui::TextDisabled("No friends yet.");
|
|
}
|
|
|
|
ImGui::EndChild();
|
|
|
|
// "Set Note" modal popup
|
|
if (ImGui::BeginPopup("##SetFriendNote")) {
|
|
const std::string& noteName = (noteEditContactIdx >= 0 &&
|
|
noteEditContactIdx < static_cast<int>(contacts.size()))
|
|
? contacts[noteEditContactIdx].name : "";
|
|
ImGui::TextDisabled("Note for %s:", noteName.c_str());
|
|
ImGui::SetNextItemWidth(180.0f);
|
|
bool confirm = ImGui::InputText("##noteinput", noteEditBuf, sizeof(noteEditBuf),
|
|
ImGuiInputTextFlags_EnterReturnsTrue);
|
|
ImGui::SameLine();
|
|
if (confirm || ImGui::Button("OK")) {
|
|
if (!noteName.empty())
|
|
gameHandler.setFriendNote(noteName, noteEditBuf);
|
|
noteEditContactIdx = -1;
|
|
ImGui::CloseCurrentPopup();
|
|
}
|
|
ImGui::SameLine();
|
|
if (ImGui::Button("Cancel")) {
|
|
noteEditContactIdx = -1;
|
|
ImGui::CloseCurrentPopup();
|
|
}
|
|
ImGui::EndPopup();
|
|
}
|
|
|
|
ImGui::Separator();
|
|
|
|
// Add friend
|
|
static char addFriendBuf[64] = {};
|
|
ImGui::SetNextItemWidth(140.0f);
|
|
ImGui::InputText("##sf_addfriend", addFriendBuf, sizeof(addFriendBuf));
|
|
ImGui::SameLine();
|
|
if (ImGui::Button("+##addfriend") && addFriendBuf[0] != '\0') {
|
|
gameHandler.addFriend(addFriendBuf);
|
|
addFriendBuf[0] = '\0';
|
|
}
|
|
|
|
ImGui::EndTabItem();
|
|
}
|
|
|
|
// ---- Ignore tab ----
|
|
if (ImGui::BeginTabItem("Ignore")) {
|
|
const auto& ignores = gameHandler.getIgnoreCache();
|
|
ImGui::BeginChild("##IgnoreList", ImVec2(200, 200), false);
|
|
|
|
if (ignores.empty()) {
|
|
ImGui::TextDisabled("Ignore list is empty.");
|
|
} else {
|
|
for (const auto& kv : ignores) {
|
|
ImGui::PushID(kv.first.c_str());
|
|
ImGui::TextUnformatted(kv.first.c_str());
|
|
if (ImGui::BeginPopupContextItem("IgnoreCtx")) {
|
|
ImGui::TextDisabled("%s", kv.first.c_str());
|
|
ImGui::Separator();
|
|
if (ImGui::MenuItem("Unignore"))
|
|
gameHandler.removeIgnore(kv.first);
|
|
ImGui::EndPopup();
|
|
}
|
|
ImGui::PopID();
|
|
}
|
|
}
|
|
|
|
ImGui::EndChild();
|
|
ImGui::Separator();
|
|
|
|
// Add ignore
|
|
static char addIgnBuf[64] = {};
|
|
ImGui::SetNextItemWidth(140.0f);
|
|
ImGui::InputText("##sf_addignore", addIgnBuf, sizeof(addIgnBuf));
|
|
ImGui::SameLine();
|
|
if (ImGui::Button("+##addignore") && addIgnBuf[0] != '\0') {
|
|
gameHandler.addIgnore(addIgnBuf);
|
|
addIgnBuf[0] = '\0';
|
|
}
|
|
|
|
ImGui::EndTabItem();
|
|
}
|
|
|
|
// ---- Channels tab ----
|
|
if (ImGui::BeginTabItem("Channels")) {
|
|
const auto& channels = gameHandler.getJoinedChannels();
|
|
ImGui::BeginChild("##ChannelList", ImVec2(200, 200), false);
|
|
|
|
if (channels.empty()) {
|
|
ImGui::TextDisabled("Not in any channels.");
|
|
} else {
|
|
for (size_t ci = 0; ci < channels.size(); ++ci) {
|
|
ImGui::PushID(static_cast<int>(ci));
|
|
ImGui::TextUnformatted(channels[ci].c_str());
|
|
if (ImGui::BeginPopupContextItem("ChanCtx")) {
|
|
ImGui::TextDisabled("%s", channels[ci].c_str());
|
|
ImGui::Separator();
|
|
if (ImGui::MenuItem("Leave Channel"))
|
|
gameHandler.leaveChannel(channels[ci]);
|
|
ImGui::EndPopup();
|
|
}
|
|
ImGui::PopID();
|
|
}
|
|
}
|
|
|
|
ImGui::EndChild();
|
|
ImGui::Separator();
|
|
|
|
// Join a channel
|
|
static char joinChanBuf[64] = {};
|
|
ImGui::SetNextItemWidth(140.0f);
|
|
ImGui::InputText("##sf_joinchan", joinChanBuf, sizeof(joinChanBuf));
|
|
ImGui::SameLine();
|
|
if (ImGui::Button("+##joinchan") && joinChanBuf[0] != '\0') {
|
|
gameHandler.joinChannel(joinChanBuf);
|
|
joinChanBuf[0] = '\0';
|
|
}
|
|
|
|
ImGui::EndTabItem();
|
|
}
|
|
|
|
// ---- Arena tab (WotLK: shows per-team rating/record + roster) ----
|
|
const auto& arenaStats = gameHandler.getArenaTeamStats();
|
|
if (!arenaStats.empty()) {
|
|
if (ImGui::BeginTabItem("Arena")) {
|
|
ImGui::BeginChild("##ArenaList", ImVec2(0, 0), false);
|
|
|
|
for (size_t ai = 0; ai < arenaStats.size(); ++ai) {
|
|
const auto& ts = arenaStats[ai];
|
|
ImGui::PushID(static_cast<int>(ai));
|
|
|
|
// Team header: "2v2: Team Name" or fallback "Team #id"
|
|
std::string teamLabel;
|
|
if (ts.teamType > 0)
|
|
teamLabel = std::to_string(ts.teamType) + "v" + std::to_string(ts.teamType) + ": ";
|
|
if (!ts.teamName.empty())
|
|
teamLabel += ts.teamName;
|
|
else
|
|
teamLabel += "Team #" + std::to_string(ts.teamId);
|
|
ImGui::TextColored(ImVec4(1.0f, 0.85f, 0.2f, 1.0f), "%s", teamLabel.c_str());
|
|
|
|
ImGui::Indent(8.0f);
|
|
// Rating and rank
|
|
ImGui::Text("Rating: %u", ts.rating);
|
|
if (ts.rank > 0) {
|
|
ImGui::SameLine(0, 6);
|
|
ImGui::TextDisabled("(Rank #%u)", ts.rank);
|
|
}
|
|
|
|
// Weekly record
|
|
uint32_t weekLosses = ts.weekGames > ts.weekWins
|
|
? ts.weekGames - ts.weekWins : 0;
|
|
ImGui::Text("Week: %u W / %u L", ts.weekWins, weekLosses);
|
|
|
|
// Season record
|
|
uint32_t seasLosses = ts.seasonGames > ts.seasonWins
|
|
? ts.seasonGames - ts.seasonWins : 0;
|
|
ImGui::Text("Season: %u W / %u L", ts.seasonWins, seasLosses);
|
|
|
|
// Roster members (from SMSG_ARENA_TEAM_ROSTER)
|
|
const auto* roster = gameHandler.getArenaTeamRoster(ts.teamId);
|
|
if (roster && !roster->members.empty()) {
|
|
ImGui::Spacing();
|
|
ImGui::TextDisabled("-- Roster (%zu members) --",
|
|
roster->members.size());
|
|
ImGui::SameLine();
|
|
if (ImGui::SmallButton("Refresh"))
|
|
gameHandler.requestArenaTeamRoster(ts.teamId);
|
|
|
|
// Column headers
|
|
ImGui::Columns(4, "##arenaRosterCols", false);
|
|
ImGui::SetColumnWidth(0, 110.0f);
|
|
ImGui::SetColumnWidth(1, 60.0f);
|
|
ImGui::SetColumnWidth(2, 60.0f);
|
|
ImGui::SetColumnWidth(3, 60.0f);
|
|
ImGui::TextDisabled("Name"); ImGui::NextColumn();
|
|
ImGui::TextDisabled("Rating"); ImGui::NextColumn();
|
|
ImGui::TextDisabled("Week"); ImGui::NextColumn();
|
|
ImGui::TextDisabled("Season"); ImGui::NextColumn();
|
|
ImGui::Separator();
|
|
|
|
for (const auto& m : roster->members) {
|
|
// Name coloured green (online) or grey (offline)
|
|
if (m.online)
|
|
ImGui::TextColored(ImVec4(0.4f,1.0f,0.4f,1.0f),
|
|
"%s", m.name.c_str());
|
|
else
|
|
ImGui::TextDisabled("%s", m.name.c_str());
|
|
ImGui::NextColumn();
|
|
|
|
ImGui::Text("%u", m.personalRating);
|
|
ImGui::NextColumn();
|
|
|
|
uint32_t wL = m.weekGames > m.weekWins
|
|
? m.weekGames - m.weekWins : 0;
|
|
ImGui::Text("%uW/%uL", m.weekWins, wL);
|
|
ImGui::NextColumn();
|
|
|
|
uint32_t sL = m.seasonGames > m.seasonWins
|
|
? m.seasonGames - m.seasonWins : 0;
|
|
ImGui::Text("%uW/%uL", m.seasonWins, sL);
|
|
ImGui::NextColumn();
|
|
}
|
|
ImGui::Columns(1);
|
|
} else {
|
|
ImGui::Spacing();
|
|
if (ImGui::SmallButton("Load Roster"))
|
|
gameHandler.requestArenaTeamRoster(ts.teamId);
|
|
}
|
|
|
|
ImGui::Unindent(8.0f);
|
|
|
|
if (ai + 1 < arenaStats.size())
|
|
ImGui::Separator();
|
|
|
|
ImGui::PopID();
|
|
}
|
|
|
|
ImGui::EndChild();
|
|
ImGui::EndTabItem();
|
|
}
|
|
}
|
|
|
|
ImGui::EndTabBar();
|
|
}
|
|
}
|
|
ImGui::End();
|
|
showSocialFrame_ = open;
|
|
|
|
ImGui::PopStyleColor();
|
|
ImGui::PopStyleVar();
|
|
}
|
|
|
|
void SocialPanel::renderDungeonFinderWindow(game::GameHandler& gameHandler,
|
|
ChatPanel& chatPanel) {
|
|
// Toggle Dungeon Finder (customizable keybind)
|
|
if (!chatPanel.isChatInputActive() && !ImGui::GetIO().WantTextInput &&
|
|
KeybindingManager::getInstance().isActionPressed(KeybindingManager::Action::TOGGLE_DUNGEON_FINDER)) {
|
|
showDungeonFinder_ = !showDungeonFinder_;
|
|
}
|
|
|
|
if (!showDungeonFinder_) return;
|
|
|
|
auto* window = services_.window;
|
|
float screenW = window ? static_cast<float>(window->getWidth()) : 1280.0f;
|
|
float screenH = window ? static_cast<float>(window->getHeight()) : 720.0f;
|
|
|
|
ImGui::SetNextWindowPos(ImVec2(screenW * 0.5f - 175.0f, screenH * 0.2f),
|
|
ImGuiCond_FirstUseEver);
|
|
ImGui::SetNextWindowSize(ImVec2(350, 0), ImGuiCond_Always);
|
|
|
|
bool open = true;
|
|
ImGuiWindowFlags flags = ImGuiWindowFlags_NoCollapse | ImGuiWindowFlags_AlwaysAutoResize;
|
|
if (!ImGui::Begin("Dungeon Finder", &open, flags)) {
|
|
ImGui::End();
|
|
if (!open) showDungeonFinder_ = false;
|
|
return;
|
|
}
|
|
if (!open) {
|
|
ImGui::End();
|
|
showDungeonFinder_ = false;
|
|
return;
|
|
}
|
|
|
|
using LfgState = game::GameHandler::LfgState;
|
|
LfgState state = gameHandler.getLfgState();
|
|
|
|
// ---- Status banner ----
|
|
switch (state) {
|
|
case LfgState::None:
|
|
ImGui::TextColored(kColorGray, "Status: Not queued");
|
|
break;
|
|
case LfgState::RoleCheck:
|
|
ImGui::TextColored(ImVec4(1.0f, 0.8f, 0.2f, 1.0f), "Status: Role check in progress...");
|
|
break;
|
|
case LfgState::Queued: {
|
|
int32_t avgSec = gameHandler.getLfgAvgWaitSec();
|
|
uint32_t qMs = gameHandler.getLfgTimeInQueueMs();
|
|
int qMin = static_cast<int>(qMs / 60000);
|
|
int qSec = static_cast<int>((qMs % 60000) / 1000);
|
|
std::string dName = gameHandler.getCurrentLfgDungeonName();
|
|
if (!dName.empty())
|
|
ImGui::TextColored(colors::kQueueGreen,
|
|
"Status: In queue for %s (%d:%02d)", dName.c_str(), qMin, qSec);
|
|
else
|
|
ImGui::TextColored(colors::kQueueGreen, "Status: In queue (%d:%02d)", qMin, qSec);
|
|
if (avgSec >= 0) {
|
|
int aMin = avgSec / 60;
|
|
int aSec = avgSec % 60;
|
|
ImGui::TextColored(colors::kSilver,
|
|
"Avg wait: %d:%02d", aMin, aSec);
|
|
}
|
|
break;
|
|
}
|
|
case LfgState::Proposal: {
|
|
std::string dName = gameHandler.getCurrentLfgDungeonName();
|
|
if (!dName.empty())
|
|
ImGui::TextColored(ImVec4(1.0f, 0.5f, 0.1f, 1.0f), "Status: Group found for %s!", dName.c_str());
|
|
else
|
|
ImGui::TextColored(ImVec4(1.0f, 0.5f, 0.1f, 1.0f), "Status: Group found!");
|
|
break;
|
|
}
|
|
case LfgState::Boot:
|
|
ImGui::TextColored(kColorRed, "Status: Vote kick in progress");
|
|
break;
|
|
case LfgState::InDungeon: {
|
|
std::string dName = gameHandler.getCurrentLfgDungeonName();
|
|
if (!dName.empty())
|
|
ImGui::TextColored(ImVec4(0.4f, 0.8f, 1.0f, 1.0f), "Status: In dungeon (%s)", dName.c_str());
|
|
else
|
|
ImGui::TextColored(ImVec4(0.4f, 0.8f, 1.0f, 1.0f), "Status: In dungeon");
|
|
break;
|
|
}
|
|
case LfgState::FinishedDungeon: {
|
|
std::string dName = gameHandler.getCurrentLfgDungeonName();
|
|
if (!dName.empty())
|
|
ImGui::TextColored(colors::kLightGreen, "Status: %s complete", dName.c_str());
|
|
else
|
|
ImGui::TextColored(colors::kLightGreen, "Status: Dungeon complete");
|
|
break;
|
|
}
|
|
case LfgState::RaidBrowser:
|
|
ImGui::TextColored(ImVec4(0.8f, 0.6f, 1.0f, 1.0f), "Status: Raid browser");
|
|
break;
|
|
}
|
|
|
|
ImGui::Separator();
|
|
|
|
// ---- Proposal accept/decline ----
|
|
if (state == LfgState::Proposal) {
|
|
std::string dName = gameHandler.getCurrentLfgDungeonName();
|
|
if (!dName.empty())
|
|
ImGui::TextColored(ImVec4(1.0f, 0.9f, 0.3f, 1.0f),
|
|
"A group has been found for %s!", dName.c_str());
|
|
else
|
|
ImGui::TextColored(ImVec4(1.0f, 0.9f, 0.3f, 1.0f),
|
|
"A group has been found for your dungeon!");
|
|
ImGui::Spacing();
|
|
if (ImGui::Button("Accept", ImVec2(120, 0))) {
|
|
gameHandler.lfgAcceptProposal(gameHandler.getLfgProposalId(), true);
|
|
}
|
|
ImGui::SameLine();
|
|
if (ImGui::Button("Decline", ImVec2(120, 0))) {
|
|
gameHandler.lfgAcceptProposal(gameHandler.getLfgProposalId(), false);
|
|
}
|
|
ImGui::Separator();
|
|
}
|
|
|
|
// ---- Vote-to-kick buttons ----
|
|
if (state == LfgState::Boot) {
|
|
ImGui::TextColored(kColorRed, "Vote to kick in progress:");
|
|
const std::string& bootTarget = gameHandler.getLfgBootTargetName();
|
|
const std::string& bootReason = gameHandler.getLfgBootReason();
|
|
if (!bootTarget.empty()) {
|
|
ImGui::Text("Player: ");
|
|
ImGui::SameLine();
|
|
ImGui::TextColored(ImVec4(1.0f, 0.8f, 0.3f, 1.0f), "%s", bootTarget.c_str());
|
|
}
|
|
if (!bootReason.empty()) {
|
|
ImGui::Text("Reason: ");
|
|
ImGui::SameLine();
|
|
ImGui::TextWrapped("%s", bootReason.c_str());
|
|
}
|
|
uint32_t bootVotes = gameHandler.getLfgBootVotes();
|
|
uint32_t bootTotal = gameHandler.getLfgBootTotal();
|
|
uint32_t bootNeeded = gameHandler.getLfgBootNeeded();
|
|
uint32_t bootTimeLeft= gameHandler.getLfgBootTimeLeft();
|
|
if (bootNeeded > 0) {
|
|
ImGui::Text("Votes: %u / %u (need %u) %us left",
|
|
bootVotes, bootTotal, bootNeeded, bootTimeLeft);
|
|
}
|
|
ImGui::Spacing();
|
|
if (ImGui::Button("Vote Yes (kick)", ImVec2(140, 0))) {
|
|
gameHandler.lfgSetBootVote(true);
|
|
}
|
|
ImGui::SameLine();
|
|
if (ImGui::Button("Vote No (keep)", ImVec2(140, 0))) {
|
|
gameHandler.lfgSetBootVote(false);
|
|
}
|
|
ImGui::Separator();
|
|
}
|
|
|
|
// ---- Teleport button (in dungeon) ----
|
|
if (state == LfgState::InDungeon) {
|
|
if (ImGui::Button("Teleport to Dungeon", ImVec2(-1, 0))) {
|
|
gameHandler.lfgTeleport(true);
|
|
}
|
|
ImGui::Separator();
|
|
}
|
|
|
|
// ---- Role selection (only when not queued/in dungeon) ----
|
|
bool canConfigure = (state == LfgState::None || state == LfgState::FinishedDungeon);
|
|
|
|
if (canConfigure) {
|
|
ImGui::Text("Role:");
|
|
ImGui::SameLine();
|
|
bool isTank = (lfgRoles_ & 0x02) != 0;
|
|
bool isHealer = (lfgRoles_ & 0x04) != 0;
|
|
bool isDps = (lfgRoles_ & 0x08) != 0;
|
|
if (ImGui::Checkbox("Tank", &isTank)) lfgRoles_ = (lfgRoles_ & ~0x02) | (isTank ? 0x02 : 0);
|
|
ImGui::SameLine();
|
|
if (ImGui::Checkbox("Healer", &isHealer)) lfgRoles_ = (lfgRoles_ & ~0x04) | (isHealer ? 0x04 : 0);
|
|
ImGui::SameLine();
|
|
if (ImGui::Checkbox("DPS", &isDps)) lfgRoles_ = (lfgRoles_ & ~0x08) | (isDps ? 0x08 : 0);
|
|
|
|
ImGui::Spacing();
|
|
|
|
// ---- Dungeon selection ----
|
|
ImGui::Text("Dungeon:");
|
|
|
|
struct DungeonEntry { uint32_t id; const char* name; };
|
|
// Category 0=Random, 1=Classic, 2=TBC, 3=WotLK
|
|
struct DungeonEntryEx { uint32_t id; const char* name; uint8_t cat; };
|
|
static const DungeonEntryEx kDungeons[] = {
|
|
{ 861, "Random Dungeon", 0 },
|
|
{ 862, "Random Heroic", 0 },
|
|
{ 36, "Deadmines", 1 },
|
|
{ 43, "Ragefire Chasm", 1 },
|
|
{ 47, "Razorfen Kraul", 1 },
|
|
{ 48, "Blackfathom Deeps", 1 },
|
|
{ 52, "Uldaman", 1 },
|
|
{ 57, "Dire Maul: East", 1 },
|
|
{ 70, "Onyxia's Lair", 1 },
|
|
{ 264, "The Blood Furnace", 2 },
|
|
{ 269, "The Shattered Halls", 2 },
|
|
{ 576, "The Nexus", 3 },
|
|
{ 578, "The Oculus", 3 },
|
|
{ 595, "The Culling of Stratholme", 3 },
|
|
{ 599, "Halls of Stone", 3 },
|
|
{ 600, "Drak'Tharon Keep", 3 },
|
|
{ 601, "Azjol-Nerub", 3 },
|
|
{ 604, "Gundrak", 3 },
|
|
{ 608, "Violet Hold", 3 },
|
|
{ 619, "Ahn'kahet: Old Kingdom", 3 },
|
|
{ 623, "Halls of Lightning", 3 },
|
|
{ 632, "The Forge of Souls", 3 },
|
|
{ 650, "Trial of the Champion", 3 },
|
|
{ 658, "Pit of Saron", 3 },
|
|
{ 668, "Halls of Reflection", 3 },
|
|
};
|
|
static constexpr const char* kCatHeaders[] = { nullptr, "-- Classic --", "-- TBC --", "-- WotLK --" };
|
|
|
|
// Find current index
|
|
int curIdx = 0;
|
|
for (int i = 0; i < static_cast<int>(sizeof(kDungeons)/sizeof(kDungeons[0])); ++i) {
|
|
if (kDungeons[i].id == lfgSelectedDungeon_) { curIdx = i; break; }
|
|
}
|
|
|
|
ImGui::SetNextItemWidth(-1);
|
|
if (ImGui::BeginCombo("##dungeon", kDungeons[curIdx].name)) {
|
|
uint8_t lastCat = 255;
|
|
for (int i = 0; i < static_cast<int>(sizeof(kDungeons)/sizeof(kDungeons[0])); ++i) {
|
|
if (kDungeons[i].cat != lastCat && kCatHeaders[kDungeons[i].cat]) {
|
|
if (lastCat != 255) ImGui::Separator();
|
|
ImGui::TextDisabled("%s", kCatHeaders[kDungeons[i].cat]);
|
|
lastCat = kDungeons[i].cat;
|
|
} else if (kDungeons[i].cat != lastCat) {
|
|
lastCat = kDungeons[i].cat;
|
|
}
|
|
bool selected = (kDungeons[i].id == lfgSelectedDungeon_);
|
|
if (ImGui::Selectable(kDungeons[i].name, selected))
|
|
lfgSelectedDungeon_ = kDungeons[i].id;
|
|
if (selected) ImGui::SetItemDefaultFocus();
|
|
}
|
|
ImGui::EndCombo();
|
|
}
|
|
|
|
ImGui::Spacing();
|
|
|
|
// ---- Join button ----
|
|
bool rolesOk = (lfgRoles_ != 0);
|
|
if (!rolesOk) {
|
|
ImGui::BeginDisabled();
|
|
}
|
|
if (ImGui::Button("Join Dungeon Finder", ImVec2(-1, 0))) {
|
|
gameHandler.lfgJoin(lfgSelectedDungeon_, lfgRoles_);
|
|
}
|
|
if (!rolesOk) {
|
|
ImGui::EndDisabled();
|
|
ImGui::TextColored(colors::kSoftRed, "Select at least one role.");
|
|
}
|
|
}
|
|
|
|
// ---- Leave button (when queued or role check) ----
|
|
if (state == LfgState::Queued || state == LfgState::RoleCheck) {
|
|
if (ImGui::Button("Leave Queue", ImVec2(-1, 0))) {
|
|
gameHandler.lfgLeave();
|
|
}
|
|
}
|
|
|
|
ImGui::End();
|
|
}
|
|
|
|
void SocialPanel::renderWhoWindow(game::GameHandler& gameHandler,
|
|
ChatPanel& chatPanel) {
|
|
if (!showWhoWindow_) return;
|
|
|
|
const auto& results = gameHandler.getWhoResults();
|
|
|
|
ImGui::SetNextWindowSize(ImVec2(500, 300), ImGuiCond_FirstUseEver);
|
|
ImGui::SetNextWindowPos(ImVec2(200, 180), ImGuiCond_FirstUseEver);
|
|
|
|
char title[64];
|
|
uint32_t onlineCount = gameHandler.getWhoOnlineCount();
|
|
if (onlineCount > 0)
|
|
snprintf(title, sizeof(title), "Players Online: %u###WhoWindow", onlineCount);
|
|
else
|
|
snprintf(title, sizeof(title), "Who###WhoWindow");
|
|
|
|
if (!ImGui::Begin(title, &showWhoWindow_)) {
|
|
ImGui::End();
|
|
return;
|
|
}
|
|
|
|
// Search bar with Send button
|
|
static char whoSearchBuf[64] = {};
|
|
bool doSearch = false;
|
|
ImGui::SetNextItemWidth(ImGui::GetContentRegionAvail().x - 60.0f);
|
|
if (ImGui::InputTextWithHint("##whosearch", "Search players...", whoSearchBuf, sizeof(whoSearchBuf),
|
|
ImGuiInputTextFlags_EnterReturnsTrue))
|
|
doSearch = true;
|
|
ImGui::SameLine();
|
|
if (ImGui::Button("Search", ImVec2(-1, 0)))
|
|
doSearch = true;
|
|
if (doSearch) {
|
|
gameHandler.queryWho(std::string(whoSearchBuf));
|
|
}
|
|
ImGui::Separator();
|
|
|
|
if (results.empty()) {
|
|
ImGui::TextDisabled("No results. Type a filter above or use /who [filter].");
|
|
ImGui::End();
|
|
return;
|
|
}
|
|
|
|
// Table: Name | Guild | Level | Class | Zone
|
|
if (ImGui::BeginTable("##WhoTable", 5,
|
|
ImGuiTableFlags_Borders | ImGuiTableFlags_RowBg |
|
|
ImGuiTableFlags_ScrollY | ImGuiTableFlags_SizingStretchProp,
|
|
ImVec2(0, 0))) {
|
|
ImGui::TableSetupScrollFreeze(0, 1);
|
|
ImGui::TableSetupColumn("Name", ImGuiTableColumnFlags_WidthStretch, 0.22f);
|
|
ImGui::TableSetupColumn("Guild", ImGuiTableColumnFlags_WidthStretch, 0.20f);
|
|
ImGui::TableSetupColumn("Level", ImGuiTableColumnFlags_WidthFixed, 40.0f);
|
|
ImGui::TableSetupColumn("Class", ImGuiTableColumnFlags_WidthStretch, 0.20f);
|
|
ImGui::TableSetupColumn("Zone", ImGuiTableColumnFlags_WidthStretch, 0.28f);
|
|
ImGui::TableHeadersRow();
|
|
|
|
for (size_t i = 0; i < results.size(); ++i) {
|
|
const auto& e = results[i];
|
|
ImGui::TableNextRow();
|
|
ImGui::PushID(static_cast<int>(i));
|
|
|
|
// Name (class-colored if class is known)
|
|
ImGui::TableSetColumnIndex(0);
|
|
uint8_t cid = static_cast<uint8_t>(e.classId);
|
|
ImVec4 nameCol = classColorVec4(cid);
|
|
ImGui::TextColored(nameCol, "%s", e.name.c_str());
|
|
|
|
// Right-click context menu on the name
|
|
if (ImGui::BeginPopupContextItem("##WhoCtx")) {
|
|
ImGui::TextDisabled("%s", e.name.c_str());
|
|
ImGui::Separator();
|
|
if (ImGui::MenuItem("Whisper")) {
|
|
chatPanel.setWhisperTarget(e.name);
|
|
}
|
|
if (ImGui::MenuItem("Invite to Group"))
|
|
gameHandler.inviteToGroup(e.name);
|
|
if (ImGui::MenuItem("Add Friend"))
|
|
gameHandler.addFriend(e.name);
|
|
if (ImGui::MenuItem("Ignore"))
|
|
gameHandler.addIgnore(e.name);
|
|
ImGui::EndPopup();
|
|
}
|
|
|
|
// Guild
|
|
ImGui::TableSetColumnIndex(1);
|
|
if (!e.guildName.empty())
|
|
ImGui::TextDisabled("<%s>", e.guildName.c_str());
|
|
|
|
// Level
|
|
ImGui::TableSetColumnIndex(2);
|
|
ImGui::Text("%u", e.level);
|
|
|
|
// Class
|
|
ImGui::TableSetColumnIndex(3);
|
|
const char* className = game::getClassName(static_cast<game::Class>(e.classId));
|
|
ImGui::TextColored(nameCol, "%s", className);
|
|
|
|
// Zone
|
|
ImGui::TableSetColumnIndex(4);
|
|
if (e.zoneId != 0) {
|
|
std::string zoneName = gameHandler.getWhoAreaName(e.zoneId);
|
|
if (!zoneName.empty())
|
|
ImGui::TextUnformatted(zoneName.c_str());
|
|
else {
|
|
char zfb[32];
|
|
snprintf(zfb, sizeof(zfb), "Zone #%u", e.zoneId);
|
|
ImGui::TextUnformatted(zfb);
|
|
}
|
|
}
|
|
|
|
ImGui::PopID();
|
|
}
|
|
|
|
ImGui::EndTable();
|
|
}
|
|
|
|
ImGui::End();
|
|
}
|
|
|
|
void SocialPanel::renderInspectWindow(game::GameHandler& gameHandler,
|
|
InventoryScreen& inventoryScreen) {
|
|
if (!showInspectWindow_) return;
|
|
|
|
// Lazy-load SpellItemEnchantment.dbc for enchant name lookup
|
|
static std::unordered_map<uint32_t, std::string> s_enchantNames;
|
|
static bool s_enchantDbLoaded = false;
|
|
auto* assetMgrEnchant = services_.assetManager;
|
|
if (!s_enchantDbLoaded && assetMgrEnchant && assetMgrEnchant->isInitialized()) {
|
|
s_enchantDbLoaded = true;
|
|
auto dbc = assetMgrEnchant->loadDBC("SpellItemEnchantment.dbc");
|
|
if (dbc && dbc->isLoaded()) {
|
|
const auto* layout = pipeline::getActiveDBCLayout()
|
|
? pipeline::getActiveDBCLayout()->getLayout("SpellItemEnchantment")
|
|
: nullptr;
|
|
uint32_t idField = layout ? (*layout)["ID"] : 0;
|
|
uint32_t nameField = layout ? (*layout)["Name"] : 8;
|
|
for (uint32_t i = 0; i < dbc->getRecordCount(); ++i) {
|
|
uint32_t id = dbc->getUInt32(i, idField);
|
|
if (id == 0) continue;
|
|
std::string nm = dbc->getString(i, nameField);
|
|
if (!nm.empty()) s_enchantNames[id] = std::move(nm);
|
|
}
|
|
}
|
|
}
|
|
|
|
// Slot index 0..18 maps to equipment slots 1..19 (WoW convention: slot 0 unused on server)
|
|
static constexpr const char* kSlotNames[19] = {
|
|
"Head", "Neck", "Shoulder", "Shirt", "Chest",
|
|
"Waist", "Legs", "Feet", "Wrist", "Hands",
|
|
"Finger 1", "Finger 2", "Trinket 1", "Trinket 2", "Back",
|
|
"Main Hand", "Off Hand", "Ranged", "Tabard"
|
|
};
|
|
|
|
ImGui::SetNextWindowSize(ImVec2(360, 440), ImGuiCond_FirstUseEver);
|
|
ImGui::SetNextWindowPos(ImVec2(350, 120), ImGuiCond_FirstUseEver);
|
|
|
|
const game::GameHandler::InspectResult* result = gameHandler.getInspectResult();
|
|
|
|
std::string title = result ? ("Inspect: " + result->playerName + "###InspectWin")
|
|
: "Inspect###InspectWin";
|
|
if (!ImGui::Begin(title.c_str(), &showInspectWindow_, ImGuiWindowFlags_NoCollapse)) {
|
|
ImGui::End();
|
|
return;
|
|
}
|
|
|
|
if (!result) {
|
|
ImGui::TextDisabled("No inspect data yet. Target a player and use Inspect.");
|
|
ImGui::End();
|
|
return;
|
|
}
|
|
|
|
// Player name — class-colored if entity is loaded, else gold
|
|
{
|
|
auto ent = gameHandler.getEntityManager().getEntity(result->guid);
|
|
uint8_t cid = entityClassId(ent.get());
|
|
ImVec4 nameColor = (cid != 0) ? classColorVec4(cid) : ui::colors::kTooltipGold;
|
|
ImGui::PushStyleColor(ImGuiCol_Text, nameColor);
|
|
ImGui::Text("%s", result->playerName.c_str());
|
|
ImGui::PopStyleColor();
|
|
if (cid != 0) {
|
|
ImGui::SameLine();
|
|
ImGui::TextColored(classColorVec4(cid), "(%s)", classNameStr(cid));
|
|
}
|
|
}
|
|
ImGui::SameLine();
|
|
ImGui::TextDisabled(" %u talent pts", result->totalTalents);
|
|
if (result->unspentTalents > 0) {
|
|
ImGui::SameLine();
|
|
ImGui::TextColored(colors::kSoftRed, "(%u unspent)", result->unspentTalents);
|
|
}
|
|
if (result->talentGroups > 1) {
|
|
ImGui::SameLine();
|
|
ImGui::TextDisabled(" Dual spec (active %u)", static_cast<unsigned>(result->activeTalentGroup) + 1);
|
|
}
|
|
|
|
ImGui::Separator();
|
|
|
|
// Equipment list
|
|
bool hasAnyGear = false;
|
|
for (int s = 0; s < 19; ++s) {
|
|
if (result->itemEntries[s] != 0) { hasAnyGear = true; break; }
|
|
}
|
|
|
|
if (!hasAnyGear) {
|
|
ImGui::TextDisabled("Equipment data not yet available.");
|
|
ImGui::TextDisabled("(Gear loads after the player is inspected in-range)");
|
|
} else {
|
|
// Average item level (only slots that have loaded info and are not shirt/tabard)
|
|
// Shirt=slot3, Tabard=slot18 — excluded from gear score by WoW convention
|
|
uint32_t iLevelSum = 0;
|
|
int iLevelCount = 0;
|
|
for (int s = 0; s < 19; ++s) {
|
|
if (s == 3 || s == 18) continue; // shirt, tabard
|
|
uint32_t entry = result->itemEntries[s];
|
|
if (entry == 0) continue;
|
|
const game::ItemQueryResponseData* info = gameHandler.getItemInfo(entry);
|
|
if (info && info->valid && info->itemLevel > 0) {
|
|
iLevelSum += info->itemLevel;
|
|
++iLevelCount;
|
|
}
|
|
}
|
|
if (iLevelCount > 0) {
|
|
float avgIlvl = static_cast<float>(iLevelSum) / static_cast<float>(iLevelCount);
|
|
ImGui::TextColored(ImVec4(0.8f, 0.9f, 1.0f, 1.0f), "Avg iLvl: %.1f", avgIlvl);
|
|
ImGui::SameLine();
|
|
ImGui::TextDisabled("(%d/%d slots loaded)", iLevelCount,
|
|
[&]{ int c=0; for(int s=0;s<19;++s){if(s==3||s==18)continue;if(result->itemEntries[s])++c;} return c; }());
|
|
}
|
|
if (ImGui::BeginChild("##inspect_gear", ImVec2(0, 0), false)) {
|
|
constexpr float kIconSz = 28.0f;
|
|
for (int s = 0; s < 19; ++s) {
|
|
uint32_t entry = result->itemEntries[s];
|
|
if (entry == 0) continue;
|
|
|
|
const game::ItemQueryResponseData* info = gameHandler.getItemInfo(entry);
|
|
if (!info) {
|
|
gameHandler.ensureItemInfo(entry);
|
|
ImGui::PushID(s);
|
|
ImGui::TextDisabled("[%s] (loading…)", kSlotNames[s]);
|
|
ImGui::PopID();
|
|
continue;
|
|
}
|
|
|
|
ImGui::PushID(s);
|
|
auto qColor = InventoryScreen::getQualityColor(
|
|
static_cast<game::ItemQuality>(info->quality));
|
|
uint16_t enchantId = result->enchantIds[s];
|
|
|
|
// Item icon
|
|
VkDescriptorSet iconTex = inventoryScreen.getItemIcon(info->displayInfoId);
|
|
if (iconTex) {
|
|
ImGui::Image((ImTextureID)(uintptr_t)iconTex, ImVec2(kIconSz, kIconSz),
|
|
ImVec2(0,0), ImVec2(1,1),
|
|
colors::kWhite, qColor);
|
|
} else {
|
|
ImGui::GetWindowDrawList()->AddRectFilled(
|
|
ImGui::GetCursorScreenPos(),
|
|
ImVec2(ImGui::GetCursorScreenPos().x + kIconSz,
|
|
ImGui::GetCursorScreenPos().y + kIconSz),
|
|
IM_COL32(40, 40, 50, 200));
|
|
ImGui::Dummy(ImVec2(kIconSz, kIconSz));
|
|
}
|
|
bool hovered = ImGui::IsItemHovered();
|
|
|
|
ImGui::SameLine();
|
|
ImGui::SetCursorPosY(ImGui::GetCursorPosY() + (kIconSz - ImGui::GetTextLineHeight()) * 0.5f);
|
|
ImGui::BeginGroup();
|
|
ImGui::TextDisabled("%s", kSlotNames[s]);
|
|
ImGui::TextColored(qColor, "%s", info->name.c_str());
|
|
// Enchant indicator on the same row as the name
|
|
if (enchantId != 0) {
|
|
auto enchIt = s_enchantNames.find(enchantId);
|
|
const std::string& enchName = (enchIt != s_enchantNames.end())
|
|
? enchIt->second : std::string{};
|
|
ImGui::SameLine();
|
|
if (!enchName.empty()) {
|
|
ImGui::TextColored(ImVec4(0.6f, 0.85f, 1.0f, 1.0f),
|
|
"\xe2\x9c\xa6 %s", enchName.c_str()); // UTF-8 ✦
|
|
} else {
|
|
ImGui::TextColored(ImVec4(0.6f, 0.85f, 1.0f, 1.0f), "\xe2\x9c\xa6");
|
|
if (ImGui::IsItemHovered())
|
|
ImGui::SetTooltip("Enchanted (ID %u)", static_cast<unsigned>(enchantId));
|
|
}
|
|
}
|
|
ImGui::EndGroup();
|
|
hovered = hovered || ImGui::IsItemHovered();
|
|
|
|
if (hovered && info->valid) {
|
|
inventoryScreen.renderItemTooltip(*info);
|
|
} else if (hovered) {
|
|
ImGui::SetTooltip("%s", info->name.c_str());
|
|
}
|
|
|
|
ImGui::PopID();
|
|
ImGui::Spacing();
|
|
}
|
|
}
|
|
ImGui::EndChild();
|
|
}
|
|
|
|
// Arena teams (WotLK — from MSG_INSPECT_ARENA_TEAMS)
|
|
if (!result->arenaTeams.empty()) {
|
|
ImGui::Separator();
|
|
ImGui::TextColored(ImVec4(1.0f, 0.75f, 0.2f, 1.0f), "Arena Teams");
|
|
ImGui::Spacing();
|
|
for (const auto& team : result->arenaTeams) {
|
|
const char* bracket = (team.type == 2) ? "2v2"
|
|
: (team.type == 3) ? "3v3"
|
|
: (team.type == 5) ? "5v5" : "?v?";
|
|
ImGui::TextColored(ImVec4(0.9f, 0.9f, 0.9f, 1.0f),
|
|
"[%s] %s", bracket, team.name.c_str());
|
|
ImGui::SameLine();
|
|
ImGui::TextColored(ImVec4(0.4f, 0.85f, 1.0f, 1.0f),
|
|
" Rating: %u", team.personalRating);
|
|
if (team.weekGames > 0 || team.seasonGames > 0) {
|
|
ImGui::TextDisabled(" Week: %u/%u Season: %u/%u",
|
|
team.weekWins, team.weekGames,
|
|
team.seasonWins, team.seasonGames);
|
|
}
|
|
}
|
|
}
|
|
|
|
ImGui::End();
|
|
}
|
|
|
|
} // namespace ui
|
|
} // namespace wowee
|