mirror of
https://github.com/Kelsidavis/WoWee.git
synced 2026-04-03 20:03:50 +00:00
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.
4246 lines
190 KiB
C++
4246 lines
190 KiB
C++
// ============================================================
|
||
// 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
|