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
This commit is contained in:
Kelsi 2026-03-09 13:58:02 -07:00
parent e4f53ce0c3
commit 2d124e7e54
7 changed files with 129 additions and 3 deletions

View file

@ -0,0 +1 @@
{"sessionId":"55a28c7e-8043-44c2-9829-702f303c84ba","pid":3880168,"acquiredAt":1773085726967}

View file

@ -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<std::string> guildRankNames_;

View file

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

View file

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

View file

@ -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<Unit*>(entity.get())) {
duelChallengerName_ = unit->getName();
}
if (duelChallengerName_.empty()) {
char tmp[32];
std::snprintf(tmp, sizeof(tmp), "0x%llX",
static_cast<unsigned long long>(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<int>(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;

View file

@ -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");

View file

@ -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<float>(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;