From 2d124e7e54fff1047f1edb0146ebb1930c169180 Mon Sep 17 00:00:00 2001 From: Kelsi Date: Mon, 9 Mar 2026 13:58:02 -0700 Subject: [PATCH] Implement duel request/accept/decline UI and packet handling - Parse SMSG_DUEL_REQUESTED: store challenger guid/name, set pendingDuelRequest_ flag, show chat notification - Parse SMSG_DUEL_COMPLETE: clear pending flag, notify on cancel - Parse SMSG_DUEL_WINNER: show "X defeated Y in a duel!" chat message - Handle SMSG_DUEL_OUTOFBOUNDS with warning message - Add acceptDuel() method sending CMSG_DUEL_ACCEPTED (new builder) - Wire forfeitDuel() to clear pendingDuelRequest_ on decline - Add renderDuelRequestPopup() ImGui window (Accept/Decline buttons) positioned near group invite popup; shown when challenge is pending - Add DuelAcceptPacket builder to world_packets.hpp/cpp --- .claude/scheduled_tasks.lock | 1 + include/game/game_handler.hpp | 15 +++++++ include/game/world_packets.hpp | 6 +++ include/ui/game_screen.hpp | 1 + src/game/game_handler.cpp | 78 ++++++++++++++++++++++++++++++++-- src/game/world_packets.cpp | 6 +++ src/ui/game_screen.cpp | 25 +++++++++++ 7 files changed, 129 insertions(+), 3 deletions(-) create mode 100644 .claude/scheduled_tasks.lock diff --git a/.claude/scheduled_tasks.lock b/.claude/scheduled_tasks.lock new file mode 100644 index 00000000..5b970dd9 --- /dev/null +++ b/.claude/scheduled_tasks.lock @@ -0,0 +1 @@ +{"sessionId":"55a28c7e-8043-44c2-9829-702f303c84ba","pid":3880168,"acquiredAt":1773085726967} \ No newline at end of file diff --git a/include/game/game_handler.hpp b/include/game/game_handler.hpp index 48f28f52..1daf0492 100644 --- a/include/game/game_handler.hpp +++ b/include/game/game_handler.hpp @@ -707,6 +707,12 @@ public: bool hasPendingGroupInvite() const { return pendingGroupInvite; } const std::string& getPendingInviterName() const { return pendingInviterName; } + // ---- Duel ---- + bool hasPendingDuelRequest() const { return pendingDuelRequest_; } + const std::string& getDuelChallengerName() const { return duelChallengerName_; } + void acceptDuel(); + // forfeitDuel() already declared at line ~399 + // ---- Instance lockouts ---- struct InstanceLockout { uint32_t mapId = 0; @@ -1249,6 +1255,9 @@ private: // ---- Instance lockout handler ---- void handleRaidInstanceInfo(network::Packet& packet); + void handleDuelRequested(network::Packet& packet); + void handleDuelComplete(network::Packet& packet); + void handleDuelWinner(network::Packet& packet); // ---- LFG / Dungeon Finder handlers ---- void handleLfgJoinResult(network::Packet& packet); @@ -1601,6 +1610,12 @@ private: bool pendingGroupInvite = false; std::string pendingInviterName; + // Duel state + bool pendingDuelRequest_ = false; + uint64_t duelChallengerGuid_= 0; + uint64_t duelFlagGuid_ = 0; + std::string duelChallengerName_; + // ---- Guild state ---- std::string guildName_; std::vector guildRankNames_; diff --git a/include/game/world_packets.hpp b/include/game/world_packets.hpp index 4067746e..7b272c45 100644 --- a/include/game/world_packets.hpp +++ b/include/game/world_packets.hpp @@ -1265,6 +1265,12 @@ public: // Duel // ============================================================ +/** CMSG_DUEL_ACCEPTED packet builder (no payload) */ +class DuelAcceptPacket { +public: + static network::Packet build(); +}; + /** CMSG_DUEL_CANCELLED packet builder */ class DuelCancelPacket { public: diff --git a/include/ui/game_screen.hpp b/include/ui/game_screen.hpp index f1075d75..6cc922f8 100644 --- a/include/ui/game_screen.hpp +++ b/include/ui/game_screen.hpp @@ -204,6 +204,7 @@ private: void renderCombatText(game::GameHandler& gameHandler); void renderPartyFrames(game::GameHandler& gameHandler); void renderGroupInvitePopup(game::GameHandler& gameHandler); + void renderDuelRequestPopup(game::GameHandler& gameHandler); void renderBuffBar(game::GameHandler& gameHandler); void renderLootWindow(game::GameHandler& gameHandler); void renderGossipWindow(game::GameHandler& gameHandler); diff --git a/src/game/game_handler.cpp b/src/game/game_handler.cpp index a0afadf1..7e114002 100644 --- a/src/game/game_handler.cpp +++ b/src/game/game_handler.cpp @@ -1891,8 +1891,22 @@ void GameHandler::handlePacket(network::Packet& packet) { handleRaidInstanceInfo(packet); break; case Opcode::SMSG_DUEL_REQUESTED: - // Duel request UI flow not implemented yet. - packet.setReadPos(packet.getSize()); + handleDuelRequested(packet); + break; + case Opcode::SMSG_DUEL_COMPLETE: + handleDuelComplete(packet); + break; + case Opcode::SMSG_DUEL_WINNER: + handleDuelWinner(packet); + break; + case Opcode::SMSG_DUEL_OUTOFBOUNDS: + addSystemChatMessage("You are out of the duel area!"); + break; + case Opcode::SMSG_DUEL_INBOUNDS: + // Re-entered the duel area; no special action needed. + break; + case Opcode::SMSG_DUEL_COUNTDOWN: + // Countdown timer — no action needed; server also sends UNIT_FIELD_FLAGS update. break; case Opcode::SMSG_PARTYKILLLOG: // Classic-era packet: killer GUID + victim GUID. @@ -7023,18 +7037,76 @@ void GameHandler::respondToReadyCheck(bool ready) { LOG_INFO("Responded to ready check: ", ready ? "ready" : "not ready"); } +void GameHandler::acceptDuel() { + if (!pendingDuelRequest_ || state != WorldState::IN_WORLD || !socket) return; + pendingDuelRequest_ = false; + auto pkt = DuelAcceptPacket::build(); + socket->send(pkt); + addSystemChatMessage("You accept the duel."); + LOG_INFO("Accepted duel from guid=0x", std::hex, duelChallengerGuid_, std::dec); +} + void GameHandler::forfeitDuel() { if (state != WorldState::IN_WORLD || !socket) { LOG_WARNING("Cannot forfeit duel: not in world or not connected"); return; } - + pendingDuelRequest_ = false; // cancel request if still pending auto packet = DuelCancelPacket::build(); socket->send(packet); addSystemChatMessage("You have forfeited the duel."); LOG_INFO("Forfeited duel"); } +void GameHandler::handleDuelRequested(network::Packet& packet) { + if (packet.getSize() - packet.getReadPos() < 16) { + packet.setReadPos(packet.getSize()); + return; + } + duelChallengerGuid_ = packet.readUInt64(); + duelFlagGuid_ = packet.readUInt64(); + + // Resolve challenger name from entity list + duelChallengerName_.clear(); + auto entity = entityManager.getEntity(duelChallengerGuid_); + if (auto* unit = dynamic_cast(entity.get())) { + duelChallengerName_ = unit->getName(); + } + if (duelChallengerName_.empty()) { + char tmp[32]; + std::snprintf(tmp, sizeof(tmp), "0x%llX", + static_cast(duelChallengerGuid_)); + duelChallengerName_ = tmp; + } + pendingDuelRequest_ = true; + + addSystemChatMessage(duelChallengerName_ + " challenges you to a duel!"); + LOG_INFO("SMSG_DUEL_REQUESTED: challenger=0x", std::hex, duelChallengerGuid_, + " flag=0x", duelFlagGuid_, std::dec, " name=", duelChallengerName_); +} + +void GameHandler::handleDuelComplete(network::Packet& packet) { + if (packet.getSize() - packet.getReadPos() < 1) return; + uint8_t started = packet.readUInt8(); + // started=1: duel began, started=0: duel was cancelled before starting + pendingDuelRequest_ = false; + if (!started) { + addSystemChatMessage("The duel was cancelled."); + } + LOG_INFO("SMSG_DUEL_COMPLETE: started=", static_cast(started)); +} + +void GameHandler::handleDuelWinner(network::Packet& packet) { + if (packet.getSize() - packet.getReadPos() < 3) return; + /*uint8_t type =*/ packet.readUInt8(); // 0=normal, 1=flee + std::string winner = packet.readString(); + std::string loser = packet.readString(); + + std::string msg = winner + " has defeated " + loser + " in a duel!"; + addSystemChatMessage(msg); + LOG_INFO("SMSG_DUEL_WINNER: winner=", winner, " loser=", loser); +} + void GameHandler::toggleAfk(const std::string& message) { afkStatus_ = !afkStatus_; afkMessage_ = message; diff --git a/src/game/world_packets.cpp b/src/game/world_packets.cpp index bb66e416..e41c4693 100644 --- a/src/game/world_packets.cpp +++ b/src/game/world_packets.cpp @@ -2083,6 +2083,12 @@ network::Packet ReadyCheckConfirmPacket::build(bool ready) { // Duel // ============================================================ +network::Packet DuelAcceptPacket::build() { + network::Packet packet(wireOpcode(Opcode::CMSG_DUEL_ACCEPTED)); + LOG_DEBUG("Built CMSG_DUEL_ACCEPTED"); + return packet; +} + network::Packet DuelCancelPacket::build() { network::Packet packet(wireOpcode(Opcode::CMSG_DUEL_CANCELLED)); LOG_DEBUG("Built CMSG_DUEL_CANCELLED"); diff --git a/src/ui/game_screen.cpp b/src/ui/game_screen.cpp index dd1fa6ef..c652ba1c 100644 --- a/src/ui/game_screen.cpp +++ b/src/ui/game_screen.cpp @@ -396,6 +396,7 @@ void GameScreen::render(game::GameHandler& gameHandler) { renderCombatText(gameHandler); renderPartyFrames(gameHandler); renderGroupInvitePopup(gameHandler); + renderDuelRequestPopup(gameHandler); renderGuildInvitePopup(gameHandler); renderGuildRoster(gameHandler); renderBuffBar(gameHandler); @@ -4376,6 +4377,30 @@ void GameScreen::renderGroupInvitePopup(game::GameHandler& gameHandler) { ImGui::End(); } +void GameScreen::renderDuelRequestPopup(game::GameHandler& gameHandler) { + if (!gameHandler.hasPendingDuelRequest()) return; + + auto* window = core::Application::getInstance().getWindow(); + float screenW = window ? static_cast(window->getWidth()) : 1280.0f; + + ImGui::SetNextWindowPos(ImVec2(screenW / 2 - 150, 250), ImGuiCond_Always); + ImGui::SetNextWindowSize(ImVec2(300, 0), ImGuiCond_Always); + + if (ImGui::Begin("Duel Request", nullptr, ImGuiWindowFlags_NoCollapse | ImGuiWindowFlags_NoResize)) { + ImGui::Text("%s challenges you to a duel!", gameHandler.getDuelChallengerName().c_str()); + ImGui::Spacing(); + + if (ImGui::Button("Accept", ImVec2(130, 30))) { + gameHandler.acceptDuel(); + } + ImGui::SameLine(); + if (ImGui::Button("Decline", ImVec2(130, 30))) { + gameHandler.forfeitDuel(); + } + } + ImGui::End(); +} + void GameScreen::renderGuildInvitePopup(game::GameHandler& gameHandler) { if (!gameHandler.hasPendingGuildInvite()) return;