// ============================================================ // 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 #include #include #include #include #include #include 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(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(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(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(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(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(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(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 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(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(npcEntity); std::string name = unit->getName(); std::transform(name.begin(), name.end(), name.begin(), [](unsigned char c){ return static_cast(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(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(window->getWidth()) : 1280.0f; float screenH = window ? static_cast(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(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(window->getWidth()) : 1280.0f; float screenH = window ? static_cast(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(window->getWidth()) : 1280.0f; float screenH = window ? static_cast(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 { 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(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(i)); ImGui::PushID(static_cast(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(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(quest.choiceRewards.size())) { // Server expects the original slot index from its fixed-size reward array. rewardIdx = quest.choiceRewards[static_cast(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(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(game::EquipSlot::BAG1); s++) { const auto& slot = inv.getEquipSlot(static_cast(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(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(sellPrice) * static_cast(entry.count > 0 ? entry.count : 1); uint32_t g = static_cast(price / 10000); uint32_t s = static_cast((price / 100) % 100); uint32_t c = static_cast(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(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(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(std::tolower(static_cast(c))); for (int vi = 0; vi < static_cast(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(std::tolower(static_cast(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(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(qty); if (totalCost >= 10000) { // >= 1 gold: confirm vendorConfirmOpen_ = true; vendorConfirmGuid_ = vendor.vendorGuid; vendorConfirmItemId_ = item.itemId; vendorConfirmSlot_ = item.slot; vendorConfirmQty_ = static_cast(qty); vendorConfirmPrice_ = totalCost; vendorConfirmItemName_ = (info && info->valid) ? info->name : "Item"; } else { gameHandler.buyItem(vendor.vendorGuid, item.itemId, item.slot, static_cast(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(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(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& 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(std::tolower(static_cast(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(std::tolower(static_cast(c))); if (nameLC.find(trainerFilter) == std::string::npos) { ImGui::PushID(static_cast(spell->spellId)); ImGui::PopID(); continue; } } ImGui::TableNextRow(); ImGui::PushID(static_cast(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(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& 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 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(totalCost / 10000); uint32_t tas = static_cast((totalCost / 100) % 100); uint32_t tac = static_cast(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 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(ch->race); game::Gender gender = ch->gender; game::Race raceEnum = ch->race; // Initialize sliders from current appearance if (!barberInitialized_) { barberOrigHairStyle_ = static_cast((ch->appearanceBytes >> 16) & 0xFF); barberOrigHairColor_ = static_cast((ch->appearanceBytes >> 24) & 0xFF); barberOrigFacialHair_ = static_cast(ch->facialFeatures); barberHairStyle_ = barberOrigHairStyle_; barberHairColor_ = barberOrigHairColor_; barberFacialHair_ = barberOrigFacialHair_; barberInitialized_ = true; } int maxHairStyle = static_cast(game::getMaxHairStyle(raceEnum, gender)); int maxHairColor = static_cast(game::getMaxHairColor(raceEnum, gender)); int maxFacialHair = static_cast(game::getMaxFacialFeature(raceEnum, gender)); auto* window = services_.window; float screenW = window ? static_cast(window->getWidth()) : 1280.0f; float screenH = window ? static_cast(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(barberHairStyle_), static_cast(barberHairColor_), static_cast(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(window->getWidth()) : 1280.0f; float screenH = window ? static_cast(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(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(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(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(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(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(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(window->getWidth()) : 1280.0f; float screenH = window ? static_cast(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(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(window->getWidth()) : 1280.0f; float screenH = window ? static_cast(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(timeLeft) / 60; int secs = static_cast(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(window->getWidth()) : 1280.0f; float screenH = window ? static_cast(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(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(i)); bool selected = (gameHandler.getSelectedMailIndex() == static_cast(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(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(std::time(nullptr)); float secsLeft = mail.expirationTime - nowSec; if (secsLeft < 3.0f * 86400.0f && secsLeft > 0.0f) { ImGui::SameLine(); int daysLeft = static_cast(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(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(std::time(nullptr)); float secsLeft = mail.expirationTime - nowSec; // Format absolute expiry as a date using struct tm time_t expT = static_cast(mail.expirationTime); struct tm* tmExp = std::localtime(&expT); if (tmExp) { const char* mname = kMonthAbbrev[tmExp->tm_mon]; int daysLeft = static_cast(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(g), static_cast(s), static_cast(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(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(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(window->getWidth()) : 1280.0f; float screenH = window ? static_cast(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(mailComposeMoney_[0]) * 10000 + static_cast(mailComposeMoney_[1]) * 100 + static_cast(mailComposeMoney_[2]); uint32_t sendCost = attachCount > 0 ? static_cast(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(info2->quality) : static_cast(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(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(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(67 + bagIdx), static_cast(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(data.money / 10000); uint32_t silver = static_cast((data.money / 100) % 100); uint32_t copper = static_cast(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(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(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(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(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(auctionQuality_ - 1) : 0xFFFFFFFF; gameHandler.auctionSearch(auctionSearchName_, static_cast(auctionLevelMin_), static_cast(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(auction.randomPropertyId)); if (!suffix.empty()) name += " " + suffix; } game::ItemQuality quality = info ? static_cast(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(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(auctionSellBid_[0]) * 10000 + static_cast(auctionSellBid_[1]) * 100 + static_cast(auctionSellBid_[2]); uint32_t buyoutCopper = static_cast(auctionSellBuyout_[0]) * 10000 + static_cast(auctionSellBuyout_[1]) * 100 + static_cast(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(a.randomPropertyId)); if (!suffix.empty()) name += " " + suffix; } game::ItemQuality quality = info ? static_cast(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(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(a.randomPropertyId)); if (!suffix.empty()) name += " " + suffix; } game::ItemQuality quality = info ? static_cast(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(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(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(days), static_cast(hours)); } else { uint64_t mins = (remaining % 3600) / 60; ImGui::Text("%lluh %llum", static_cast(hours), static_cast(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(tolower(static_cast(c))); if (ImGui::BeginTabBar("##achtabs")) { // --- Earned tab --- char earnedLabel[32]; snprintf(earnedLabel, sizeof(earnedLabel), "Earned (%u)###earned", static_cast(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 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(tolower(static_cast(c))); if (lower.find(filter) == std::string::npos) continue; } ImGui::PushID(static_cast(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(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 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> 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(tolower(static_cast(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(tolower(static_cast(c))); if (achLower.find(filter) == std::string::npos) continue; } else if (lower.find(filter) == std::string::npos) { continue; } } ImGui::PushID(static_cast(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(cval), static_cast(ceIt->second.quantity)); } else { ImGui::TextColored(colors::kLightGreen, "%llu", static_cast(cval)); } } else { ImGui::TextDisabled("Criteria %u:", cid); ImGui::SameLine(); ImGui::Text("%llu", static_cast(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(pages.size())) bookCurrentPage_ = static_cast(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(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(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 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(chosen) == bit); ImGui::PushID(static_cast(bit)); if (isActive) { ImGui::PushStyleColor(ImGuiCol_Text, colors::kBrightGold); } if (ImGui::Selectable(display.c_str(), isActive)) { if (!isActive) gameHandler.sendSetTitle(static_cast(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(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> 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(e.skillId)); // Name column ImGui::TextUnformatted(displayName); ImGui::SameLine(170.0f); // Progress bar float fraction = (maxVal > 0) ? static_cast(val) / static_cast(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