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
|
// Random roll
|
||||||
void randomRoll(uint32_t minRoll = 1, uint32_t maxRoll = 100);
|
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
|
// Battleground
|
||||||
bool hasPendingBgInvite() const;
|
bool hasPendingBgInvite() const;
|
||||||
void acceptBattlefield(uint32_t queueSlot = 0xFFFFFFFF);
|
void acceptBattlefield(uint32_t queueSlot = 0xFFFFFFFF);
|
||||||
|
void declineBattlefield(uint32_t queueSlot = 0xFFFFFFFF);
|
||||||
|
const std::array<BgQueueSlot, 3>& getBgQueues() const { return bgQueues_; }
|
||||||
|
|
||||||
// Logout commands
|
// Logout commands
|
||||||
void requestLogout();
|
void requestLogout();
|
||||||
|
|
@ -1970,12 +1982,6 @@ private:
|
||||||
std::unordered_set<uint32_t> petAutocastSpells_; // spells with autocast on
|
std::unordered_set<uint32_t> petAutocastSpells_; // spells with autocast on
|
||||||
|
|
||||||
// ---- Battleground queue state ----
|
// ---- 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_{};
|
std::array<BgQueueSlot, 3> bgQueues_{};
|
||||||
|
|
||||||
// Instance difficulty
|
// Instance difficulty
|
||||||
|
|
|
||||||
|
|
@ -248,6 +248,7 @@ private:
|
||||||
void renderGuildRoster(game::GameHandler& gameHandler);
|
void renderGuildRoster(game::GameHandler& gameHandler);
|
||||||
void renderGuildInvitePopup(game::GameHandler& gameHandler);
|
void renderGuildInvitePopup(game::GameHandler& gameHandler);
|
||||||
void renderReadyCheckPopup(game::GameHandler& gameHandler);
|
void renderReadyCheckPopup(game::GameHandler& gameHandler);
|
||||||
|
void renderBgInvitePopup(game::GameHandler& gameHandler);
|
||||||
void renderChatBubbles(game::GameHandler& gameHandler);
|
void renderChatBubbles(game::GameHandler& gameHandler);
|
||||||
void renderMailWindow(game::GameHandler& gameHandler);
|
void renderMailWindow(game::GameHandler& gameHandler);
|
||||||
void renderMailComposeWindow(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";
|
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
|
// Store queue state
|
||||||
if (queueSlot < bgQueues_.size()) {
|
if (queueSlot < bgQueues_.size()) {
|
||||||
|
bool wasInvite = (bgQueues_[queueSlot].statusId == 2);
|
||||||
bgQueues_[queueSlot].queueSlot = queueSlot;
|
bgQueues_[queueSlot].queueSlot = queueSlot;
|
||||||
bgQueues_[queueSlot].bgTypeId = bgTypeId;
|
bgQueues_[queueSlot].bgTypeId = bgTypeId;
|
||||||
bgQueues_[queueSlot].arenaType = arenaType;
|
bgQueues_[queueSlot].arenaType = arenaType;
|
||||||
bgQueues_[queueSlot].statusId = statusId;
|
bgQueues_[queueSlot].statusId = statusId;
|
||||||
|
if (statusId == 2 && !wasInvite) {
|
||||||
|
bgQueues_[queueSlot].inviteTimeout = inviteTimeout;
|
||||||
|
bgQueues_[queueSlot].inviteReceivedTime = std::chrono::steady_clock::now();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
switch (statusId) {
|
switch (statusId) {
|
||||||
|
|
@ -11849,8 +11878,10 @@ void GameHandler::handleBattlefieldStatus(network::Packet& packet) {
|
||||||
LOG_INFO("Battlefield status: WAIT_QUEUE for ", bgName);
|
LOG_INFO("Battlefield status: WAIT_QUEUE for ", bgName);
|
||||||
break;
|
break;
|
||||||
case 2: // STATUS_WAIT_JOIN
|
case 2: // STATUS_WAIT_JOIN
|
||||||
addSystemChatMessage(bgName + " is ready! Type /join to enter.");
|
// Popup shown by the UI; add chat notification too.
|
||||||
LOG_INFO("Battlefield status: WAIT_JOIN for ", bgName);
|
addSystemChatMessage(bgName + " is ready!");
|
||||||
|
LOG_INFO("Battlefield status: WAIT_JOIN for ", bgName,
|
||||||
|
" timeout=", inviteTimeout, "s");
|
||||||
break;
|
break;
|
||||||
case 3: // STATUS_IN_PROGRESS
|
case 3: // STATUS_IN_PROGRESS
|
||||||
addSystemChatMessage("Entered " + bgName + ".");
|
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 {
|
bool GameHandler::hasPendingBgInvite() const {
|
||||||
for (const auto& slot : bgQueues_) {
|
for (const auto& slot : bgQueues_) {
|
||||||
if (slot.statusId == 2) return true; // STATUS_WAIT_JOIN
|
if (slot.statusId == 2) return true; // STATUS_WAIT_JOIN
|
||||||
|
|
@ -11901,6 +11970,12 @@ void GameHandler::acceptBattlefield(uint32_t queueSlot) {
|
||||||
|
|
||||||
socket->send(pkt);
|
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...");
|
addSystemChatMessage("Accepting battleground invitation...");
|
||||||
LOG_INFO("Sent CMSG_BATTLEFIELD_PORT: accept bgTypeId=", slot->bgTypeId);
|
LOG_INFO("Sent CMSG_BATTLEFIELD_PORT: accept bgTypeId=", slot->bgTypeId);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -414,6 +414,7 @@ void GameScreen::render(game::GameHandler& gameHandler) {
|
||||||
renderItemTextWindow(gameHandler);
|
renderItemTextWindow(gameHandler);
|
||||||
renderGuildInvitePopup(gameHandler);
|
renderGuildInvitePopup(gameHandler);
|
||||||
renderReadyCheckPopup(gameHandler);
|
renderReadyCheckPopup(gameHandler);
|
||||||
|
renderBgInvitePopup(gameHandler);
|
||||||
renderGuildRoster(gameHandler);
|
renderGuildRoster(gameHandler);
|
||||||
renderBuffBar(gameHandler);
|
renderBuffBar(gameHandler);
|
||||||
renderLootWindow(gameHandler);
|
renderLootWindow(gameHandler);
|
||||||
|
|
@ -5867,6 +5868,99 @@ void GameScreen::renderReadyCheckPopup(game::GameHandler& gameHandler) {
|
||||||
ImGui::End();
|
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) {
|
void GameScreen::renderGuildRoster(game::GameHandler& gameHandler) {
|
||||||
// O key toggle (WoW default Social/Guild keybind)
|
// O key toggle (WoW default Social/Guild keybind)
|
||||||
if (!ImGui::GetIO().WantCaptureKeyboard && ImGui::IsKeyPressed(ImGuiKey_O)) {
|
if (!ImGui::GetIO().WantCaptureKeyboard && ImGui::IsKeyPressed(ImGuiKey_O)) {
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue