diff --git a/include/game/game_handler.hpp b/include/game/game_handler.hpp index e48a585b..235def82 100644 --- a/include/game/game_handler.hpp +++ b/include/game/game_handler.hpp @@ -340,9 +340,21 @@ public: // Random roll void randomRoll(uint32_t minRoll = 1, uint32_t maxRoll = 100); + // Battleground queue slot (public so UI can read invite details) + struct BgQueueSlot { + uint32_t queueSlot = 0; + uint32_t bgTypeId = 0; + uint8_t arenaType = 0; + uint32_t statusId = 0; // 0=none, 1=wait_queue, 2=wait_join, 3=in_progress + uint32_t inviteTimeout = 80; + std::chrono::steady_clock::time_point inviteReceivedTime{}; + }; + // Battleground bool hasPendingBgInvite() const; void acceptBattlefield(uint32_t queueSlot = 0xFFFFFFFF); + void declineBattlefield(uint32_t queueSlot = 0xFFFFFFFF); + const std::array& getBgQueues() const { return bgQueues_; } // Logout commands void requestLogout(); @@ -1970,12 +1982,6 @@ private: std::unordered_set petAutocastSpells_; // spells with autocast on // ---- Battleground queue state ---- - struct BgQueueSlot { - uint32_t queueSlot = 0; - uint32_t bgTypeId = 0; - uint8_t arenaType = 0; - uint32_t statusId = 0; // 0=none, 1=wait_queue, 2=wait_join, 3=in_progress - }; std::array bgQueues_{}; // Instance difficulty diff --git a/include/ui/game_screen.hpp b/include/ui/game_screen.hpp index 1ffc804f..d5938e7b 100644 --- a/include/ui/game_screen.hpp +++ b/include/ui/game_screen.hpp @@ -248,6 +248,7 @@ private: void renderGuildRoster(game::GameHandler& gameHandler); void renderGuildInvitePopup(game::GameHandler& gameHandler); void renderReadyCheckPopup(game::GameHandler& gameHandler); + void renderBgInvitePopup(game::GameHandler& gameHandler); void renderChatBubbles(game::GameHandler& gameHandler); void renderMailWindow(game::GameHandler& gameHandler); void renderMailComposeWindow(game::GameHandler& gameHandler); diff --git a/src/game/game_handler.cpp b/src/game/game_handler.cpp index 0895685b..360e2312 100644 --- a/src/game/game_handler.cpp +++ b/src/game/game_handler.cpp @@ -11832,12 +11832,41 @@ void GameHandler::handleBattlefieldStatus(network::Packet& packet) { bgName = std::to_string(arenaType) + "v" + std::to_string(arenaType) + " Arena"; } + // Parse status-specific fields + uint32_t inviteTimeout = 80; // default WoW BG invite window (seconds) + if (statusId == 1) { + // STATUS_WAIT_QUEUE: avgWaitTime(4) + timeInQueue(4) + if (packet.getSize() - packet.getReadPos() >= 8) { + /*uint32_t avgWait =*/ packet.readUInt32(); + /*uint32_t inQueue =*/ packet.readUInt32(); + } + } else if (statusId == 2) { + // STATUS_WAIT_JOIN: timeout(4) + mapId(4) + if (packet.getSize() - packet.getReadPos() >= 4) { + inviteTimeout = packet.readUInt32(); + } + if (packet.getSize() - packet.getReadPos() >= 4) { + /*uint32_t mapId =*/ packet.readUInt32(); + } + } else if (statusId == 3) { + // STATUS_IN_PROGRESS: mapId(4) + timeSinceStart(4) + if (packet.getSize() - packet.getReadPos() >= 8) { + /*uint32_t mapId =*/ packet.readUInt32(); + /*uint32_t elapsed =*/ packet.readUInt32(); + } + } + // Store queue state if (queueSlot < bgQueues_.size()) { + bool wasInvite = (bgQueues_[queueSlot].statusId == 2); bgQueues_[queueSlot].queueSlot = queueSlot; bgQueues_[queueSlot].bgTypeId = bgTypeId; bgQueues_[queueSlot].arenaType = arenaType; bgQueues_[queueSlot].statusId = statusId; + if (statusId == 2 && !wasInvite) { + bgQueues_[queueSlot].inviteTimeout = inviteTimeout; + bgQueues_[queueSlot].inviteReceivedTime = std::chrono::steady_clock::now(); + } } switch (statusId) { @@ -11849,8 +11878,10 @@ void GameHandler::handleBattlefieldStatus(network::Packet& packet) { LOG_INFO("Battlefield status: WAIT_QUEUE for ", bgName); break; case 2: // STATUS_WAIT_JOIN - addSystemChatMessage(bgName + " is ready! Type /join to enter."); - LOG_INFO("Battlefield status: WAIT_JOIN for ", bgName); + // Popup shown by the UI; add chat notification too. + addSystemChatMessage(bgName + " is ready!"); + LOG_INFO("Battlefield status: WAIT_JOIN for ", bgName, + " timeout=", inviteTimeout, "s"); break; case 3: // STATUS_IN_PROGRESS addSystemChatMessage("Entered " + bgName + "."); @@ -11865,6 +11896,44 @@ void GameHandler::handleBattlefieldStatus(network::Packet& packet) { } } +void GameHandler::declineBattlefield(uint32_t queueSlot) { + if (state != WorldState::IN_WORLD) return; + if (!socket) return; + + const BgQueueSlot* slot = nullptr; + if (queueSlot == 0xFFFFFFFF) { + for (const auto& s : bgQueues_) { + if (s.statusId == 2) { slot = &s; break; } + } + } else if (queueSlot < bgQueues_.size() && bgQueues_[queueSlot].statusId == 2) { + slot = &bgQueues_[queueSlot]; + } + + if (!slot) { + addSystemChatMessage("No battleground invitation pending."); + return; + } + + // CMSG_BATTLEFIELD_PORT with action=0 (decline) + network::Packet pkt(wireOpcode(Opcode::CMSG_BATTLEFIELD_PORT)); + pkt.writeUInt8(slot->arenaType); + pkt.writeUInt8(0x00); + pkt.writeUInt32(slot->bgTypeId); + pkt.writeUInt16(0x0000); + pkt.writeUInt8(0); // 0 = decline + + socket->send(pkt); + + // Clear queue slot + uint32_t clearSlot = slot->queueSlot; + if (clearSlot < bgQueues_.size()) { + bgQueues_[clearSlot] = BgQueueSlot{}; + } + + addSystemChatMessage("Battleground invitation declined."); + LOG_INFO("Sent CMSG_BATTLEFIELD_PORT: decline"); +} + bool GameHandler::hasPendingBgInvite() const { for (const auto& slot : bgQueues_) { if (slot.statusId == 2) return true; // STATUS_WAIT_JOIN @@ -11901,6 +11970,12 @@ void GameHandler::acceptBattlefield(uint32_t queueSlot) { socket->send(pkt); + // Optimistically clear the invite so the popup disappears immediately. + uint32_t clearSlot = slot->queueSlot; + if (clearSlot < bgQueues_.size()) { + bgQueues_[clearSlot].statusId = 3; // STATUS_IN_PROGRESS (server will confirm) + } + addSystemChatMessage("Accepting battleground invitation..."); LOG_INFO("Sent CMSG_BATTLEFIELD_PORT: accept bgTypeId=", slot->bgTypeId); } diff --git a/src/ui/game_screen.cpp b/src/ui/game_screen.cpp index e70bf2ac..1e996ce1 100644 --- a/src/ui/game_screen.cpp +++ b/src/ui/game_screen.cpp @@ -414,6 +414,7 @@ void GameScreen::render(game::GameHandler& gameHandler) { renderItemTextWindow(gameHandler); renderGuildInvitePopup(gameHandler); renderReadyCheckPopup(gameHandler); + renderBgInvitePopup(gameHandler); renderGuildRoster(gameHandler); renderBuffBar(gameHandler); renderLootWindow(gameHandler); @@ -5867,6 +5868,99 @@ void GameScreen::renderReadyCheckPopup(game::GameHandler& gameHandler) { ImGui::End(); } +void GameScreen::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 = 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 / 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 + std::string bgName; + if (slot->arenaType > 0) { + bgName = std::to_string(slot->arenaType) + "v" + std::to_string(slot->arenaType) + " Arena"; + } else { + switch (slot->bgTypeId) { + case 1: bgName = "Alterac Valley"; break; + case 2: bgName = "Warsong Gulch"; break; + case 3: bgName = "Arathi Basin"; break; + case 7: bgName = "Eye of the Storm"; break; + case 9: bgName = "Strand of the Ancients"; break; + case 11: bgName = "Isle of Conquest"; break; + default: bgName = "Battleground"; break; + } + } + + 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 ? ImVec4(0.2f, 0.8f, 0.2f, 1.0f) + : frac > 0.25f ? ImVec4(0.9f, 0.7f, 0.1f, 1.0f) + : ImVec4(0.9f, 0.2f, 0.2f, 1.0f); + 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, ImVec4(0.15f, 0.5f, 0.15f, 1.0f)); + ImGui::PushStyleColor(ImGuiCol_ButtonHovered, ImVec4(0.2f, 0.7f, 0.2f, 1.0f)); + if (ImGui::Button("Enter Battleground", ImVec2(180, 30))) { + gameHandler.acceptBattlefield(slot->queueSlot); + } + ImGui::PopStyleColor(2); + + ImGui::SameLine(); + + ImGui::PushStyleColor(ImGuiCol_Button, ImVec4(0.5f, 0.15f, 0.15f, 1.0f)); + ImGui::PushStyleColor(ImGuiCol_ButtonHovered, ImVec4(0.7f, 0.2f, 0.2f, 1.0f)); + if (ImGui::Button("Leave Queue", ImVec2(175, 30))) { + gameHandler.declineBattlefield(slot->queueSlot); + } + ImGui::PopStyleColor(2); + } + ImGui::End(); + + ImGui::PopStyleVar(); + ImGui::PopStyleColor(3); +} + void GameScreen::renderGuildRoster(game::GameHandler& gameHandler) { // O key toggle (WoW default Social/Guild keybind) if (!ImGui::GetIO().WantCaptureKeyboard && ImGui::IsKeyPressed(ImGuiKey_O)) {