mirror of
https://github.com/Kelsidavis/WoWee.git
synced 2026-03-22 23:30:14 +00:00
feat: battleground invitation popup with countdown timer
Replace the text-only "/join to enter" message with an interactive popup that shows the BG name, a live countdown progress bar, and Enter/Leave Queue buttons. - Parse STATUS_WAIT_JOIN timeout from SMSG_BATTLEFIELD_STATUS - Store inviteReceivedTime (steady_clock) on the queue slot - BgQueueSlot moved to public section so UI can read invite details - Add declineBattlefield() that sends CMSG_BATTLEFIELD_PORT(action=0) - acceptBattlefield() optimistically sets statusId=3 to dismiss popup - renderBgInvitePopup: colored countdown bar (green→yellow→red), named BG (Alterac Valley, Warsong Gulch, etc.), auto-dismisses on expiry
This commit is contained in:
parent
4986308581
commit
a7a559cdcc
4 changed files with 184 additions and 8 deletions
|
|
@ -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<BgQueueSlot, 3>& getBgQueues() const { return bgQueues_; }
|
||||
|
||||
// Logout commands
|
||||
void requestLogout();
|
||||
|
|
@ -1970,12 +1982,6 @@ private:
|
|||
std::unordered_set<uint32_t> 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<BgQueueSlot, 3> bgQueues_{};
|
||||
|
||||
// Instance difficulty
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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<double>(now - slot->inviteReceivedTime).count();
|
||||
double remaining = static_cast<double>(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<float>(window->getWidth()) : 1280.0f;
|
||||
float screenH = window ? static_cast<float>(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<int>(remaining));
|
||||
ImGui::Spacing();
|
||||
|
||||
// Countdown progress bar
|
||||
float frac = static_cast<float>(remaining / static_cast<double>(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<int>(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)) {
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue