From 200a00d4f5b09dcb21fb29621a0db4b853f0e337 Mon Sep 17 00:00:00 2001 From: Kelsi Date: Mon, 9 Mar 2026 13:47:07 -0700 Subject: [PATCH] Implement Dungeon Finder UI window with role/dungeon selection - Add renderDungeonFinderWindow() with status display (not queued / role check / queued+wait time / proposal / in dungeon / finished) - Role checkboxes (Tank/Healer/DPS) and dungeon combo (25 entries covering Vanilla, TBC, and WotLK including Random/Heroic) - Accept/Decline buttons during Proposal state, Teleport button while InDungeon, Leave Queue button while Queued/RoleCheck - Store lfgProposalId_ on GameHandler so UI can pass it to lfgAcceptProposal(); expose getLfgProposalId() and getLfgTimeInQueueMs() getters - Toggle window with I key (when chat input is not active) --- include/game/game_handler.hpp | 7 +- include/ui/game_screen.hpp | 6 ++ src/game/game_handler.cpp | 10 +- src/ui/game_screen.cpp | 195 ++++++++++++++++++++++++++++++++++ 4 files changed, 212 insertions(+), 6 deletions(-) diff --git a/include/game/game_handler.hpp b/include/game/game_handler.hpp index 5e0e5908..2790bf00 100644 --- a/include/game/game_handler.hpp +++ b/include/game/game_handler.hpp @@ -737,8 +737,10 @@ public: LfgState getLfgState() const { return lfgState_; } bool isLfgQueued() const { return lfgState_ == LfgState::Queued; } bool isLfgInDungeon() const { return lfgState_ == LfgState::InDungeon; } - uint32_t getLfgDungeonId() const { return lfgDungeonId_; } - int32_t getLfgAvgWaitSec() const { return lfgAvgWaitSec_; } + uint32_t getLfgDungeonId() const { return lfgDungeonId_; } + uint32_t getLfgProposalId() const { return lfgProposalId_; } + int32_t getLfgAvgWaitSec() const { return lfgAvgWaitSec_; } + uint32_t getLfgTimeInQueueMs() const { return lfgTimeInQueueMs_; } // ---- Phase 5: Loot ---- void lootTarget(uint64_t guid); @@ -1585,6 +1587,7 @@ private: // LFG / Dungeon Finder state LfgState lfgState_ = LfgState::None; uint32_t lfgDungeonId_ = 0; // current dungeon entry + uint32_t lfgProposalId_ = 0; // pending proposal id (0 = none) int32_t lfgAvgWaitSec_ = -1; // estimated wait, -1=unknown uint32_t lfgTimeInQueueMs_= 0; // ms already in queue diff --git a/include/ui/game_screen.hpp b/include/ui/game_screen.hpp index 2a65abab..d2713a55 100644 --- a/include/ui/game_screen.hpp +++ b/include/ui/game_screen.hpp @@ -227,6 +227,7 @@ private: void renderBankWindow(game::GameHandler& gameHandler); void renderGuildBankWindow(game::GameHandler& gameHandler); void renderAuctionHouseWindow(game::GameHandler& gameHandler); + void renderDungeonFinderWindow(game::GameHandler& gameHandler); /** * Inventory screen @@ -259,6 +260,11 @@ private: int bagBarPickedSlot_ = -1; // Visual drag in progress (-1 = none) int bagBarDragSource_ = -1; // Mouse pressed on this slot, waiting for drag or click (-1 = none) + // Dungeon Finder state + bool showDungeonFinder_ = false; + uint8_t lfgRoles_ = 0x08; // default: DPS (0x02=tank, 0x04=healer, 0x08=dps) + uint32_t lfgSelectedDungeon_ = 861; // default: random dungeon (entry 861 = Random Dungeon WotLK) + // Chat settings bool chatShowTimestamps_ = false; int chatFontSize_ = 1; // 0=small, 1=medium, 2=large diff --git a/src/game/game_handler.cpp b/src/game/game_handler.cpp index 2acbb324..bfc1ef9c 100644 --- a/src/game/game_handler.cpp +++ b/src/game/game_handler.cpp @@ -9073,15 +9073,18 @@ void GameHandler::handleLfgProposalUpdate(network::Packet& packet) { if (remaining < 17) return; /*bool canOverride =*/ packet.readUInt8(); - lfgDungeonId_ = dungeonId; + lfgDungeonId_ = dungeonId; + lfgProposalId_ = proposalId; switch (proposalState) { case 0: - lfgState_ = LfgState::Queued; + lfgState_ = LfgState::Queued; + lfgProposalId_ = 0; addSystemChatMessage("Dungeon Finder: Group proposal failed."); break; case 1: - lfgState_ = LfgState::InDungeon; + lfgState_ = LfgState::InDungeon; + lfgProposalId_ = 0; addSystemChatMessage("Dungeon Finder: Group found! Entering dungeon..."); break; case 2: @@ -9094,7 +9097,6 @@ void GameHandler::handleLfgProposalUpdate(network::Packet& packet) { LOG_INFO("SMSG_LFG_PROPOSAL_UPDATE: dungeonId=", dungeonId, " proposalId=", proposalId, " state=", proposalState); - (void)proposalId; } void GameHandler::handleLfgRoleCheckUpdate(network::Packet& packet) { diff --git a/src/ui/game_screen.cpp b/src/ui/game_screen.cpp index 10d5aa54..0f275f23 100644 --- a/src/ui/game_screen.cpp +++ b/src/ui/game_screen.cpp @@ -412,6 +412,7 @@ void GameScreen::render(game::GameHandler& gameHandler) { renderBankWindow(gameHandler); renderGuildBankWindow(gameHandler); renderAuctionHouseWindow(gameHandler); + renderDungeonFinderWindow(gameHandler); // renderQuestMarkers(gameHandler); // Disabled - using 3D billboard markers now renderMinimapMarkers(gameHandler); renderDeathScreen(gameHandler); @@ -8877,4 +8878,198 @@ void GameScreen::renderDingEffect() { } } +// --------------------------------------------------------------------------- +// Dungeon Finder window (toggle with hotkey or bag-bar button) +// --------------------------------------------------------------------------- +void GameScreen::renderDungeonFinderWindow(game::GameHandler& gameHandler) { + // Toggle on I key when not typing + if (!chatInputActive && ImGui::IsKeyPressed(ImGuiKey_I, false)) { + showDungeonFinder_ = !showDungeonFinder_; + } + + if (!showDungeonFinder_) return; + + auto* window = core::Application::getInstance().getWindow(); + float screenW = window ? static_cast(window->getWidth()) : 1280.0f; + float screenH = window ? static_cast(window->getHeight()) : 720.0f; + + ImGui::SetNextWindowPos(ImVec2(screenW * 0.5f - 175.0f, screenH * 0.2f), + ImGuiCond_FirstUseEver); + ImGui::SetNextWindowSize(ImVec2(350, 0), ImGuiCond_Always); + + bool open = true; + ImGuiWindowFlags flags = ImGuiWindowFlags_NoCollapse | ImGuiWindowFlags_AlwaysAutoResize; + if (!ImGui::Begin("Dungeon Finder", &open, flags)) { + ImGui::End(); + if (!open) showDungeonFinder_ = false; + return; + } + if (!open) { + ImGui::End(); + showDungeonFinder_ = false; + return; + } + + using LfgState = game::GameHandler::LfgState; + LfgState state = gameHandler.getLfgState(); + + // ---- Status banner ---- + switch (state) { + case LfgState::None: + ImGui::TextColored(ImVec4(0.6f, 0.6f, 0.6f, 1.0f), "Status: Not queued"); + break; + case LfgState::RoleCheck: + ImGui::TextColored(ImVec4(1.0f, 0.8f, 0.2f, 1.0f), "Status: Role check in progress..."); + break; + case LfgState::Queued: { + int32_t avgSec = gameHandler.getLfgAvgWaitSec(); + uint32_t qMs = gameHandler.getLfgTimeInQueueMs(); + int qMin = static_cast(qMs / 60000); + int qSec = static_cast((qMs % 60000) / 1000); + ImGui::TextColored(ImVec4(0.3f, 0.9f, 0.3f, 1.0f), "Status: In queue (%d:%02d)", qMin, qSec); + if (avgSec >= 0) { + int aMin = avgSec / 60; + int aSec = avgSec % 60; + ImGui::TextColored(ImVec4(0.8f, 0.8f, 0.8f, 1.0f), + "Avg wait: %d:%02d", aMin, aSec); + } + break; + } + case LfgState::Proposal: + ImGui::TextColored(ImVec4(1.0f, 0.5f, 0.1f, 1.0f), "Status: Group found!"); + break; + case LfgState::Boot: + ImGui::TextColored(ImVec4(1.0f, 0.3f, 0.3f, 1.0f), "Status: Vote kick in progress"); + break; + case LfgState::InDungeon: + ImGui::TextColored(ImVec4(0.4f, 0.8f, 1.0f, 1.0f), "Status: In dungeon"); + break; + case LfgState::FinishedDungeon: + ImGui::TextColored(ImVec4(0.6f, 1.0f, 0.6f, 1.0f), "Status: Dungeon complete"); + break; + case LfgState::RaidBrowser: + ImGui::TextColored(ImVec4(0.8f, 0.6f, 1.0f, 1.0f), "Status: Raid browser"); + break; + } + + ImGui::Separator(); + + // ---- Proposal accept/decline ---- + if (state == LfgState::Proposal) { + ImGui::TextColored(ImVec4(1.0f, 0.9f, 0.3f, 1.0f), + "A group has been found for your dungeon!"); + ImGui::Spacing(); + if (ImGui::Button("Accept", ImVec2(120, 0))) { + gameHandler.lfgAcceptProposal(gameHandler.getLfgProposalId(), true); + } + ImGui::SameLine(); + if (ImGui::Button("Decline", ImVec2(120, 0))) { + gameHandler.lfgAcceptProposal(gameHandler.getLfgProposalId(), false); + } + ImGui::Separator(); + } + + // ---- Teleport button (in dungeon) ---- + if (state == LfgState::InDungeon) { + if (ImGui::Button("Teleport to Dungeon", ImVec2(-1, 0))) { + gameHandler.lfgTeleport(true); + } + ImGui::Separator(); + } + + // ---- Role selection (only when not queued/in dungeon) ---- + bool canConfigure = (state == LfgState::None || state == LfgState::FinishedDungeon); + + if (canConfigure) { + ImGui::Text("Role:"); + ImGui::SameLine(); + bool isTank = (lfgRoles_ & 0x02) != 0; + bool isHealer = (lfgRoles_ & 0x04) != 0; + bool isDps = (lfgRoles_ & 0x08) != 0; + if (ImGui::Checkbox("Tank", &isTank)) lfgRoles_ = (lfgRoles_ & ~0x02) | (isTank ? 0x02 : 0); + ImGui::SameLine(); + if (ImGui::Checkbox("Healer", &isHealer)) lfgRoles_ = (lfgRoles_ & ~0x04) | (isHealer ? 0x04 : 0); + ImGui::SameLine(); + if (ImGui::Checkbox("DPS", &isDps)) lfgRoles_ = (lfgRoles_ & ~0x08) | (isDps ? 0x08 : 0); + + ImGui::Spacing(); + + // ---- Dungeon selection ---- + ImGui::Text("Dungeon:"); + + struct DungeonEntry { uint32_t id; const char* name; }; + static const DungeonEntry kDungeons[] = { + { 861, "Random Dungeon" }, + { 862, "Random Heroic" }, + // Vanilla classics + { 36, "Deadmines" }, + { 43, "Ragefire Chasm" }, + { 47, "Razorfen Kraul" }, + { 48, "Blackfathom Deeps" }, + { 52, "Uldaman" }, + { 57, "Dire Maul: East" }, + { 70, "Onyxia's Lair" }, + // TBC heroics + { 264, "The Blood Furnace" }, + { 269, "The Shattered Halls" }, + // WotLK normals/heroics + { 576, "The Nexus" }, + { 578, "The Oculus" }, + { 595, "The Culling of Stratholme" }, + { 599, "Halls of Stone" }, + { 600, "Drak'Tharon Keep" }, + { 601, "Azjol-Nerub" }, + { 604, "Gundrak" }, + { 608, "Violet Hold" }, + { 619, "Ahn'kahet: Old Kingdom" }, + { 623, "Halls of Lightning" }, + { 632, "The Forge of Souls" }, + { 650, "Trial of the Champion" }, + { 658, "Pit of Saron" }, + { 668, "Halls of Reflection" }, + }; + + // Find current index + int curIdx = 0; + for (int i = 0; i < (int)(sizeof(kDungeons)/sizeof(kDungeons[0])); ++i) { + if (kDungeons[i].id == lfgSelectedDungeon_) { curIdx = i; break; } + } + + ImGui::SetNextItemWidth(-1); + if (ImGui::BeginCombo("##dungeon", kDungeons[curIdx].name)) { + for (int i = 0; i < (int)(sizeof(kDungeons)/sizeof(kDungeons[0])); ++i) { + bool selected = (kDungeons[i].id == lfgSelectedDungeon_); + if (ImGui::Selectable(kDungeons[i].name, selected)) + lfgSelectedDungeon_ = kDungeons[i].id; + if (selected) ImGui::SetItemDefaultFocus(); + } + ImGui::EndCombo(); + } + + ImGui::Spacing(); + + // ---- Join button ---- + bool rolesOk = (lfgRoles_ != 0); + if (!rolesOk) { + ImGui::BeginDisabled(); + } + if (ImGui::Button("Join Dungeon Finder", ImVec2(-1, 0))) { + gameHandler.lfgJoin(lfgSelectedDungeon_, lfgRoles_); + } + if (!rolesOk) { + ImGui::EndDisabled(); + ImGui::TextColored(ImVec4(1.0f, 0.4f, 0.4f, 1.0f), "Select at least one role."); + } + } + + // ---- Leave button (when queued or role check) ---- + if (state == LfgState::Queued || state == LfgState::RoleCheck) { + if (ImGui::Button("Leave Queue", ImVec2(-1, 0))) { + gameHandler.lfgLeave(); + } + } + + ImGui::End(); +} + }} // namespace wowee::ui