Kelsidavis-WoWee/src/ui/window_manager.cpp
Kelsi fe1c4c622b chore: remove dead functions left behind by handler extractions
685 lines of unused code duplicated into extracted handler files
(entity_controller, spell_handler, quest_handler, warden_handler,
social_handler, action_bar_panel, chat_panel, window_manager)
during PRs #33-#38. Build is now warning-free.
2026-04-02 14:47:04 -07:00

4246 lines
190 KiB
C++
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

// ============================================================
// WindowManager — extracted from GameScreen
// Owns all NPC interaction windows, popup dialogs, etc.
// ============================================================
#include "ui/window_manager.hpp"
#include "ui/chat_panel.hpp"
#include "ui/settings_panel.hpp"
#include "ui/spellbook_screen.hpp"
#include "ui/inventory_screen.hpp"
#include "ui/ui_colors.hpp"
#include "core/application.hpp"
#include "core/logger.hpp"
#include "rendering/renderer.hpp"
#include "rendering/vk_context.hpp"
#include "core/window.hpp"
#include "game/game_handler.hpp"
#include "pipeline/asset_manager.hpp"
#include "pipeline/dbc_layout.hpp"
#include "audio/audio_coordinator.hpp"
#include "audio/ui_sound_manager.hpp"
#include "audio/music_manager.hpp"
#include <imgui.h>
#include <imgui_internal.h>
#include <algorithm>
#include <cmath>
#include <cstdio>
#include <string>
#include <fstream>
namespace {
using namespace wowee::ui::colors;
// Abbreviated month names (indexed 0-11)
constexpr const char* kMonthAbbrev[12] = {
"Jan","Feb","Mar","Apr","May","Jun",
"Jul","Aug","Sep","Oct","Nov","Dec"
};
constexpr auto& kColorRed = kRed;
constexpr auto& kColorGreen = kGreen;
constexpr auto& kColorBrightGreen = kBrightGreen;
constexpr auto& kColorYellow = kYellow;
constexpr auto& kColorGray = kGray;
constexpr auto& kColorDarkGray = kDarkGray;
// Common ImGui window flags for popup dialogs
const ImGuiWindowFlags kDialogFlags = ImGuiWindowFlags_NoCollapse | ImGuiWindowFlags_NoResize;
// Build a WoW-format item link string for chat insertion.
std::string buildItemChatLink(uint32_t itemId, uint8_t quality, const std::string& name) {
static constexpr const char* kQualHex[] = {"9d9d9d","ffffff","1eff00","0070dd","a335ee","ff8000","e6cc80","e6cc80"};
uint8_t qi = quality < 8 ? quality : 1;
char buf[512];
snprintf(buf, sizeof(buf), "|cff%s|Hitem:%u:0:0:0:0:0:0:0:0|h[%s]|h|r",
kQualHex[qi], itemId, name.c_str());
return buf;
}
} // anonymous namespace
namespace wowee {
namespace ui {
void WindowManager::renderLootWindow(game::GameHandler& gameHandler,
InventoryScreen& inventoryScreen,
ChatPanel& chatPanel) {
if (!gameHandler.isLootWindowOpen()) return;
auto* window = services_.window;
float screenW = window ? static_cast<float>(window->getWidth()) : 1280.0f;
ImGui::SetNextWindowPos(ImVec2(screenW / 2 - 150, 200), ImGuiCond_Appearing);
ImGui::SetNextWindowSize(ImVec2(300, 0), ImGuiCond_Always);
bool open = true;
if (ImGui::Begin("Loot", &open, ImGuiWindowFlags_NoCollapse | ImGuiWindowFlags_AlwaysAutoResize)) {
const auto& loot = gameHandler.getCurrentLoot();
// Gold (auto-looted on open; shown for feedback)
if (loot.gold > 0) {
ImGui::TextDisabled("Gold:");
ImGui::SameLine(0, 4);
renderCoinsText(loot.getGold(), loot.getSilver(), loot.getCopper());
ImGui::Separator();
}
// Items with icons and labels
constexpr float iconSize = 32.0f;
int lootSlotClicked = -1; // defer loot pickup to avoid iterator invalidation
for (const auto& item : loot.items) {
ImGui::PushID(item.slotIndex);
// Get item info for name and quality
const auto* info = gameHandler.getItemInfo(item.itemId);
std::string itemName;
game::ItemQuality quality = game::ItemQuality::COMMON;
if (info && !info->name.empty()) {
itemName = info->name;
quality = static_cast<game::ItemQuality>(info->quality);
} else {
itemName = "Item #" + std::to_string(item.itemId);
}
ImVec4 qColor = InventoryScreen::getQualityColor(quality);
bool startsQuest = (info && info->startQuestId != 0);
// Get item icon
uint32_t displayId = item.displayInfoId;
if (displayId == 0 && info) displayId = info->displayInfoId;
VkDescriptorSet iconTex = inventoryScreen.getItemIcon(displayId);
ImVec2 cursor = ImGui::GetCursorScreenPos();
float rowH = std::max(iconSize, ImGui::GetTextLineHeight() * 2.0f);
// Invisible selectable for click handling
if (ImGui::Selectable("##loot", false, 0, ImVec2(0, rowH))) {
if (ImGui::GetIO().KeyShift && info && !info->name.empty()) {
// Shift-click: insert item link into chat
std::string link = buildItemChatLink(info->entry, info->quality, info->name);
chatPanel.insertChatLink(link);
} else {
lootSlotClicked = item.slotIndex;
}
}
if (ImGui::IsItemClicked(ImGuiMouseButton_Right)) {
lootSlotClicked = item.slotIndex;
}
bool hovered = ImGui::IsItemHovered();
// Show item tooltip on hover
if (hovered && info && info->valid) {
inventoryScreen.renderItemTooltip(*info);
} else if (hovered && info && !info->name.empty()) {
// Item info received but not yet fully valid — show name at minimum
ImGui::SetTooltip("%s", info->name.c_str());
}
ImDrawList* drawList = ImGui::GetWindowDrawList();
// Draw hover highlight
if (hovered) {
drawList->AddRectFilled(cursor,
ImVec2(cursor.x + ImGui::GetContentRegionAvail().x + iconSize + 8.0f,
cursor.y + rowH),
IM_COL32(255, 255, 255, 30));
}
// Draw icon
if (iconTex) {
drawList->AddImage((ImTextureID)(uintptr_t)iconTex,
cursor, ImVec2(cursor.x + iconSize, cursor.y + iconSize));
drawList->AddRect(cursor, ImVec2(cursor.x + iconSize, cursor.y + iconSize),
ImGui::ColorConvertFloat4ToU32(qColor));
} else {
drawList->AddRectFilled(cursor,
ImVec2(cursor.x + iconSize, cursor.y + iconSize),
IM_COL32(40, 40, 50, 200));
drawList->AddRect(cursor, ImVec2(cursor.x + iconSize, cursor.y + iconSize),
IM_COL32(80, 80, 80, 200));
}
// Quest-starter: gold outer glow border + "!" badge on top-right corner
if (startsQuest) {
drawList->AddRect(ImVec2(cursor.x - 2.0f, cursor.y - 2.0f),
ImVec2(cursor.x + iconSize + 2.0f, cursor.y + iconSize + 2.0f),
IM_COL32(255, 210, 0, 210), 0.0f, 0, 2.0f);
drawList->AddText(ImVec2(cursor.x + iconSize - 10.0f, cursor.y + 1.0f),
IM_COL32(255, 210, 0, 255), "!");
}
// Draw item name
float textX = cursor.x + iconSize + 6.0f;
float textY = cursor.y + 2.0f;
drawList->AddText(ImVec2(textX, textY),
ImGui::ColorConvertFloat4ToU32(qColor), itemName.c_str());
// Draw count or "Begins a Quest" label on second line
float secondLineY = textY + ImGui::GetTextLineHeight();
if (startsQuest) {
drawList->AddText(ImVec2(textX, secondLineY),
IM_COL32(255, 210, 0, 255), "Begins a Quest");
} else if (item.count > 1) {
char countStr[32];
snprintf(countStr, sizeof(countStr), "x%u", item.count);
drawList->AddText(ImVec2(textX, secondLineY), IM_COL32(200, 200, 200, 220), countStr);
}
ImGui::PopID();
}
// Process deferred loot pickup (after loop to avoid iterator invalidation)
if (lootSlotClicked >= 0) {
if (gameHandler.hasMasterLootCandidates()) {
// Master looter: open popup to choose recipient
char popupId[32];
snprintf(popupId, sizeof(popupId), "##MLGive%d", lootSlotClicked);
ImGui::OpenPopup(popupId);
} else {
gameHandler.lootItem(static_cast<uint8_t>(lootSlotClicked));
}
}
// Master loot "Give to" popups
if (gameHandler.hasMasterLootCandidates()) {
for (const auto& item : loot.items) {
char popupId[32];
snprintf(popupId, sizeof(popupId), "##MLGive%d", item.slotIndex);
if (ImGui::BeginPopup(popupId)) {
ImGui::TextDisabled("Give to:");
ImGui::Separator();
const auto& candidates = gameHandler.getMasterLootCandidates();
for (uint64_t candidateGuid : candidates) {
auto entity = gameHandler.getEntityManager().getEntity(candidateGuid);
auto* unit = (entity && entity->isUnit()) ? static_cast<game::Unit*>(entity.get()) : nullptr;
const char* cName = unit ? unit->getName().c_str() : nullptr;
char nameBuf[64];
if (!cName || cName[0] == '\0') {
snprintf(nameBuf, sizeof(nameBuf), "Player 0x%llx",
static_cast<unsigned long long>(candidateGuid));
cName = nameBuf;
}
if (ImGui::MenuItem(cName)) {
gameHandler.lootMasterGive(item.slotIndex, candidateGuid);
ImGui::CloseCurrentPopup();
}
}
ImGui::EndPopup();
}
}
}
if (loot.items.empty() && loot.gold == 0) {
gameHandler.closeLoot();
}
ImGui::Spacing();
bool hasItems = !loot.items.empty();
if (hasItems) {
if (ImGui::Button("Loot All", ImVec2(-1, 0))) {
for (const auto& item : loot.items) {
gameHandler.lootItem(item.slotIndex);
}
}
ImGui::Spacing();
}
if (ImGui::Button("Close", ImVec2(-1, 0))) {
gameHandler.closeLoot();
}
}
ImGui::End();
if (!open) {
gameHandler.closeLoot();
}
}
void WindowManager::renderGossipWindow(game::GameHandler& gameHandler,
ChatPanel& chatPanel) {
if (!gameHandler.isGossipWindowOpen()) return;
auto* window = services_.window;
float screenW = window ? static_cast<float>(window->getWidth()) : 1280.0f;
ImGui::SetNextWindowPos(ImVec2(screenW / 2 - 200, 150), ImGuiCond_Appearing);
ImGui::SetNextWindowSize(ImVec2(400, 0), ImGuiCond_Always);
bool open = true;
if (ImGui::Begin("NPC Dialog", &open, ImGuiWindowFlags_NoCollapse | ImGuiWindowFlags_AlwaysAutoResize)) {
const auto& gossip = gameHandler.getCurrentGossip();
// NPC name (from creature cache)
auto npcEntity = gameHandler.getEntityManager().getEntity(gossip.npcGuid);
if (npcEntity && npcEntity->getType() == game::ObjectType::UNIT) {
auto unit = std::static_pointer_cast<game::Unit>(npcEntity);
if (!unit->getName().empty()) {
ImGui::TextColored(ImVec4(1.0f, 0.8f, 0.0f, 1.0f), "%s", unit->getName().c_str());
ImGui::Separator();
}
}
ImGui::Spacing();
// Gossip option icons - matches WoW GossipOptionIcon enum
static constexpr const char* gossipIcons[] = {
"[Chat]", // 0 = GOSSIP_ICON_CHAT
"[Vendor]", // 1 = GOSSIP_ICON_VENDOR
"[Taxi]", // 2 = GOSSIP_ICON_TAXI
"[Trainer]", // 3 = GOSSIP_ICON_TRAINER
"[Interact]", // 4 = GOSSIP_ICON_INTERACT_1
"[Interact]", // 5 = GOSSIP_ICON_INTERACT_2
"[Banker]", // 6 = GOSSIP_ICON_MONEY_BAG (banker)
"[Chat]", // 7 = GOSSIP_ICON_TALK
"[Tabard]", // 8 = GOSSIP_ICON_TABARD
"[Battlemaster]", // 9 = GOSSIP_ICON_BATTLE
"[Option]", // 10 = GOSSIP_ICON_DOT
};
// Default text for server-sent gossip option placeholders
static const std::unordered_map<std::string, std::string> gossipPlaceholders = {
{"GOSSIP_OPTION_BANKER", "I would like to check my deposit box."},
{"GOSSIP_OPTION_AUCTIONEER", "I'd like to browse your auctions."},
{"GOSSIP_OPTION_VENDOR", "I want to browse your goods."},
{"GOSSIP_OPTION_TAXIVENDOR", "I'd like to fly."},
{"GOSSIP_OPTION_TRAINER", "I seek training."},
{"GOSSIP_OPTION_INNKEEPER", "Make this inn your home."},
{"GOSSIP_OPTION_SPIRITGUIDE", "Return me to life."},
{"GOSSIP_OPTION_SPIRITHEALER", "Bring me back to life."},
{"GOSSIP_OPTION_STABLEPET", "I'd like to stable my pet."},
{"GOSSIP_OPTION_ARMORER", "I need to repair my equipment."},
{"GOSSIP_OPTION_GOSSIP", "What can you tell me?"},
{"GOSSIP_OPTION_BATTLEFIELD", "I'd like to go to the battleground."},
{"GOSSIP_OPTION_TABARDDESIGNER", "I want to create a guild tabard."},
{"GOSSIP_OPTION_PETITIONER", "I want to create a guild."},
};
for (const auto& opt : gossip.options) {
ImGui::PushID(static_cast<int>(opt.id));
// Determine icon label - use text-based detection for shared icons
const char* icon = (opt.icon < 11) ? gossipIcons[opt.icon] : "[Option]";
if (opt.text == "GOSSIP_OPTION_AUCTIONEER") icon = "[Auctioneer]";
else if (opt.text == "GOSSIP_OPTION_BANKER") icon = "[Banker]";
else if (opt.text == "GOSSIP_OPTION_VENDOR") icon = "[Vendor]";
else if (opt.text == "GOSSIP_OPTION_TRAINER") icon = "[Trainer]";
else if (opt.text == "GOSSIP_OPTION_INNKEEPER") icon = "[Innkeeper]";
else if (opt.text == "GOSSIP_OPTION_STABLEPET") icon = "[Stable Master]";
else if (opt.text == "GOSSIP_OPTION_ARMORER") icon = "[Repair]";
// Resolve placeholder text from server
std::string displayText = opt.text;
auto placeholderIt = gossipPlaceholders.find(displayText);
if (placeholderIt != gossipPlaceholders.end()) {
displayText = placeholderIt->second;
}
std::string processedText = chatPanel.replaceGenderPlaceholders(displayText, gameHandler);
std::string label = std::string(icon) + " " + processedText;
if (ImGui::Selectable(label.c_str())) {
if (opt.text == "GOSSIP_OPTION_ARMORER") {
gameHandler.setVendorCanRepair(true);
}
gameHandler.selectGossipOption(opt.id);
}
ImGui::PopID();
}
// Fallback: some spirit healers don't send gossip options.
if (gossip.options.empty() && gameHandler.isPlayerGhost()) {
bool isSpirit = false;
if (npcEntity && npcEntity->getType() == game::ObjectType::UNIT) {
auto unit = std::static_pointer_cast<game::Unit>(npcEntity);
std::string name = unit->getName();
std::transform(name.begin(), name.end(), name.begin(),
[](unsigned char c){ return static_cast<char>(std::tolower(c)); });
if (name.find("spirit healer") != std::string::npos ||
name.find("spirit guide") != std::string::npos) {
isSpirit = true;
}
}
if (isSpirit) {
if (ImGui::Selectable("[Spiritguide] Return to Graveyard")) {
gameHandler.activateSpiritHealer(gossip.npcGuid);
gameHandler.closeGossip();
}
}
}
// Quest items
if (!gossip.quests.empty()) {
ImGui::Spacing();
ImGui::Separator();
ImGui::TextColored(kColorYellow, "Quests:");
for (size_t qi = 0; qi < gossip.quests.size(); qi++) {
const auto& quest = gossip.quests[qi];
ImGui::PushID(static_cast<int>(qi));
// Determine icon and color based on QuestGiverStatus stored in questIcon
// 5=INCOMPLETE (gray?), 6=REWARD_REP (yellow?), 7=AVAILABLE_LOW (gray!),
// 8=AVAILABLE (yellow!), 10=REWARD (yellow?)
const char* statusIcon = "!";
ImVec4 statusColor = kColorYellow; // yellow
switch (quest.questIcon) {
case 5: // INCOMPLETE — in progress but not done
statusIcon = "?";
statusColor = colors::kMediumGray; // gray
break;
case 6: // REWARD_REP — repeatable, ready to turn in
case 10: // REWARD — ready to turn in
statusIcon = "?";
statusColor = kColorYellow; // yellow
break;
case 7: // AVAILABLE_LOW — available but gray (low-level)
statusIcon = "!";
statusColor = colors::kMediumGray; // gray
break;
default: // AVAILABLE (8) and any others
statusIcon = "!";
statusColor = kColorYellow; // yellow
break;
}
// Render: colored icon glyph then [Lv] Title
ImGui::TextColored(statusColor, "%s", statusIcon);
ImGui::SameLine(0, 4);
char qlabel[256];
snprintf(qlabel, sizeof(qlabel), "[%d] %s", quest.questLevel, quest.title.c_str());
ImGui::PushStyleColor(ImGuiCol_Text, statusColor);
if (ImGui::Selectable(qlabel)) {
gameHandler.selectGossipQuest(quest.questId);
}
ImGui::PopStyleColor();
ImGui::PopID();
}
}
ImGui::Spacing();
if (ImGui::Button("Close", ImVec2(-1, 0))) {
gameHandler.closeGossip();
}
}
ImGui::End();
if (!open) {
gameHandler.closeGossip();
}
}
void WindowManager::renderQuestDetailsWindow(game::GameHandler& gameHandler,
ChatPanel& chatPanel,
InventoryScreen& inventoryScreen) {
if (!gameHandler.isQuestDetailsOpen()) 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 / 2 - 225, screenH / 2 - 200), ImGuiCond_Appearing);
ImGui::SetNextWindowSize(ImVec2(450, 400), ImGuiCond_Appearing);
bool open = true;
const auto& quest = gameHandler.getQuestDetails();
std::string processedTitle = chatPanel.replaceGenderPlaceholders(quest.title, gameHandler);
if (ImGui::Begin(processedTitle.c_str(), &open)) {
// Quest description
if (!quest.details.empty()) {
std::string processedDetails = chatPanel.replaceGenderPlaceholders(quest.details, gameHandler);
ImGui::TextWrapped("%s", processedDetails.c_str());
}
// Objectives
if (!quest.objectives.empty()) {
ImGui::Spacing();
ImGui::Separator();
ImGui::TextColored(ui::colors::kTooltipGold, "Objectives:");
std::string processedObjectives = chatPanel.replaceGenderPlaceholders(quest.objectives, gameHandler);
ImGui::TextWrapped("%s", processedObjectives.c_str());
}
// Choice reward items (player picks one)
auto renderQuestRewardItem = [&](const game::QuestRewardItem& ri) {
gameHandler.ensureItemInfo(ri.itemId);
auto* info = gameHandler.getItemInfo(ri.itemId);
VkDescriptorSet iconTex = VK_NULL_HANDLE;
uint32_t dispId = ri.displayInfoId;
if (info && info->valid && info->displayInfoId != 0) dispId = info->displayInfoId;
if (dispId != 0) iconTex = inventoryScreen.getItemIcon(dispId);
std::string label;
ImVec4 nameCol = ui::colors::kWhite;
if (info && info->valid && !info->name.empty()) {
label = info->name;
nameCol = InventoryScreen::getQualityColor(static_cast<game::ItemQuality>(info->quality));
} else {
label = "Item " + std::to_string(ri.itemId);
}
if (ri.count > 1) label += " x" + std::to_string(ri.count);
if (iconTex) {
ImGui::Image((void*)(intptr_t)iconTex, ImVec2(18, 18));
if (ImGui::IsItemHovered() && info && info->valid)
inventoryScreen.renderItemTooltip(*info);
ImGui::SameLine();
}
ImGui::TextColored(nameCol, " %s", label.c_str());
if (ImGui::IsItemHovered() && info && info->valid)
inventoryScreen.renderItemTooltip(*info);
if (ImGui::IsItemHovered() && ImGui::IsMouseClicked(ImGuiMouseButton_Left) &&
ImGui::GetIO().KeyShift && info && info->valid && !info->name.empty()) {
std::string link = buildItemChatLink(info->entry, info->quality, info->name);
chatPanel.insertChatLink(link);
}
};
if (!quest.rewardChoiceItems.empty()) {
ImGui::Spacing();
ImGui::Separator();
ImGui::TextColored(ui::colors::kTooltipGold, "Choose one reward:");
for (const auto& ri : quest.rewardChoiceItems) {
renderQuestRewardItem(ri);
}
}
// Fixed reward items (always given)
if (!quest.rewardItems.empty()) {
ImGui::Spacing();
ImGui::Separator();
ImGui::TextColored(ui::colors::kTooltipGold, "You will receive:");
for (const auto& ri : quest.rewardItems) {
renderQuestRewardItem(ri);
}
}
// XP and money rewards
if (quest.rewardXp > 0 || quest.rewardMoney > 0) {
ImGui::Spacing();
ImGui::Separator();
ImGui::TextColored(ui::colors::kTooltipGold, "Rewards:");
if (quest.rewardXp > 0) {
ImGui::Text(" %u experience", quest.rewardXp);
}
if (quest.rewardMoney > 0) {
ImGui::TextDisabled(" Money:"); ImGui::SameLine(0, 4);
renderCoinsFromCopper(quest.rewardMoney);
}
}
if (quest.suggestedPlayers > 1) {
ImGui::TextColored(ui::colors::kLightGray,
"Suggested players: %u", quest.suggestedPlayers);
}
// Accept / Decline buttons
ImGui::Spacing();
ImGui::Separator();
ImGui::Spacing();
float buttonW = (ImGui::GetContentRegionAvail().x - ImGui::GetStyle().ItemSpacing.x) * 0.5f;
if (ImGui::Button("Accept", ImVec2(buttonW, 0))) {
gameHandler.acceptQuest();
}
ImGui::SameLine();
if (ImGui::Button("Decline", ImVec2(buttonW, 0))) {
gameHandler.declineQuest();
}
}
ImGui::End();
if (!open) {
gameHandler.declineQuest();
}
}
void WindowManager::renderQuestRequestItemsWindow(game::GameHandler& gameHandler,
ChatPanel& chatPanel,
InventoryScreen& inventoryScreen) {
if (!gameHandler.isQuestRequestItemsOpen()) 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 / 2 - 225, screenH / 2 - 200), ImGuiCond_Appearing);
ImGui::SetNextWindowSize(ImVec2(450, 350), ImGuiCond_Appearing);
bool open = true;
const auto& quest = gameHandler.getQuestRequestItems();
auto countItemInInventory = [&](uint32_t itemId) -> uint32_t {
const auto& inv = gameHandler.getInventory();
uint32_t total = 0;
for (int i = 0; i < inv.getBackpackSize(); ++i) {
const auto& slot = inv.getBackpackSlot(i);
if (!slot.empty() && slot.item.itemId == itemId) total += slot.item.stackCount;
}
for (int bag = 0; bag < game::Inventory::NUM_BAG_SLOTS; ++bag) {
int bagSize = inv.getBagSize(bag);
for (int s = 0; s < bagSize; ++s) {
const auto& slot = inv.getBagSlot(bag, s);
if (!slot.empty() && slot.item.itemId == itemId) total += slot.item.stackCount;
}
}
return total;
};
std::string processedTitle = chatPanel.replaceGenderPlaceholders(quest.title, gameHandler);
if (ImGui::Begin(processedTitle.c_str(), &open, ImGuiWindowFlags_NoCollapse)) {
if (!quest.completionText.empty()) {
std::string processedCompletionText = chatPanel.replaceGenderPlaceholders(quest.completionText, gameHandler);
ImGui::TextWrapped("%s", processedCompletionText.c_str());
}
// Required items
if (!quest.requiredItems.empty()) {
ImGui::Spacing();
ImGui::Separator();
ImGui::TextColored(ui::colors::kTooltipGold, "Required Items:");
for (const auto& item : quest.requiredItems) {
uint32_t have = countItemInInventory(item.itemId);
bool enough = have >= item.count;
ImVec4 textCol = enough ? colors::kLightGreen : ImVec4(1.0f, 0.6f, 0.6f, 1.0f);
auto* info = gameHandler.getItemInfo(item.itemId);
const char* name = (info && info->valid) ? info->name.c_str() : nullptr;
// Show icon if display info is available
uint32_t dispId = item.displayInfoId;
if (info && info->valid && info->displayInfoId != 0) dispId = info->displayInfoId;
if (dispId != 0) {
VkDescriptorSet iconTex = inventoryScreen.getItemIcon(dispId);
if (iconTex) {
ImGui::Image((ImTextureID)(uintptr_t)iconTex, ImVec2(18, 18));
ImGui::SameLine();
}
}
if (name && *name) {
ImGui::TextColored(textCol, "%s %u/%u", name, have, item.count);
} else {
ImGui::TextColored(textCol, "Item %u %u/%u", item.itemId, have, item.count);
}
if (ImGui::IsItemHovered() && ImGui::IsMouseClicked(ImGuiMouseButton_Left) &&
ImGui::GetIO().KeyShift && info && info->valid && !info->name.empty()) {
std::string link = buildItemChatLink(info->entry, info->quality, info->name);
chatPanel.insertChatLink(link);
}
}
}
if (quest.requiredMoney > 0) {
ImGui::Spacing();
ImGui::TextDisabled("Required money:"); ImGui::SameLine(0, 4);
renderCoinsFromCopper(quest.requiredMoney);
}
// Complete / Cancel buttons
ImGui::Spacing();
ImGui::Separator();
ImGui::Spacing();
float buttonW = (ImGui::GetContentRegionAvail().x - ImGui::GetStyle().ItemSpacing.x) * 0.5f;
if (ImGui::Button("Complete Quest", ImVec2(buttonW, 0))) {
gameHandler.completeQuest();
}
ImGui::SameLine();
if (ImGui::Button("Cancel", ImVec2(buttonW, 0))) {
gameHandler.closeQuestRequestItems();
}
if (!quest.isCompletable()) {
ImGui::TextDisabled("Server flagged this quest as incomplete; completion will be server-validated.");
}
}
ImGui::End();
if (!open) {
gameHandler.closeQuestRequestItems();
}
}
void WindowManager::renderQuestOfferRewardWindow(game::GameHandler& gameHandler,
ChatPanel& chatPanel,
InventoryScreen& inventoryScreen) {
if (!gameHandler.isQuestOfferRewardOpen()) 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 / 2 - 225, screenH / 2 - 200), ImGuiCond_Appearing);
ImGui::SetNextWindowSize(ImVec2(450, 400), ImGuiCond_Appearing);
bool open = true;
const auto& quest = gameHandler.getQuestOfferReward();
static int selectedChoice = -1;
// Auto-select if only one choice reward
if (quest.choiceRewards.size() == 1 && selectedChoice == -1) {
selectedChoice = 0;
}
std::string processedTitle = chatPanel.replaceGenderPlaceholders(quest.title, gameHandler);
if (ImGui::Begin(processedTitle.c_str(), &open, ImGuiWindowFlags_NoCollapse)) {
if (!quest.rewardText.empty()) {
std::string processedRewardText = chatPanel.replaceGenderPlaceholders(quest.rewardText, gameHandler);
ImGui::TextWrapped("%s", processedRewardText.c_str());
}
// Choice rewards (pick one)
// Trigger item info fetch for all reward items
for (const auto& item : quest.choiceRewards) gameHandler.ensureItemInfo(item.itemId);
for (const auto& item : quest.fixedRewards) gameHandler.ensureItemInfo(item.itemId);
// Helper: resolve icon tex + quality color for a reward item
auto resolveRewardItemVis = [&](const game::QuestRewardItem& ri)
-> std::pair<VkDescriptorSet, ImVec4>
{
auto* info = gameHandler.getItemInfo(ri.itemId);
uint32_t dispId = ri.displayInfoId;
if (info && info->valid && info->displayInfoId != 0) dispId = info->displayInfoId;
VkDescriptorSet iconTex = dispId ? inventoryScreen.getItemIcon(dispId) : VK_NULL_HANDLE;
ImVec4 col = (info && info->valid)
? InventoryScreen::getQualityColor(static_cast<game::ItemQuality>(info->quality))
: ui::colors::kWhite;
return {iconTex, col};
};
// Helper: show full item tooltip (reuses InventoryScreen's rich tooltip)
auto rewardItemTooltip = [&](const game::QuestRewardItem& ri, ImVec4 /*nameCol*/) {
auto* info = gameHandler.getItemInfo(ri.itemId);
if (!info || !info->valid) {
ImGui::BeginTooltip();
ImGui::TextDisabled("Loading item data...");
ImGui::EndTooltip();
return;
}
inventoryScreen.renderItemTooltip(*info);
};
if (!quest.choiceRewards.empty()) {
ImGui::Spacing();
ImGui::Separator();
ImGui::TextColored(ui::colors::kTooltipGold, "Choose a reward:");
for (size_t i = 0; i < quest.choiceRewards.size(); ++i) {
const auto& item = quest.choiceRewards[i];
auto* info = gameHandler.getItemInfo(item.itemId);
auto [iconTex, qualityColor] = resolveRewardItemVis(item);
std::string label;
if (info && info->valid && !info->name.empty()) label = info->name;
else label = "Item " + std::to_string(item.itemId);
if (item.count > 1) label += " x" + std::to_string(item.count);
bool selected = (selectedChoice == static_cast<int>(i));
ImGui::PushID(static_cast<int>(i));
// Icon then selectable on same line
if (iconTex) {
ImGui::Image((void*)(intptr_t)iconTex, ImVec2(20, 20));
if (ImGui::IsItemHovered()) rewardItemTooltip(item, qualityColor);
ImGui::SameLine();
}
ImGui::PushStyleColor(ImGuiCol_Text, qualityColor);
if (ImGui::Selectable(label.c_str(), selected, 0, ImVec2(0, 20))) {
if (ImGui::GetIO().KeyShift && info && info->valid && !info->name.empty()) {
std::string link = buildItemChatLink(info->entry, info->quality, info->name);
chatPanel.insertChatLink(link);
} else {
selectedChoice = static_cast<int>(i);
}
}
ImGui::PopStyleColor();
if (ImGui::IsItemHovered()) rewardItemTooltip(item, qualityColor);
ImGui::PopID();
}
}
// Fixed rewards (always given)
if (!quest.fixedRewards.empty()) {
ImGui::Spacing();
ImGui::Separator();
ImGui::TextColored(ui::colors::kTooltipGold, "You will also receive:");
for (const auto& item : quest.fixedRewards) {
auto* info = gameHandler.getItemInfo(item.itemId);
auto [iconTex, qualityColor] = resolveRewardItemVis(item);
std::string label;
if (info && info->valid && !info->name.empty()) label = info->name;
else label = "Item " + std::to_string(item.itemId);
if (item.count > 1) label += " x" + std::to_string(item.count);
if (iconTex) {
ImGui::Image((void*)(intptr_t)iconTex, ImVec2(18, 18));
if (ImGui::IsItemHovered()) rewardItemTooltip(item, qualityColor);
ImGui::SameLine();
}
ImGui::TextColored(qualityColor, " %s", label.c_str());
if (ImGui::IsItemHovered()) rewardItemTooltip(item, qualityColor);
if (ImGui::IsItemHovered() && ImGui::IsMouseClicked(ImGuiMouseButton_Left) &&
ImGui::GetIO().KeyShift && info && info->valid && !info->name.empty()) {
std::string link = buildItemChatLink(info->entry, info->quality, info->name);
chatPanel.insertChatLink(link);
}
}
}
// Money / XP rewards
if (quest.rewardXp > 0 || quest.rewardMoney > 0) {
ImGui::Spacing();
ImGui::Separator();
ImGui::TextColored(ui::colors::kTooltipGold, "Rewards:");
if (quest.rewardXp > 0)
ImGui::Text(" %u experience", quest.rewardXp);
if (quest.rewardMoney > 0) {
ImGui::TextDisabled(" Money:"); ImGui::SameLine(0, 4);
renderCoinsFromCopper(quest.rewardMoney);
}
}
// Complete button
ImGui::Spacing();
ImGui::Separator();
ImGui::Spacing();
float buttonW = (ImGui::GetContentRegionAvail().x - ImGui::GetStyle().ItemSpacing.x) * 0.5f;
bool canComplete = quest.choiceRewards.empty() || selectedChoice >= 0;
if (!canComplete) ImGui::BeginDisabled();
if (ImGui::Button("Complete Quest", ImVec2(buttonW, 0))) {
uint32_t rewardIdx = 0;
if (!quest.choiceRewards.empty() && selectedChoice >= 0 &&
selectedChoice < static_cast<int>(quest.choiceRewards.size())) {
// Server expects the original slot index from its fixed-size reward array.
rewardIdx = quest.choiceRewards[static_cast<size_t>(selectedChoice)].choiceSlot;
}
gameHandler.chooseQuestReward(rewardIdx);
selectedChoice = -1;
}
if (!canComplete) ImGui::EndDisabled();
ImGui::SameLine();
if (ImGui::Button("Cancel", ImVec2(buttonW, 0))) {
gameHandler.closeQuestOfferReward();
selectedChoice = -1;
}
}
ImGui::End();
if (!open) {
gameHandler.closeQuestOfferReward();
selectedChoice = -1;
}
}
void WindowManager::loadExtendedCostDBC() {
if (extendedCostDbLoaded_) return;
extendedCostDbLoaded_ = true;
auto* am = services_.assetManager;
if (!am || !am->isInitialized()) return;
auto dbc = am->loadDBC("ItemExtendedCost.dbc");
if (!dbc || !dbc->isLoaded()) return;
// WotLK ItemExtendedCost.dbc: field 0=ID, 1=honorPoints, 2=arenaPoints,
// 3=arenaSlotRestrictions, 4-8=itemId[5], 9-13=itemCount[5], 14=reqRating, 15=purchaseGroup
for (uint32_t i = 0; i < dbc->getRecordCount(); ++i) {
uint32_t id = dbc->getUInt32(i, 0);
if (id == 0) continue;
ExtendedCostEntry e;
e.honorPoints = dbc->getUInt32(i, 1);
e.arenaPoints = dbc->getUInt32(i, 2);
for (int j = 0; j < 5; ++j) {
e.itemId[j] = dbc->getUInt32(i, 4 + j);
e.itemCount[j] = dbc->getUInt32(i, 9 + j);
}
extendedCostCache_[id] = e;
}
LOG_INFO("ItemExtendedCost.dbc: loaded ", extendedCostCache_.size(), " entries");
}
std::string WindowManager::formatExtendedCost(uint32_t extendedCostId, game::GameHandler& gameHandler) {
loadExtendedCostDBC();
auto it = extendedCostCache_.find(extendedCostId);
if (it == extendedCostCache_.end()) return "[Tokens]";
const auto& e = it->second;
std::string result;
if (e.honorPoints > 0) {
result += std::to_string(e.honorPoints) + " Honor";
}
if (e.arenaPoints > 0) {
if (!result.empty()) result += ", ";
result += std::to_string(e.arenaPoints) + " Arena";
}
for (int j = 0; j < 5; ++j) {
if (e.itemId[j] == 0 || e.itemCount[j] == 0) continue;
if (!result.empty()) result += ", ";
gameHandler.ensureItemInfo(e.itemId[j]); // query if not cached
const auto* itemInfo = gameHandler.getItemInfo(e.itemId[j]);
if (itemInfo && itemInfo->valid && !itemInfo->name.empty()) {
result += std::to_string(e.itemCount[j]) + "x " + itemInfo->name;
} else {
result += std::to_string(e.itemCount[j]) + "x Item#" + std::to_string(e.itemId[j]);
}
}
return result.empty() ? "[Tokens]" : result;
}
void WindowManager::renderVendorWindow(game::GameHandler& gameHandler,
InventoryScreen& inventoryScreen,
ChatPanel& chatPanel) {
if (!gameHandler.isVendorWindowOpen()) return;
auto* window = services_.window;
float screenW = window ? static_cast<float>(window->getWidth()) : 1280.0f;
ImGui::SetNextWindowPos(ImVec2(screenW / 2 - 200, 100), ImGuiCond_Appearing);
ImGui::SetNextWindowSize(ImVec2(450, 400), ImGuiCond_Appearing);
bool open = true;
if (ImGui::Begin("Vendor", &open)) {
const auto& vendor = gameHandler.getVendorItems();
// Show player money
uint64_t money = gameHandler.getMoneyCopper();
ImGui::TextDisabled("Your money:"); ImGui::SameLine(0, 4);
renderCoinsFromCopper(money);
if (vendor.canRepair) {
ImGui::SameLine();
ImGui::SetCursorPosX(ImGui::GetCursorPosX() + 8.0f);
if (ImGui::SmallButton("Repair All")) {
gameHandler.repairAll(vendor.vendorGuid, false);
}
if (ImGui::IsItemHovered()) {
// Show durability summary of all equipment
const auto& inv = gameHandler.getInventory();
int damagedCount = 0;
int brokenCount = 0;
for (int s = 0; s < static_cast<int>(game::EquipSlot::BAG1); s++) {
const auto& slot = inv.getEquipSlot(static_cast<game::EquipSlot>(s));
if (slot.empty() || slot.item.maxDurability == 0) continue;
if (slot.item.curDurability == 0) brokenCount++;
else if (slot.item.curDurability < slot.item.maxDurability) damagedCount++;
}
if (brokenCount > 0)
ImGui::SetTooltip("Repair all equipped items\n%d damaged, %d broken", damagedCount, brokenCount);
else if (damagedCount > 0)
ImGui::SetTooltip("Repair all equipped items\n%d item%s need repair", damagedCount, damagedCount > 1 ? "s" : "");
else
ImGui::SetTooltip("All equipment is in good condition");
}
if (gameHandler.isInGuild()) {
ImGui::SameLine();
if (ImGui::SmallButton("Repair (Guild)")) {
gameHandler.repairAll(vendor.vendorGuid, true);
}
if (ImGui::IsItemHovered()) {
ImGui::SetTooltip("Repair all equipped items using guild bank funds");
}
}
}
ImGui::Separator();
ImGui::TextColored(ui::colors::kLightGray, "Right-click bag items to sell");
// Count grey (POOR quality) sellable items across backpack and bags
const auto& inv = gameHandler.getInventory();
int junkCount = 0;
for (int i = 0; i < inv.getBackpackSize(); ++i) {
const auto& sl = inv.getBackpackSlot(i);
if (!sl.empty() && sl.item.quality == game::ItemQuality::POOR && sl.item.sellPrice > 0)
++junkCount;
}
for (int b = 0; b < game::Inventory::NUM_BAG_SLOTS; ++b) {
for (int s = 0; s < inv.getBagSize(b); ++s) {
const auto& sl = inv.getBagSlot(b, s);
if (!sl.empty() && sl.item.quality == game::ItemQuality::POOR && sl.item.sellPrice > 0)
++junkCount;
}
}
if (junkCount > 0) {
char junkLabel[64];
snprintf(junkLabel, sizeof(junkLabel), "Sell All Junk (%d item%s)",
junkCount, junkCount == 1 ? "" : "s");
if (ImGui::Button(junkLabel, ImVec2(-1, 0))) {
for (int i = 0; i < inv.getBackpackSize(); ++i) {
const auto& sl = inv.getBackpackSlot(i);
if (!sl.empty() && sl.item.quality == game::ItemQuality::POOR && sl.item.sellPrice > 0)
gameHandler.sellItemBySlot(i);
}
for (int b = 0; b < game::Inventory::NUM_BAG_SLOTS; ++b) {
for (int s = 0; s < inv.getBagSize(b); ++s) {
const auto& sl = inv.getBagSlot(b, s);
if (!sl.empty() && sl.item.quality == game::ItemQuality::POOR && sl.item.sellPrice > 0)
gameHandler.sellItemInBag(b, s);
}
}
}
}
ImGui::Separator();
const auto& buyback = gameHandler.getBuybackItems();
if (!buyback.empty()) {
ImGui::TextColored(ui::colors::kTooltipGold, "Buy Back");
if (ImGui::BeginTable("BuybackTable", 4, ImGuiTableFlags_Borders | ImGuiTableFlags_RowBg)) {
ImGui::TableSetupColumn("##icon", ImGuiTableColumnFlags_WidthFixed, 22.0f);
ImGui::TableSetupColumn("Item", ImGuiTableColumnFlags_WidthStretch);
ImGui::TableSetupColumn("Price", ImGuiTableColumnFlags_WidthFixed, 110.0f);
ImGui::TableSetupColumn("Buy", ImGuiTableColumnFlags_WidthFixed, 62.0f);
ImGui::TableHeadersRow();
// Show all buyback items (most recently sold first)
for (int i = 0; i < static_cast<int>(buyback.size()); ++i) {
const auto& entry = buyback[i];
gameHandler.ensureItemInfo(entry.item.itemId);
auto* bbInfo = gameHandler.getItemInfo(entry.item.itemId);
uint32_t sellPrice = entry.item.sellPrice;
if (sellPrice == 0) {
if (bbInfo && bbInfo->valid) sellPrice = bbInfo->sellPrice;
}
uint64_t price = static_cast<uint64_t>(sellPrice) *
static_cast<uint64_t>(entry.count > 0 ? entry.count : 1);
uint32_t g = static_cast<uint32_t>(price / 10000);
uint32_t s = static_cast<uint32_t>((price / 100) % 100);
uint32_t c = static_cast<uint32_t>(price % 100);
bool canAfford = money >= price;
ImGui::TableNextRow();
ImGui::PushID(8000 + i);
ImGui::TableSetColumnIndex(0);
{
uint32_t dispId = entry.item.displayInfoId;
if (bbInfo && bbInfo->valid && bbInfo->displayInfoId != 0) dispId = bbInfo->displayInfoId;
if (dispId != 0) {
VkDescriptorSet iconTex = inventoryScreen.getItemIcon(dispId);
if (iconTex) ImGui::Image((ImTextureID)(uintptr_t)iconTex, ImVec2(18, 18));
}
}
ImGui::TableSetColumnIndex(1);
game::ItemQuality bbQuality = entry.item.quality;
if (bbInfo && bbInfo->valid) bbQuality = static_cast<game::ItemQuality>(bbInfo->quality);
ImVec4 bbQc = InventoryScreen::getQualityColor(bbQuality);
const char* name = entry.item.name.empty() ? "Unknown Item" : entry.item.name.c_str();
if (entry.count > 1) {
ImGui::TextColored(bbQc, "%s x%u", name, entry.count);
} else {
ImGui::TextColored(bbQc, "%s", name);
}
if (ImGui::IsItemHovered() && bbInfo && bbInfo->valid)
inventoryScreen.renderItemTooltip(*bbInfo);
ImGui::TableSetColumnIndex(2);
if (canAfford) {
renderCoinsText(g, s, c);
} else {
ImGui::TextColored(kColorRed, "%ug %us %uc", g, s, c);
}
ImGui::TableSetColumnIndex(3);
if (!canAfford) ImGui::BeginDisabled();
char bbLabel[32];
snprintf(bbLabel, sizeof(bbLabel), "Buy Back##bb%d", i);
if (ImGui::SmallButton(bbLabel)) {
gameHandler.buyBackItem(static_cast<uint32_t>(i));
}
if (!canAfford) ImGui::EndDisabled();
ImGui::PopID();
}
ImGui::EndTable();
}
ImGui::Separator();
}
if (vendor.items.empty()) {
ImGui::TextDisabled("This vendor has nothing for sale.");
} else {
// Search + quantity controls on one row
ImGui::SetNextItemWidth(200.0f);
ImGui::InputTextWithHint("##VendorSearch", "Search...", vendorSearchFilter_, sizeof(vendorSearchFilter_));
ImGui::SameLine();
ImGui::Text("Qty:");
ImGui::SameLine();
ImGui::SetNextItemWidth(60.0f);
static int vendorBuyQty = 1;
ImGui::InputInt("##VendorQty", &vendorBuyQty, 1, 5);
if (vendorBuyQty < 1) vendorBuyQty = 1;
if (vendorBuyQty > 99) vendorBuyQty = 99;
ImGui::Spacing();
if (ImGui::BeginTable("VendorTable", 5, ImGuiTableFlags_Borders | ImGuiTableFlags_RowBg | ImGuiTableFlags_ScrollY)) {
ImGui::TableSetupColumn("##icon", ImGuiTableColumnFlags_WidthFixed, 22.0f);
ImGui::TableSetupColumn("Item", ImGuiTableColumnFlags_WidthStretch);
ImGui::TableSetupColumn("Price", ImGuiTableColumnFlags_WidthFixed, 120.0f);
ImGui::TableSetupColumn("Stock", ImGuiTableColumnFlags_WidthFixed, 60.0f);
ImGui::TableSetupColumn("Buy", ImGuiTableColumnFlags_WidthFixed, 50.0f);
ImGui::TableHeadersRow();
std::string vendorFilter(vendorSearchFilter_);
// Lowercase filter for case-insensitive match
for (char& c : vendorFilter) c = static_cast<char>(std::tolower(static_cast<unsigned char>(c)));
for (int vi = 0; vi < static_cast<int>(vendor.items.size()); ++vi) {
const auto& item = vendor.items[vi];
// Proactively ensure vendor item info is loaded
gameHandler.ensureItemInfo(item.itemId);
auto* info = gameHandler.getItemInfo(item.itemId);
// Apply search filter
if (!vendorFilter.empty()) {
std::string nameLC = info && info->valid ? info->name : ("Item " + std::to_string(item.itemId));
for (char& c : nameLC) c = static_cast<char>(std::tolower(static_cast<unsigned char>(c)));
if (nameLC.find(vendorFilter) == std::string::npos) {
ImGui::PushID(vi);
ImGui::PopID();
continue;
}
}
ImGui::TableNextRow();
ImGui::PushID(vi);
// Icon column
ImGui::TableSetColumnIndex(0);
{
uint32_t dispId = item.displayInfoId;
if (info && info->valid && info->displayInfoId != 0) dispId = info->displayInfoId;
if (dispId != 0) {
VkDescriptorSet iconTex = inventoryScreen.getItemIcon(dispId);
if (iconTex) ImGui::Image((ImTextureID)(uintptr_t)iconTex, ImVec2(18, 18));
}
}
// Name column
ImGui::TableSetColumnIndex(1);
if (info && info->valid) {
ImVec4 qc = InventoryScreen::getQualityColor(static_cast<game::ItemQuality>(info->quality));
ImGui::TextColored(qc, "%s", info->name.c_str());
if (ImGui::IsItemHovered()) {
inventoryScreen.renderItemTooltip(*info, &gameHandler.getInventory());
}
// Shift-click: insert item link into chat
if (ImGui::IsItemClicked() && ImGui::GetIO().KeyShift) {
std::string link = buildItemChatLink(info->entry, info->quality, info->name);
chatPanel.insertChatLink(link);
}
} else {
ImGui::Text("Item %u", item.itemId);
}
ImGui::TableSetColumnIndex(2);
if (item.buyPrice == 0 && item.extendedCost != 0) {
// Token-only item — show detailed cost from ItemExtendedCost.dbc
std::string costStr = formatExtendedCost(item.extendedCost, gameHandler);
ImGui::TextColored(ImVec4(0.4f, 0.8f, 1.0f, 1.0f), "%s", costStr.c_str());
} else {
uint32_t g = item.buyPrice / 10000;
uint32_t s = (item.buyPrice / 100) % 100;
uint32_t c = item.buyPrice % 100;
bool canAfford = money >= item.buyPrice;
if (canAfford) {
renderCoinsText(g, s, c);
} else {
ImGui::TextColored(kColorRed, "%ug %us %uc", g, s, c);
}
// Show additional token cost if both gold and tokens are required
if (item.extendedCost != 0) {
std::string costStr = formatExtendedCost(item.extendedCost, gameHandler);
if (costStr != "[Tokens]") {
ImGui::TextColored(ImVec4(0.4f, 0.8f, 1.0f, 0.8f), "+ %s", costStr.c_str());
}
}
}
ImGui::TableSetColumnIndex(3);
if (item.maxCount < 0) {
ImGui::TextDisabled("Inf");
} else if (item.maxCount == 0) {
ImGui::TextColored(kColorRed, "Out");
} else if (item.maxCount <= 5) {
ImGui::TextColored(ImVec4(1.0f, 0.6f, 0.1f, 1.0f), "%d", item.maxCount);
} else {
ImGui::Text("%d", item.maxCount);
}
ImGui::TableSetColumnIndex(4);
bool outOfStock = (item.maxCount == 0);
if (outOfStock) ImGui::BeginDisabled();
std::string buyBtnId = "Buy##vendor_" + std::to_string(vi);
if (ImGui::SmallButton(buyBtnId.c_str())) {
int qty = vendorBuyQty;
if (item.maxCount > 0 && qty > item.maxCount) qty = item.maxCount;
uint32_t totalCost = item.buyPrice * static_cast<uint32_t>(qty);
if (totalCost >= 10000) { // >= 1 gold: confirm
vendorConfirmOpen_ = true;
vendorConfirmGuid_ = vendor.vendorGuid;
vendorConfirmItemId_ = item.itemId;
vendorConfirmSlot_ = item.slot;
vendorConfirmQty_ = static_cast<uint32_t>(qty);
vendorConfirmPrice_ = totalCost;
vendorConfirmItemName_ = (info && info->valid) ? info->name : "Item";
} else {
gameHandler.buyItem(vendor.vendorGuid, item.itemId, item.slot,
static_cast<uint32_t>(qty));
}
}
if (outOfStock) ImGui::EndDisabled();
ImGui::PopID();
}
ImGui::EndTable();
}
}
}
ImGui::End();
if (!open) {
gameHandler.closeVendor();
}
// Vendor purchase confirmation popup for expensive items
if (vendorConfirmOpen_) {
ImGui::OpenPopup("Confirm Purchase##vendor");
vendorConfirmOpen_ = false;
}
if (ImGui::BeginPopupModal("Confirm Purchase##vendor", nullptr,
ImGuiWindowFlags_AlwaysAutoResize | ImGuiWindowFlags_NoMove)) {
ImGui::Text("Buy %s", vendorConfirmItemName_.c_str());
if (vendorConfirmQty_ > 1)
ImGui::Text("Quantity: %u", vendorConfirmQty_);
uint32_t g = vendorConfirmPrice_ / 10000;
uint32_t s = (vendorConfirmPrice_ / 100) % 100;
uint32_t c = vendorConfirmPrice_ % 100;
ImGui::Text("Cost: %ug %us %uc", g, s, c);
ImGui::Spacing();
if (ImGui::Button("Buy", ImVec2(80, 0))) {
gameHandler.buyItem(vendorConfirmGuid_, vendorConfirmItemId_,
vendorConfirmSlot_, vendorConfirmQty_);
ImGui::CloseCurrentPopup();
}
ImGui::SameLine();
if (ImGui::Button("Cancel", ImVec2(80, 0))) {
ImGui::CloseCurrentPopup();
}
ImGui::EndPopup();
}
}
void WindowManager::renderTrainerWindow(game::GameHandler& gameHandler,
SpellIconFn getSpellIcon) {
if (!gameHandler.isTrainerWindowOpen()) return;
auto* window = services_.window;
float screenW = window ? static_cast<float>(window->getWidth()) : 1280.0f;
auto* assetMgr = services_.assetManager;
ImGui::SetNextWindowPos(ImVec2(screenW / 2 - 225, 100), ImGuiCond_Appearing);
ImGui::SetNextWindowSize(ImVec2(500, 450), ImGuiCond_Appearing);
bool open = true;
if (ImGui::Begin("Trainer", &open)) {
// If user clicked window close, short-circuit before rendering large trainer tables.
if (!open) {
ImGui::End();
gameHandler.closeTrainer();
return;
}
const auto& trainer = gameHandler.getTrainerSpells();
const bool isProfessionTrainer = (trainer.trainerType == 2);
// NPC name
auto npcEntity = gameHandler.getEntityManager().getEntity(trainer.trainerGuid);
if (npcEntity && npcEntity->getType() == game::ObjectType::UNIT) {
auto unit = std::static_pointer_cast<game::Unit>(npcEntity);
if (!unit->getName().empty()) {
ImGui::TextColored(ImVec4(1.0f, 0.8f, 0.0f, 1.0f), "%s", unit->getName().c_str());
}
}
// Greeting
if (!trainer.greeting.empty()) {
ImGui::TextWrapped("%s", trainer.greeting.c_str());
}
ImGui::Separator();
// Player money
uint64_t money = gameHandler.getMoneyCopper();
ImGui::TextDisabled("Your money:"); ImGui::SameLine(0, 4);
renderCoinsFromCopper(money);
// Filter controls
static bool showUnavailable = false;
ImGui::Checkbox("Show unavailable spells", &showUnavailable);
ImGui::SameLine();
ImGui::SetNextItemWidth(-1.0f);
ImGui::InputTextWithHint("##TrainerSearch", "Search...", trainerSearchFilter_, sizeof(trainerSearchFilter_));
ImGui::Separator();
if (trainer.spells.empty()) {
ImGui::TextDisabled("This trainer has nothing to teach you.");
} else {
// Known spells for checking
const auto& knownSpells = gameHandler.getKnownSpells();
auto isKnown = [&](uint32_t id) {
if (id == 0) return true;
// Check if spell is in knownSpells list
bool found = knownSpells.count(id);
if (found) return true;
// Also check if spell is in trainer list with state=2 (explicitly known)
// state=0 means unavailable (could be no prereqs, wrong level, etc.) - don't count as known
for (const auto& ts : trainer.spells) {
if (ts.spellId == id && ts.state == 2) {
return true;
}
}
return false;
};
uint32_t playerLevel = gameHandler.getPlayerLevel();
// Renders spell rows into the current table
auto renderSpellRows = [&](const std::vector<const game::TrainerSpell*>& spells) {
for (const auto* spell : spells) {
// Check prerequisites client-side first
bool prereq1Met = isKnown(spell->chainNode1);
bool prereq2Met = isKnown(spell->chainNode2);
bool prereq3Met = isKnown(spell->chainNode3);
bool prereqsMet = prereq1Met && prereq2Met && prereq3Met;
bool levelMet = (spell->reqLevel == 0 || playerLevel >= spell->reqLevel);
bool alreadyKnown = isKnown(spell->spellId);
// Dynamically determine effective state based on current prerequisites
// Server sends state, but we override if prerequisites are now met
uint8_t effectiveState = spell->state;
if (spell->state == 1 && prereqsMet && levelMet) {
// Server said unavailable, but we now meet all requirements
effectiveState = 0; // Treat as available
}
// Filter: skip unavailable spells if checkbox is unchecked
// Use effectiveState so spells with newly met prereqs aren't filtered
if (!showUnavailable && effectiveState == 1) {
continue;
}
// Apply text search filter
if (trainerSearchFilter_[0] != '\0') {
std::string trainerFilter(trainerSearchFilter_);
for (char& c : trainerFilter) c = static_cast<char>(std::tolower(static_cast<unsigned char>(c)));
const std::string& spellName = gameHandler.getSpellName(spell->spellId);
std::string nameLC = spellName.empty() ? std::to_string(spell->spellId) : spellName;
for (char& c : nameLC) c = static_cast<char>(std::tolower(static_cast<unsigned char>(c)));
if (nameLC.find(trainerFilter) == std::string::npos) {
ImGui::PushID(static_cast<int>(spell->spellId));
ImGui::PopID();
continue;
}
}
ImGui::TableNextRow();
ImGui::PushID(static_cast<int>(spell->spellId));
ImVec4 color;
const char* statusLabel;
// WotLK trainer states: 0=available, 1=unavailable, 2=known
if (effectiveState == 2 || alreadyKnown) {
color = colors::kQueueGreen;
statusLabel = "Known";
} else if (effectiveState == 0) {
color = ui::colors::kWhite;
statusLabel = "Available";
} else {
color = ImVec4(0.6f, 0.3f, 0.3f, 1.0f);
statusLabel = "Unavailable";
}
// Icon column
ImGui::TableSetColumnIndex(0);
{
VkDescriptorSet spellIcon = getSpellIcon(spell->spellId, assetMgr);
if (spellIcon) {
if (effectiveState == 1 && !alreadyKnown) {
ImGui::ImageWithBg((ImTextureID)(uintptr_t)spellIcon, ImVec2(18, 18),
ImVec2(0, 0), ImVec2(1, 1),
ImVec4(0, 0, 0, 0), ImVec4(0.5f, 0.5f, 0.5f, 0.6f));
} else {
ImGui::Image((ImTextureID)(uintptr_t)spellIcon, ImVec2(18, 18));
}
}
}
// Spell name
ImGui::TableSetColumnIndex(1);
const std::string& name = gameHandler.getSpellName(spell->spellId);
const std::string& rank = gameHandler.getSpellRank(spell->spellId);
if (!name.empty()) {
if (!rank.empty())
ImGui::TextColored(color, "%s (%s)", name.c_str(), rank.c_str());
else
ImGui::TextColored(color, "%s", name.c_str());
} else {
ImGui::TextColored(color, "Spell #%u", spell->spellId);
}
if (ImGui::IsItemHovered()) {
ImGui::BeginTooltip();
if (!name.empty()) {
ImGui::TextColored(kColorYellow, "%s", name.c_str());
if (!rank.empty()) ImGui::TextColored(kColorGray, "%s", rank.c_str());
}
const std::string& spDesc = gameHandler.getSpellDescription(spell->spellId);
if (!spDesc.empty()) {
ImGui::Spacing();
ImGui::PushTextWrapPos(ImGui::GetCursorPosX() + 300.0f);
ImGui::TextWrapped("%s", spDesc.c_str());
ImGui::PopTextWrapPos();
ImGui::Spacing();
}
ImGui::TextDisabled("Status: %s", statusLabel);
if (spell->reqLevel > 0) {
ImVec4 lvlColor = levelMet ? ui::colors::kLightGray : kColorRed;
ImGui::TextColored(lvlColor, "Required Level: %u", spell->reqLevel);
}
if (spell->reqSkill > 0) ImGui::Text("Required Skill: %u (value %u)", spell->reqSkill, spell->reqSkillValue);
auto showPrereq = [&](uint32_t node) {
if (node == 0) return;
bool met = isKnown(node);
const std::string& pname = gameHandler.getSpellName(node);
ImVec4 pcolor = met ? colors::kQueueGreen : kColorRed;
if (!pname.empty())
ImGui::TextColored(pcolor, "Requires: %s%s", pname.c_str(), met ? " (known)" : "");
else
ImGui::TextColored(pcolor, "Requires: Spell #%u%s", node, met ? " (known)" : "");
};
showPrereq(spell->chainNode1);
showPrereq(spell->chainNode2);
showPrereq(spell->chainNode3);
ImGui::EndTooltip();
}
// Level
ImGui::TableSetColumnIndex(2);
ImGui::TextColored(color, "%u", spell->reqLevel);
// Cost
ImGui::TableSetColumnIndex(3);
if (spell->spellCost > 0) {
uint32_t g = spell->spellCost / 10000;
uint32_t s = (spell->spellCost / 100) % 100;
uint32_t c = spell->spellCost % 100;
bool canAfford = money >= spell->spellCost;
if (canAfford) {
renderCoinsText(g, s, c);
} else {
ImGui::TextColored(kColorRed, "%ug %us %uc", g, s, c);
}
} else {
ImGui::TextColored(color, "Free");
}
// Train button - only enabled if available, affordable, prereqs met
ImGui::TableSetColumnIndex(4);
// Use effectiveState so newly available spells (after learning prereqs) can be trained
bool canTrain = !alreadyKnown && effectiveState == 0
&& prereqsMet && levelMet
&& (money >= spell->spellCost);
// Debug logging for first 3 spells to see why buttons are disabled
static int logCount = 0;
static uint64_t lastTrainerGuid = 0;
if (trainer.trainerGuid != lastTrainerGuid) {
logCount = 0;
lastTrainerGuid = trainer.trainerGuid;
}
if (logCount < 3) {
LOG_INFO("Trainer button debug: spellId=", spell->spellId,
" alreadyKnown=", alreadyKnown, " state=", static_cast<int>(spell->state),
" prereqsMet=", prereqsMet, " (", prereq1Met, ",", prereq2Met, ",", prereq3Met, ")",
" levelMet=", levelMet,
" reqLevel=", spell->reqLevel, " playerLevel=", playerLevel,
" chain1=", spell->chainNode1, " chain2=", spell->chainNode2, " chain3=", spell->chainNode3,
" canAfford=", (money >= spell->spellCost),
" canTrain=", canTrain);
logCount++;
}
if (isProfessionTrainer && alreadyKnown) {
// Profession trainer: known recipes show "Create" button to craft
bool isCasting = gameHandler.isCasting();
if (isCasting) ImGui::BeginDisabled();
if (ImGui::SmallButton("Create")) {
gameHandler.castSpell(spell->spellId, 0);
}
if (isCasting) ImGui::EndDisabled();
} else {
if (!canTrain) ImGui::BeginDisabled();
if (ImGui::SmallButton("Train")) {
gameHandler.trainSpell(spell->spellId);
}
if (!canTrain) ImGui::EndDisabled();
}
ImGui::PopID();
}
};
auto renderSpellTable = [&](const char* tableId, const std::vector<const game::TrainerSpell*>& spells) {
if (ImGui::BeginTable(tableId, 5,
ImGuiTableFlags_Borders | ImGuiTableFlags_RowBg | ImGuiTableFlags_ScrollY)) {
ImGui::TableSetupColumn("##icon", ImGuiTableColumnFlags_WidthFixed, 22.0f);
ImGui::TableSetupColumn("Spell", ImGuiTableColumnFlags_WidthStretch);
ImGui::TableSetupColumn("Level", ImGuiTableColumnFlags_WidthFixed, 40.0f);
ImGui::TableSetupColumn("Cost", ImGuiTableColumnFlags_WidthFixed, 120.0f);
ImGui::TableSetupColumn("##action", ImGuiTableColumnFlags_WidthFixed, 55.0f);
ImGui::TableHeadersRow();
renderSpellRows(spells);
ImGui::EndTable();
}
};
const auto& tabs = gameHandler.getTrainerTabs();
if (tabs.size() > 1) {
// Multiple tabs - show tab bar
if (ImGui::BeginTabBar("TrainerTabs")) {
for (size_t i = 0; i < tabs.size(); i++) {
char tabLabel[64];
snprintf(tabLabel, sizeof(tabLabel), "%s (%zu)",
tabs[i].name.c_str(), tabs[i].spells.size());
if (ImGui::BeginTabItem(tabLabel)) {
char tableId[32];
snprintf(tableId, sizeof(tableId), "TT%zu", i);
renderSpellTable(tableId, tabs[i].spells);
ImGui::EndTabItem();
}
}
ImGui::EndTabBar();
}
} else {
// Single tab or no categorization - flat list
std::vector<const game::TrainerSpell*> allSpells;
allSpells.reserve(trainer.spells.size());
for (const auto& spell : trainer.spells) {
allSpells.push_back(&spell);
}
renderSpellTable("TrainerTable", allSpells);
}
// Count how many spells are trainable right now
int trainableCount = 0;
uint64_t totalCost = 0;
for (const auto& spell : trainer.spells) {
bool prereq1Met = isKnown(spell.chainNode1);
bool prereq2Met = isKnown(spell.chainNode2);
bool prereq3Met = isKnown(spell.chainNode3);
bool prereqsMet = prereq1Met && prereq2Met && prereq3Met;
bool levelMet = (spell.reqLevel == 0 || playerLevel >= spell.reqLevel);
bool alreadyKnown = isKnown(spell.spellId);
uint8_t effectiveState = spell.state;
if (spell.state == 1 && prereqsMet && levelMet) effectiveState = 0;
bool canTrain = !alreadyKnown && effectiveState == 0
&& prereqsMet && levelMet
&& (money >= spell.spellCost);
if (canTrain) {
++trainableCount;
totalCost += spell.spellCost;
}
}
ImGui::Separator();
bool canAffordAll = (money >= totalCost);
bool hasTrainable = (trainableCount > 0) && canAffordAll;
if (!hasTrainable) ImGui::BeginDisabled();
uint32_t tag = static_cast<uint32_t>(totalCost / 10000);
uint32_t tas = static_cast<uint32_t>((totalCost / 100) % 100);
uint32_t tac = static_cast<uint32_t>(totalCost % 100);
char trainAllLabel[80];
if (trainableCount == 0) {
snprintf(trainAllLabel, sizeof(trainAllLabel), "Train All Available (none)");
} else {
snprintf(trainAllLabel, sizeof(trainAllLabel),
"Train All Available (%d spell%s, %ug %us %uc)",
trainableCount, trainableCount == 1 ? "" : "s",
tag, tas, tac);
}
if (ImGui::Button(trainAllLabel, ImVec2(-1.0f, 0.0f))) {
for (const auto& spell : trainer.spells) {
bool prereq1Met = isKnown(spell.chainNode1);
bool prereq2Met = isKnown(spell.chainNode2);
bool prereq3Met = isKnown(spell.chainNode3);
bool prereqsMet = prereq1Met && prereq2Met && prereq3Met;
bool levelMet = (spell.reqLevel == 0 || playerLevel >= spell.reqLevel);
bool alreadyKnown = isKnown(spell.spellId);
uint8_t effectiveState = spell.state;
if (spell.state == 1 && prereqsMet && levelMet) effectiveState = 0;
bool canTrain = !alreadyKnown && effectiveState == 0
&& prereqsMet && levelMet
&& (money >= spell.spellCost);
if (canTrain) {
gameHandler.trainSpell(spell.spellId);
}
}
}
if (!hasTrainable) ImGui::EndDisabled();
// Profession trainer: craft quantity controls
if (isProfessionTrainer) {
ImGui::Separator();
static int craftQuantity = 1;
static uint32_t selectedCraftSpell = 0;
// Show craft queue status if active
int queueRemaining = gameHandler.getCraftQueueRemaining();
if (queueRemaining > 0) {
ImGui::TextColored(ImVec4(1.0f, 0.8f, 0.0f, 1.0f),
"Crafting... %d remaining", queueRemaining);
ImGui::SameLine();
if (ImGui::SmallButton("Stop")) {
gameHandler.cancelCraftQueue();
gameHandler.cancelCast();
}
} else {
// Spell selector + quantity input
// Build list of known (craftable) spells
std::vector<const game::TrainerSpell*> craftable;
for (const auto& spell : trainer.spells) {
if (isKnown(spell.spellId)) {
craftable.push_back(&spell);
}
}
if (!craftable.empty()) {
// Combo box for recipe selection
const char* previewName = "Select recipe...";
for (const auto* sp : craftable) {
if (sp->spellId == selectedCraftSpell) {
const std::string& n = gameHandler.getSpellName(sp->spellId);
if (!n.empty()) previewName = n.c_str();
break;
}
}
ImGui::SetNextItemWidth(ImGui::GetContentRegionAvail().x * 0.55f);
if (ImGui::BeginCombo("##CraftSelect", previewName)) {
for (const auto* sp : craftable) {
const std::string& n = gameHandler.getSpellName(sp->spellId);
const std::string& r = gameHandler.getSpellRank(sp->spellId);
char label[128];
if (!r.empty())
snprintf(label, sizeof(label), "%s (%s)##%u",
n.empty() ? "???" : n.c_str(), r.c_str(), sp->spellId);
else
snprintf(label, sizeof(label), "%s##%u",
n.empty() ? "???" : n.c_str(), sp->spellId);
if (ImGui::Selectable(label, sp->spellId == selectedCraftSpell)) {
selectedCraftSpell = sp->spellId;
}
}
ImGui::EndCombo();
}
ImGui::SameLine();
ImGui::SetNextItemWidth(50.0f);
ImGui::InputInt("##CraftQty", &craftQuantity, 0, 0);
if (craftQuantity < 1) craftQuantity = 1;
if (craftQuantity > 99) craftQuantity = 99;
ImGui::SameLine();
bool canCraft = selectedCraftSpell != 0 && !gameHandler.isCasting();
if (!canCraft) ImGui::BeginDisabled();
if (ImGui::Button("Create")) {
if (craftQuantity == 1) {
gameHandler.castSpell(selectedCraftSpell, 0);
} else {
gameHandler.startCraftQueue(selectedCraftSpell, craftQuantity);
}
}
ImGui::SameLine();
if (ImGui::Button("Create All")) {
// Queue a large count — server stops the queue automatically
// when materials run out (sends SPELL_FAILED_REAGENTS).
gameHandler.startCraftQueue(selectedCraftSpell, 999);
}
if (!canCraft) ImGui::EndDisabled();
}
}
}
}
}
ImGui::End();
if (!open) {
gameHandler.closeTrainer();
}
}
void WindowManager::renderEscapeMenu(SettingsPanel& settingsPanel) {
if (!showEscapeMenu) return;
ImGuiIO& io = ImGui::GetIO();
float screenW = io.DisplaySize.x;
float screenH = io.DisplaySize.y;
ImVec2 size(260.0f, 248.0f);
ImVec2 pos((screenW - size.x) * 0.5f, (screenH - size.y) * 0.5f);
ImGui::SetNextWindowPos(pos, ImGuiCond_Always);
ImGui::SetNextWindowSize(size, ImGuiCond_Always);
ImGuiWindowFlags flags = ImGuiWindowFlags_NoResize | ImGuiWindowFlags_NoMove |
ImGuiWindowFlags_NoCollapse | ImGuiWindowFlags_NoTitleBar;
if (ImGui::Begin("##EscapeMenu", nullptr, flags)) {
ImGui::Text("Game Menu");
ImGui::Separator();
if (ImGui::Button("Logout", ImVec2(-1, 0))) {
core::Application::getInstance().logoutToLogin();
showEscapeMenu = false;
settingsPanel.showEscapeSettingsNotice = false;
}
if (ImGui::Button("Quit", ImVec2(-1, 0))) {
auto* ac = services_.audioCoordinator;
if (ac) {
if (auto* music = ac->getMusicManager()) {
music->stopMusic(0.0f);
}
}
core::Application::getInstance().shutdown();
}
if (ImGui::Button("Settings", ImVec2(-1, 0))) {
settingsPanel.showEscapeSettingsNotice = false;
settingsPanel.showSettingsWindow = true;
settingsPanel.settingsInit = false;
showEscapeMenu = false;
}
if (ImGui::Button("Instance Lockouts", ImVec2(-1, 0))) {
showInstanceLockouts_ = true;
showEscapeMenu = false;
}
if (ImGui::Button("Help / GM Ticket", ImVec2(-1, 0))) {
showGmTicketWindow_ = true;
showEscapeMenu = false;
}
ImGui::Spacing();
ImGui::PushStyleVar(ImGuiStyleVar_FramePadding, ImVec2(10.0f, 10.0f));
if (ImGui::Button("Back to Game", ImVec2(-1, 0))) {
showEscapeMenu = false;
settingsPanel.showEscapeSettingsNotice = false;
}
ImGui::PopStyleVar();
}
ImGui::End();
}
void WindowManager::renderBarberShopWindow(game::GameHandler& gameHandler) {
if (!gameHandler.isBarberShopOpen()) {
barberInitialized_ = false;
return;
}
const auto* ch = gameHandler.getActiveCharacter();
if (!ch) return;
uint8_t race = static_cast<uint8_t>(ch->race);
game::Gender gender = ch->gender;
game::Race raceEnum = ch->race;
// Initialize sliders from current appearance
if (!barberInitialized_) {
barberOrigHairStyle_ = static_cast<int>((ch->appearanceBytes >> 16) & 0xFF);
barberOrigHairColor_ = static_cast<int>((ch->appearanceBytes >> 24) & 0xFF);
barberOrigFacialHair_ = static_cast<int>(ch->facialFeatures);
barberHairStyle_ = barberOrigHairStyle_;
barberHairColor_ = barberOrigHairColor_;
barberFacialHair_ = barberOrigFacialHair_;
barberInitialized_ = true;
}
int maxHairStyle = static_cast<int>(game::getMaxHairStyle(raceEnum, gender));
int maxHairColor = static_cast<int>(game::getMaxHairColor(raceEnum, gender));
int maxFacialHair = static_cast<int>(game::getMaxFacialFeature(raceEnum, gender));
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 winW = 300.0f;
float winH = 220.0f;
ImGui::SetNextWindowPos(ImVec2((screenW - winW) / 2.0f, (screenH - winH) / 2.0f), ImGuiCond_Appearing);
ImGui::SetNextWindowSize(ImVec2(winW, winH), ImGuiCond_Appearing);
ImGuiWindowFlags flags = ImGuiWindowFlags_NoCollapse;
bool open = true;
if (ImGui::Begin("Barber Shop", &open, flags)) {
ImGui::Text("Choose your new look:");
ImGui::Separator();
ImGui::Spacing();
ImGui::PushItemWidth(-1);
// Hair Style
ImGui::Text("Hair Style");
ImGui::SliderInt("##HairStyle", &barberHairStyle_, 0, maxHairStyle,
"%d");
// Hair Color
ImGui::Text("Hair Color");
ImGui::SliderInt("##HairColor", &barberHairColor_, 0, maxHairColor,
"%d");
// Facial Hair / Piercings / Markings
const char* facialLabel = (gender == game::Gender::FEMALE) ? "Piercings" : "Facial Hair";
// Some races use "Markings" or "Tusks" etc.
if (race == 8 || race == 6) facialLabel = "Features"; // Trolls, Tauren
ImGui::Text("%s", facialLabel);
ImGui::SliderInt("##FacialHair", &barberFacialHair_, 0, maxFacialHair,
"%d");
ImGui::PopItemWidth();
ImGui::Spacing();
ImGui::Separator();
// Show whether anything changed
bool changed = (barberHairStyle_ != barberOrigHairStyle_ ||
barberHairColor_ != barberOrigHairColor_ ||
barberFacialHair_ != barberOrigFacialHair_);
// OK / Reset / Cancel buttons
float btnW = 80.0f;
float totalW = btnW * 3 + ImGui::GetStyle().ItemSpacing.x * 2;
ImGui::SetCursorPosX((ImGui::GetWindowWidth() - totalW) / 2.0f);
if (!changed) ImGui::BeginDisabled();
if (ImGui::Button("OK", ImVec2(btnW, 0))) {
gameHandler.sendAlterAppearance(
static_cast<uint32_t>(barberHairStyle_),
static_cast<uint32_t>(barberHairColor_),
static_cast<uint32_t>(barberFacialHair_));
// Keep window open — server will respond with SMSG_BARBER_SHOP_RESULT
}
if (!changed) ImGui::EndDisabled();
ImGui::SameLine();
if (!changed) ImGui::BeginDisabled();
if (ImGui::Button("Reset", ImVec2(btnW, 0))) {
barberHairStyle_ = barberOrigHairStyle_;
barberHairColor_ = barberOrigHairColor_;
barberFacialHair_ = barberOrigFacialHair_;
}
if (!changed) ImGui::EndDisabled();
ImGui::SameLine();
if (ImGui::Button("Cancel", ImVec2(btnW, 0))) {
gameHandler.closeBarberShop();
}
}
ImGui::End();
if (!open) {
gameHandler.closeBarberShop();
}
}
void WindowManager::renderStableWindow(game::GameHandler& gameHandler) {
if (!gameHandler.isStableWindowOpen()) 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 / 2.0f - 240.0f, screenH / 2.0f - 180.0f),
ImGuiCond_Once);
ImGui::SetNextWindowSize(ImVec2(480.0f, 360.0f), ImGuiCond_Once);
bool open = true;
if (!ImGui::Begin("Pet Stable", &open,
kDialogFlags)) {
ImGui::End();
if (!open) {
// User closed the window; clear stable state
gameHandler.closeStableWindow();
}
return;
}
const auto& pets = gameHandler.getStabledPets();
uint8_t numSlots = gameHandler.getStableSlots();
ImGui::TextDisabled("Stable slots: %u", static_cast<unsigned>(numSlots));
ImGui::Separator();
// Active pets section
bool hasActivePets = false;
for (const auto& p : pets) {
if (p.isActive) { hasActivePets = true; break; }
}
if (hasActivePets) {
ImGui::TextColored(ImVec4(0.4f, 0.9f, 0.4f, 1.0f), "Active / Summoned");
for (const auto& p : pets) {
if (!p.isActive) continue;
ImGui::PushID(static_cast<int>(p.petNumber) * -1 - 1);
const std::string displayName = p.name.empty()
? ("Pet #" + std::to_string(p.petNumber))
: p.name;
ImGui::Text(" %s (Level %u)", displayName.c_str(), p.level);
ImGui::SameLine();
ImGui::TextDisabled("[Active]");
// Offer to stable the active pet if there are free slots
uint8_t usedSlots = 0;
for (const auto& sp : pets) { if (!sp.isActive) ++usedSlots; }
if (usedSlots < numSlots) {
ImGui::SameLine();
if (ImGui::SmallButton("Store in stable")) {
// Slot 1 is first stable slot; server handles free slot assignment.
gameHandler.stablePet(1);
}
}
ImGui::PopID();
}
ImGui::Separator();
}
// Stabled pets section
ImGui::TextColored(ImVec4(0.9f, 0.8f, 0.4f, 1.0f), "Stabled Pets");
bool hasStabledPets = false;
for (const auto& p : pets) {
if (!p.isActive) { hasStabledPets = true; break; }
}
if (!hasStabledPets) {
ImGui::TextDisabled(" (No pets in stable)");
} else {
for (const auto& p : pets) {
if (p.isActive) continue;
ImGui::PushID(static_cast<int>(p.petNumber));
const std::string displayName = p.name.empty()
? ("Pet #" + std::to_string(p.petNumber))
: p.name;
ImGui::Text(" %s (Level %u, Entry %u)",
displayName.c_str(), p.level, p.entry);
ImGui::SameLine();
if (ImGui::SmallButton("Retrieve")) {
gameHandler.unstablePet(p.petNumber);
}
ImGui::PopID();
}
}
// Empty slots
uint8_t usedStableSlots = 0;
for (const auto& p : pets) { if (!p.isActive) ++usedStableSlots; }
if (usedStableSlots < numSlots) {
ImGui::TextDisabled(" %u empty slot(s) available",
static_cast<unsigned>(numSlots - usedStableSlots));
}
ImGui::Separator();
if (ImGui::Button("Refresh")) {
gameHandler.requestStabledPetList();
}
ImGui::SameLine();
if (ImGui::Button("Close")) {
gameHandler.closeStableWindow();
}
ImGui::End();
if (!open) {
gameHandler.closeStableWindow();
}
}
void WindowManager::renderTaxiWindow(game::GameHandler& gameHandler) {
if (!gameHandler.isTaxiWindowOpen()) return;
auto* window = services_.window;
float screenW = window ? static_cast<float>(window->getWidth()) : 1280.0f;
ImGui::SetNextWindowPos(ImVec2(screenW / 2 - 200, 150), ImGuiCond_Appearing);
ImGui::SetNextWindowSize(ImVec2(400, 0), ImGuiCond_Always);
bool open = true;
if (ImGui::Begin("Flight Master", &open, ImGuiWindowFlags_NoCollapse | ImGuiWindowFlags_AlwaysAutoResize)) {
const auto& taxiData = gameHandler.getTaxiData();
const auto& nodes = gameHandler.getTaxiNodes();
uint32_t currentNode = gameHandler.getTaxiCurrentNode();
// Get current node's map to filter destinations
uint32_t currentMapId = 0;
auto curIt = nodes.find(currentNode);
if (curIt != nodes.end()) {
currentMapId = curIt->second.mapId;
ImGui::TextColored(colors::kActiveGreen, "Current: %s", curIt->second.name.c_str());
ImGui::Separator();
}
ImGui::Text("Select a destination:");
ImGui::Spacing();
static uint32_t selectedNodeId = 0;
int destCount = 0;
if (ImGui::BeginTable("TaxiNodes", 3, ImGuiTableFlags_SizingFixedFit | ImGuiTableFlags_RowBg)) {
ImGui::TableSetupColumn("Destination", ImGuiTableColumnFlags_WidthStretch);
ImGui::TableSetupColumn("Cost", ImGuiTableColumnFlags_WidthFixed, 120.0f);
ImGui::TableSetupColumn("Action", ImGuiTableColumnFlags_WidthFixed, 60.0f);
ImGui::TableHeadersRow();
for (const auto& [nodeId, node] : nodes) {
if (nodeId == currentNode) continue;
if (node.mapId != currentMapId) continue;
if (!taxiData.isNodeKnown(nodeId)) continue;
uint32_t costCopper = gameHandler.getTaxiCostTo(nodeId);
uint32_t gold = costCopper / 10000;
uint32_t silver = (costCopper / 100) % 100;
uint32_t copper = costCopper % 100;
ImGui::PushID(static_cast<int>(nodeId));
ImGui::TableNextRow();
ImGui::TableSetColumnIndex(0);
bool isSelected = (selectedNodeId == nodeId);
if (ImGui::Selectable(node.name.c_str(), isSelected,
ImGuiSelectableFlags_SpanAllColumns |
ImGuiSelectableFlags_AllowDoubleClick)) {
selectedNodeId = nodeId;
LOG_INFO("Taxi UI: Selected dest=", nodeId);
if (ImGui::IsMouseDoubleClicked(ImGuiMouseButton_Left)) {
LOG_INFO("Taxi UI: Double-click activate dest=", nodeId);
gameHandler.activateTaxi(nodeId);
}
}
ImGui::TableSetColumnIndex(1);
renderCoinsText(gold, silver, copper);
ImGui::TableSetColumnIndex(2);
if (ImGui::SmallButton("Fly")) {
selectedNodeId = nodeId;
LOG_INFO("Taxi UI: Fly clicked dest=", nodeId);
gameHandler.activateTaxi(nodeId);
}
ImGui::PopID();
destCount++;
}
ImGui::EndTable();
}
if (destCount == 0) {
ImGui::TextColored(ui::colors::kLightGray, "No destinations available.");
}
ImGui::Spacing();
ImGui::Separator();
if (selectedNodeId != 0 && ImGui::Button("Fly Selected", ImVec2(-1, 0))) {
LOG_INFO("Taxi UI: Fly Selected dest=", selectedNodeId);
gameHandler.activateTaxi(selectedNodeId);
}
if (ImGui::Button("Close", ImVec2(-1, 0))) {
gameHandler.closeTaxi();
}
}
ImGui::End();
if (!open) {
gameHandler.closeTaxi();
}
}
void WindowManager::renderLogoutCountdown(game::GameHandler& gameHandler) {
if (!gameHandler.isLoggingOut()) 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;
constexpr float W = 280.0f;
constexpr float H = 80.0f;
ImGui::SetNextWindowPos(ImVec2((screenW - W) * 0.5f, screenH * 0.5f - H * 0.5f - 60.0f),
ImGuiCond_Always);
ImGui::SetNextWindowSize(ImVec2(W, H), ImGuiCond_Always);
ImGui::SetNextWindowBgAlpha(0.88f);
ImGui::PushStyleVar(ImGuiStyleVar_WindowRounding, 6.0f);
ImGui::PushStyleColor(ImGuiCol_WindowBg, ImVec4(0.08f, 0.08f, 0.18f, 0.95f));
ImGui::PushStyleColor(ImGuiCol_Border, ImVec4(0.5f, 0.5f, 0.8f, 1.0f));
if (ImGui::Begin("##LogoutCountdown", nullptr,
ImGuiWindowFlags_NoDecoration | ImGuiWindowFlags_NoMove |
ImGuiWindowFlags_NoNav | ImGuiWindowFlags_NoBringToFrontOnFocus)) {
float cd = gameHandler.getLogoutCountdown();
if (cd > 0.0f) {
ImGui::SetCursorPosY(ImGui::GetCursorPosY() + 6.0f);
ImGui::SetCursorPosX((W - ImGui::CalcTextSize("Logging out in 20s...").x) * 0.5f);
ImGui::TextColored(ImVec4(1.0f, 0.85f, 0.3f, 1.0f),
"Logging out in %ds...", static_cast<int>(std::ceil(cd)));
// Progress bar (20 second countdown)
float frac = 1.0f - std::min(cd / 20.0f, 1.0f);
ImGui::PushStyleColor(ImGuiCol_PlotHistogram, ImVec4(0.5f, 0.5f, 0.9f, 1.0f));
ImGui::ProgressBar(frac, ImVec2(-1.0f, 8.0f), "");
ImGui::PopStyleColor();
ImGui::Spacing();
} else {
ImGui::SetCursorPosY(ImGui::GetCursorPosY() + 14.0f);
ImGui::SetCursorPosX((W - ImGui::CalcTextSize("Logging out...").x) * 0.5f);
ImGui::TextColored(ImVec4(1.0f, 0.85f, 0.3f, 1.0f), "Logging out...");
ImGui::Spacing();
}
// Cancel button — only while countdown is still running
if (cd > 0.0f) {
float btnW = 100.0f;
ImGui::SetCursorPosX((W - btnW) * 0.5f);
ImGui::PushStyleColor(ImGuiCol_Button, ImVec4(0.5f, 0.1f, 0.1f, 1.0f));
ImGui::PushStyleColor(ImGuiCol_ButtonHovered, ImVec4(0.7f, 0.15f, 0.15f, 1.0f));
if (ImGui::Button("Cancel", ImVec2(btnW, 0))) {
gameHandler.cancelLogout();
}
ImGui::PopStyleColor(2);
}
}
ImGui::End();
ImGui::PopStyleColor(2);
ImGui::PopStyleVar();
}
void WindowManager::renderDeathScreen(game::GameHandler& gameHandler) {
if (!gameHandler.showDeathDialog()) {
deathTimerRunning_ = false;
deathElapsed_ = 0.0f;
return;
}
float dt = ImGui::GetIO().DeltaTime;
if (!deathTimerRunning_) {
deathElapsed_ = 0.0f;
deathTimerRunning_ = true;
} else {
deathElapsed_ += dt;
}
auto* window = services_.window;
float screenW = window ? static_cast<float>(window->getWidth()) : 1280.0f;
float screenH = window ? static_cast<float>(window->getHeight()) : 720.0f;
// Dark red overlay covering the whole screen
ImGui::SetNextWindowPos(ImVec2(0, 0));
ImGui::SetNextWindowSize(ImVec2(screenW, screenH));
ImGui::PushStyleColor(ImGuiCol_WindowBg, ImVec4(0.15f, 0.0f, 0.0f, 0.45f));
ImGui::Begin("##DeathOverlay", nullptr,
ImGuiWindowFlags_NoDecoration | ImGuiWindowFlags_NoMove | ImGuiWindowFlags_NoInputs |
ImGuiWindowFlags_NoBringToFrontOnFocus | ImGuiWindowFlags_NoFocusOnAppearing);
ImGui::End();
ImGui::PopStyleColor();
// "Release Spirit" dialog centered on screen
const bool hasSelfRes = gameHandler.canSelfRes();
float dlgW = 280.0f;
// Extra height when self-res button is available; +20 for the "wait for res" hint
float dlgH = hasSelfRes ? 190.0f : 150.0f;
ImGui::SetNextWindowPos(ImVec2(screenW / 2 - dlgW / 2, screenH * 0.35f), ImGuiCond_Always);
ImGui::SetNextWindowSize(ImVec2(dlgW, dlgH), ImGuiCond_Always);
ImGui::PushStyleVar(ImGuiStyleVar_WindowRounding, 8.0f);
ImGui::PushStyleColor(ImGuiCol_WindowBg, ImVec4(0.1f, 0.0f, 0.0f, 0.9f));
ImGui::PushStyleColor(ImGuiCol_Border, ImVec4(0.6f, 0.1f, 0.1f, 1.0f));
if (ImGui::Begin("##DeathDialog", nullptr,
ImGuiWindowFlags_NoResize | ImGuiWindowFlags_NoMove |
ImGuiWindowFlags_NoCollapse | ImGuiWindowFlags_NoTitleBar)) {
ImGui::Spacing();
// Center "You are dead." text
const char* deathText = "You are dead.";
float textW = ImGui::CalcTextSize(deathText).x;
ImGui::SetCursorPosX((dlgW - textW) / 2);
ImGui::TextColored(colors::kBrightRed, "%s", deathText);
// Respawn timer: show how long until the server auto-releases the spirit
float timeLeft = kForcedReleaseSec - deathElapsed_;
if (timeLeft > 0.0f) {
int mins = static_cast<int>(timeLeft) / 60;
int secs = static_cast<int>(timeLeft) % 60;
char timerBuf[48];
snprintf(timerBuf, sizeof(timerBuf), "Auto-release in %d:%02d", mins, secs);
float tw = ImGui::CalcTextSize(timerBuf).x;
ImGui::SetCursorPosX((dlgW - tw) / 2);
ImGui::TextColored(colors::kMediumGray, "%s", timerBuf);
}
ImGui::Spacing();
ImGui::Spacing();
// Self-resurrection button (Reincarnation / Twisting Nether / Deathpact)
if (hasSelfRes) {
float btnW2 = 220.0f;
ImGui::SetCursorPosX((dlgW - btnW2) / 2);
ImGui::PushStyleColor(ImGuiCol_Button, ImVec4(0.15f, 0.35f, 0.55f, 1.0f));
ImGui::PushStyleColor(ImGuiCol_ButtonHovered, ImVec4(0.2f, 0.5f, 0.75f, 1.0f));
if (ImGui::Button("Use Self-Resurrection", ImVec2(btnW2, 30))) {
gameHandler.useSelfRes();
}
ImGui::PopStyleColor(2);
ImGui::Spacing();
}
// Center the Release Spirit button
float btnW = 180.0f;
ImGui::SetCursorPosX((dlgW - btnW) / 2);
ImGui::PushStyleColor(ImGuiCol_Button, ImVec4(0.5f, 0.1f, 0.1f, 1.0f));
ImGui::PushStyleColor(ImGuiCol_ButtonHovered, ImVec4(0.7f, 0.15f, 0.15f, 1.0f));
if (ImGui::Button("Release Spirit", ImVec2(btnW, 30))) {
gameHandler.releaseSpirit();
}
ImGui::PopStyleColor(2);
// Hint: player can stay dead and wait for another player to cast Resurrection
const char* resHint = "Or wait for a player to resurrect you.";
float hw = ImGui::CalcTextSize(resHint).x;
ImGui::SetCursorPosX((dlgW - hw) / 2);
ImGui::TextColored(ImVec4(0.5f, 0.6f, 0.5f, 0.85f), "%s", resHint);
}
ImGui::End();
ImGui::PopStyleColor(2);
ImGui::PopStyleVar();
}
void WindowManager::renderReclaimCorpseButton(game::GameHandler& gameHandler) {
if (!gameHandler.isPlayerGhost() || !gameHandler.canReclaimCorpse()) 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;
float delaySec = gameHandler.getCorpseReclaimDelaySec();
bool onDelay = (delaySec > 0.0f);
float btnW = 220.0f, btnH = 36.0f;
float winH = btnH + 16.0f + (onDelay ? 20.0f : 0.0f);
ImGui::SetNextWindowPos(ImVec2(screenW / 2 - btnW / 2, screenH * 0.72f), ImGuiCond_Always);
ImGui::SetNextWindowSize(ImVec2(btnW + 16.0f, winH), ImGuiCond_Always);
ImGui::PushStyleVar(ImGuiStyleVar_WindowRounding, 6.0f);
ImGui::PushStyleVar(ImGuiStyleVar_WindowPadding, ImVec2(8.0f, 8.0f));
ImGui::PushStyleColor(ImGuiCol_WindowBg, ImVec4(0.0f, 0.0f, 0.0f, 0.7f));
if (ImGui::Begin("##ReclaimCorpse", nullptr,
ImGuiWindowFlags_NoDecoration | ImGuiWindowFlags_NoMove |
ImGuiWindowFlags_NoBringToFrontOnFocus)) {
if (onDelay) {
// Greyed-out button while PvP reclaim timer ticks down
ImGui::PushStyleColor(ImGuiCol_Button, ImVec4(0.25f, 0.25f, 0.25f, 1.0f));
ImGui::PushStyleColor(ImGuiCol_ButtonHovered, ImVec4(0.25f, 0.25f, 0.25f, 1.0f));
ImGui::BeginDisabled(true);
char delayLabel[64];
snprintf(delayLabel, sizeof(delayLabel), "Resurrect from Corpse (%.0fs)", delaySec);
ImGui::Button(delayLabel, ImVec2(btnW, btnH));
ImGui::EndDisabled();
ImGui::PopStyleColor(2);
const char* waitMsg = "You cannot reclaim your corpse yet.";
float tw = ImGui::CalcTextSize(waitMsg).x;
ImGui::SetCursorPosX((btnW + 16.0f - tw) * 0.5f);
ImGui::TextColored(ImVec4(0.8f, 0.5f, 0.2f, 1.0f), "%s", waitMsg);
} else {
ImGui::PushStyleColor(ImGuiCol_Button, ImVec4(0.15f, 0.35f, 0.15f, 1.0f));
ImGui::PushStyleColor(ImGuiCol_ButtonHovered, ImVec4(0.25f, 0.55f, 0.25f, 1.0f));
if (ImGui::Button("Resurrect from Corpse", ImVec2(btnW, btnH))) {
gameHandler.reclaimCorpse();
}
ImGui::PopStyleColor(2);
float corpDist = gameHandler.getCorpseDistance();
if (corpDist >= 0.0f) {
char distBuf[48];
snprintf(distBuf, sizeof(distBuf), "Corpse: %.0f yards away", corpDist);
float dw = ImGui::CalcTextSize(distBuf).x;
ImGui::SetCursorPosX((btnW + 16.0f - dw) * 0.5f);
ImGui::TextDisabled("%s", distBuf);
}
}
}
ImGui::End();
ImGui::PopStyleColor();
ImGui::PopStyleVar(2);
}
void WindowManager::renderMailWindow(game::GameHandler& gameHandler,
InventoryScreen& inventoryScreen,
ChatPanel& chatPanel) {
if (!gameHandler.isMailboxOpen()) return;
auto* window = services_.window;
float screenW = window ? static_cast<float>(window->getWidth()) : 1280.0f;
ImGui::SetNextWindowPos(ImVec2(screenW / 2 - 250, 80), ImGuiCond_Appearing);
ImGui::SetNextWindowSize(ImVec2(600, 500), ImGuiCond_Appearing);
bool open = true;
if (ImGui::Begin("Mailbox", &open)) {
const auto& inbox = gameHandler.getMailInbox();
// Top bar: money + compose button
ImGui::TextDisabled("Your money:"); ImGui::SameLine(0, 4);
renderCoinsFromCopper(gameHandler.getMoneyCopper());
ImGui::SameLine(ImGui::GetWindowWidth() - 100);
if (ImGui::Button("Compose")) {
mailRecipientBuffer_[0] = '\0';
mailSubjectBuffer_[0] = '\0';
mailBodyBuffer_[0] = '\0';
mailComposeMoney_[0] = 0;
mailComposeMoney_[1] = 0;
mailComposeMoney_[2] = 0;
gameHandler.openMailCompose();
}
ImGui::Separator();
if (inbox.empty()) {
ImGui::TextDisabled("No mail.");
} else {
// Two-panel layout: left = mail list, right = selected mail detail
float listWidth = 220.0f;
// Left panel - mail list
ImGui::BeginChild("MailList", ImVec2(listWidth, 0), true);
for (size_t i = 0; i < inbox.size(); ++i) {
const auto& mail = inbox[i];
ImGui::PushID(static_cast<int>(i));
bool selected = (gameHandler.getSelectedMailIndex() == static_cast<int>(i));
std::string label = mail.subject.empty() ? "(No Subject)" : mail.subject;
// Unread indicator
if (!mail.read) {
ImGui::PushStyleColor(ImGuiCol_Text, ImVec4(1.0f, 1.0f, 0.5f, 1.0f));
}
if (ImGui::Selectable(label.c_str(), selected)) {
gameHandler.setSelectedMailIndex(static_cast<int>(i));
// Mark as read
if (!mail.read) {
gameHandler.mailMarkAsRead(mail.messageId);
}
}
if (!mail.read) {
ImGui::PopStyleColor();
}
// Sub-info line
ImGui::TextColored(kColorGray, " From: %s", mail.senderName.c_str());
if (mail.money > 0) {
ImGui::SameLine();
ImGui::TextColored(colors::kWarmGold, " [G]");
}
if (!mail.attachments.empty()) {
ImGui::SameLine();
ImGui::TextColored(ImVec4(0.5f, 0.8f, 1.0f, 1.0f), " [A]");
}
// Expiry warning if within 3 days
if (mail.expirationTime > 0.0f) {
auto nowSec = static_cast<float>(std::time(nullptr));
float secsLeft = mail.expirationTime - nowSec;
if (secsLeft < 3.0f * 86400.0f && secsLeft > 0.0f) {
ImGui::SameLine();
int daysLeft = static_cast<int>(secsLeft / 86400.0f);
if (daysLeft == 0) {
ImGui::TextColored(colors::kBrightRed, " [expires today!]");
} else {
ImGui::TextColored(ImVec4(1.0f, 0.6f, 0.1f, 1.0f),
" [expires in %dd]", daysLeft);
}
}
}
ImGui::PopID();
}
ImGui::EndChild();
ImGui::SameLine();
// Right panel - selected mail detail
ImGui::BeginChild("MailDetail", ImVec2(0, 0), true);
int sel = gameHandler.getSelectedMailIndex();
if (sel >= 0 && sel < static_cast<int>(inbox.size())) {
const auto& mail = inbox[sel];
ImGui::TextColored(colors::kWarmGold, "%s",
mail.subject.empty() ? "(No Subject)" : mail.subject.c_str());
ImGui::Text("From: %s", mail.senderName.c_str());
if (mail.messageType == 2) {
ImGui::TextColored(ImVec4(0.8f, 0.6f, 0.2f, 1.0f), "[Auction House]");
}
// Show expiry date in the detail panel
if (mail.expirationTime > 0.0f) {
auto nowSec = static_cast<float>(std::time(nullptr));
float secsLeft = mail.expirationTime - nowSec;
// Format absolute expiry as a date using struct tm
time_t expT = static_cast<time_t>(mail.expirationTime);
struct tm* tmExp = std::localtime(&expT);
if (tmExp) {
const char* mname = kMonthAbbrev[tmExp->tm_mon];
int daysLeft = static_cast<int>(secsLeft / 86400.0f);
if (secsLeft <= 0.0f) {
ImGui::TextColored(kColorGray,
"Expired: %s %d, %d", mname, tmExp->tm_mday, 1900 + tmExp->tm_year);
} else if (secsLeft < 3.0f * 86400.0f) {
ImGui::TextColored(kColorRed,
"Expires: %s %d, %d (%d day%s!)",
mname, tmExp->tm_mday, 1900 + tmExp->tm_year,
daysLeft, daysLeft == 1 ? "" : "s");
} else {
ImGui::TextDisabled("Expires: %s %d, %d",
mname, tmExp->tm_mday, 1900 + tmExp->tm_year);
}
}
}
ImGui::Separator();
// Body text
if (!mail.body.empty()) {
ImGui::TextWrapped("%s", mail.body.c_str());
ImGui::Separator();
}
// Money
if (mail.money > 0) {
ImGui::TextDisabled("Money:"); ImGui::SameLine(0, 4);
renderCoinsFromCopper(mail.money);
ImGui::SameLine();
if (ImGui::SmallButton("Take Money")) {
gameHandler.mailTakeMoney(mail.messageId);
}
}
// COD warning
if (mail.cod > 0) {
uint64_t g = mail.cod / 10000;
uint64_t s = (mail.cod / 100) % 100;
uint64_t c = mail.cod % 100;
ImGui::TextColored(kColorRed,
"COD: %llug %llus %lluc (you pay this to take items)",
static_cast<unsigned long long>(g),
static_cast<unsigned long long>(s),
static_cast<unsigned long long>(c));
}
// Attachments
if (!mail.attachments.empty()) {
ImGui::Text("Attachments: %zu", mail.attachments.size());
ImDrawList* mailDraw = ImGui::GetWindowDrawList();
constexpr float MAIL_SLOT = 34.0f;
for (size_t j = 0; j < mail.attachments.size(); ++j) {
const auto& att = mail.attachments[j];
ImGui::PushID(static_cast<int>(j));
auto* info = gameHandler.getItemInfo(att.itemId);
game::ItemQuality quality = game::ItemQuality::COMMON;
std::string name = "Item " + std::to_string(att.itemId);
uint32_t displayInfoId = 0;
if (info && info->valid) {
quality = static_cast<game::ItemQuality>(info->quality);
name = info->name;
displayInfoId = info->displayInfoId;
} else {
gameHandler.ensureItemInfo(att.itemId);
}
ImVec4 qc = InventoryScreen::getQualityColor(quality);
ImU32 borderCol = ImGui::ColorConvertFloat4ToU32(qc);
ImVec2 pos = ImGui::GetCursorScreenPos();
VkDescriptorSet iconTex = displayInfoId
? inventoryScreen.getItemIcon(displayInfoId) : VK_NULL_HANDLE;
if (iconTex) {
mailDraw->AddImage((ImTextureID)(uintptr_t)iconTex, pos,
ImVec2(pos.x + MAIL_SLOT, pos.y + MAIL_SLOT));
mailDraw->AddRect(pos, ImVec2(pos.x + MAIL_SLOT, pos.y + MAIL_SLOT),
borderCol, 0.0f, 0, 1.5f);
} else {
mailDraw->AddRectFilled(pos,
ImVec2(pos.x + MAIL_SLOT, pos.y + MAIL_SLOT),
IM_COL32(40, 35, 30, 220));
mailDraw->AddRect(pos,
ImVec2(pos.x + MAIL_SLOT, pos.y + MAIL_SLOT),
borderCol, 0.0f, 0, 1.5f);
}
if (att.stackCount > 1) {
char cnt[16];
snprintf(cnt, sizeof(cnt), "%u", att.stackCount);
float cw = ImGui::CalcTextSize(cnt).x;
mailDraw->AddText(ImVec2(pos.x + 1.0f, pos.y + 1.0f),
IM_COL32(0, 0, 0, 200), cnt);
mailDraw->AddText(
ImVec2(pos.x + MAIL_SLOT - cw - 2.0f, pos.y + MAIL_SLOT - 14.0f),
IM_COL32(255, 255, 255, 220), cnt);
}
ImGui::InvisibleButton("##mailatt", ImVec2(MAIL_SLOT, MAIL_SLOT));
if (ImGui::IsItemHovered() && info && info->valid)
inventoryScreen.renderItemTooltip(*info);
if (ImGui::IsItemHovered() && ImGui::IsMouseClicked(ImGuiMouseButton_Left) &&
ImGui::GetIO().KeyShift && info && info->valid && !info->name.empty()) {
std::string link = buildItemChatLink(info->entry, info->quality, info->name);
chatPanel.insertChatLink(link);
}
ImGui::SameLine();
ImGui::TextColored(qc, "%s", name.c_str());
if (ImGui::IsItemHovered() && info && info->valid)
inventoryScreen.renderItemTooltip(*info);
if (ImGui::IsItemHovered() && ImGui::IsMouseClicked(ImGuiMouseButton_Left) &&
ImGui::GetIO().KeyShift && info && info->valid && !info->name.empty()) {
std::string link = buildItemChatLink(info->entry, info->quality, info->name);
chatPanel.insertChatLink(link);
}
ImGui::SameLine();
if (ImGui::SmallButton("Take")) {
gameHandler.mailTakeItem(mail.messageId, att.itemGuidLow);
}
ImGui::PopID();
}
// "Take All" button when there are multiple attachments
if (mail.attachments.size() > 1) {
if (ImGui::SmallButton("Take All")) {
for (const auto& att2 : mail.attachments) {
gameHandler.mailTakeItem(mail.messageId, att2.itemGuidLow);
}
}
}
}
ImGui::Spacing();
ImGui::Separator();
// Action buttons
if (ImGui::Button("Delete")) {
gameHandler.mailDelete(mail.messageId);
}
ImGui::SameLine();
if (mail.messageType == 0 && ImGui::Button("Reply")) {
// Pre-fill compose with sender as recipient
strncpy(mailRecipientBuffer_, mail.senderName.c_str(), sizeof(mailRecipientBuffer_) - 1);
mailRecipientBuffer_[sizeof(mailRecipientBuffer_) - 1] = '\0';
std::string reSubject = "Re: " + mail.subject;
strncpy(mailSubjectBuffer_, reSubject.c_str(), sizeof(mailSubjectBuffer_) - 1);
mailSubjectBuffer_[sizeof(mailSubjectBuffer_) - 1] = '\0';
mailBodyBuffer_[0] = '\0';
mailComposeMoney_[0] = 0;
mailComposeMoney_[1] = 0;
mailComposeMoney_[2] = 0;
gameHandler.openMailCompose();
}
} else {
ImGui::TextDisabled("Select a mail to read.");
}
ImGui::EndChild();
}
}
ImGui::End();
if (!open) {
gameHandler.closeMailbox();
}
}
void WindowManager::renderMailComposeWindow(game::GameHandler& gameHandler,
InventoryScreen& inventoryScreen) {
if (!gameHandler.isMailComposeOpen()) 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 / 2 - 190, screenH / 2 - 250), ImGuiCond_Appearing);
ImGui::SetNextWindowSize(ImVec2(400, 500), ImGuiCond_Appearing);
bool open = true;
if (ImGui::Begin("Send Mail", &open)) {
ImGui::Text("To:");
ImGui::SameLine(60);
ImGui::SetNextItemWidth(-1);
ImGui::InputText("##MailTo", mailRecipientBuffer_, sizeof(mailRecipientBuffer_));
ImGui::Text("Subject:");
ImGui::SameLine(60);
ImGui::SetNextItemWidth(-1);
ImGui::InputText("##MailSubject", mailSubjectBuffer_, sizeof(mailSubjectBuffer_));
ImGui::Text("Body:");
ImGui::InputTextMultiline("##MailBody", mailBodyBuffer_, sizeof(mailBodyBuffer_),
ImVec2(-1, 120));
// Attachments section
int attachCount = gameHandler.getMailAttachmentCount();
ImGui::Text("Attachments (%d/12):", attachCount);
ImGui::SameLine();
ImGui::TextColored(kColorGray, "Right-click items in bags to attach");
const auto& attachments = gameHandler.getMailAttachments();
// Show attachment slots in a grid (6 per row)
for (int i = 0; i < game::GameHandler::MAIL_MAX_ATTACHMENTS; ++i) {
if (i % 6 != 0) ImGui::SameLine();
ImGui::PushID(i + 5000);
const auto& att = attachments[i];
if (att.occupied()) {
// Show item with quality color border
ImVec4 qualColor = ui::InventoryScreen::getQualityColor(att.item.quality);
ImGui::PushStyleColor(ImGuiCol_Button, ImVec4(qualColor.x * 0.3f, qualColor.y * 0.3f, qualColor.z * 0.3f, 0.8f));
ImGui::PushStyleColor(ImGuiCol_ButtonHovered, ImVec4(qualColor.x * 0.5f, qualColor.y * 0.5f, qualColor.z * 0.5f, 0.9f));
// Try to show icon
VkDescriptorSet icon = inventoryScreen.getItemIcon(att.item.displayInfoId);
bool clicked = false;
if (icon) {
clicked = ImGui::ImageButton("##att", (ImTextureID)icon, ImVec2(30, 30));
} else {
// Truncate name to fit
std::string label = att.item.name.substr(0, 4);
clicked = ImGui::Button(label.c_str(), ImVec2(36, 36));
}
ImGui::PopStyleColor(2);
if (clicked) {
gameHandler.detachMailAttachment(i);
}
if (ImGui::IsItemHovered()) {
ImGui::BeginTooltip();
ImGui::TextColored(qualColor, "%s", att.item.name.c_str());
ImGui::TextColored(ui::colors::kLightGray, "Click to remove");
ImGui::EndTooltip();
}
} else {
ImGui::PushStyleColor(ImGuiCol_Button, ImVec4(0.15f, 0.15f, 0.15f, 0.5f));
ImGui::Button("##empty", ImVec2(36, 36));
ImGui::PopStyleColor();
}
ImGui::PopID();
}
ImGui::Spacing();
ImGui::Text("Money:");
ImGui::SameLine(60);
ImGui::SetNextItemWidth(60);
ImGui::InputInt("##MailGold", &mailComposeMoney_[0], 0, 0);
if (mailComposeMoney_[0] < 0) mailComposeMoney_[0] = 0;
ImGui::SameLine();
ImGui::Text("g");
ImGui::SameLine();
ImGui::SetNextItemWidth(40);
ImGui::InputInt("##MailSilver", &mailComposeMoney_[1], 0, 0);
if (mailComposeMoney_[1] < 0) mailComposeMoney_[1] = 0;
if (mailComposeMoney_[1] > 99) mailComposeMoney_[1] = 99;
ImGui::SameLine();
ImGui::Text("s");
ImGui::SameLine();
ImGui::SetNextItemWidth(40);
ImGui::InputInt("##MailCopper", &mailComposeMoney_[2], 0, 0);
if (mailComposeMoney_[2] < 0) mailComposeMoney_[2] = 0;
if (mailComposeMoney_[2] > 99) mailComposeMoney_[2] = 99;
ImGui::SameLine();
ImGui::Text("c");
uint64_t totalMoney = static_cast<uint64_t>(mailComposeMoney_[0]) * 10000 +
static_cast<uint64_t>(mailComposeMoney_[1]) * 100 +
static_cast<uint64_t>(mailComposeMoney_[2]);
uint32_t sendCost = attachCount > 0 ? static_cast<uint32_t>(30 * attachCount) : 30u;
ImGui::TextColored(kColorGray, "Sending cost: %uc", sendCost);
ImGui::Spacing();
bool canSend = (strlen(mailRecipientBuffer_) > 0);
if (!canSend) ImGui::BeginDisabled();
if (ImGui::Button("Send", ImVec2(80, 0))) {
gameHandler.sendMail(mailRecipientBuffer_, mailSubjectBuffer_,
mailBodyBuffer_, totalMoney);
}
if (!canSend) ImGui::EndDisabled();
ImGui::SameLine();
if (ImGui::Button("Cancel", ImVec2(80, 0))) {
gameHandler.closeMailCompose();
}
}
ImGui::End();
if (!open) {
gameHandler.closeMailCompose();
}
}
void WindowManager::renderBankWindow(game::GameHandler& gameHandler,
InventoryScreen& inventoryScreen,
ChatPanel& chatPanel) {
if (!gameHandler.isBankOpen()) return;
bool open = true;
ImGui::SetNextWindowSize(ImVec2(480, 420), ImGuiCond_FirstUseEver);
if (!ImGui::Begin("Bank", &open)) {
ImGui::End();
if (!open) gameHandler.closeBank();
return;
}
auto& inv = gameHandler.getInventory();
bool isHolding = inventoryScreen.isHoldingItem();
constexpr float SLOT_SIZE = 42.0f;
static constexpr float kBankPickupHold = 0.10f; // seconds
// Persistent pickup tracking for bank (mirrors inventory_screen's pickupPending_)
static bool bankPickupPending = false;
static float bankPickupPressTime = 0.0f;
static int bankPickupType = 0; // 0=main bank, 1=bank bag slot, 2=bank bag equip slot
static int bankPickupIndex = -1;
static int bankPickupBagIndex = -1;
static int bankPickupBagSlotIndex = -1;
// Helper: render a bank item slot with icon, click-and-hold pickup, drop, tooltip
auto renderBankItemSlot = [&](const game::ItemSlot& slot, int pickType, int mainIdx,
int bagIdx, int bagSlotIdx, uint8_t dstBag, uint8_t dstSlot) {
ImDrawList* drawList = ImGui::GetWindowDrawList();
ImVec2 pos = ImGui::GetCursorScreenPos();
if (slot.empty()) {
ImU32 bgCol = IM_COL32(30, 30, 30, 200);
ImU32 borderCol = IM_COL32(60, 60, 60, 200);
if (isHolding) {
bgCol = IM_COL32(20, 50, 20, 200);
borderCol = IM_COL32(0, 180, 0, 200);
}
drawList->AddRectFilled(pos, ImVec2(pos.x + SLOT_SIZE, pos.y + SLOT_SIZE), bgCol);
drawList->AddRect(pos, ImVec2(pos.x + SLOT_SIZE, pos.y + SLOT_SIZE), borderCol);
ImGui::InvisibleButton("slot", ImVec2(SLOT_SIZE, SLOT_SIZE));
if (isHolding && ImGui::IsItemHovered() && ImGui::IsMouseReleased(ImGuiMouseButton_Left)) {
inventoryScreen.dropIntoBankSlot(gameHandler, dstBag, dstSlot);
}
} else {
const auto& item = slot.item;
ImVec4 qc = InventoryScreen::getQualityColor(item.quality);
ImU32 borderCol = ImGui::ColorConvertFloat4ToU32(qc);
VkDescriptorSet iconTex = inventoryScreen.getItemIcon(item.displayInfoId);
if (iconTex) {
drawList->AddImage((ImTextureID)(uintptr_t)iconTex, pos,
ImVec2(pos.x + SLOT_SIZE, pos.y + SLOT_SIZE));
drawList->AddRect(pos, ImVec2(pos.x + SLOT_SIZE, pos.y + SLOT_SIZE),
borderCol, 0.0f, 0, 2.0f);
} else {
ImU32 bgCol = IM_COL32(40, 35, 30, 220);
drawList->AddRectFilled(pos, ImVec2(pos.x + SLOT_SIZE, pos.y + SLOT_SIZE), bgCol);
drawList->AddRect(pos, ImVec2(pos.x + SLOT_SIZE, pos.y + SLOT_SIZE),
borderCol, 0.0f, 0, 2.0f);
if (!item.name.empty()) {
char abbr[3] = { item.name[0], item.name.size() > 1 ? item.name[1] : '\0', '\0' };
float tw = ImGui::CalcTextSize(abbr).x;
drawList->AddText(ImVec2(pos.x + (SLOT_SIZE - tw) * 0.5f, pos.y + 2.0f),
ImGui::ColorConvertFloat4ToU32(qc), abbr);
}
}
if (item.stackCount > 1) {
char countStr[16];
snprintf(countStr, sizeof(countStr), "%u", item.stackCount);
float cw = ImGui::CalcTextSize(countStr).x;
drawList->AddText(ImVec2(pos.x + SLOT_SIZE - cw - 2.0f, pos.y + SLOT_SIZE - 14.0f),
IM_COL32(255, 255, 255, 220), countStr);
}
ImGui::InvisibleButton("slot", ImVec2(SLOT_SIZE, SLOT_SIZE));
if (!isHolding) {
// Start pickup tracking on mouse press
if (ImGui::IsItemClicked(ImGuiMouseButton_Left)) {
bankPickupPending = true;
bankPickupPressTime = ImGui::GetTime();
bankPickupType = pickType;
bankPickupIndex = mainIdx;
bankPickupBagIndex = bagIdx;
bankPickupBagSlotIndex = bagSlotIdx;
}
// Check if held long enough to pick up
if (bankPickupPending && ImGui::IsMouseDown(ImGuiMouseButton_Left) &&
(ImGui::GetTime() - bankPickupPressTime) >= kBankPickupHold) {
bool sameSlot = (bankPickupType == pickType);
if (pickType == 0)
sameSlot = sameSlot && (bankPickupIndex == mainIdx);
else if (pickType == 1)
sameSlot = sameSlot && (bankPickupBagIndex == bagIdx) && (bankPickupBagSlotIndex == bagSlotIdx);
else if (pickType == 2)
sameSlot = sameSlot && (bankPickupIndex == mainIdx);
if (sameSlot && ImGui::IsItemHovered()) {
bankPickupPending = false;
if (pickType == 0) {
inventoryScreen.pickupFromBank(inv, mainIdx);
} else if (pickType == 1) {
inventoryScreen.pickupFromBankBag(inv, bagIdx, bagSlotIdx);
} else if (pickType == 2) {
inventoryScreen.pickupFromBankBagEquip(inv, mainIdx);
}
}
}
} else {
// Drop/swap on mouse release
if (ImGui::IsItemHovered() && ImGui::IsMouseReleased(ImGuiMouseButton_Left)) {
inventoryScreen.dropIntoBankSlot(gameHandler, dstBag, dstSlot);
}
}
// Tooltip
if (ImGui::IsItemHovered() && !isHolding) {
auto* info = gameHandler.getItemInfo(item.itemId);
if (info && info->valid)
inventoryScreen.renderItemTooltip(*info);
else {
ImGui::BeginTooltip();
ImGui::TextColored(qc, "%s", item.name.c_str());
ImGui::EndTooltip();
}
// Shift-click to insert item link into chat
if (ImGui::IsMouseClicked(ImGuiMouseButton_Left) && ImGui::GetIO().KeyShift
&& !item.name.empty()) {
auto* info2 = gameHandler.getItemInfo(item.itemId);
uint8_t q = (info2 && info2->valid)
? static_cast<uint8_t>(info2->quality)
: static_cast<uint8_t>(item.quality);
const std::string& lname = (info2 && info2->valid && !info2->name.empty())
? info2->name : item.name;
std::string link = buildItemChatLink(item.itemId, q, lname);
chatPanel.insertChatLink(link);
}
}
}
};
// Main bank slots (24 for Classic, 28 for TBC/WotLK)
int bankSlotCount = gameHandler.getEffectiveBankSlots();
int bankBagCount = gameHandler.getEffectiveBankBagSlots();
ImGui::Text("Bank Slots");
ImGui::Separator();
for (int i = 0; i < bankSlotCount; i++) {
if (i % 7 != 0) ImGui::SameLine();
ImGui::PushID(i + 1000);
renderBankItemSlot(inv.getBankSlot(i), 0, i, -1, -1, 0xFF, static_cast<uint8_t>(39 + i));
ImGui::PopID();
}
// Bank bag equip slots — show bag icon with pickup/drop, or "Buy Slot"
ImGui::Spacing();
ImGui::Separator();
ImGui::Text("Bank Bags");
uint8_t purchased = inv.getPurchasedBankBagSlots();
for (int i = 0; i < bankBagCount; i++) {
if (i > 0) ImGui::SameLine();
ImGui::PushID(i + 2000);
int bagSize = inv.getBankBagSize(i);
if (i < purchased || bagSize > 0) {
const auto& bagSlot = inv.getBankBagItem(i);
// Render as an item slot: icon with pickup/drop (pickType=2 for bag equip)
renderBankItemSlot(bagSlot, 2, i, -1, -1, 0xFF, static_cast<uint8_t>(67 + i));
} else {
if (ImGui::Button("Buy Slot", ImVec2(50, 30))) {
gameHandler.buyBankSlot();
}
}
ImGui::PopID();
}
// Show expanded bank bag contents
for (int bagIdx = 0; bagIdx < bankBagCount; bagIdx++) {
int bagSize = inv.getBankBagSize(bagIdx);
if (bagSize <= 0) continue;
ImGui::Spacing();
ImGui::Text("Bank Bag %d (%d slots)", bagIdx + 1, bagSize);
for (int s = 0; s < bagSize; s++) {
if (s % 7 != 0) ImGui::SameLine();
ImGui::PushID(3000 + bagIdx * 100 + s);
renderBankItemSlot(inv.getBankBagSlot(bagIdx, s), 1, -1, bagIdx, s,
static_cast<uint8_t>(67 + bagIdx), static_cast<uint8_t>(s));
ImGui::PopID();
}
}
ImGui::End();
if (!open) gameHandler.closeBank();
}
void WindowManager::renderGuildBankWindow(game::GameHandler& gameHandler,
InventoryScreen& inventoryScreen,
ChatPanel& chatPanel) {
if (!gameHandler.isGuildBankOpen()) return;
bool open = true;
ImGui::SetNextWindowSize(ImVec2(520, 500), ImGuiCond_FirstUseEver);
if (!ImGui::Begin("Guild Bank", &open)) {
ImGui::End();
if (!open) gameHandler.closeGuildBank();
return;
}
const auto& data = gameHandler.getGuildBankData();
uint8_t activeTab = gameHandler.getGuildBankActiveTab();
// Money display
uint32_t gold = static_cast<uint32_t>(data.money / 10000);
uint32_t silver = static_cast<uint32_t>((data.money / 100) % 100);
uint32_t copper = static_cast<uint32_t>(data.money % 100);
ImGui::TextDisabled("Guild Bank Money:"); ImGui::SameLine(0, 4);
renderCoinsText(gold, silver, copper);
// Tab bar
if (!data.tabs.empty()) {
for (size_t i = 0; i < data.tabs.size(); i++) {
if (i > 0) ImGui::SameLine();
bool selected = (i == activeTab);
if (selected) ImGui::PushStyleColor(ImGuiCol_Button, ImVec4(0.3f, 0.5f, 0.8f, 1.0f));
std::string tabLabel = data.tabs[i].tabName.empty() ? ("Tab " + std::to_string(i + 1)) : data.tabs[i].tabName;
if (ImGui::Button(tabLabel.c_str())) {
gameHandler.queryGuildBankTab(static_cast<uint8_t>(i));
}
if (selected) ImGui::PopStyleColor();
}
}
// Buy tab button
if (data.tabs.size() < 6) {
ImGui::SameLine();
if (ImGui::Button("Buy Tab")) {
gameHandler.buyGuildBankTab();
}
}
ImGui::Separator();
// Tab items (98 slots = 14 columns × 7 rows)
constexpr float GB_SLOT = 34.0f;
ImDrawList* gbDraw = ImGui::GetWindowDrawList();
for (size_t i = 0; i < data.tabItems.size(); i++) {
if (i % 14 != 0) ImGui::SameLine(0.0f, 2.0f);
const auto& item = data.tabItems[i];
ImGui::PushID(static_cast<int>(i) + 5000);
ImVec2 pos = ImGui::GetCursorScreenPos();
if (item.itemEntry == 0) {
gbDraw->AddRectFilled(pos, ImVec2(pos.x + GB_SLOT, pos.y + GB_SLOT),
IM_COL32(30, 30, 30, 200));
gbDraw->AddRect(pos, ImVec2(pos.x + GB_SLOT, pos.y + GB_SLOT),
IM_COL32(60, 60, 60, 180));
ImGui::InvisibleButton("##gbempty", ImVec2(GB_SLOT, GB_SLOT));
} else {
auto* info = gameHandler.getItemInfo(item.itemEntry);
game::ItemQuality quality = game::ItemQuality::COMMON;
std::string name = "Item " + std::to_string(item.itemEntry);
uint32_t displayInfoId = 0;
if (info) {
quality = static_cast<game::ItemQuality>(info->quality);
name = info->name;
displayInfoId = info->displayInfoId;
}
ImVec4 qc = InventoryScreen::getQualityColor(quality);
ImU32 borderCol = ImGui::ColorConvertFloat4ToU32(qc);
VkDescriptorSet iconTex = displayInfoId ? inventoryScreen.getItemIcon(displayInfoId) : VK_NULL_HANDLE;
if (iconTex) {
gbDraw->AddImage((ImTextureID)(uintptr_t)iconTex, pos,
ImVec2(pos.x + GB_SLOT, pos.y + GB_SLOT));
gbDraw->AddRect(pos, ImVec2(pos.x + GB_SLOT, pos.y + GB_SLOT),
borderCol, 0.0f, 0, 1.5f);
} else {
gbDraw->AddRectFilled(pos, ImVec2(pos.x + GB_SLOT, pos.y + GB_SLOT),
IM_COL32(40, 35, 30, 220));
gbDraw->AddRect(pos, ImVec2(pos.x + GB_SLOT, pos.y + GB_SLOT),
borderCol, 0.0f, 0, 1.5f);
if (!name.empty() && name[0] != 'I') {
char abbr[3] = { name[0], name.size() > 1 ? name[1] : '\0', '\0' };
float tw = ImGui::CalcTextSize(abbr).x;
gbDraw->AddText(ImVec2(pos.x + (GB_SLOT - tw) * 0.5f, pos.y + 2.0f),
borderCol, abbr);
}
}
if (item.stackCount > 1) {
char cnt[16];
snprintf(cnt, sizeof(cnt), "%u", item.stackCount);
float cw = ImGui::CalcTextSize(cnt).x;
gbDraw->AddText(ImVec2(pos.x + 1.0f, pos.y + 1.0f), IM_COL32(0, 0, 0, 200), cnt);
gbDraw->AddText(ImVec2(pos.x + GB_SLOT - cw - 2.0f, pos.y + GB_SLOT - 14.0f),
IM_COL32(255, 255, 255, 220), cnt);
}
ImGui::InvisibleButton("##gbslot", ImVec2(GB_SLOT, GB_SLOT));
if (ImGui::IsItemClicked(ImGuiMouseButton_Left) && !ImGui::GetIO().KeyShift) {
gameHandler.guildBankWithdrawItem(activeTab, item.slotId, 0xFF, 0);
}
if (ImGui::IsItemHovered()) {
if (info && info->valid)
inventoryScreen.renderItemTooltip(*info);
// Shift-click to insert item link into chat
if (ImGui::IsMouseClicked(ImGuiMouseButton_Left) && ImGui::GetIO().KeyShift
&& !name.empty() && item.itemEntry != 0) {
uint8_t q = static_cast<uint8_t>(quality);
std::string link = buildItemChatLink(item.itemEntry, q, name);
chatPanel.insertChatLink(link);
}
}
}
ImGui::PopID();
}
// Money deposit/withdraw
ImGui::Separator();
ImGui::Text("Money:");
ImGui::SameLine();
ImGui::SetNextItemWidth(60);
ImGui::InputInt("##gbg", &guildBankMoneyInput_[0], 0); ImGui::SameLine(); ImGui::Text("g");
ImGui::SameLine();
ImGui::SetNextItemWidth(40);
ImGui::InputInt("##gbs", &guildBankMoneyInput_[1], 0); ImGui::SameLine(); ImGui::Text("s");
ImGui::SameLine();
ImGui::SetNextItemWidth(40);
ImGui::InputInt("##gbc", &guildBankMoneyInput_[2], 0); ImGui::SameLine(); ImGui::Text("c");
ImGui::SameLine();
if (ImGui::Button("Deposit")) {
uint32_t amount = guildBankMoneyInput_[0] * 10000 + guildBankMoneyInput_[1] * 100 + guildBankMoneyInput_[2];
if (amount > 0) gameHandler.depositGuildBankMoney(amount);
}
ImGui::SameLine();
if (ImGui::Button("Withdraw")) {
uint32_t amount = guildBankMoneyInput_[0] * 10000 + guildBankMoneyInput_[1] * 100 + guildBankMoneyInput_[2];
if (amount > 0) gameHandler.withdrawGuildBankMoney(amount);
}
if (data.withdrawAmount >= 0) {
ImGui::Text("Remaining withdrawals: %d", data.withdrawAmount);
}
ImGui::End();
if (!open) gameHandler.closeGuildBank();
}
void WindowManager::renderAuctionHouseWindow(game::GameHandler& gameHandler,
InventoryScreen& inventoryScreen,
ChatPanel& chatPanel) {
if (!gameHandler.isAuctionHouseOpen()) return;
bool open = true;
ImGui::SetNextWindowSize(ImVec2(650, 500), ImGuiCond_FirstUseEver);
if (!ImGui::Begin("Auction House", &open)) {
ImGui::End();
if (!open) gameHandler.closeAuctionHouse();
return;
}
int tab = gameHandler.getAuctionActiveTab();
// Tab buttons
const char* tabNames[] = {"Browse", "Bids", "Auctions"};
for (int i = 0; i < 3; i++) {
if (i > 0) ImGui::SameLine();
bool selected = (tab == i);
if (selected) ImGui::PushStyleColor(ImGuiCol_Button, ImVec4(0.3f, 0.5f, 0.8f, 1.0f));
if (ImGui::Button(tabNames[i], ImVec2(100, 0))) {
gameHandler.setAuctionActiveTab(i);
if (i == 1) gameHandler.auctionListBidderItems();
else if (i == 2) gameHandler.auctionListOwnerItems();
}
if (selected) ImGui::PopStyleColor();
}
ImGui::Separator();
if (tab == 0) {
// Browse tab - Search filters
// --- Helper: resolve current UI filter state into wire-format search params ---
// WoW 3.3.5a item class IDs:
// 0=Consumable, 1=Container, 2=Weapon, 3=Gem, 4=Armor,
// 7=Projectile/TradeGoods, 9=Recipe, 11=Quiver, 15=Miscellaneous
struct AHClassMapping { const char* label; uint32_t classId; };
static const AHClassMapping classMappings[] = {
{"All", 0xFFFFFFFF},
{"Weapon", 2},
{"Armor", 4},
{"Container", 1},
{"Consumable", 0},
{"Trade Goods", 7},
{"Gem", 3},
{"Recipe", 9},
{"Quiver", 11},
{"Miscellaneous", 15},
};
static constexpr int NUM_CLASSES = 10;
// Weapon subclass IDs (WoW 3.3.5a)
struct AHSubMapping { const char* label; uint32_t subId; };
static const AHSubMapping weaponSubs[] = {
{"All", 0xFFFFFFFF}, {"Axe (1H)", 0}, {"Axe (2H)", 1}, {"Bow", 2},
{"Gun", 3}, {"Mace (1H)", 4}, {"Mace (2H)", 5}, {"Polearm", 6},
{"Sword (1H)", 7}, {"Sword (2H)", 8}, {"Staff", 10},
{"Fist Weapon", 13}, {"Dagger", 15}, {"Thrown", 16},
{"Crossbow", 18}, {"Wand", 19},
};
static constexpr int NUM_WEAPON_SUBS = 16;
// Armor subclass IDs
static const AHSubMapping armorSubs[] = {
{"All", 0xFFFFFFFF}, {"Cloth", 1}, {"Leather", 2}, {"Mail", 3},
{"Plate", 4}, {"Shield", 6}, {"Miscellaneous", 0},
};
static constexpr int NUM_ARMOR_SUBS = 7;
auto getSearchClassId = [&]() -> uint32_t {
if (auctionItemClass_ < 0 || auctionItemClass_ >= NUM_CLASSES) return 0xFFFFFFFF;
return classMappings[auctionItemClass_].classId;
};
auto getSearchSubClassId = [&]() -> uint32_t {
if (auctionItemSubClass_ < 0) return 0xFFFFFFFF;
uint32_t cid = getSearchClassId();
if (cid == 2 && auctionItemSubClass_ < NUM_WEAPON_SUBS)
return weaponSubs[auctionItemSubClass_].subId;
if (cid == 4 && auctionItemSubClass_ < NUM_ARMOR_SUBS)
return armorSubs[auctionItemSubClass_].subId;
return 0xFFFFFFFF;
};
auto doSearch = [&](uint32_t offset) {
auctionBrowseOffset_ = offset;
if (auctionLevelMin_ < 0) auctionLevelMin_ = 0;
if (auctionLevelMax_ < 0) auctionLevelMax_ = 0;
uint32_t q = auctionQuality_ > 0 ? static_cast<uint32_t>(auctionQuality_ - 1) : 0xFFFFFFFF;
gameHandler.auctionSearch(auctionSearchName_,
static_cast<uint8_t>(auctionLevelMin_),
static_cast<uint8_t>(auctionLevelMax_),
q, getSearchClassId(), getSearchSubClassId(), 0,
auctionUsableOnly_ ? 1 : 0, offset);
};
// Row 1: Name + Level range
ImGui::SetNextItemWidth(200);
bool enterPressed = ImGui::InputText("Name", auctionSearchName_, sizeof(auctionSearchName_),
ImGuiInputTextFlags_EnterReturnsTrue);
ImGui::SameLine();
ImGui::SetNextItemWidth(50);
ImGui::InputInt("Min Lv", &auctionLevelMin_, 0);
ImGui::SameLine();
ImGui::SetNextItemWidth(50);
ImGui::InputInt("Max Lv", &auctionLevelMax_, 0);
// Row 2: Quality + Category + Subcategory + Search button
const char* qualities[] = {"All", "Poor", "Common", "Uncommon", "Rare", "Epic", "Legendary"};
ImGui::SetNextItemWidth(100);
ImGui::Combo("Quality", &auctionQuality_, qualities, 7);
ImGui::SameLine();
// Build class label list from mappings
const char* classLabels[NUM_CLASSES];
for (int c = 0; c < NUM_CLASSES; c++) classLabels[c] = classMappings[c].label;
ImGui::SetNextItemWidth(120);
int classIdx = auctionItemClass_ < 0 ? 0 : auctionItemClass_;
if (ImGui::Combo("Category", &classIdx, classLabels, NUM_CLASSES)) {
if (classIdx != auctionItemClass_) auctionItemSubClass_ = -1;
auctionItemClass_ = classIdx;
}
// Subcategory (only for Weapon and Armor)
uint32_t curClassId = getSearchClassId();
if (curClassId == 2 || curClassId == 4) {
const AHSubMapping* subs = (curClassId == 2) ? weaponSubs : armorSubs;
int numSubs = (curClassId == 2) ? NUM_WEAPON_SUBS : NUM_ARMOR_SUBS;
const char* subLabels[20];
for (int s = 0; s < numSubs && s < 20; s++) subLabels[s] = subs[s].label;
int subIdx = auctionItemSubClass_ + 1; // -1 → 0 ("All")
if (subIdx < 0 || subIdx >= numSubs) subIdx = 0;
ImGui::SameLine();
ImGui::SetNextItemWidth(110);
if (ImGui::Combo("Subcat", &subIdx, subLabels, numSubs)) {
auctionItemSubClass_ = subIdx - 1; // 0 → -1 ("All")
}
}
ImGui::SameLine();
ImGui::Checkbox("Usable", &auctionUsableOnly_);
ImGui::SameLine();
float delay = gameHandler.getAuctionSearchDelay();
if (delay > 0.0f) {
char delayBuf[32];
snprintf(delayBuf, sizeof(delayBuf), "Search (%.0fs)", delay);
ImGui::BeginDisabled();
ImGui::Button(delayBuf);
ImGui::EndDisabled();
} else {
if (ImGui::Button("Search") || enterPressed) {
doSearch(0);
}
}
ImGui::Separator();
// Results table
const auto& results = gameHandler.getAuctionBrowseResults();
constexpr uint32_t AH_PAGE_SIZE = 50;
ImGui::Text("%zu results (of %u total)", results.auctions.size(), results.totalCount);
// Pagination
if (results.totalCount > AH_PAGE_SIZE) {
ImGui::SameLine();
uint32_t page = auctionBrowseOffset_ / AH_PAGE_SIZE + 1;
uint32_t totalPages = (results.totalCount + AH_PAGE_SIZE - 1) / AH_PAGE_SIZE;
if (auctionBrowseOffset_ == 0) ImGui::BeginDisabled();
if (ImGui::SmallButton("< Prev")) {
uint32_t newOff = (auctionBrowseOffset_ >= AH_PAGE_SIZE) ? auctionBrowseOffset_ - AH_PAGE_SIZE : 0;
doSearch(newOff);
}
if (auctionBrowseOffset_ == 0) ImGui::EndDisabled();
ImGui::SameLine();
ImGui::Text("Page %u/%u", page, totalPages);
ImGui::SameLine();
if (auctionBrowseOffset_ + AH_PAGE_SIZE >= results.totalCount) ImGui::BeginDisabled();
if (ImGui::SmallButton("Next >")) {
doSearch(auctionBrowseOffset_ + AH_PAGE_SIZE);
}
if (auctionBrowseOffset_ + AH_PAGE_SIZE >= results.totalCount) ImGui::EndDisabled();
}
if (ImGui::BeginChild("AuctionResults", ImVec2(0, -110), true)) {
if (ImGui::BeginTable("AuctionTable", 6, ImGuiTableFlags_Borders | ImGuiTableFlags_RowBg | ImGuiTableFlags_ScrollY)) {
ImGui::TableSetupColumn("Item", ImGuiTableColumnFlags_WidthStretch);
ImGui::TableSetupColumn("Qty", ImGuiTableColumnFlags_WidthFixed, 40);
ImGui::TableSetupColumn("Time", ImGuiTableColumnFlags_WidthFixed, 60);
ImGui::TableSetupColumn("Bid", ImGuiTableColumnFlags_WidthFixed, 90);
ImGui::TableSetupColumn("Buyout", ImGuiTableColumnFlags_WidthFixed, 90);
ImGui::TableSetupColumn("##act", ImGuiTableColumnFlags_WidthFixed, 60);
ImGui::TableHeadersRow();
for (size_t i = 0; i < results.auctions.size(); i++) {
const auto& auction = results.auctions[i];
auto* info = gameHandler.getItemInfo(auction.itemEntry);
std::string name = info ? info->name : ("Item #" + std::to_string(auction.itemEntry));
// Append random suffix name (e.g., "of the Eagle") if present
if (auction.randomPropertyId != 0) {
std::string suffix = gameHandler.getRandomPropertyName(
static_cast<int32_t>(auction.randomPropertyId));
if (!suffix.empty()) name += " " + suffix;
}
game::ItemQuality quality = info ? static_cast<game::ItemQuality>(info->quality) : game::ItemQuality::COMMON;
ImVec4 qc = InventoryScreen::getQualityColor(quality);
ImGui::TableNextRow();
ImGui::TableSetColumnIndex(0);
// Item icon
if (info && info->valid && info->displayInfoId != 0) {
VkDescriptorSet iconTex = inventoryScreen.getItemIcon(info->displayInfoId);
if (iconTex) {
ImGui::Image((void*)(intptr_t)iconTex, ImVec2(16, 16));
ImGui::SameLine();
}
}
ImGui::TextColored(qc, "%s", name.c_str());
// Item tooltip on hover; shift-click to insert chat link
if (ImGui::IsItemHovered() && info && info->valid) {
inventoryScreen.renderItemTooltip(*info);
}
if (ImGui::IsItemHovered() && ImGui::IsMouseClicked(ImGuiMouseButton_Left) &&
ImGui::GetIO().KeyShift && info && info->valid && !info->name.empty()) {
std::string link = buildItemChatLink(info->entry, info->quality, info->name);
chatPanel.insertChatLink(link);
}
ImGui::TableSetColumnIndex(1);
ImGui::Text("%u", auction.stackCount);
ImGui::TableSetColumnIndex(2);
// Time left display
uint32_t mins = auction.timeLeftMs / 60000;
if (mins > 720) ImGui::Text("Long");
else if (mins > 120) ImGui::Text("Medium");
else ImGui::TextColored(ImVec4(1, 0.3f, 0.3f, 1), "Short");
ImGui::TableSetColumnIndex(3);
{
uint32_t bid = auction.currentBid > 0 ? auction.currentBid : auction.startBid;
renderCoinsFromCopper(bid);
}
ImGui::TableSetColumnIndex(4);
if (auction.buyoutPrice > 0) {
renderCoinsFromCopper(auction.buyoutPrice);
} else {
ImGui::TextDisabled("--");
}
ImGui::TableSetColumnIndex(5);
ImGui::PushID(static_cast<int>(i) + 7000);
if (auction.buyoutPrice > 0 && ImGui::SmallButton("Buy")) {
gameHandler.auctionBuyout(auction.auctionId, auction.buyoutPrice);
}
if (auction.buyoutPrice > 0) ImGui::SameLine();
if (ImGui::SmallButton("Bid")) {
uint32_t bidAmt = auction.currentBid > 0
? auction.currentBid + auction.minBidIncrement
: auction.startBid;
gameHandler.auctionPlaceBid(auction.auctionId, bidAmt);
}
ImGui::PopID();
}
ImGui::EndTable();
}
}
ImGui::EndChild();
// Sell section
ImGui::Separator();
ImGui::Text("Sell Item:");
// Item picker from backpack
{
auto& inv = gameHandler.getInventory();
// Build list of non-empty backpack slots
std::string preview = (auctionSellSlotIndex_ >= 0)
? ([&]() -> std::string {
const auto& slot = inv.getBackpackSlot(auctionSellSlotIndex_);
if (!slot.empty()) {
std::string s = slot.item.name;
if (slot.item.stackCount > 1) s += " x" + std::to_string(slot.item.stackCount);
return s;
}
return "Select item...";
})()
: "Select item...";
ImGui::SetNextItemWidth(250);
if (ImGui::BeginCombo("##sellitem", preview.c_str())) {
for (int i = 0; i < game::Inventory::BACKPACK_SLOTS; i++) {
const auto& slot = inv.getBackpackSlot(i);
if (slot.empty()) continue;
ImGui::PushID(i + 9000);
// Item icon
if (slot.item.displayInfoId != 0) {
VkDescriptorSet sIcon = inventoryScreen.getItemIcon(slot.item.displayInfoId);
if (sIcon) {
ImGui::Image((void*)(intptr_t)sIcon, ImVec2(16, 16));
ImGui::SameLine();
}
}
std::string label = slot.item.name;
if (slot.item.stackCount > 1) label += " x" + std::to_string(slot.item.stackCount);
ImVec4 iqc = InventoryScreen::getQualityColor(slot.item.quality);
ImGui::PushStyleColor(ImGuiCol_Text, iqc);
if (ImGui::Selectable(label.c_str(), auctionSellSlotIndex_ == i)) {
auctionSellSlotIndex_ = i;
}
ImGui::PopStyleColor();
ImGui::PopID();
}
ImGui::EndCombo();
}
}
ImGui::Text("Bid:");
ImGui::SameLine();
ImGui::SetNextItemWidth(50);
ImGui::InputInt("##sbg", &auctionSellBid_[0], 0); ImGui::SameLine(); ImGui::Text("g");
ImGui::SameLine();
ImGui::SetNextItemWidth(35);
ImGui::InputInt("##sbs", &auctionSellBid_[1], 0); ImGui::SameLine(); ImGui::Text("s");
ImGui::SameLine();
ImGui::SetNextItemWidth(35);
ImGui::InputInt("##sbc", &auctionSellBid_[2], 0); ImGui::SameLine(); ImGui::Text("c");
ImGui::SameLine(0, 20);
ImGui::Text("Buyout:");
ImGui::SameLine();
ImGui::SetNextItemWidth(50);
ImGui::InputInt("##sbog", &auctionSellBuyout_[0], 0); ImGui::SameLine(); ImGui::Text("g");
ImGui::SameLine();
ImGui::SetNextItemWidth(35);
ImGui::InputInt("##sbos", &auctionSellBuyout_[1], 0); ImGui::SameLine(); ImGui::Text("s");
ImGui::SameLine();
ImGui::SetNextItemWidth(35);
ImGui::InputInt("##sboc", &auctionSellBuyout_[2], 0); ImGui::SameLine(); ImGui::Text("c");
const char* durations[] = {"12 hours", "24 hours", "48 hours"};
ImGui::SetNextItemWidth(90);
ImGui::Combo("##dur", &auctionSellDuration_, durations, 3);
ImGui::SameLine();
// Create Auction button
bool canCreate = auctionSellSlotIndex_ >= 0 &&
!gameHandler.getInventory().getBackpackSlot(auctionSellSlotIndex_).empty() &&
(auctionSellBid_[0] > 0 || auctionSellBid_[1] > 0 || auctionSellBid_[2] > 0);
if (!canCreate) ImGui::BeginDisabled();
if (ImGui::Button("Create Auction")) {
uint32_t bidCopper = static_cast<uint32_t>(auctionSellBid_[0]) * 10000
+ static_cast<uint32_t>(auctionSellBid_[1]) * 100
+ static_cast<uint32_t>(auctionSellBid_[2]);
uint32_t buyoutCopper = static_cast<uint32_t>(auctionSellBuyout_[0]) * 10000
+ static_cast<uint32_t>(auctionSellBuyout_[1]) * 100
+ static_cast<uint32_t>(auctionSellBuyout_[2]);
const uint32_t durationMins[] = {720, 1440, 2880};
uint32_t dur = durationMins[auctionSellDuration_];
uint64_t itemGuid = gameHandler.getBackpackItemGuid(auctionSellSlotIndex_);
const auto& slot = gameHandler.getInventory().getBackpackSlot(auctionSellSlotIndex_);
uint32_t stackCount = slot.item.stackCount;
if (itemGuid != 0) {
gameHandler.auctionSellItem(itemGuid, stackCount, bidCopper, buyoutCopper, dur);
// Clear sell inputs
auctionSellSlotIndex_ = -1;
auctionSellBid_[0] = auctionSellBid_[1] = auctionSellBid_[2] = 0;
auctionSellBuyout_[0] = auctionSellBuyout_[1] = auctionSellBuyout_[2] = 0;
}
}
if (!canCreate) ImGui::EndDisabled();
} else if (tab == 1) {
// Bids tab
const auto& results = gameHandler.getAuctionBidderResults();
ImGui::Text("Your Bids: %zu items", results.auctions.size());
if (ImGui::BeginTable("BidTable", 6, ImGuiTableFlags_Borders | ImGuiTableFlags_RowBg)) {
ImGui::TableSetupColumn("Item", ImGuiTableColumnFlags_WidthStretch);
ImGui::TableSetupColumn("Qty", ImGuiTableColumnFlags_WidthFixed, 40);
ImGui::TableSetupColumn("Your Bid", ImGuiTableColumnFlags_WidthFixed, 90);
ImGui::TableSetupColumn("Buyout", ImGuiTableColumnFlags_WidthFixed, 90);
ImGui::TableSetupColumn("Time", ImGuiTableColumnFlags_WidthFixed, 60);
ImGui::TableSetupColumn("##act", ImGuiTableColumnFlags_WidthFixed, 60);
ImGui::TableHeadersRow();
for (size_t bi = 0; bi < results.auctions.size(); bi++) {
const auto& a = results.auctions[bi];
auto* info = gameHandler.getItemInfo(a.itemEntry);
std::string name = info ? info->name : ("Item #" + std::to_string(a.itemEntry));
if (a.randomPropertyId != 0) {
std::string suffix = gameHandler.getRandomPropertyName(
static_cast<int32_t>(a.randomPropertyId));
if (!suffix.empty()) name += " " + suffix;
}
game::ItemQuality quality = info ? static_cast<game::ItemQuality>(info->quality) : game::ItemQuality::COMMON;
ImVec4 bqc = InventoryScreen::getQualityColor(quality);
ImGui::TableNextRow();
ImGui::TableSetColumnIndex(0);
if (info && info->valid && info->displayInfoId != 0) {
VkDescriptorSet bIcon = inventoryScreen.getItemIcon(info->displayInfoId);
if (bIcon) {
ImGui::Image((void*)(intptr_t)bIcon, ImVec2(16, 16));
ImGui::SameLine();
}
}
// High bidder indicator
bool isHighBidder = (a.bidderGuid != 0 && a.bidderGuid == gameHandler.getPlayerGuid());
if (isHighBidder) {
ImGui::TextColored(ImVec4(0.2f, 0.9f, 0.2f, 1.0f), "[Winning]");
ImGui::SameLine();
} else if (a.bidderGuid != 0) {
ImGui::TextColored(ImVec4(0.9f, 0.3f, 0.3f, 1.0f), "[Outbid]");
ImGui::SameLine();
}
ImGui::TextColored(bqc, "%s", name.c_str());
// Tooltip and shift-click
if (ImGui::IsItemHovered() && info && info->valid)
inventoryScreen.renderItemTooltip(*info);
if (ImGui::IsItemHovered() && ImGui::IsMouseClicked(ImGuiMouseButton_Left) &&
ImGui::GetIO().KeyShift && info && info->valid && !info->name.empty()) {
std::string link = buildItemChatLink(info->entry, info->quality, info->name);
chatPanel.insertChatLink(link);
}
ImGui::TableSetColumnIndex(1);
ImGui::Text("%u", a.stackCount);
ImGui::TableSetColumnIndex(2);
renderCoinsFromCopper(a.currentBid);
ImGui::TableSetColumnIndex(3);
if (a.buyoutPrice > 0)
renderCoinsFromCopper(a.buyoutPrice);
else
ImGui::TextDisabled("--");
ImGui::TableSetColumnIndex(4);
uint32_t mins = a.timeLeftMs / 60000;
if (mins > 720) ImGui::Text("Long");
else if (mins > 120) ImGui::Text("Medium");
else ImGui::TextColored(ImVec4(1, 0.3f, 0.3f, 1), "Short");
ImGui::TableSetColumnIndex(5);
ImGui::PushID(static_cast<int>(bi) + 7500);
if (a.buyoutPrice > 0 && ImGui::SmallButton("Buy")) {
gameHandler.auctionBuyout(a.auctionId, a.buyoutPrice);
}
if (a.buyoutPrice > 0) ImGui::SameLine();
if (ImGui::SmallButton("Bid")) {
uint32_t bidAmt = a.currentBid > 0
? a.currentBid + a.minBidIncrement
: a.startBid;
gameHandler.auctionPlaceBid(a.auctionId, bidAmt);
}
ImGui::PopID();
}
ImGui::EndTable();
}
} else if (tab == 2) {
// Auctions tab (your listings)
const auto& results = gameHandler.getAuctionOwnerResults();
ImGui::Text("Your Auctions: %zu items", results.auctions.size());
if (ImGui::BeginTable("OwnerTable", 5, ImGuiTableFlags_Borders | ImGuiTableFlags_RowBg)) {
ImGui::TableSetupColumn("Item", ImGuiTableColumnFlags_WidthStretch);
ImGui::TableSetupColumn("Qty", ImGuiTableColumnFlags_WidthFixed, 40);
ImGui::TableSetupColumn("Bid", ImGuiTableColumnFlags_WidthFixed, 90);
ImGui::TableSetupColumn("Buyout", ImGuiTableColumnFlags_WidthFixed, 90);
ImGui::TableSetupColumn("##cancel", ImGuiTableColumnFlags_WidthFixed, 60);
ImGui::TableHeadersRow();
for (size_t i = 0; i < results.auctions.size(); i++) {
const auto& a = results.auctions[i];
auto* info = gameHandler.getItemInfo(a.itemEntry);
std::string name = info ? info->name : ("Item #" + std::to_string(a.itemEntry));
if (a.randomPropertyId != 0) {
std::string suffix = gameHandler.getRandomPropertyName(
static_cast<int32_t>(a.randomPropertyId));
if (!suffix.empty()) name += " " + suffix;
}
game::ItemQuality quality = info ? static_cast<game::ItemQuality>(info->quality) : game::ItemQuality::COMMON;
ImGui::TableNextRow();
ImGui::TableSetColumnIndex(0);
ImVec4 oqc = InventoryScreen::getQualityColor(quality);
if (info && info->valid && info->displayInfoId != 0) {
VkDescriptorSet oIcon = inventoryScreen.getItemIcon(info->displayInfoId);
if (oIcon) {
ImGui::Image((void*)(intptr_t)oIcon, ImVec2(16, 16));
ImGui::SameLine();
}
}
// Bid activity indicator for seller
if (a.bidderGuid != 0) {
ImGui::TextColored(ImVec4(1.0f, 0.85f, 0.2f, 1.0f), "[Bid]");
ImGui::SameLine();
}
ImGui::TextColored(oqc, "%s", name.c_str());
if (ImGui::IsItemHovered() && info && info->valid)
inventoryScreen.renderItemTooltip(*info);
if (ImGui::IsItemHovered() && ImGui::IsMouseClicked(ImGuiMouseButton_Left) &&
ImGui::GetIO().KeyShift && info && info->valid && !info->name.empty()) {
std::string link = buildItemChatLink(info->entry, info->quality, info->name);
chatPanel.insertChatLink(link);
}
ImGui::TableSetColumnIndex(1);
ImGui::Text("%u", a.stackCount);
ImGui::TableSetColumnIndex(2);
{
uint32_t bid = a.currentBid > 0 ? a.currentBid : a.startBid;
renderCoinsFromCopper(bid);
}
ImGui::TableSetColumnIndex(3);
if (a.buyoutPrice > 0)
renderCoinsFromCopper(a.buyoutPrice);
else
ImGui::TextDisabled("--");
ImGui::TableSetColumnIndex(4);
ImGui::PushID(static_cast<int>(i) + 8000);
if (ImGui::SmallButton("Cancel")) {
gameHandler.auctionCancelItem(a.auctionId);
}
ImGui::PopID();
}
ImGui::EndTable();
}
}
ImGui::End();
if (!open) gameHandler.closeAuctionHouse();
}
void WindowManager::renderInstanceLockouts(game::GameHandler& gameHandler) {
if (!showInstanceLockouts_) return;
ImGui::SetNextWindowSize(ImVec2(480, 0), ImGuiCond_Appearing);
ImGui::SetNextWindowPos(
ImVec2(ImGui::GetIO().DisplaySize.x / 2 - 240, 140), ImGuiCond_Appearing);
if (!ImGui::Begin("Instance Lockouts", &showInstanceLockouts_,
ImGuiWindowFlags_NoCollapse | ImGuiWindowFlags_AlwaysAutoResize)) {
ImGui::End();
return;
}
const auto& lockouts = gameHandler.getInstanceLockouts();
if (lockouts.empty()) {
ImGui::TextColored(kColorGray, "No active instance lockouts.");
} else {
auto difficultyLabel = [](uint32_t diff) -> const char* {
switch (diff) {
case 0: return "Normal";
case 1: return "Heroic";
case 2: return "25-Man";
case 3: return "25-Man Heroic";
default: return "Unknown";
}
};
// Current UTC time for reset countdown
auto nowSec = static_cast<uint64_t>(std::time(nullptr));
if (ImGui::BeginTable("lockouts", 4,
ImGuiTableFlags_SizingStretchProp |
ImGuiTableFlags_RowBg | ImGuiTableFlags_BordersOuter)) {
ImGui::TableSetupColumn("Instance", ImGuiTableColumnFlags_WidthStretch);
ImGui::TableSetupColumn("Difficulty", ImGuiTableColumnFlags_WidthFixed, 110.0f);
ImGui::TableSetupColumn("Resets In", ImGuiTableColumnFlags_WidthFixed, 100.0f);
ImGui::TableSetupColumn("Status", ImGuiTableColumnFlags_WidthFixed, 60.0f);
ImGui::TableHeadersRow();
for (const auto& lo : lockouts) {
ImGui::TableNextRow();
// Instance name — use GameHandler's Map.dbc cache (avoids duplicate DBC load)
ImGui::TableSetColumnIndex(0);
std::string mapName = gameHandler.getMapName(lo.mapId);
if (!mapName.empty()) {
ImGui::TextUnformatted(mapName.c_str());
} else {
ImGui::Text("Map %u", lo.mapId);
}
// Difficulty
ImGui::TableSetColumnIndex(1);
ImGui::TextUnformatted(difficultyLabel(lo.difficulty));
// Reset countdown
ImGui::TableSetColumnIndex(2);
if (lo.resetTime > nowSec) {
uint64_t remaining = lo.resetTime - nowSec;
uint64_t days = remaining / 86400;
uint64_t hours = (remaining % 86400) / 3600;
if (days > 0) {
ImGui::Text("%llud %lluh",
static_cast<unsigned long long>(days),
static_cast<unsigned long long>(hours));
} else {
uint64_t mins = (remaining % 3600) / 60;
ImGui::Text("%lluh %llum",
static_cast<unsigned long long>(hours),
static_cast<unsigned long long>(mins));
}
} else {
ImGui::TextColored(kColorDarkGray, "Expired");
}
// Locked / Extended status
ImGui::TableSetColumnIndex(3);
if (lo.extended) {
ImGui::TextColored(ImVec4(0.3f, 0.7f, 1.0f, 1.0f), "Ext");
} else if (lo.locked) {
ImGui::TextColored(colors::kSoftRed, "Locked");
} else {
ImGui::TextColored(ImVec4(0.5f, 0.9f, 0.5f, 1.0f), "Open");
}
}
ImGui::EndTable();
}
}
ImGui::End();
}
// ============================================================================
// Battleground score frame
//
// Displays the current score for the player's battleground using world states.
// Shown in the top-centre of the screen whenever SMSG_INIT_WORLD_STATES has
// been received for a known BG map. The layout adapts per battleground:
//
// WSG 489 Alliance / Horde flag captures (max 3)
// AB 529 Alliance / Horde resource scores (max 1600)
// AV 30 Alliance / Horde reinforcements
// EotS 566 Alliance / Horde resource scores (max 1600)
// ============================================================================
// ─── Who Results Window ───────────────────────────────────────────────────────
// ─── Combat Log Window ────────────────────────────────────────────────────────
// ─── Achievement Window ───────────────────────────────────────────────────────
void WindowManager::renderAchievementWindow(game::GameHandler& gameHandler) {
if (!showAchievementWindow_) return;
ImGui::SetNextWindowSize(ImVec2(420, 480), ImGuiCond_FirstUseEver);
ImGui::SetNextWindowPos(ImVec2(200, 150), ImGuiCond_FirstUseEver);
if (!ImGui::Begin("Achievements", &showAchievementWindow_)) {
ImGui::End();
return;
}
const auto& earned = gameHandler.getEarnedAchievements();
const auto& criteria = gameHandler.getCriteriaProgress();
ImGui::SetNextItemWidth(180.0f);
ImGui::InputText("##achsearch", achievementSearchBuf_, sizeof(achievementSearchBuf_));
ImGui::SameLine();
if (ImGui::Button("Clear")) achievementSearchBuf_[0] = '\0';
ImGui::Separator();
std::string filter(achievementSearchBuf_);
for (char& c : filter) c = static_cast<char>(tolower(static_cast<unsigned char>(c)));
if (ImGui::BeginTabBar("##achtabs")) {
// --- Earned tab ---
char earnedLabel[32];
snprintf(earnedLabel, sizeof(earnedLabel), "Earned (%u)###earned", static_cast<unsigned>(earned.size()));
if (ImGui::BeginTabItem(earnedLabel)) {
if (earned.empty()) {
ImGui::TextDisabled("No achievements earned yet.");
} else {
ImGui::BeginChild("##achlist", ImVec2(0, 0), false);
std::vector<uint32_t> ids(earned.begin(), earned.end());
std::sort(ids.begin(), ids.end());
for (uint32_t id : ids) {
const std::string& name = gameHandler.getAchievementName(id);
const std::string& display = name.empty() ? std::to_string(id) : name;
if (!filter.empty()) {
std::string lower = display;
for (char& c : lower) c = static_cast<char>(tolower(static_cast<unsigned char>(c)));
if (lower.find(filter) == std::string::npos) continue;
}
ImGui::PushID(static_cast<int>(id));
ImGui::TextColored(colors::kBrightGold, "\xE2\x98\x85");
ImGui::SameLine();
ImGui::TextUnformatted(display.c_str());
if (ImGui::IsItemHovered()) {
ImGui::BeginTooltip();
// Points badge
uint32_t pts = gameHandler.getAchievementPoints(id);
if (pts > 0) {
ImGui::TextColored(colors::kBrightGold,
"%u Achievement Point%s", pts, pts == 1 ? "" : "s");
ImGui::Separator();
}
// Description
const std::string& desc = gameHandler.getAchievementDescription(id);
if (!desc.empty()) {
ImGui::PushTextWrapPos(ImGui::GetCursorPosX() + 320.0f);
ImGui::TextUnformatted(desc.c_str());
ImGui::PopTextWrapPos();
ImGui::Spacing();
}
// Earn date
uint32_t packed = gameHandler.getAchievementDate(id);
if (packed != 0) {
// WoW PackedTime: year[31:25] month[24:21] day[20:17] weekday[16:14] hour[13:9] minute[8:3]
int minute = (packed >> 3) & 0x3F;
int hour = (packed >> 9) & 0x1F;
int day = (packed >> 17) & 0x1F;
int month = (packed >> 21) & 0x0F;
int year = ((packed >> 25) & 0x7F) + 2000;
const char* mname = (month >= 1 && month <= 12) ? kMonthAbbrev[month - 1] : "?";
ImGui::TextDisabled("Earned: %s %d, %d %02d:%02d", mname, day, year, hour, minute);
}
ImGui::EndTooltip();
}
ImGui::PopID();
}
ImGui::EndChild();
}
ImGui::EndTabItem();
}
// --- Criteria progress tab ---
char critLabel[32];
snprintf(critLabel, sizeof(critLabel), "Criteria (%u)###crit", static_cast<unsigned>(criteria.size()));
if (ImGui::BeginTabItem(critLabel)) {
// Lazy-load AchievementCriteria.dbc for descriptions
struct CriteriaEntry { uint32_t achievementId; uint64_t quantity; std::string description; };
static std::unordered_map<uint32_t, CriteriaEntry> s_criteriaData;
static bool s_criteriaDataLoaded = false;
if (!s_criteriaDataLoaded) {
s_criteriaDataLoaded = true;
auto* am = services_.assetManager;
if (am && am->isInitialized()) {
auto dbc = am->loadDBC("AchievementCriteria.dbc");
if (dbc && dbc->isLoaded() && dbc->getFieldCount() >= 10) {
const auto* acL = pipeline::getActiveDBCLayout()
? pipeline::getActiveDBCLayout()->getLayout("AchievementCriteria") : nullptr;
uint32_t achField = acL ? acL->field("AchievementID") : 1u;
uint32_t qtyField = acL ? acL->field("Quantity") : 4u;
uint32_t descField = acL ? acL->field("Description") : 9u;
if (achField == 0xFFFFFFFF) achField = 1;
if (qtyField == 0xFFFFFFFF) qtyField = 4;
if (descField == 0xFFFFFFFF) descField = 9;
uint32_t fc = dbc->getFieldCount();
for (uint32_t r = 0; r < dbc->getRecordCount(); ++r) {
uint32_t cid = dbc->getUInt32(r, 0);
if (cid == 0) continue;
CriteriaEntry ce;
ce.achievementId = (achField < fc) ? dbc->getUInt32(r, achField) : 0;
ce.quantity = (qtyField < fc) ? dbc->getUInt32(r, qtyField) : 0;
ce.description = (descField < fc) ? dbc->getString(r, descField) : std::string{};
s_criteriaData[cid] = std::move(ce);
}
}
}
}
if (criteria.empty()) {
ImGui::TextDisabled("No criteria progress received yet.");
} else {
ImGui::BeginChild("##critlist", ImVec2(0, 0), false);
std::vector<std::pair<uint32_t, uint64_t>> clist(criteria.begin(), criteria.end());
std::sort(clist.begin(), clist.end());
for (const auto& [cid, cval] : clist) {
auto ceIt = s_criteriaData.find(cid);
// Build display text for filtering
std::string display;
if (ceIt != s_criteriaData.end() && !ceIt->second.description.empty()) {
display = ceIt->second.description;
} else {
display = std::to_string(cid);
}
if (!filter.empty()) {
std::string lower = display;
for (char& c : lower) c = static_cast<char>(tolower(static_cast<unsigned char>(c)));
// Also allow filtering by achievement name
if (lower.find(filter) == std::string::npos && ceIt != s_criteriaData.end()) {
const std::string& achName = gameHandler.getAchievementName(ceIt->second.achievementId);
std::string achLower = achName;
for (char& c : achLower) c = static_cast<char>(tolower(static_cast<unsigned char>(c)));
if (achLower.find(filter) == std::string::npos) continue;
} else if (lower.find(filter) == std::string::npos) {
continue;
}
}
ImGui::PushID(static_cast<int>(cid));
if (ceIt != s_criteriaData.end()) {
// Show achievement name as header (dim)
const std::string& achName = gameHandler.getAchievementName(ceIt->second.achievementId);
if (!achName.empty()) {
ImGui::TextColored(ImVec4(1.0f, 0.85f, 0.0f, 0.8f), "%s", achName.c_str());
ImGui::SameLine();
ImGui::TextDisabled(">");
ImGui::SameLine();
}
if (!ceIt->second.description.empty()) {
ImGui::TextUnformatted(ceIt->second.description.c_str());
} else {
ImGui::TextDisabled("Criteria %u", cid);
}
ImGui::SameLine();
if (ceIt->second.quantity > 0) {
ImGui::TextColored(colors::kLightGreen,
"%llu/%llu",
static_cast<unsigned long long>(cval),
static_cast<unsigned long long>(ceIt->second.quantity));
} else {
ImGui::TextColored(colors::kLightGreen,
"%llu", static_cast<unsigned long long>(cval));
}
} else {
ImGui::TextDisabled("Criteria %u:", cid);
ImGui::SameLine();
ImGui::Text("%llu", static_cast<unsigned long long>(cval));
}
ImGui::PopID();
}
ImGui::EndChild();
}
ImGui::EndTabItem();
}
ImGui::EndTabBar();
}
ImGui::End();
}
// ─── GM Ticket Window ─────────────────────────────────────────────────────────
void WindowManager::renderGmTicketWindow(game::GameHandler& gameHandler) {
// Fire a one-shot query when the window first becomes visible
if (showGmTicketWindow_ && !gmTicketWindowWasOpen_) {
gameHandler.requestGmTicket();
}
gmTicketWindowWasOpen_ = showGmTicketWindow_;
if (!showGmTicketWindow_) return;
ImGui::SetNextWindowSize(ImVec2(440, 320), ImGuiCond_FirstUseEver);
ImGui::SetNextWindowPos(ImVec2(300, 200), ImGuiCond_FirstUseEver);
if (!ImGui::Begin("GM Ticket", &showGmTicketWindow_,
ImGuiWindowFlags_NoCollapse | ImGuiWindowFlags_AlwaysAutoResize)) {
ImGui::End();
return;
}
// Show GM support availability
if (!gameHandler.isGmSupportAvailable()) {
ImGui::TextColored(colors::kSoftRed, "GM support is currently unavailable.");
ImGui::Spacing();
}
// Show existing open ticket if any
if (gameHandler.hasActiveGmTicket()) {
ImGui::TextColored(kColorGreen, "You have an open GM ticket.");
const std::string& existingText = gameHandler.getGmTicketText();
if (!existingText.empty()) {
ImGui::TextWrapped("Current ticket: %s", existingText.c_str());
}
float waitHours = gameHandler.getGmTicketWaitHours();
if (waitHours > 0.0f) {
char waitBuf[64];
std::snprintf(waitBuf, sizeof(waitBuf), "Estimated wait: %.1f hours", waitHours);
ImGui::TextColored(ImVec4(0.8f, 0.8f, 0.4f, 1.0f), "%s", waitBuf);
}
ImGui::Separator();
ImGui::Spacing();
}
ImGui::TextWrapped("Describe your issue and a Game Master will contact you.");
ImGui::Spacing();
ImGui::InputTextMultiline("##gmticket_body", gmTicketBuf_, sizeof(gmTicketBuf_),
ImVec2(-1, 120));
ImGui::Spacing();
bool hasText = (gmTicketBuf_[0] != '\0');
if (!hasText) ImGui::BeginDisabled();
if (ImGui::Button("Submit Ticket", ImVec2(160, 0))) {
gameHandler.submitGmTicket(gmTicketBuf_);
gmTicketBuf_[0] = '\0';
showGmTicketWindow_ = false;
}
if (!hasText) ImGui::EndDisabled();
ImGui::SameLine();
if (ImGui::Button("Cancel", ImVec2(80, 0))) {
showGmTicketWindow_ = false;
}
ImGui::SameLine();
if (gameHandler.hasActiveGmTicket()) {
if (ImGui::Button("Delete Ticket", ImVec2(110, 0))) {
gameHandler.deleteGmTicket();
showGmTicketWindow_ = false;
}
}
ImGui::End();
}
// ─── Book / Scroll / Note Window ──────────────────────────────────────────────
void WindowManager::renderBookWindow(game::GameHandler& gameHandler) {
// Auto-open when new pages arrive
if (gameHandler.hasBookOpen() && !showBookWindow_) {
showBookWindow_ = true;
bookCurrentPage_ = 0;
}
if (!showBookWindow_) return;
const auto& pages = gameHandler.getBookPages();
if (pages.empty()) { showBookWindow_ = false; return; }
// Clamp page index
if (bookCurrentPage_ < 0) bookCurrentPage_ = 0;
if (bookCurrentPage_ >= static_cast<int>(pages.size()))
bookCurrentPage_ = static_cast<int>(pages.size()) - 1;
ImGui::SetNextWindowSize(ImVec2(420, 340), ImGuiCond_Appearing);
ImGui::SetNextWindowPos(ImVec2(400, 180), ImGuiCond_Appearing);
bool open = showBookWindow_;
ImGui::PushStyleColor(ImGuiCol_WindowBg, ImVec4(0.12f, 0.09f, 0.06f, 0.98f));
ImGui::PushStyleColor(ImGuiCol_TitleBgActive, ImVec4(0.25f, 0.18f, 0.08f, 1.0f));
ImGui::PushStyleColor(ImGuiCol_Border, ImVec4(0.5f, 0.37f, 0.18f, 1.0f));
char title[64];
if (pages.size() > 1)
snprintf(title, sizeof(title), "Page %d / %d###BookWin",
bookCurrentPage_ + 1, static_cast<int>(pages.size()));
else
snprintf(title, sizeof(title), "###BookWin");
if (ImGui::Begin(title, &open, ImGuiWindowFlags_NoCollapse)) {
// Parchment text colour
ImGui::PushStyleColor(ImGuiCol_Text, ImVec4(0.85f, 0.78f, 0.62f, 1.0f));
const std::string& text = pages[bookCurrentPage_].text;
// Use a child region with word-wrap
ImGui::SetNextWindowContentSize(ImVec2(ImGui::GetContentRegionAvail().x, 0));
if (ImGui::BeginChild("##BookText",
ImVec2(0, ImGui::GetContentRegionAvail().y - 34),
false, ImGuiWindowFlags_HorizontalScrollbar)) {
ImGui::SetNextItemWidth(-1);
ImGui::TextWrapped("%s", text.c_str());
}
ImGui::EndChild();
ImGui::PopStyleColor();
// Navigation row
ImGui::Separator();
bool canPrev = (bookCurrentPage_ > 0);
bool canNext = (bookCurrentPage_ < static_cast<int>(pages.size()) - 1);
if (!canPrev) ImGui::BeginDisabled();
if (ImGui::Button("< Prev", ImVec2(80, 0))) bookCurrentPage_--;
if (!canPrev) ImGui::EndDisabled();
ImGui::SameLine();
if (!canNext) ImGui::BeginDisabled();
if (ImGui::Button("Next >", ImVec2(80, 0))) bookCurrentPage_++;
if (!canNext) ImGui::EndDisabled();
ImGui::SameLine(ImGui::GetContentRegionAvail().x - 60);
if (ImGui::Button("Close", ImVec2(60, 0))) {
open = false;
}
}
ImGui::End();
ImGui::PopStyleColor(3);
if (!open) {
showBookWindow_ = false;
gameHandler.clearBook();
}
}
// ─── Inspect Window ───────────────────────────────────────────────────────────
// ─── Titles Window ────────────────────────────────────────────────────────────
void WindowManager::renderTitlesWindow(game::GameHandler& gameHandler) {
if (!showTitlesWindow_) return;
ImGui::SetNextWindowSize(ImVec2(320, 400), ImGuiCond_FirstUseEver);
ImGui::SetNextWindowPos(ImVec2(240, 170), ImGuiCond_FirstUseEver);
if (!ImGui::Begin("Titles", &showTitlesWindow_)) {
ImGui::End();
return;
}
const auto& knownBits = gameHandler.getKnownTitleBits();
const int32_t chosen = gameHandler.getChosenTitleBit();
if (knownBits.empty()) {
ImGui::TextDisabled("No titles earned yet.");
ImGui::End();
return;
}
ImGui::TextUnformatted("Select a title to display:");
ImGui::Separator();
// "No Title" option
bool noTitle = (chosen < 0);
if (ImGui::Selectable("(No Title)", noTitle)) {
if (!noTitle) gameHandler.sendSetTitle(-1);
}
if (noTitle) {
ImGui::SameLine();
ImGui::TextColored(colors::kBrightGold, "<-- active");
}
ImGui::Separator();
// Sort known bits for stable display order
std::vector<uint32_t> sortedBits(knownBits.begin(), knownBits.end());
std::sort(sortedBits.begin(), sortedBits.end());
ImGui::BeginChild("##titlelist", ImVec2(0, 0), false);
for (uint32_t bit : sortedBits) {
const std::string title = gameHandler.getFormattedTitle(bit);
const std::string display = title.empty()
? ("Title #" + std::to_string(bit)) : title;
bool isActive = (chosen >= 0 && static_cast<uint32_t>(chosen) == bit);
ImGui::PushID(static_cast<int>(bit));
if (isActive) {
ImGui::PushStyleColor(ImGuiCol_Text, colors::kBrightGold);
}
if (ImGui::Selectable(display.c_str(), isActive)) {
if (!isActive) gameHandler.sendSetTitle(static_cast<int32_t>(bit));
}
if (isActive) {
ImGui::PopStyleColor();
ImGui::SameLine();
ImGui::TextDisabled("<-- active");
}
ImGui::PopID();
}
ImGui::EndChild();
ImGui::End();
}
// ─── Equipment Set Manager Window ─────────────────────────────────────────────
void WindowManager::renderEquipSetWindow(game::GameHandler& gameHandler) {
if (!showEquipSetWindow_) return;
ImGui::SetNextWindowSize(ImVec2(280, 320), ImGuiCond_FirstUseEver);
ImGui::SetNextWindowPos(ImVec2(260, 180), ImGuiCond_FirstUseEver);
if (!ImGui::Begin("Equipment Sets##equipsets", &showEquipSetWindow_)) {
ImGui::End();
return;
}
const auto& sets = gameHandler.getEquipmentSets();
if (sets.empty()) {
ImGui::TextDisabled("No equipment sets saved.");
ImGui::Spacing();
ImGui::TextWrapped("Create equipment sets in-game using the default WoW equipment manager (Shift+click the Equipment Sets button).");
ImGui::End();
return;
}
ImGui::TextUnformatted("Click a set to equip it:");
ImGui::Separator();
ImGui::Spacing();
ImGui::BeginChild("##equipsetlist", ImVec2(0, 0), false);
for (const auto& set : sets) {
ImGui::PushID(static_cast<int>(set.setId));
// Icon placeholder (use a coloured square if no icon texture available)
ImVec2 iconSize(32.0f, 32.0f);
ImGui::PushStyleColor(ImGuiCol_Button, ImVec4(0.25f, 0.20f, 0.10f, 1.0f));
ImGui::PushStyleColor(ImGuiCol_ButtonHovered, ImVec4(0.40f, 0.30f, 0.15f, 1.0f));
ImGui::PushStyleColor(ImGuiCol_ButtonActive, ImVec4(0.60f, 0.45f, 0.20f, 1.0f));
if (ImGui::Button("##icon", iconSize)) {
gameHandler.useEquipmentSet(set.setId);
}
ImGui::PopStyleColor(3);
if (ImGui::IsItemHovered()) {
ImGui::SetTooltip("Equip set: %s", set.name.c_str());
}
ImGui::SameLine();
// Name and equip button
ImGui::BeginGroup();
ImGui::TextUnformatted(set.name.c_str());
ImGui::PushStyleColor(ImGuiCol_Button, ImVec4(0.20f, 0.35f, 0.15f, 1.0f));
ImGui::PushStyleColor(ImGuiCol_ButtonHovered, ImVec4(0.30f, 0.50f, 0.22f, 1.0f));
if (ImGui::SmallButton("Equip")) {
gameHandler.useEquipmentSet(set.setId);
}
ImGui::PopStyleColor(2);
ImGui::EndGroup();
ImGui::Spacing();
ImGui::PopID();
}
ImGui::EndChild();
ImGui::End();
}
void WindowManager::renderSkillsWindow(game::GameHandler& gameHandler) {
if (!showSkillsWindow_) return;
ImGui::SetNextWindowSize(ImVec2(380, 480), ImGuiCond_FirstUseEver);
ImGui::SetNextWindowPos(ImVec2(220, 130), ImGuiCond_FirstUseEver);
if (!ImGui::Begin("Skills & Professions", &showSkillsWindow_)) {
ImGui::End();
return;
}
const auto& skills = gameHandler.getPlayerSkills();
if (skills.empty()) {
ImGui::TextDisabled("No skill data received yet.");
ImGui::End();
return;
}
// Organise skills by category
// WoW SkillLine.dbc categories: 6=Weapon, 7=Class, 8=Armor, 9=Secondary, 11=Professions, others=Misc
struct SkillEntry {
uint32_t skillId;
const game::PlayerSkill* skill;
};
std::map<uint32_t, std::vector<SkillEntry>> byCategory;
for (const auto& [id, sk] : skills) {
uint32_t cat = gameHandler.getSkillCategory(id);
byCategory[cat].push_back({id, &sk});
}
static constexpr struct { uint32_t cat; const char* label; } kCatOrder[] = {
{11, "Professions"},
{ 9, "Secondary Skills"},
{ 7, "Class Skills"},
{ 6, "Weapon Skills"},
{ 8, "Armor"},
{ 5, "Languages"},
{ 0, "Other"},
};
// Collect handled categories to fall back to "Other" for unknowns
static const uint32_t kKnownCats[] = {11, 9, 7, 6, 8, 5};
// Redirect unknown categories into bucket 0
for (auto& [cat, vec] : byCategory) {
bool known = false;
for (uint32_t kc : kKnownCats) if (cat == kc) { known = true; break; }
if (!known && cat != 0) {
auto& other = byCategory[0];
other.insert(other.end(), vec.begin(), vec.end());
vec.clear();
}
}
ImGui::BeginChild("##skillscroll", ImVec2(0, 0), false);
for (const auto& [cat, label] : kCatOrder) {
auto it = byCategory.find(cat);
if (it == byCategory.end() || it->second.empty()) continue;
auto& entries = it->second;
// Sort alphabetically within each category
std::sort(entries.begin(), entries.end(), [&](const SkillEntry& a, const SkillEntry& b) {
return gameHandler.getSkillName(a.skillId) < gameHandler.getSkillName(b.skillId);
});
if (ImGui::CollapsingHeader(label, ImGuiTreeNodeFlags_DefaultOpen)) {
for (const auto& e : entries) {
const std::string& name = gameHandler.getSkillName(e.skillId);
const char* displayName = name.empty() ? "Unknown" : name.c_str();
uint16_t val = e.skill->effectiveValue();
uint16_t maxVal = e.skill->maxValue;
ImGui::PushID(static_cast<int>(e.skillId));
// Name column
ImGui::TextUnformatted(displayName);
ImGui::SameLine(170.0f);
// Progress bar
float fraction = (maxVal > 0) ? static_cast<float>(val) / static_cast<float>(maxVal) : 0.0f;
char overlay[32];
snprintf(overlay, sizeof(overlay), "%u / %u", val, maxVal);
ImGui::PushStyleColor(ImGuiCol_PlotHistogram, ImVec4(0.20f, 0.55f, 0.20f, 1.0f));
ImGui::ProgressBar(fraction, ImVec2(160.0f, 14.0f), overlay);
ImGui::PopStyleColor();
if (ImGui::IsItemHovered()) {
ImGui::BeginTooltip();
ImGui::Text("%s", displayName);
ImGui::Separator();
ImGui::Text("Base: %u", e.skill->value);
if (e.skill->bonusPerm > 0)
ImGui::Text("Permanent bonus: +%u", e.skill->bonusPerm);
if (e.skill->bonusTemp > 0)
ImGui::Text("Temporary bonus: +%u", e.skill->bonusTemp);
ImGui::Text("Max: %u", maxVal);
ImGui::EndTooltip();
}
ImGui::PopID();
}
ImGui::Spacing();
}
}
ImGui::EndChild();
ImGui::End();
}
} // namespace ui
} // namespace wowee