#include "ui/dialog_manager.hpp" #include "ui/inventory_screen.hpp" #include "ui/chat_panel.hpp" #include "ui/ui_colors.hpp" #include "game/game_handler.hpp" #include "core/application.hpp" #include #include #include #include #include namespace wowee { namespace ui { namespace { using namespace wowee::ui::colors; constexpr auto& kColorDarkGray = kDarkGray; constexpr auto& kColorGreen = kGreen; } // namespace // Build a WoW-format item link string for chat insertion. // Format: |cff|Hitem::0:0:0:0:0:0:0:0|h[]|h|r static 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; } // --------------------------------------------------------------------------- // Render early dialogs (group invite through LFG role check) // --------------------------------------------------------------------------- void DialogManager::renderDialogs(game::GameHandler& gameHandler, InventoryScreen& inventoryScreen, ChatPanel& chatPanel) { renderGroupInvitePopup(gameHandler); renderDuelRequestPopup(gameHandler); renderDuelCountdown(gameHandler); renderLootRollPopup(gameHandler, inventoryScreen, chatPanel); renderTradeRequestPopup(gameHandler); renderTradeWindow(gameHandler, inventoryScreen, chatPanel); renderSummonRequestPopup(gameHandler); renderSharedQuestPopup(gameHandler); renderItemTextWindow(gameHandler); renderGuildInvitePopup(gameHandler); renderReadyCheckPopup(gameHandler); renderBgInvitePopup(gameHandler); renderBfMgrInvitePopup(gameHandler); renderLfgProposalPopup(gameHandler); renderLfgRoleCheckPopup(gameHandler); } // --------------------------------------------------------------------------- // Render late dialogs (resurrect, talent wipe, pet unlearn) // --------------------------------------------------------------------------- void DialogManager::renderLateDialogs(game::GameHandler& gameHandler) { renderResurrectDialog(gameHandler); renderTalentWipeConfirmDialog(gameHandler); renderPetUnlearnConfirmDialog(gameHandler); } // ============================================================ // Group Invite Popup // ============================================================ void DialogManager::renderGroupInvitePopup(game::GameHandler& gameHandler) { if (!gameHandler.hasPendingGroupInvite()) return; auto* window = services_.window; float screenW = window ? static_cast(window->getWidth()) : 1280.0f; ImGui::SetNextWindowPos(ImVec2(screenW / 2 - 150, 200), ImGuiCond_Always); ImGui::SetNextWindowSize(ImVec2(300, 0), ImGuiCond_Always); if (ImGui::Begin("Group Invite", nullptr, kDialogFlags)) { ImGui::Text("%s has invited you to a group.", gameHandler.getPendingInviterName().c_str()); ImGui::Spacing(); if (ImGui::Button("Accept", ImVec2(130, 30))) { gameHandler.acceptGroupInvite(); } ImGui::SameLine(); if (ImGui::Button("Decline", ImVec2(130, 30))) { gameHandler.declineGroupInvite(); } } ImGui::End(); } void DialogManager::renderDuelRequestPopup(game::GameHandler& gameHandler) { if (!gameHandler.hasPendingDuelRequest()) return; auto* window = services_.window; float screenW = window ? static_cast(window->getWidth()) : 1280.0f; ImGui::SetNextWindowPos(ImVec2(screenW / 2 - 150, 250), ImGuiCond_Always); ImGui::SetNextWindowSize(ImVec2(300, 0), ImGuiCond_Always); if (ImGui::Begin("Duel Request", nullptr, kDialogFlags)) { ImGui::Text("%s challenges you to a duel!", gameHandler.getDuelChallengerName().c_str()); ImGui::Spacing(); if (ImGui::Button("Accept", ImVec2(130, 30))) { gameHandler.acceptDuel(); } ImGui::SameLine(); if (ImGui::Button("Decline", ImVec2(130, 30))) { gameHandler.forfeitDuel(); } } ImGui::End(); } void DialogManager::renderDuelCountdown(game::GameHandler& gameHandler) { float remaining = gameHandler.getDuelCountdownRemaining(); if (remaining <= 0.0f) return; ImVec2 displaySize = ImGui::GetIO().DisplaySize; float screenW = displaySize.x > 0.0f ? displaySize.x : 1280.0f; float screenH = displaySize.y > 0.0f ? displaySize.y : 720.0f; auto* dl = ImGui::GetForegroundDrawList(); ImFont* font = ImGui::GetFont(); float fontSize = ImGui::GetFontSize(); // Show integer countdown or "Fight!" when under 0.5s char buf[32]; if (remaining > 0.5f) { snprintf(buf, sizeof(buf), "%d", static_cast(std::ceil(remaining))); } else { snprintf(buf, sizeof(buf), "Fight!"); } // Large font by scaling — use 4x font size for dramatic effect float scale = 4.0f; float scaledSize = fontSize * scale; ImVec2 textSz = font->CalcTextSizeA(scaledSize, FLT_MAX, 0.0f, buf); float tx = (screenW - textSz.x) * 0.5f; float ty = screenH * 0.35f - textSz.y * 0.5f; // Pulsing alpha: fades in and out per second float pulse = 0.75f + 0.25f * std::sin(static_cast(ImGui::GetTime()) * 6.28f); uint8_t alpha = static_cast(255 * pulse); // Color: golden countdown, red "Fight!" ImU32 color = (remaining > 0.5f) ? IM_COL32(255, 200, 50, alpha) : IM_COL32(255, 60, 60, alpha); // Drop shadow dl->AddText(font, scaledSize, ImVec2(tx + 2.0f, ty + 2.0f), IM_COL32(0, 0, 0, alpha / 2), buf); dl->AddText(font, scaledSize, ImVec2(tx, ty), color, buf); } void DialogManager::renderItemTextWindow(game::GameHandler& gameHandler) { if (!gameHandler.isItemTextOpen()) 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 * 0.5f - 200, screenH * 0.15f), ImGuiCond_FirstUseEver); ImGui::SetNextWindowSize(ImVec2(400, 300), ImGuiCond_FirstUseEver); bool open = true; if (!ImGui::Begin("Book", &open, ImGuiWindowFlags_NoCollapse)) { ImGui::End(); if (!open) gameHandler.closeItemText(); return; } if (!open) { ImGui::End(); gameHandler.closeItemText(); return; } // Parchment-toned background text ImGui::PushStyleColor(ImGuiCol_Text, ImVec4(0.2f, 0.1f, 0.0f, 1.0f)); ImGui::TextWrapped("%s", gameHandler.getItemText().c_str()); ImGui::PopStyleColor(); ImGui::Spacing(); if (ImGui::Button("Close", ImVec2(80, 0))) { gameHandler.closeItemText(); } ImGui::End(); } void DialogManager::renderSharedQuestPopup(game::GameHandler& gameHandler) { if (!gameHandler.hasPendingSharedQuest()) return; auto* window = services_.window; float screenW = window ? static_cast(window->getWidth()) : 1280.0f; ImGui::SetNextWindowPos(ImVec2(screenW / 2 - 175, 490), ImGuiCond_Always); ImGui::SetNextWindowSize(ImVec2(350, 0), ImGuiCond_Always); if (ImGui::Begin("Shared Quest", nullptr, kDialogFlags)) { ImGui::Text("%s has shared a quest with you:", gameHandler.getSharedQuestSharerName().c_str()); ImGui::TextColored(colors::kBrightGold, "\"%s\"", gameHandler.getSharedQuestTitle().c_str()); ImGui::Spacing(); if (ImGui::Button("Accept", ImVec2(130, 30))) { gameHandler.acceptSharedQuest(); } ImGui::SameLine(); if (ImGui::Button("Decline", ImVec2(130, 30))) { gameHandler.declineSharedQuest(); } } ImGui::End(); } void DialogManager::renderSummonRequestPopup(game::GameHandler& gameHandler) { if (!gameHandler.hasPendingSummonRequest()) return; // Tick the timeout down float dt = ImGui::GetIO().DeltaTime; gameHandler.tickSummonTimeout(dt); if (!gameHandler.hasPendingSummonRequest()) return; // expired auto* window = services_.window; float screenW = window ? static_cast(window->getWidth()) : 1280.0f; ImGui::SetNextWindowPos(ImVec2(screenW / 2 - 175, 430), ImGuiCond_Always); ImGui::SetNextWindowSize(ImVec2(350, 0), ImGuiCond_Always); if (ImGui::Begin("Summon Request", nullptr, kDialogFlags)) { ImGui::Text("%s is summoning you.", gameHandler.getSummonerName().c_str()); float t = gameHandler.getSummonTimeoutSec(); if (t > 0.0f) { ImGui::Text("Time remaining: %.0fs", t); } ImGui::Spacing(); if (ImGui::Button("Accept", ImVec2(130, 30))) { gameHandler.acceptSummon(); } ImGui::SameLine(); if (ImGui::Button("Decline", ImVec2(130, 30))) { gameHandler.declineSummon(); } } ImGui::End(); } void DialogManager::renderTradeRequestPopup(game::GameHandler& gameHandler) { if (!gameHandler.hasPendingTradeRequest()) return; auto* window = services_.window; float screenW = window ? static_cast(window->getWidth()) : 1280.0f; ImGui::SetNextWindowPos(ImVec2(screenW / 2 - 150, 370), ImGuiCond_Always); ImGui::SetNextWindowSize(ImVec2(300, 0), ImGuiCond_Always); if (ImGui::Begin("Trade Request", nullptr, kDialogFlags)) { ImGui::Text("%s wants to trade with you.", gameHandler.getTradePeerName().c_str()); ImGui::Spacing(); if (ImGui::Button("Accept", ImVec2(130, 30))) { gameHandler.acceptTradeRequest(); } ImGui::SameLine(); if (ImGui::Button("Decline", ImVec2(130, 30))) { gameHandler.declineTradeRequest(); } } ImGui::End(); } void DialogManager::renderTradeWindow(game::GameHandler& gameHandler, InventoryScreen& inventoryScreen, ChatPanel& chatPanel) { if (!gameHandler.isTradeOpen()) return; const auto& mySlots = gameHandler.getMyTradeSlots(); const auto& peerSlots = gameHandler.getPeerTradeSlots(); const uint64_t myGold = gameHandler.getMyTradeGold(); const uint64_t peerGold = gameHandler.getPeerTradeGold(); const auto& peerName = gameHandler.getTradePeerName(); 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(("Trade with " + peerName).c_str(), &open, kDialogFlags)) { auto formatGold = [](uint64_t copper, char* buf, size_t bufsz) { uint64_t g = copper / 10000; uint64_t s = (copper % 10000) / 100; uint64_t c = copper % 100; if (g > 0) std::snprintf(buf, bufsz, "%llug %llus %lluc", (unsigned long long)g, (unsigned long long)s, (unsigned long long)c); else if (s > 0) std::snprintf(buf, bufsz, "%llus %lluc", (unsigned long long)s, (unsigned long long)c); else std::snprintf(buf, bufsz, "%lluc", (unsigned long long)c); }; auto renderSlotColumn = [&](const char* label, const std::array& slots, uint64_t gold, bool isMine) { ImGui::Text("%s", label); ImGui::Separator(); for (int i = 0; i < game::GameHandler::TRADE_SLOT_COUNT; ++i) { const auto& slot = slots[i]; ImGui::PushID(i * (isMine ? 1 : -1) - (isMine ? 0 : 100)); if (slot.occupied && slot.itemId != 0) { const auto* info = gameHandler.getItemInfo(slot.itemId); std::string name = (info && info->valid && !info->name.empty()) ? info->name : ("Item " + std::to_string(slot.itemId)); if (slot.stackCount > 1) name += " x" + std::to_string(slot.stackCount); ImVec4 qc = (info && info->valid) ? InventoryScreen::getQualityColor(static_cast(info->quality)) : ImVec4(1.0f, 0.9f, 0.5f, 1.0f); if (info && info->valid && info->displayInfoId != 0) { VkDescriptorSet iconTex = inventoryScreen.getItemIcon(info->displayInfoId); if (iconTex) { ImGui::Image((ImTextureID)(uintptr_t)iconTex, ImVec2(16, 16)); ImGui::SameLine(); } } ImGui::TextColored(qc, "%d. %s", i + 1, name.c_str()); if (isMine && ImGui::IsItemHovered() && ImGui::IsMouseDoubleClicked(ImGuiMouseButton_Left)) { gameHandler.clearTradeItem(static_cast(i)); } if (ImGui::IsItemHovered()) { if (info && info->valid) inventoryScreen.renderItemTooltip(*info); else if (isMine) ImGui::SetTooltip("Double-click to remove"); } 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); } } else { ImGui::TextDisabled(" %d. (empty)", i + 1); // Allow dragging inventory items into trade slots via right-click context menu char addItemId[16]; snprintf(addItemId, sizeof(addItemId), "##additem%d", i); if (isMine && ImGui::IsItemClicked(ImGuiMouseButton_Right)) { ImGui::OpenPopup(addItemId); } } if (isMine) { char addItemId[16]; snprintf(addItemId, sizeof(addItemId), "##additem%d", i); // Drag-from-inventory: show small popup listing bag items if (ImGui::BeginPopup(addItemId)) { ImGui::TextDisabled("Add from inventory:"); const auto& inv = gameHandler.getInventory(); // Backpack slots 0-15 (bag=255) for (int si = 0; si < game::Inventory::BACKPACK_SLOTS; ++si) { const auto& slot = inv.getBackpackSlot(si); if (slot.empty()) continue; const auto* ii = gameHandler.getItemInfo(slot.item.itemId); std::string iname = (ii && ii->valid && !ii->name.empty()) ? ii->name : (!slot.item.name.empty() ? slot.item.name : ("Item " + std::to_string(slot.item.itemId))); if (ImGui::Selectable(iname.c_str())) { // bag=255 = main backpack gameHandler.setTradeItem(static_cast(i), 255u, static_cast(si)); ImGui::CloseCurrentPopup(); } } ImGui::EndPopup(); } } ImGui::PopID(); } // Gold row char gbuf[48]; formatGold(gold, gbuf, sizeof(gbuf)); ImGui::Spacing(); if (isMine) { ImGui::Text("Gold offered: %s", gbuf); static char goldInput[32] = "0"; ImGui::SetNextItemWidth(120.0f); if (ImGui::InputText("##goldset", goldInput, sizeof(goldInput), ImGuiInputTextFlags_CharsDecimal | ImGuiInputTextFlags_EnterReturnsTrue)) { uint64_t copper = std::strtoull(goldInput, nullptr, 10); gameHandler.setTradeGold(copper); } ImGui::SameLine(); ImGui::TextDisabled("(copper, Enter to set)"); } else { ImGui::Text("Gold offered: %s", gbuf); } }; // Two-column layout: my offer | peer offer float colW = ImGui::GetContentRegionAvail().x * 0.5f - 4.0f; ImGui::BeginChild("##myoffer", ImVec2(colW, 240.0f), true); renderSlotColumn("Your offer", mySlots, myGold, true); ImGui::EndChild(); ImGui::SameLine(); ImGui::BeginChild("##peroffer", ImVec2(colW, 240.0f), true); renderSlotColumn((peerName + "'s offer").c_str(), peerSlots, peerGold, false); ImGui::EndChild(); // Buttons ImGui::Spacing(); ImGui::Separator(); float bw = (ImGui::GetContentRegionAvail().x - ImGui::GetStyle().ItemSpacing.x) * 0.5f; if (ImGui::Button("Accept Trade", ImVec2(bw, 0))) { gameHandler.acceptTrade(); } ImGui::SameLine(); if (ImGui::Button("Cancel", ImVec2(bw, 0))) { gameHandler.cancelTrade(); } } ImGui::End(); if (!open) { gameHandler.cancelTrade(); } } void DialogManager::renderLootRollPopup(game::GameHandler& gameHandler, InventoryScreen& inventoryScreen, ChatPanel& chatPanel) { if (!gameHandler.hasPendingLootRoll()) return; const auto& roll = gameHandler.getPendingLootRoll(); auto* window = services_.window; float screenW = window ? static_cast(window->getWidth()) : 1280.0f; ImGui::SetNextWindowPos(ImVec2(screenW / 2 - 175, 310), ImGuiCond_Always); ImGui::SetNextWindowSize(ImVec2(350, 0), ImGuiCond_Always); if (ImGui::Begin("Loot Roll", nullptr, kDialogFlags)) { // Quality color for item name uint8_t q = roll.itemQuality; ImVec4 col = ui::getQualityColor(static_cast(q)); // Countdown bar { auto now = std::chrono::steady_clock::now(); float elapsedMs = static_cast( std::chrono::duration_cast(now - roll.rollStartedAt).count()); float totalMs = static_cast(roll.rollCountdownMs > 0 ? roll.rollCountdownMs : 60000); float fraction = 1.0f - std::min(elapsedMs / totalMs, 1.0f); float remainSec = (totalMs - elapsedMs) / 1000.0f; if (remainSec < 0.0f) remainSec = 0.0f; // Color: green → yellow → red ImVec4 barColor; if (fraction > 0.5f) barColor = ImVec4(0.2f + (1.0f - fraction) * 1.4f, 0.85f, 0.2f, 1.0f); else if (fraction > 0.2f) barColor = ImVec4(1.0f, fraction * 1.7f, 0.1f, 1.0f); else { float pulse = 0.7f + 0.3f * std::sin(static_cast(ImGui::GetTime()) * 6.0f); barColor = ImVec4(pulse, 0.1f * pulse, 0.1f * pulse, 1.0f); } char timeBuf[16]; std::snprintf(timeBuf, sizeof(timeBuf), "%.0fs", remainSec); ImGui::PushStyleColor(ImGuiCol_PlotHistogram, barColor); ImGui::ProgressBar(fraction, ImVec2(-1, 12), timeBuf); ImGui::PopStyleColor(); } ImGui::Text("An item is up for rolls:"); // Show item icon if available const auto* rollInfo = gameHandler.getItemInfo(roll.itemId); uint32_t rollDisplayId = rollInfo ? rollInfo->displayInfoId : 0; VkDescriptorSet rollIcon = rollDisplayId ? inventoryScreen.getItemIcon(rollDisplayId) : VK_NULL_HANDLE; if (rollIcon) { ImGui::Image((ImTextureID)(uintptr_t)rollIcon, ImVec2(24, 24)); ImGui::SameLine(); } // Prefer live item info (arrives via SMSG_ITEM_QUERY_SINGLE_RESPONSE after the // roll popup opens); fall back to the name cached at SMSG_LOOT_START_ROLL time. const char* displayName = (rollInfo && rollInfo->valid && !rollInfo->name.empty()) ? rollInfo->name.c_str() : roll.itemName.c_str(); if (rollInfo && rollInfo->valid) col = ui::getQualityColor(static_cast(rollInfo->quality)); ImGui::TextColored(col, "[%s]", displayName); if (ImGui::IsItemHovered() && rollInfo && rollInfo->valid) { inventoryScreen.renderItemTooltip(*rollInfo); } if (ImGui::IsItemHovered() && ImGui::IsMouseClicked(ImGuiMouseButton_Left) && ImGui::GetIO().KeyShift && rollInfo && rollInfo->valid && !rollInfo->name.empty()) { std::string link = buildItemChatLink(rollInfo->entry, rollInfo->quality, rollInfo->name); chatPanel.insertChatLink(link); } ImGui::Spacing(); // voteMask bits: 0x01=pass, 0x02=need, 0x04=greed, 0x08=disenchant const uint8_t vm = roll.voteMask; bool first = true; if (vm & 0x02) { if (ImGui::Button("Need", ImVec2(80, 30))) gameHandler.sendLootRoll(roll.objectGuid, roll.slot, 0); first = false; } if (vm & 0x04) { if (!first) ImGui::SameLine(); if (ImGui::Button("Greed", ImVec2(80, 30))) gameHandler.sendLootRoll(roll.objectGuid, roll.slot, 1); first = false; } if (vm & 0x08) { if (!first) ImGui::SameLine(); if (ImGui::Button("Disenchant", ImVec2(95, 30))) gameHandler.sendLootRoll(roll.objectGuid, roll.slot, 2); first = false; } if (vm & 0x01) { if (!first) ImGui::SameLine(); if (ImGui::Button("Pass", ImVec2(70, 30))) gameHandler.sendLootRoll(roll.objectGuid, roll.slot, 96); } // Live roll results from group members if (!roll.playerRolls.empty()) { ImGui::Separator(); ImGui::TextDisabled("Rolls so far:"); // Roll-type label + color static constexpr const char* kRollLabels[] = {"Need", "Greed", "Disenchant", "Pass"}; static constexpr ImVec4 kRollColors[] = { ImVec4(0.2f, 0.9f, 0.2f, 1.0f), // Need — green ImVec4(0.3f, 0.6f, 1.0f, 1.0f), // Greed — blue ImVec4(0.7f, 0.3f, 0.9f, 1.0f), // Disenchant — purple kColorDarkGray, // Pass — gray }; auto rollTypeIndex = [](uint8_t t) -> int { if (t == 0) return 0; if (t == 1) return 1; if (t == 2) return 2; return 3; // pass (96 or unknown) }; if (ImGui::BeginTable("##lootrolls", 3, ImGuiTableFlags_SizingFixedFit | ImGuiTableFlags_RowBg)) { ImGui::TableSetupColumn("Player", ImGuiTableColumnFlags_WidthStretch); ImGui::TableSetupColumn("Type", ImGuiTableColumnFlags_WidthFixed, 72.0f); ImGui::TableSetupColumn("Roll", ImGuiTableColumnFlags_WidthFixed, 32.0f); for (const auto& r : roll.playerRolls) { int ri = rollTypeIndex(r.rollType); ImGui::TableNextRow(); ImGui::TableSetColumnIndex(0); ImGui::TextUnformatted(r.playerName.c_str()); ImGui::TableSetColumnIndex(1); ImGui::TextColored(kRollColors[ri], "%s", kRollLabels[ri]); ImGui::TableSetColumnIndex(2); if (r.rollType != 96) { ImGui::TextColored(kRollColors[ri], "%d", static_cast(r.rollNum)); } else { ImGui::TextDisabled("—"); } } ImGui::EndTable(); } } } ImGui::End(); } void DialogManager::renderGuildInvitePopup(game::GameHandler& gameHandler) { if (!gameHandler.hasPendingGuildInvite()) return; auto* window = services_.window; float screenW = window ? static_cast(window->getWidth()) : 1280.0f; ImGui::SetNextWindowPos(ImVec2(screenW / 2 - 175, 250), ImGuiCond_Always); ImGui::SetNextWindowSize(ImVec2(350, 0), ImGuiCond_Always); if (ImGui::Begin("Guild Invite", nullptr, kDialogFlags)) { ImGui::TextWrapped("%s has invited you to join %s.", gameHandler.getPendingGuildInviterName().c_str(), gameHandler.getPendingGuildInviteGuildName().c_str()); ImGui::Spacing(); if (ImGui::Button("Accept", ImVec2(155, 30))) { gameHandler.acceptGuildInvite(); } ImGui::SameLine(); if (ImGui::Button("Decline", ImVec2(155, 30))) { gameHandler.declineGuildInvite(); } } ImGui::End(); } void DialogManager::renderReadyCheckPopup(game::GameHandler& gameHandler) { if (!gameHandler.hasPendingReadyCheck()) 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 - 175, screenH / 2 - 60), ImGuiCond_Always); ImGui::SetNextWindowSize(ImVec2(350, 0), ImGuiCond_Always); if (ImGui::Begin("Ready Check", nullptr, kDialogFlags)) { const std::string& initiator = gameHandler.getReadyCheckInitiator(); if (initiator.empty()) { ImGui::Text("A ready check has been initiated!"); } else { ImGui::TextWrapped("%s has initiated a ready check!", initiator.c_str()); } ImGui::Spacing(); if (ImGui::Button("Ready", ImVec2(155, 30))) { gameHandler.respondToReadyCheck(true); gameHandler.dismissReadyCheck(); } ImGui::SameLine(); if (ImGui::Button("Not Ready", ImVec2(155, 30))) { gameHandler.respondToReadyCheck(false); gameHandler.dismissReadyCheck(); } // Live player responses const auto& results = gameHandler.getReadyCheckResults(); if (!results.empty()) { ImGui::Separator(); if (ImGui::BeginTable("##rcresults", 2, ImGuiTableFlags_SizingFixedFit | ImGuiTableFlags_RowBg)) { ImGui::TableSetupColumn("Player", ImGuiTableColumnFlags_WidthStretch); ImGui::TableSetupColumn("Status", ImGuiTableColumnFlags_WidthFixed, 72.0f); for (const auto& r : results) { ImGui::TableNextRow(); ImGui::TableSetColumnIndex(0); ImGui::TextUnformatted(r.name.c_str()); ImGui::TableSetColumnIndex(1); if (r.ready) { ImGui::TextColored(ImVec4(0.2f, 0.9f, 0.2f, 1.0f), "Ready"); } else { ImGui::TextColored(ImVec4(0.9f, 0.3f, 0.3f, 1.0f), "Not Ready"); } } ImGui::EndTable(); } } } ImGui::End(); } void DialogManager::renderBgInvitePopup(game::GameHandler& gameHandler) { if (!gameHandler.hasPendingBgInvite()) return; const auto& queues = gameHandler.getBgQueues(); // Find the first WAIT_JOIN slot const game::GameHandler::BgQueueSlot* slot = nullptr; for (const auto& s : queues) { if (s.statusId == 2) { slot = &s; break; } } if (!slot) return; // Compute time remaining auto now = std::chrono::steady_clock::now(); double elapsed = std::chrono::duration(now - slot->inviteReceivedTime).count(); double remaining = static_cast(slot->inviteTimeout) - elapsed; // If invite has expired, clear it silently (server will handle the queue) if (remaining <= 0.0) { gameHandler.declineBattlefield(slot->queueSlot); 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 - 70), ImGuiCond_Always); ImGui::SetNextWindowSize(ImVec2(380, 0), ImGuiCond_Always); ImGui::PushStyleColor(ImGuiCol_WindowBg, ImVec4(0.08f, 0.08f, 0.18f, 0.95f)); ImGui::PushStyleColor(ImGuiCol_Border, ImVec4(0.4f, 0.4f, 1.0f, 1.0f)); ImGui::PushStyleColor(ImGuiCol_TitleBgActive, ImVec4(0.15f, 0.15f, 0.4f, 1.0f)); ImGui::PushStyleVar(ImGuiStyleVar_WindowRounding, 6.0f); const ImGuiWindowFlags popupFlags = ImGuiWindowFlags_NoResize | ImGuiWindowFlags_NoMove | ImGuiWindowFlags_NoCollapse; if (ImGui::Begin("Battleground Ready!", nullptr, popupFlags)) { // BG name from stored queue data std::string bgName = slot->bgName.empty() ? "Battleground" : slot->bgName; ImGui::TextColored(ImVec4(1.0f, 0.85f, 0.2f, 1.0f), "%s", bgName.c_str()); ImGui::TextWrapped("A spot has opened! You have %d seconds to enter.", static_cast(remaining)); ImGui::Spacing(); // Countdown progress bar float frac = static_cast(remaining / static_cast(slot->inviteTimeout)); frac = std::clamp(frac, 0.0f, 1.0f); ImVec4 barColor = frac > 0.5f ? colors::kHealthGreen : frac > 0.25f ? ImVec4(0.9f, 0.7f, 0.1f, 1.0f) : colors::kDarkRed; ImGui::PushStyleColor(ImGuiCol_PlotHistogram, barColor); char countdownLabel[32]; snprintf(countdownLabel, sizeof(countdownLabel), "%ds", static_cast(remaining)); ImGui::ProgressBar(frac, ImVec2(-1, 16), countdownLabel); ImGui::PopStyleColor(); ImGui::Spacing(); ImGui::PushStyleColor(ImGuiCol_Button, colors::kBtnGreen); ImGui::PushStyleColor(ImGuiCol_ButtonHovered, colors::kFriendlyGreen); if (ImGui::Button("Enter Battleground", ImVec2(180, 30))) { gameHandler.acceptBattlefield(slot->queueSlot); } ImGui::PopStyleColor(2); ImGui::SameLine(); ImGui::PushStyleColor(ImGuiCol_Button, colors::kBtnRed); ImGui::PushStyleColor(ImGuiCol_ButtonHovered, colors::kDangerRed); if (ImGui::Button("Leave Queue", ImVec2(175, 30))) { gameHandler.declineBattlefield(slot->queueSlot); } ImGui::PopStyleColor(2); } ImGui::End(); ImGui::PopStyleVar(); ImGui::PopStyleColor(3); } void DialogManager::renderBfMgrInvitePopup(game::GameHandler& gameHandler) { // Only shown on WotLK servers (outdoor battlefields like Wintergrasp use the BF Manager) if (!gameHandler.hasBfMgrInvite()) 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 - 190.0f, screenH / 2.0f - 55.0f), ImGuiCond_Always); ImGui::SetNextWindowSize(ImVec2(380.0f, 0.0f), ImGuiCond_Always); ImGui::PushStyleColor(ImGuiCol_WindowBg, ImVec4(0.08f, 0.10f, 0.20f, 0.96f)); ImGui::PushStyleColor(ImGuiCol_Border, ImVec4(0.5f, 0.5f, 1.0f, 1.0f)); ImGui::PushStyleColor(ImGuiCol_TitleBgActive, ImVec4(0.15f, 0.15f, 0.45f, 1.0f)); ImGui::PushStyleVar(ImGuiStyleVar_WindowRounding, 6.0f); const ImGuiWindowFlags flags = ImGuiWindowFlags_NoResize | ImGuiWindowFlags_NoMove | ImGuiWindowFlags_NoCollapse; if (ImGui::Begin("Battlefield", nullptr, flags)) { // Resolve zone name for Wintergrasp (zoneId 4197) uint32_t zoneId = gameHandler.getBfMgrZoneId(); const char* zoneName = nullptr; if (zoneId == 4197) zoneName = "Wintergrasp"; else if (zoneId == 5095) zoneName = "Tol Barad"; if (zoneName) { ImGui::TextColored(ImVec4(1.0f, 0.85f, 0.2f, 1.0f), "%s", zoneName); } else { ImGui::TextColored(ImVec4(1.0f, 0.85f, 0.2f, 1.0f), "Outdoor Battlefield"); } ImGui::Spacing(); ImGui::TextWrapped("You are invited to join the outdoor battlefield. Do you want to enter?"); ImGui::Spacing(); ImGui::PushStyleColor(ImGuiCol_Button, colors::kBtnGreen); ImGui::PushStyleColor(ImGuiCol_ButtonHovered, colors::kFriendlyGreen); if (ImGui::Button("Enter Battlefield", ImVec2(178, 28))) { gameHandler.acceptBfMgrInvite(); } ImGui::PopStyleColor(2); ImGui::SameLine(); ImGui::PushStyleColor(ImGuiCol_Button, colors::kBtnRed); ImGui::PushStyleColor(ImGuiCol_ButtonHovered, colors::kDangerRed); if (ImGui::Button("Decline", ImVec2(175, 28))) { gameHandler.declineBfMgrInvite(); } ImGui::PopStyleColor(2); } ImGui::End(); ImGui::PopStyleVar(); ImGui::PopStyleColor(3); } void DialogManager::renderLfgProposalPopup(game::GameHandler& gameHandler) { using LfgState = game::GameHandler::LfgState; if (gameHandler.getLfgState() != LfgState::Proposal) 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 - 175.0f, screenH / 2.0f - 65.0f), ImGuiCond_Always); ImGui::SetNextWindowSize(ImVec2(350.0f, 0.0f), ImGuiCond_Always); ImGui::PushStyleColor(ImGuiCol_WindowBg, ImVec4(0.08f, 0.14f, 0.08f, 0.96f)); ImGui::PushStyleColor(ImGuiCol_Border, ImVec4(0.3f, 0.8f, 0.3f, 1.0f)); ImGui::PushStyleColor(ImGuiCol_TitleBgActive, ImVec4(0.1f, 0.3f, 0.1f, 1.0f)); ImGui::PushStyleVar(ImGuiStyleVar_WindowRounding, 6.0f); const ImGuiWindowFlags flags = ImGuiWindowFlags_NoResize | ImGuiWindowFlags_NoMove | ImGuiWindowFlags_NoCollapse; if (ImGui::Begin("Dungeon Finder", nullptr, flags)) { ImGui::TextColored(kColorGreen, "A group has been found!"); ImGui::Spacing(); ImGui::TextWrapped("Please accept or decline to join the dungeon."); ImGui::Spacing(); ImGui::Separator(); ImGui::Spacing(); ImGui::PushStyleColor(ImGuiCol_Button, colors::kBtnGreen); ImGui::PushStyleColor(ImGuiCol_ButtonHovered, colors::kFriendlyGreen); if (ImGui::Button("Accept", ImVec2(155.0f, 30.0f))) { gameHandler.lfgAcceptProposal(gameHandler.getLfgProposalId(), true); } ImGui::PopStyleColor(2); ImGui::SameLine(); ImGui::PushStyleColor(ImGuiCol_Button, colors::kBtnRed); ImGui::PushStyleColor(ImGuiCol_ButtonHovered, colors::kDangerRed); if (ImGui::Button("Decline", ImVec2(155.0f, 30.0f))) { gameHandler.lfgAcceptProposal(gameHandler.getLfgProposalId(), false); } ImGui::PopStyleColor(2); } ImGui::End(); ImGui::PopStyleVar(); ImGui::PopStyleColor(3); } void DialogManager::renderLfgRoleCheckPopup(game::GameHandler& gameHandler) { using LfgState = game::GameHandler::LfgState; if (gameHandler.getLfgState() != LfgState::RoleCheck) 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 - 160.0f, screenH / 2.0f - 80.0f), ImGuiCond_Always); ImGui::SetNextWindowSize(ImVec2(320.0f, 0.0f), ImGuiCond_Always); ImGui::PushStyleColor(ImGuiCol_WindowBg, ImVec4(0.08f, 0.08f, 0.18f, 0.96f)); ImGui::PushStyleColor(ImGuiCol_Border, ImVec4(0.3f, 0.5f, 0.9f, 1.0f)); ImGui::PushStyleColor(ImGuiCol_TitleBgActive, ImVec4(0.1f, 0.1f, 0.3f, 1.0f)); ImGui::PushStyleVar(ImGuiStyleVar_WindowRounding, 6.0f); const ImGuiWindowFlags flags = ImGuiWindowFlags_NoResize | ImGuiWindowFlags_NoMove | ImGuiWindowFlags_NoCollapse; if (ImGui::Begin("Role Check##LfgRoleCheck", nullptr, flags)) { ImGui::TextColored(ImVec4(0.4f, 0.7f, 1.0f, 1.0f), "Confirm your role:"); ImGui::Spacing(); // Role checkboxes bool isTank = (lfgRoles_ & 0x02) != 0; bool isHealer = (lfgRoles_ & 0x04) != 0; bool isDps = (lfgRoles_ & 0x08) != 0; if (ImGui::Checkbox("Tank", &isTank)) lfgRoles_ = (lfgRoles_ & ~0x02) | (isTank ? 0x02 : 0); ImGui::SameLine(120.0f); if (ImGui::Checkbox("Healer", &isHealer)) lfgRoles_ = (lfgRoles_ & ~0x04) | (isHealer ? 0x04 : 0); ImGui::SameLine(220.0f); if (ImGui::Checkbox("DPS", &isDps)) lfgRoles_ = (lfgRoles_ & ~0x08) | (isDps ? 0x08 : 0); ImGui::Spacing(); ImGui::Separator(); ImGui::Spacing(); bool hasRole = (lfgRoles_ & 0x0E) != 0; if (!hasRole) ImGui::BeginDisabled(); ImGui::PushStyleColor(ImGuiCol_Button, ImVec4(0.15f, 0.4f, 0.15f, 1.0f)); ImGui::PushStyleColor(ImGuiCol_ButtonHovered, ImVec4(0.2f, 0.6f, 0.2f, 1.0f)); if (ImGui::Button("Accept", ImVec2(140.0f, 28.0f))) { gameHandler.lfgSetRoles(lfgRoles_); } ImGui::PopStyleColor(2); if (!hasRole) ImGui::EndDisabled(); ImGui::SameLine(); ImGui::PushStyleColor(ImGuiCol_Button, ImVec4(0.4f, 0.15f, 0.15f, 1.0f)); ImGui::PushStyleColor(ImGuiCol_ButtonHovered, ImVec4(0.6f, 0.2f, 0.2f, 1.0f)); if (ImGui::Button("Leave Queue", ImVec2(140.0f, 28.0f))) { gameHandler.lfgLeave(); } ImGui::PopStyleColor(2); } ImGui::End(); ImGui::PopStyleVar(); ImGui::PopStyleColor(3); } void DialogManager::renderResurrectDialog(game::GameHandler& gameHandler) { if (!gameHandler.showResurrectDialog()) return; auto* window = services_.window; float screenW = window ? static_cast(window->getWidth()) : 1280.0f; float screenH = window ? static_cast(window->getHeight()) : 720.0f; float dlgW = 300.0f; float dlgH = 110.0f; ImGui::SetNextWindowPos(ImVec2(screenW / 2 - dlgW / 2, screenH * 0.3f), ImGuiCond_Always); ImGui::SetNextWindowSize(ImVec2(dlgW, dlgH), ImGuiCond_Always); ImGui::PushStyleVar(ImGuiStyleVar_WindowRounding, 8.0f); ImGui::PushStyleColor(ImGuiCol_WindowBg, ImVec4(0.1f, 0.1f, 0.15f, 0.95f)); ImGui::PushStyleColor(ImGuiCol_Border, ImVec4(0.4f, 0.4f, 0.8f, 1.0f)); if (ImGui::Begin("##ResurrectDialog", nullptr, ImGuiWindowFlags_NoResize | ImGuiWindowFlags_NoMove | ImGuiWindowFlags_NoCollapse | ImGuiWindowFlags_NoTitleBar)) { ImGui::Spacing(); const std::string& casterName = gameHandler.getResurrectCasterName(); std::string text = casterName.empty() ? "Return to life?" : casterName + " wishes to resurrect you."; float textW = ImGui::CalcTextSize(text.c_str()).x; ImGui::SetCursorPosX(std::max(4.0f, (dlgW - textW) / 2)); ImGui::TextColored(ImVec4(0.8f, 0.9f, 1.0f, 1.0f), "%s", text.c_str()); ImGui::Spacing(); ImGui::Spacing(); float btnW = 100.0f; float spacing = 20.0f; ImGui::SetCursorPosX((dlgW - btnW * 2 - spacing) / 2); ImGui::PushStyleColor(ImGuiCol_Button, colors::kBtnDkGreen); ImGui::PushStyleColor(ImGuiCol_ButtonHovered, colors::kBtnDkGreenHover); if (ImGui::Button("Accept", ImVec2(btnW, 30))) { gameHandler.acceptResurrect(); } ImGui::PopStyleColor(2); ImGui::SameLine(0, spacing); ImGui::PushStyleColor(ImGuiCol_Button, colors::kBtnDkRed); ImGui::PushStyleColor(ImGuiCol_ButtonHovered, colors::kBtnDkRedHover); if (ImGui::Button("Decline", ImVec2(btnW, 30))) { gameHandler.declineResurrect(); } ImGui::PopStyleColor(2); } ImGui::End(); ImGui::PopStyleColor(2); ImGui::PopStyleVar(); } // ============================================================ // Talent Wipe Confirm Dialog // ============================================================ void DialogManager::renderTalentWipeConfirmDialog(game::GameHandler& gameHandler) { if (!gameHandler.showTalentWipeConfirmDialog()) return; auto* window = services_.window; float screenW = window ? static_cast(window->getWidth()) : 1280.0f; float screenH = window ? static_cast(window->getHeight()) : 720.0f; float dlgW = 340.0f; float dlgH = 130.0f; ImGui::SetNextWindowPos(ImVec2(screenW / 2 - dlgW / 2, screenH * 0.3f), ImGuiCond_Always); ImGui::SetNextWindowSize(ImVec2(dlgW, dlgH), ImGuiCond_Always); ImGui::PushStyleVar(ImGuiStyleVar_WindowRounding, 8.0f); ImGui::PushStyleColor(ImGuiCol_WindowBg, ImVec4(0.1f, 0.1f, 0.15f, 0.95f)); ImGui::PushStyleColor(ImGuiCol_Border, ImVec4(0.8f, 0.7f, 0.2f, 1.0f)); if (ImGui::Begin("##TalentWipeDialog", nullptr, ImGuiWindowFlags_NoResize | ImGuiWindowFlags_NoMove | ImGuiWindowFlags_NoCollapse | ImGuiWindowFlags_NoTitleBar)) { ImGui::Spacing(); uint32_t cost = gameHandler.getTalentWipeCost(); uint32_t gold = cost / 10000; uint32_t silver = (cost % 10000) / 100; uint32_t copper = cost % 100; char costStr[64]; if (gold > 0) std::snprintf(costStr, sizeof(costStr), "%ug %us %uc", gold, silver, copper); else if (silver > 0) std::snprintf(costStr, sizeof(costStr), "%us %uc", silver, copper); else std::snprintf(costStr, sizeof(costStr), "%uc", copper); std::string text = "Reset your talents for "; text += costStr; text += "?"; float textW = ImGui::CalcTextSize(text.c_str()).x; ImGui::SetCursorPosX(std::max(4.0f, (dlgW - textW) / 2)); ImGui::TextColored(ImVec4(1.0f, 0.9f, 0.4f, 1.0f), "%s", text.c_str()); ImGui::Spacing(); ImGui::SetCursorPosX(8.0f); ImGui::TextDisabled("All talent points will be refunded."); ImGui::Spacing(); float btnW = 110.0f; float spacing = 20.0f; ImGui::SetCursorPosX((dlgW - btnW * 2 - spacing) / 2); ImGui::PushStyleColor(ImGuiCol_Button, colors::kBtnDkGreen); ImGui::PushStyleColor(ImGuiCol_ButtonHovered, colors::kBtnDkGreenHover); if (ImGui::Button("Confirm", ImVec2(btnW, 30))) { gameHandler.confirmTalentWipe(); } ImGui::PopStyleColor(2); ImGui::SameLine(0, spacing); ImGui::PushStyleColor(ImGuiCol_Button, colors::kBtnDkRed); ImGui::PushStyleColor(ImGuiCol_ButtonHovered, colors::kBtnDkRedHover); if (ImGui::Button("Cancel", ImVec2(btnW, 30))) { gameHandler.cancelTalentWipe(); } ImGui::PopStyleColor(2); } ImGui::End(); ImGui::PopStyleColor(2); ImGui::PopStyleVar(); } void DialogManager::renderPetUnlearnConfirmDialog(game::GameHandler& gameHandler) { if (!gameHandler.showPetUnlearnDialog()) return; auto* window = services_.window; float screenW = window ? static_cast(window->getWidth()) : 1280.0f; float screenH = window ? static_cast(window->getHeight()) : 720.0f; float dlgW = 340.0f; float dlgH = 130.0f; ImGui::SetNextWindowPos(ImVec2(screenW / 2 - dlgW / 2, screenH * 0.3f), ImGuiCond_Always); ImGui::SetNextWindowSize(ImVec2(dlgW, dlgH), ImGuiCond_Always); ImGui::PushStyleVar(ImGuiStyleVar_WindowRounding, 8.0f); ImGui::PushStyleColor(ImGuiCol_WindowBg, ImVec4(0.1f, 0.1f, 0.15f, 0.95f)); ImGui::PushStyleColor(ImGuiCol_Border, ImVec4(0.8f, 0.7f, 0.2f, 1.0f)); if (ImGui::Begin("##PetUnlearnDialog", nullptr, ImGuiWindowFlags_NoResize | ImGuiWindowFlags_NoMove | ImGuiWindowFlags_NoCollapse | ImGuiWindowFlags_NoTitleBar)) { ImGui::Spacing(); uint32_t cost = gameHandler.getPetUnlearnCost(); uint32_t gold = cost / 10000; uint32_t silver = (cost % 10000) / 100; uint32_t copper = cost % 100; char costStr[64]; if (gold > 0) std::snprintf(costStr, sizeof(costStr), "%ug %us %uc", gold, silver, copper); else if (silver > 0) std::snprintf(costStr, sizeof(costStr), "%us %uc", silver, copper); else std::snprintf(costStr, sizeof(costStr), "%uc", copper); std::string text = std::string("Reset your pet's talents for ") + costStr + "?"; float textW = ImGui::CalcTextSize(text.c_str()).x; ImGui::SetCursorPosX(std::max(4.0f, (dlgW - textW) / 2)); ImGui::TextColored(ImVec4(1.0f, 0.9f, 0.4f, 1.0f), "%s", text.c_str()); ImGui::Spacing(); ImGui::SetCursorPosX(8.0f); ImGui::TextDisabled("All pet talent points will be refunded."); ImGui::Spacing(); float btnW = 110.0f; float spacing = 20.0f; ImGui::SetCursorPosX((dlgW - btnW * 2 - spacing) / 2); ImGui::PushStyleColor(ImGuiCol_Button, colors::kBtnDkGreen); ImGui::PushStyleColor(ImGuiCol_ButtonHovered, colors::kBtnDkGreenHover); if (ImGui::Button("Confirm##petunlearn", ImVec2(btnW, 30))) { gameHandler.confirmPetUnlearn(); } ImGui::PopStyleColor(2); ImGui::SameLine(0, spacing); ImGui::PushStyleColor(ImGuiCol_Button, colors::kBtnDkRed); ImGui::PushStyleColor(ImGuiCol_ButtonHovered, colors::kBtnDkRedHover); if (ImGui::Button("Cancel##petunlearn", ImVec2(btnW, 30))) { gameHandler.cancelPetUnlearn(); } ImGui::PopStyleColor(2); } ImGui::End(); ImGui::PopStyleColor(2); ImGui::PopStyleVar(); } } // namespace ui } // namespace wowee