From bca3f64af61368df4a5de55b8b7d587ab9bd7f0f Mon Sep 17 00:00:00 2001 From: kelsi davis Date: Sat, 7 Feb 2026 13:36:50 -0800 Subject: [PATCH] Add Tier 7 commands: combat and trade Combat Commands: - /duel - Challenge target to a duel (CMSG_DUEL_PROPOSED 0x166) - /trade - Open trade window with target (CMSG_INITIATE_TRADE 0x116) - /startattack - Begin auto-attacking target - /stopattack - Stop auto-attacking - /stopcasting - Cancel current spell cast New opcodes: - CMSG_DUEL_PROPOSED (0x166) for initiating duels - CMSG_INITIATE_TRADE (0x116) for starting trades Packet builders: - DuelProposedPacket - sends duel challenge to target GUID - InitiateTradePacket - sends trade request to target GUID - AttackSwingPacket, AttackStopPacket, CancelCastPacket reused from existing Game handler methods: - proposeDuel(targetGuid) - challenge target to duel - initiateTrade(targetGuid) - open trade with target - stopCasting() - cancel current spell cast (uses existing casting state) All commands include validation for target selection and world state. Removed duplicate packet class definitions from previous phases. --- include/game/game_handler.hpp | 5 +++ include/game/opcodes.hpp | 4 +++ include/game/world_packets.hpp | 52 ++++++++++++++++++---------- src/game/game_handler.cpp | 57 +++++++++++++++++++++++++++++++ src/game/world_packets.cpp | 59 +++++++++++++++++++++----------- src/ui/game_screen.cpp | 62 ++++++++++++++++++++++++++++++++++ 6 files changed, 201 insertions(+), 38 deletions(-) diff --git a/include/game/game_handler.hpp b/include/game/game_handler.hpp index 9f2cb352..4c2b2e40 100644 --- a/include/game/game_handler.hpp +++ b/include/game/game_handler.hpp @@ -270,6 +270,11 @@ public: void clearMainAssist(); void requestRaidInfo(); + // Combat and Trade + void proposeDuel(uint64_t targetGuid); + void initiateTrade(uint64_t targetGuid); + void stopCasting(); + // ---- Phase 1: Name queries ---- void queryPlayerName(uint64_t guid); void queryCreatureInfo(uint32_t entry, uint64_t guid); diff --git a/include/game/opcodes.hpp b/include/game/opcodes.hpp index b292f338..4dbc0559 100644 --- a/include/game/opcodes.hpp +++ b/include/game/opcodes.hpp @@ -106,10 +106,14 @@ enum class Opcode : uint16_t { MSG_RAID_READY_CHECK_CONFIRM = 0x3AE, // ---- Duel ---- + CMSG_DUEL_PROPOSED = 0x166, CMSG_DUEL_ACCEPTED = 0x16C, CMSG_DUEL_CANCELLED = 0x16D, SMSG_DUEL_REQUESTED = 0x167, + // ---- Trade ---- + CMSG_INITIATE_TRADE = 0x116, + // ---- Random Roll ---- MSG_RANDOM_ROLL = 0x1FB, diff --git a/include/game/world_packets.hpp b/include/game/world_packets.hpp index 09587147..5c43c2ea 100644 --- a/include/game/world_packets.hpp +++ b/include/game/world_packets.hpp @@ -917,6 +917,40 @@ public: static network::Packet build(); }; +// ============================================================ +// Combat and Trade +// ============================================================ + +/** CMSG_DUEL_PROPOSED packet builder */ +class DuelProposedPacket { +public: + static network::Packet build(uint64_t targetGuid); +}; + +/** CMSG_INITIATE_TRADE packet builder */ +class InitiateTradePacket { +public: + static network::Packet build(uint64_t targetGuid); +}; + +/** CMSG_ATTACKSWING packet builder */ +class AttackSwingPacket { +public: + static network::Packet build(uint64_t targetGuid); +}; + +/** CMSG_ATTACKSTOP packet builder */ +class AttackStopPacket { +public: + static network::Packet build(); +}; + +/** CMSG_CANCEL_CAST packet builder */ +class CancelCastPacket { +public: + static network::Packet build(uint32_t spellId); +}; + // ============================================================ // Random Roll // ============================================================ @@ -1074,18 +1108,6 @@ public: static bool parse(network::Packet& packet, MonsterMoveData& data); }; -/** CMSG_ATTACKSWING packet builder */ -class AttackSwingPacket { -public: - static network::Packet build(uint64_t targetGuid); -}; - -/** CMSG_ATTACKSTOP packet builder */ -class AttackStopPacket { -public: - static network::Packet build(); -}; - /** SMSG_ATTACKSTART data */ struct AttackStartData { uint64_t attackerGuid = 0; @@ -1223,12 +1245,6 @@ public: static network::Packet build(uint32_t spellId, uint64_t targetGuid, uint8_t castCount); }; -/** CMSG_CANCEL_CAST packet builder */ -class CancelCastPacket { -public: - static network::Packet build(uint32_t spellId); -}; - /** CMSG_CANCEL_AURA packet builder */ class CancelAuraPacket { public: diff --git a/src/game/game_handler.cpp b/src/game/game_handler.cpp index f5f9c2b8..a6c58717 100644 --- a/src/game/game_handler.cpp +++ b/src/game/game_handler.cpp @@ -2219,6 +2219,63 @@ void GameHandler::requestRaidInfo() { LOG_INFO("Requested raid info"); } +void GameHandler::proposeDuel(uint64_t targetGuid) { + if (state != WorldState::IN_WORLD || !socket) { + LOG_WARNING("Cannot propose duel: not in world or not connected"); + return; + } + + if (targetGuid == 0) { + addSystemChatMessage("You must target a player to challenge to a duel."); + return; + } + + auto packet = DuelProposedPacket::build(targetGuid); + socket->send(packet); + addSystemChatMessage("You have challenged your target to a duel."); + LOG_INFO("Proposed duel to target: 0x", std::hex, targetGuid, std::dec); +} + +void GameHandler::initiateTrade(uint64_t targetGuid) { + if (state != WorldState::IN_WORLD || !socket) { + LOG_WARNING("Cannot initiate trade: not in world or not connected"); + return; + } + + if (targetGuid == 0) { + addSystemChatMessage("You must target a player to trade with."); + return; + } + + auto packet = InitiateTradePacket::build(targetGuid); + socket->send(packet); + addSystemChatMessage("Requesting trade with target."); + LOG_INFO("Initiated trade with target: 0x", std::hex, targetGuid, std::dec); +} + +void GameHandler::stopCasting() { + if (state != WorldState::IN_WORLD || !socket) { + LOG_WARNING("Cannot stop casting: not in world or not connected"); + return; + } + + if (!casting) { + return; // Not casting anything + } + + // Send cancel cast packet with current spell ID + auto packet = CancelCastPacket::build(currentCastSpellId); + socket->send(packet); + + // Reset casting state + casting = false; + currentCastSpellId = 0; + castTimeRemaining = 0.0f; + castTimeTotal = 0.0f; + + LOG_INFO("Cancelled spell cast"); +} + void GameHandler::releaseSpirit() { if (!playerDead_) return; if (socket && state == WorldState::IN_WORLD) { diff --git a/src/game/world_packets.cpp b/src/game/world_packets.cpp index b6a2aa83..a0bbb3c8 100644 --- a/src/game/world_packets.cpp +++ b/src/game/world_packets.cpp @@ -1452,6 +1452,45 @@ network::Packet RequestRaidInfoPacket::build() { return packet; } +// ============================================================ +// Combat and Trade +// ============================================================ + +network::Packet DuelProposedPacket::build(uint64_t targetGuid) { + network::Packet packet(static_cast(Opcode::CMSG_DUEL_PROPOSED)); + packet.writeUInt64(targetGuid); + LOG_DEBUG("Built CMSG_DUEL_PROPOSED for target: 0x", std::hex, targetGuid, std::dec); + return packet; +} + +network::Packet InitiateTradePacket::build(uint64_t targetGuid) { + network::Packet packet(static_cast(Opcode::CMSG_INITIATE_TRADE)); + packet.writeUInt64(targetGuid); + LOG_DEBUG("Built CMSG_INITIATE_TRADE for target: 0x", std::hex, targetGuid, std::dec); + return packet; +} + +network::Packet AttackSwingPacket::build(uint64_t targetGuid) { + network::Packet packet(static_cast(Opcode::CMSG_ATTACKSWING)); + packet.writeUInt64(targetGuid); + LOG_DEBUG("Built CMSG_ATTACKSWING for target: 0x", std::hex, targetGuid, std::dec); + return packet; +} + +network::Packet AttackStopPacket::build() { + network::Packet packet(static_cast(Opcode::CMSG_ATTACKSTOP)); + LOG_DEBUG("Built CMSG_ATTACKSTOP"); + return packet; +} + +network::Packet CancelCastPacket::build(uint32_t spellId) { + network::Packet packet(static_cast(Opcode::CMSG_CANCEL_CAST)); + packet.writeUInt32(0); // cast count/sequence + packet.writeUInt32(spellId); + LOG_DEBUG("Built CMSG_CANCEL_CAST for spell: ", spellId); + return packet; +} + // ============================================================ // Random Roll // ============================================================ @@ -1777,19 +1816,6 @@ bool MonsterMoveParser::parse(network::Packet& packet, MonsterMoveData& data) { // Phase 2: Combat Core // ============================================================ -network::Packet AttackSwingPacket::build(uint64_t targetGuid) { - network::Packet packet(static_cast(Opcode::CMSG_ATTACKSWING)); - packet.writeUInt64(targetGuid); - LOG_DEBUG("Built CMSG_ATTACKSWING: target=0x", std::hex, targetGuid, std::dec); - return packet; -} - -network::Packet AttackStopPacket::build() { - network::Packet packet(static_cast(Opcode::CMSG_ATTACKSTOP)); - LOG_DEBUG("Built CMSG_ATTACKSTOP"); - return packet; -} - bool AttackStartParser::parse(network::Packet& packet, AttackStartData& data) { if (packet.getSize() < 16) return false; data.attackerGuid = packet.readUInt64(); @@ -1971,13 +1997,6 @@ network::Packet CastSpellPacket::build(uint32_t spellId, uint64_t targetGuid, ui return packet; } -network::Packet CancelCastPacket::build(uint32_t spellId) { - network::Packet packet(static_cast(Opcode::CMSG_CANCEL_CAST)); - packet.writeUInt32(0); // sequence - packet.writeUInt32(spellId); - return packet; -} - network::Packet CancelAuraPacket::build(uint32_t spellId) { network::Packet packet(static_cast(Opcode::CMSG_CANCEL_AURA)); packet.writeUInt32(spellId); diff --git a/src/ui/game_screen.cpp b/src/ui/game_screen.cpp index cdf697fc..c2159b0e 100644 --- a/src/ui/game_screen.cpp +++ b/src/ui/game_screen.cpp @@ -1398,6 +1398,68 @@ void GameScreen::sendChatMessage(game::GameHandler& gameHandler) { return; } + // Combat and Trade commands + if (cmdLower == "duel") { + if (gameHandler.hasTarget()) { + gameHandler.proposeDuel(gameHandler.getTargetGuid()); + } else if (spacePos != std::string::npos) { + // Target player by name (would need name-to-GUID lookup) + game::MessageChatData msg; + msg.type = game::ChatType::SYSTEM; + msg.language = game::ChatLanguage::UNIVERSAL; + msg.message = "You must target a player to challenge to a duel."; + gameHandler.addLocalChatMessage(msg); + } else { + game::MessageChatData msg; + msg.type = game::ChatType::SYSTEM; + msg.language = game::ChatLanguage::UNIVERSAL; + msg.message = "You must target a player to challenge to a duel."; + gameHandler.addLocalChatMessage(msg); + } + chatInputBuffer[0] = '\0'; + return; + } + + if (cmdLower == "trade") { + if (gameHandler.hasTarget()) { + gameHandler.initiateTrade(gameHandler.getTargetGuid()); + } else { + game::MessageChatData msg; + msg.type = game::ChatType::SYSTEM; + msg.language = game::ChatLanguage::UNIVERSAL; + msg.message = "You must target a player to trade with."; + gameHandler.addLocalChatMessage(msg); + } + chatInputBuffer[0] = '\0'; + return; + } + + if (cmdLower == "startattack") { + if (gameHandler.hasTarget()) { + gameHandler.startAutoAttack(gameHandler.getTargetGuid()); + } else { + game::MessageChatData msg; + msg.type = game::ChatType::SYSTEM; + msg.language = game::ChatLanguage::UNIVERSAL; + msg.message = "You have no target."; + gameHandler.addLocalChatMessage(msg); + } + chatInputBuffer[0] = '\0'; + return; + } + + if (cmdLower == "stopattack") { + gameHandler.stopAutoAttack(); + chatInputBuffer[0] = '\0'; + return; + } + + if (cmdLower == "stopcasting") { + gameHandler.stopCasting(); + chatInputBuffer[0] = '\0'; + return; + } + // Chat channel slash commands bool isChannelCommand = false; if (cmdLower == "s" || cmdLower == "say") {