feat: implement trade window UI with item slots and gold offering

Previously trade only showed an accept/decline popup with no way to
actually offer items or gold. This commit adds the complete trade flow:

Packets:
- CMSG_SET_TRADE_ITEM (tradeSlot, bag, bagSlot) — add item to slot
- CMSG_CLEAR_TRADE_ITEM (tradeSlot) — remove item from slot
- CMSG_SET_TRADE_GOLD (uint64 copper) — set gold offered
- CMSG_UNACCEPT_TRADE — unaccept without cancelling
- SMSG_TRADE_STATUS_EXTENDED parser — updates trade slot/gold state

State:
- TradeSlot struct: itemId, displayId, stackCount, bag, bagSlot
- myTradeSlots_/peerTradeSlots_ arrays (6 slots each)
- myTradeGold_/peerTradeGold_ (copper)
- resetTradeState() helper clears all state on cancel/complete/close

UI (renderTradeWindow):
- Two-column layout: my offer | peer offer
- Each column shows 6 item slots with item names
- Double-click own slot to remove; right-click empty slot to open
  backpack picker popup
- Gold input field (copper, Enter to set)
- Accept Trade / Cancel buttons
- Window close button triggers cancel trade
This commit is contained in:
Kelsi 2026-03-11 00:44:07 -07:00
parent 7c5d688c00
commit 06facc0060
6 changed files with 337 additions and 5 deletions

View file

@ -933,13 +933,38 @@ public:
enum class TradeStatus : uint8_t {
None = 0, PendingIncoming, Open, Accepted, Complete
};
static constexpr int TRADE_SLOT_COUNT = 6; // WoW has 6 normal trade slots + slot 6 for non-trade item
struct TradeSlot {
uint32_t itemId = 0;
uint32_t displayId = 0;
uint32_t stackCount = 0;
uint64_t itemGuid = 0;
uint8_t bag = 0xFF; // 0xFF = not set
uint8_t bagSlot = 0xFF;
bool occupied = false;
};
TradeStatus getTradeStatus() const { return tradeStatus_; }
bool hasPendingTradeRequest() const { return tradeStatus_ == TradeStatus::PendingIncoming; }
bool isTradeOpen() const { return tradeStatus_ == TradeStatus::Open || tradeStatus_ == TradeStatus::Accepted; }
const std::string& getTradePeerName() const { return tradePeerName_; }
// My trade slots (what I'm offering)
const std::array<TradeSlot, TRADE_SLOT_COUNT>& getMyTradeSlots() const { return myTradeSlots_; }
// Peer's trade slots (what they're offering)
const std::array<TradeSlot, TRADE_SLOT_COUNT>& getPeerTradeSlots() const { return peerTradeSlots_; }
uint64_t getMyTradeGold() const { return myTradeGold_; }
uint64_t getPeerTradeGold() const { return peerTradeGold_; }
void acceptTradeRequest(); // respond to incoming SMSG_TRADE_STATUS(1) with CMSG_BEGIN_TRADE
void declineTradeRequest(); // respond with CMSG_CANCEL_TRADE
void acceptTrade(); // lock in offer: CMSG_ACCEPT_TRADE
void cancelTrade(); // CMSG_CANCEL_TRADE
void setTradeItem(uint8_t tradeSlot, uint8_t bag, uint8_t bagSlot);
void clearTradeItem(uint8_t tradeSlot);
void setTradeGold(uint64_t copper);
// ---- Duel ----
bool hasPendingDuelRequest() const { return pendingDuelRequest_; }
@ -1653,6 +1678,8 @@ private:
void handleQuestConfirmAccept(network::Packet& packet);
void handleSummonRequest(network::Packet& packet);
void handleTradeStatus(network::Packet& packet);
void handleTradeStatusExtended(network::Packet& packet);
void resetTradeState();
void handleDuelRequested(network::Packet& packet);
void handleDuelComplete(network::Packet& packet);
void handleDuelWinner(network::Packet& packet);
@ -2077,6 +2104,10 @@ private:
TradeStatus tradeStatus_ = TradeStatus::None;
uint64_t tradePeerGuid_= 0;
std::string tradePeerName_;
std::array<TradeSlot, TRADE_SLOT_COUNT> myTradeSlots_{};
std::array<TradeSlot, TRADE_SLOT_COUNT> peerTradeSlots_{};
uint64_t myTradeGold_ = 0;
uint64_t peerTradeGold_ = 0;
// Duel state
bool pendingDuelRequest_ = false;

View file

@ -1356,6 +1356,33 @@ public:
static network::Packet build();
};
/** CMSG_SET_TRADE_ITEM packet builder (tradeSlot, bag, bagSlot) */
class SetTradeItemPacket {
public:
// tradeSlot: 0-5 (normal) or 6 (backpack money-only slot)
// bag: 255 = main backpack, 19-22 = bag slots
// bagSlot: slot within bag
static network::Packet build(uint8_t tradeSlot, uint8_t bag, uint8_t bagSlot);
};
/** CMSG_CLEAR_TRADE_ITEM packet builder (remove item from trade slot) */
class ClearTradeItemPacket {
public:
static network::Packet build(uint8_t tradeSlot);
};
/** CMSG_SET_TRADE_GOLD packet builder (gold offered, in copper) */
class SetTradeGoldPacket {
public:
static network::Packet build(uint64_t copper);
};
/** CMSG_UNACCEPT_TRADE packet builder (unaccept without cancelling) */
class UnacceptTradePacket {
public:
static network::Packet build();
};
/** CMSG_ATTACKSWING packet builder */
class AttackSwingPacket {
public:

View file

@ -224,6 +224,7 @@ private:
void renderDuelRequestPopup(game::GameHandler& gameHandler);
void renderLootRollPopup(game::GameHandler& gameHandler);
void renderTradeRequestPopup(game::GameHandler& gameHandler);
void renderTradeWindow(game::GameHandler& gameHandler);
void renderSummonRequestPopup(game::GameHandler& gameHandler);
void renderSharedQuestPopup(game::GameHandler& gameHandler);
void renderItemTextWindow(game::GameHandler& gameHandler);