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)
This commit is contained in:
Kelsi 2026-03-09 13:47:07 -07:00
parent 63c6039dbb
commit 200a00d4f5
4 changed files with 212 additions and 6 deletions

View file

@ -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

View file

@ -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

View file

@ -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) {

View file

@ -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<float>(window->getWidth()) : 1280.0f;
float screenH = window ? static_cast<float>(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<int>(qMs / 60000);
int qSec = static_cast<int>((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