mirror of
https://github.com/Kelsidavis/WoWee.git
synced 2026-04-17 17:43:52 +00:00
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:
parent
7c5d688c00
commit
06facc0060
6 changed files with 337 additions and 5 deletions
|
|
@ -933,13 +933,38 @@ public:
|
||||||
enum class TradeStatus : uint8_t {
|
enum class TradeStatus : uint8_t {
|
||||||
None = 0, PendingIncoming, Open, Accepted, Complete
|
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_; }
|
TradeStatus getTradeStatus() const { return tradeStatus_; }
|
||||||
bool hasPendingTradeRequest() const { return tradeStatus_ == TradeStatus::PendingIncoming; }
|
bool hasPendingTradeRequest() const { return tradeStatus_ == TradeStatus::PendingIncoming; }
|
||||||
|
bool isTradeOpen() const { return tradeStatus_ == TradeStatus::Open || tradeStatus_ == TradeStatus::Accepted; }
|
||||||
const std::string& getTradePeerName() const { return tradePeerName_; }
|
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 acceptTradeRequest(); // respond to incoming SMSG_TRADE_STATUS(1) with CMSG_BEGIN_TRADE
|
||||||
void declineTradeRequest(); // respond with CMSG_CANCEL_TRADE
|
void declineTradeRequest(); // respond with CMSG_CANCEL_TRADE
|
||||||
void acceptTrade(); // lock in offer: CMSG_ACCEPT_TRADE
|
void acceptTrade(); // lock in offer: CMSG_ACCEPT_TRADE
|
||||||
void cancelTrade(); // CMSG_CANCEL_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 ----
|
// ---- Duel ----
|
||||||
bool hasPendingDuelRequest() const { return pendingDuelRequest_; }
|
bool hasPendingDuelRequest() const { return pendingDuelRequest_; }
|
||||||
|
|
@ -1653,6 +1678,8 @@ private:
|
||||||
void handleQuestConfirmAccept(network::Packet& packet);
|
void handleQuestConfirmAccept(network::Packet& packet);
|
||||||
void handleSummonRequest(network::Packet& packet);
|
void handleSummonRequest(network::Packet& packet);
|
||||||
void handleTradeStatus(network::Packet& packet);
|
void handleTradeStatus(network::Packet& packet);
|
||||||
|
void handleTradeStatusExtended(network::Packet& packet);
|
||||||
|
void resetTradeState();
|
||||||
void handleDuelRequested(network::Packet& packet);
|
void handleDuelRequested(network::Packet& packet);
|
||||||
void handleDuelComplete(network::Packet& packet);
|
void handleDuelComplete(network::Packet& packet);
|
||||||
void handleDuelWinner(network::Packet& packet);
|
void handleDuelWinner(network::Packet& packet);
|
||||||
|
|
@ -2077,6 +2104,10 @@ private:
|
||||||
TradeStatus tradeStatus_ = TradeStatus::None;
|
TradeStatus tradeStatus_ = TradeStatus::None;
|
||||||
uint64_t tradePeerGuid_= 0;
|
uint64_t tradePeerGuid_= 0;
|
||||||
std::string tradePeerName_;
|
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
|
// Duel state
|
||||||
bool pendingDuelRequest_ = false;
|
bool pendingDuelRequest_ = false;
|
||||||
|
|
|
||||||
|
|
@ -1356,6 +1356,33 @@ public:
|
||||||
static network::Packet build();
|
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 */
|
/** CMSG_ATTACKSWING packet builder */
|
||||||
class AttackSwingPacket {
|
class AttackSwingPacket {
|
||||||
public:
|
public:
|
||||||
|
|
|
||||||
|
|
@ -224,6 +224,7 @@ private:
|
||||||
void renderDuelRequestPopup(game::GameHandler& gameHandler);
|
void renderDuelRequestPopup(game::GameHandler& gameHandler);
|
||||||
void renderLootRollPopup(game::GameHandler& gameHandler);
|
void renderLootRollPopup(game::GameHandler& gameHandler);
|
||||||
void renderTradeRequestPopup(game::GameHandler& gameHandler);
|
void renderTradeRequestPopup(game::GameHandler& gameHandler);
|
||||||
|
void renderTradeWindow(game::GameHandler& gameHandler);
|
||||||
void renderSummonRequestPopup(game::GameHandler& gameHandler);
|
void renderSummonRequestPopup(game::GameHandler& gameHandler);
|
||||||
void renderSharedQuestPopup(game::GameHandler& gameHandler);
|
void renderSharedQuestPopup(game::GameHandler& gameHandler);
|
||||||
void renderItemTextWindow(game::GameHandler& gameHandler);
|
void renderItemTextWindow(game::GameHandler& gameHandler);
|
||||||
|
|
|
||||||
|
|
@ -3102,9 +3102,11 @@ void GameHandler::handlePacket(network::Packet& packet) {
|
||||||
addSystemChatMessage("Summon cancelled.");
|
addSystemChatMessage("Summon cancelled.");
|
||||||
break;
|
break;
|
||||||
case Opcode::SMSG_TRADE_STATUS:
|
case Opcode::SMSG_TRADE_STATUS:
|
||||||
case Opcode::SMSG_TRADE_STATUS_EXTENDED:
|
|
||||||
handleTradeStatus(packet);
|
handleTradeStatus(packet);
|
||||||
break;
|
break;
|
||||||
|
case Opcode::SMSG_TRADE_STATUS_EXTENDED:
|
||||||
|
handleTradeStatusExtended(packet);
|
||||||
|
break;
|
||||||
case Opcode::SMSG_LOOT_ROLL:
|
case Opcode::SMSG_LOOT_ROLL:
|
||||||
handleLootRoll(packet);
|
handleLootRoll(packet);
|
||||||
break;
|
break;
|
||||||
|
|
@ -19047,13 +19049,17 @@ void GameHandler::handleTradeStatus(network::Packet& packet) {
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
case 2: // OPEN_WINDOW
|
case 2: // OPEN_WINDOW
|
||||||
|
myTradeSlots_.fill(TradeSlot{});
|
||||||
|
peerTradeSlots_.fill(TradeSlot{});
|
||||||
|
myTradeGold_ = 0;
|
||||||
|
peerTradeGold_ = 0;
|
||||||
tradeStatus_ = TradeStatus::Open;
|
tradeStatus_ = TradeStatus::Open;
|
||||||
addSystemChatMessage("Trade window opened.");
|
addSystemChatMessage("Trade window opened.");
|
||||||
break;
|
break;
|
||||||
case 3: // CANCELLED
|
case 3: // CANCELLED
|
||||||
case 9: // REJECTED
|
case 9: // REJECTED
|
||||||
case 12: // CLOSE_WINDOW
|
case 12: // CLOSE_WINDOW
|
||||||
tradeStatus_ = TradeStatus::None;
|
resetTradeState();
|
||||||
addSystemChatMessage("Trade cancelled.");
|
addSystemChatMessage("Trade cancelled.");
|
||||||
break;
|
break;
|
||||||
case 4: // ACCEPTED (partner accepted)
|
case 4: // ACCEPTED (partner accepted)
|
||||||
|
|
@ -19061,9 +19067,8 @@ void GameHandler::handleTradeStatus(network::Packet& packet) {
|
||||||
addSystemChatMessage("Trade accepted. Awaiting other player...");
|
addSystemChatMessage("Trade accepted. Awaiting other player...");
|
||||||
break;
|
break;
|
||||||
case 8: // COMPLETE
|
case 8: // COMPLETE
|
||||||
tradeStatus_ = TradeStatus::Complete;
|
|
||||||
addSystemChatMessage("Trade complete!");
|
addSystemChatMessage("Trade complete!");
|
||||||
tradeStatus_ = TradeStatus::None; // reset after notification
|
resetTradeState();
|
||||||
break;
|
break;
|
||||||
case 7: // BACK_TO_TRADE (unaccepted after a change)
|
case 7: // BACK_TO_TRADE (unaccepted after a change)
|
||||||
tradeStatus_ = TradeStatus::Open;
|
tradeStatus_ = TradeStatus::Open;
|
||||||
|
|
@ -19102,10 +19107,104 @@ void GameHandler::acceptTrade() {
|
||||||
|
|
||||||
void GameHandler::cancelTrade() {
|
void GameHandler::cancelTrade() {
|
||||||
if (!socket) return;
|
if (!socket) return;
|
||||||
tradeStatus_ = TradeStatus::None;
|
resetTradeState();
|
||||||
socket->send(CancelTradePacket::build());
|
socket->send(CancelTradePacket::build());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void GameHandler::setTradeItem(uint8_t tradeSlot, uint8_t bag, uint8_t bagSlot) {
|
||||||
|
if (!isTradeOpen() || !socket || tradeSlot >= TRADE_SLOT_COUNT) return;
|
||||||
|
socket->send(SetTradeItemPacket::build(tradeSlot, bag, bagSlot));
|
||||||
|
}
|
||||||
|
|
||||||
|
void GameHandler::clearTradeItem(uint8_t tradeSlot) {
|
||||||
|
if (!isTradeOpen() || !socket || tradeSlot >= TRADE_SLOT_COUNT) return;
|
||||||
|
myTradeSlots_[tradeSlot] = TradeSlot{};
|
||||||
|
socket->send(ClearTradeItemPacket::build(tradeSlot));
|
||||||
|
}
|
||||||
|
|
||||||
|
void GameHandler::setTradeGold(uint64_t copper) {
|
||||||
|
if (!isTradeOpen() || !socket) return;
|
||||||
|
myTradeGold_ = copper;
|
||||||
|
socket->send(SetTradeGoldPacket::build(copper));
|
||||||
|
}
|
||||||
|
|
||||||
|
void GameHandler::resetTradeState() {
|
||||||
|
tradeStatus_ = TradeStatus::None;
|
||||||
|
myTradeGold_ = 0;
|
||||||
|
peerTradeGold_ = 0;
|
||||||
|
myTradeSlots_.fill(TradeSlot{});
|
||||||
|
peerTradeSlots_.fill(TradeSlot{});
|
||||||
|
}
|
||||||
|
|
||||||
|
void GameHandler::handleTradeStatusExtended(network::Packet& packet) {
|
||||||
|
// WotLK 3.3.5a SMSG_TRADE_STATUS_EXTENDED format:
|
||||||
|
// uint8 isSelfState (1 = my trade window, 0 = peer's)
|
||||||
|
// uint32 tradeId
|
||||||
|
// uint32 slotCount (7: 6 normal + 1 extra for enchanting)
|
||||||
|
// Per slot (up to slotCount):
|
||||||
|
// uint8 slotIndex
|
||||||
|
// uint32 itemId
|
||||||
|
// uint32 displayId
|
||||||
|
// uint32 stackCount
|
||||||
|
// uint8 isWrapped
|
||||||
|
// uint64 giftCreatorGuid
|
||||||
|
// uint32 enchantId (and several more enchant/stat fields)
|
||||||
|
// ... (complex; we parse only the essential fields)
|
||||||
|
// uint64 coins (gold offered by the sender of this message)
|
||||||
|
|
||||||
|
size_t rem = packet.getSize() - packet.getReadPos();
|
||||||
|
if (rem < 9) return;
|
||||||
|
|
||||||
|
uint8_t isSelf = packet.readUInt8();
|
||||||
|
uint32_t tradeId = packet.readUInt32(); (void)tradeId;
|
||||||
|
uint32_t slotCount= packet.readUInt32();
|
||||||
|
|
||||||
|
auto& slots = isSelf ? myTradeSlots_ : peerTradeSlots_;
|
||||||
|
|
||||||
|
for (uint32_t i = 0; i < slotCount && (packet.getSize() - packet.getReadPos()) >= 14; ++i) {
|
||||||
|
uint8_t slotIdx = packet.readUInt8();
|
||||||
|
uint32_t itemId = packet.readUInt32();
|
||||||
|
uint32_t displayId = packet.readUInt32();
|
||||||
|
uint32_t stackCount = packet.readUInt32();
|
||||||
|
|
||||||
|
// isWrapped + giftCreatorGuid + several enchant fields — skip them all
|
||||||
|
// We need at least 1+8+4*5 = 29 bytes for the rest of this slot entry
|
||||||
|
bool isWrapped = false;
|
||||||
|
if (packet.getSize() - packet.getReadPos() >= 1) {
|
||||||
|
isWrapped = (packet.readUInt8() != 0);
|
||||||
|
}
|
||||||
|
// Skip giftCreatorGuid (8) + enchantId*5 (20) + suffixFactor (4) + randPropId (4) + lockId (4)
|
||||||
|
// + maxDurability (4) + durability (4) = 49 bytes
|
||||||
|
// Plus if wrapped: giftCreatorGuid already consumed; additional guid = 0
|
||||||
|
constexpr size_t SLOT_TRAIL = 49;
|
||||||
|
if (packet.getSize() - packet.getReadPos() >= SLOT_TRAIL) {
|
||||||
|
packet.setReadPos(packet.getReadPos() + SLOT_TRAIL);
|
||||||
|
} else {
|
||||||
|
packet.setReadPos(packet.getSize());
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
(void)isWrapped;
|
||||||
|
|
||||||
|
if (slotIdx < TRADE_SLOT_COUNT) {
|
||||||
|
TradeSlot& s = slots[slotIdx];
|
||||||
|
s.itemId = itemId;
|
||||||
|
s.displayId = displayId;
|
||||||
|
s.stackCount = stackCount;
|
||||||
|
s.occupied = (itemId != 0);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Gold offered (uint64 copper)
|
||||||
|
if (packet.getSize() - packet.getReadPos() >= 8) {
|
||||||
|
uint64_t coins = packet.readUInt64();
|
||||||
|
if (isSelf) myTradeGold_ = coins;
|
||||||
|
else peerTradeGold_ = coins;
|
||||||
|
}
|
||||||
|
|
||||||
|
LOG_DEBUG("SMSG_TRADE_STATUS_EXTENDED: isSelf=", (int)isSelf,
|
||||||
|
" myGold=", myTradeGold_, " peerGold=", peerTradeGold_);
|
||||||
|
}
|
||||||
|
|
||||||
// ---------------------------------------------------------------------------
|
// ---------------------------------------------------------------------------
|
||||||
// Group loot roll (SMSG_LOOT_ROLL / SMSG_LOOT_ROLL_WON / CMSG_LOOT_ROLL)
|
// Group loot roll (SMSG_LOOT_ROLL / SMSG_LOOT_ROLL_WON / CMSG_LOOT_ROLL)
|
||||||
// ---------------------------------------------------------------------------
|
// ---------------------------------------------------------------------------
|
||||||
|
|
|
||||||
|
|
@ -2177,6 +2177,35 @@ network::Packet AcceptTradePacket::build() {
|
||||||
return packet;
|
return packet;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
network::Packet SetTradeItemPacket::build(uint8_t tradeSlot, uint8_t bag, uint8_t bagSlot) {
|
||||||
|
network::Packet packet(wireOpcode(Opcode::CMSG_SET_TRADE_ITEM));
|
||||||
|
packet.writeUInt8(tradeSlot);
|
||||||
|
packet.writeUInt8(bag);
|
||||||
|
packet.writeUInt8(bagSlot);
|
||||||
|
LOG_DEBUG("Built CMSG_SET_TRADE_ITEM slot=", (int)tradeSlot, " bag=", (int)bag, " bagSlot=", (int)bagSlot);
|
||||||
|
return packet;
|
||||||
|
}
|
||||||
|
|
||||||
|
network::Packet ClearTradeItemPacket::build(uint8_t tradeSlot) {
|
||||||
|
network::Packet packet(wireOpcode(Opcode::CMSG_CLEAR_TRADE_ITEM));
|
||||||
|
packet.writeUInt8(tradeSlot);
|
||||||
|
LOG_DEBUG("Built CMSG_CLEAR_TRADE_ITEM slot=", (int)tradeSlot);
|
||||||
|
return packet;
|
||||||
|
}
|
||||||
|
|
||||||
|
network::Packet SetTradeGoldPacket::build(uint64_t copper) {
|
||||||
|
network::Packet packet(wireOpcode(Opcode::CMSG_SET_TRADE_GOLD));
|
||||||
|
packet.writeUInt64(copper);
|
||||||
|
LOG_DEBUG("Built CMSG_SET_TRADE_GOLD copper=", copper);
|
||||||
|
return packet;
|
||||||
|
}
|
||||||
|
|
||||||
|
network::Packet UnacceptTradePacket::build() {
|
||||||
|
network::Packet packet(wireOpcode(Opcode::CMSG_UNACCEPT_TRADE));
|
||||||
|
LOG_DEBUG("Built CMSG_UNACCEPT_TRADE");
|
||||||
|
return packet;
|
||||||
|
}
|
||||||
|
|
||||||
network::Packet InitiateTradePacket::build(uint64_t targetGuid) {
|
network::Packet InitiateTradePacket::build(uint64_t targetGuid) {
|
||||||
network::Packet packet(wireOpcode(Opcode::CMSG_INITIATE_TRADE));
|
network::Packet packet(wireOpcode(Opcode::CMSG_INITIATE_TRADE));
|
||||||
packet.writeUInt64(targetGuid);
|
packet.writeUInt64(targetGuid);
|
||||||
|
|
|
||||||
|
|
@ -414,6 +414,7 @@ void GameScreen::render(game::GameHandler& gameHandler) {
|
||||||
renderDuelRequestPopup(gameHandler);
|
renderDuelRequestPopup(gameHandler);
|
||||||
renderLootRollPopup(gameHandler);
|
renderLootRollPopup(gameHandler);
|
||||||
renderTradeRequestPopup(gameHandler);
|
renderTradeRequestPopup(gameHandler);
|
||||||
|
renderTradeWindow(gameHandler);
|
||||||
renderSummonRequestPopup(gameHandler);
|
renderSummonRequestPopup(gameHandler);
|
||||||
renderSharedQuestPopup(gameHandler);
|
renderSharedQuestPopup(gameHandler);
|
||||||
renderItemTextWindow(gameHandler);
|
renderItemTextWindow(gameHandler);
|
||||||
|
|
@ -5980,6 +5981,150 @@ void GameScreen::renderTradeRequestPopup(game::GameHandler& gameHandler) {
|
||||||
ImGui::End();
|
ImGui::End();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void GameScreen::renderTradeWindow(game::GameHandler& gameHandler) {
|
||||||
|
if (!gameHandler.isTradeOpen()) return;
|
||||||
|
|
||||||
|
const auto& mySlots = gameHandler.getMyTradeSlots();
|
||||||
|
const auto& peerSlots = gameHandler.getPeerTradeSlots();
|
||||||
|
const uint64_t myGold = gameHandler.getMyTradeGold();
|
||||||
|
const uint64_t peerGold = gameHandler.getPeerTradeGold();
|
||||||
|
const auto& peerName = gameHandler.getTradePeerName();
|
||||||
|
|
||||||
|
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.0f - 240.0f, screenH / 2.0f - 180.0f), ImGuiCond_Once);
|
||||||
|
ImGui::SetNextWindowSize(ImVec2(480.0f, 360.0f), ImGuiCond_Once);
|
||||||
|
|
||||||
|
bool open = true;
|
||||||
|
if (ImGui::Begin(("Trade with " + peerName).c_str(), &open,
|
||||||
|
ImGuiWindowFlags_NoCollapse | ImGuiWindowFlags_NoResize)) {
|
||||||
|
|
||||||
|
auto formatGold = [](uint64_t copper, char* buf, size_t bufsz) {
|
||||||
|
uint64_t g = copper / 10000;
|
||||||
|
uint64_t s = (copper % 10000) / 100;
|
||||||
|
uint64_t c = copper % 100;
|
||||||
|
if (g > 0) std::snprintf(buf, bufsz, "%llug %llus %lluc",
|
||||||
|
(unsigned long long)g, (unsigned long long)s, (unsigned long long)c);
|
||||||
|
else if (s > 0) std::snprintf(buf, bufsz, "%llus %lluc",
|
||||||
|
(unsigned long long)s, (unsigned long long)c);
|
||||||
|
else std::snprintf(buf, bufsz, "%lluc", (unsigned long long)c);
|
||||||
|
};
|
||||||
|
|
||||||
|
auto renderSlotColumn = [&](const char* label,
|
||||||
|
const std::array<game::GameHandler::TradeSlot,
|
||||||
|
game::GameHandler::TRADE_SLOT_COUNT>& slots,
|
||||||
|
uint64_t gold, bool isMine) {
|
||||||
|
ImGui::Text("%s", label);
|
||||||
|
ImGui::Separator();
|
||||||
|
|
||||||
|
for (int i = 0; i < game::GameHandler::TRADE_SLOT_COUNT; ++i) {
|
||||||
|
const auto& slot = slots[i];
|
||||||
|
ImGui::PushID(i * (isMine ? 1 : -1) - (isMine ? 0 : 100));
|
||||||
|
|
||||||
|
if (slot.occupied && slot.itemId != 0) {
|
||||||
|
const auto* info = gameHandler.getItemInfo(slot.itemId);
|
||||||
|
std::string name = (info && info->valid && !info->name.empty())
|
||||||
|
? info->name
|
||||||
|
: ("Item " + std::to_string(slot.itemId));
|
||||||
|
if (slot.stackCount > 1)
|
||||||
|
name += " x" + std::to_string(slot.stackCount);
|
||||||
|
ImGui::TextColored(ImVec4(1.0f, 0.9f, 0.5f, 1.0f), " %d. %s", i + 1, name.c_str());
|
||||||
|
|
||||||
|
if (isMine && ImGui::IsItemHovered() && ImGui::IsMouseDoubleClicked(ImGuiMouseButton_Left)) {
|
||||||
|
gameHandler.clearTradeItem(static_cast<uint8_t>(i));
|
||||||
|
}
|
||||||
|
if (isMine && ImGui::IsItemHovered()) {
|
||||||
|
ImGui::SetTooltip("Double-click to remove");
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
ImGui::TextDisabled(" %d. (empty)", i + 1);
|
||||||
|
|
||||||
|
// Allow dragging inventory items into trade slots via right-click context menu
|
||||||
|
if (isMine && ImGui::IsItemClicked(ImGuiMouseButton_Right)) {
|
||||||
|
ImGui::OpenPopup(("##additem" + std::to_string(i)).c_str());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (isMine) {
|
||||||
|
// Drag-from-inventory: show small popup listing bag items
|
||||||
|
if (ImGui::BeginPopup(("##additem" + std::to_string(i)).c_str())) {
|
||||||
|
ImGui::TextDisabled("Add from inventory:");
|
||||||
|
const auto& inv = gameHandler.getInventory();
|
||||||
|
// Backpack slots 0-15 (bag=255)
|
||||||
|
for (int si = 0; si < game::Inventory::BACKPACK_SLOTS; ++si) {
|
||||||
|
const auto& slot = inv.getBackpackSlot(si);
|
||||||
|
if (slot.empty()) continue;
|
||||||
|
const auto* ii = gameHandler.getItemInfo(slot.item.itemId);
|
||||||
|
std::string iname = (ii && ii->valid && !ii->name.empty())
|
||||||
|
? ii->name
|
||||||
|
: (!slot.item.name.empty() ? slot.item.name
|
||||||
|
: ("Item " + std::to_string(slot.item.itemId)));
|
||||||
|
if (ImGui::Selectable(iname.c_str())) {
|
||||||
|
// bag=255 = main backpack
|
||||||
|
gameHandler.setTradeItem(static_cast<uint8_t>(i), 255u,
|
||||||
|
static_cast<uint8_t>(si));
|
||||||
|
ImGui::CloseCurrentPopup();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
ImGui::EndPopup();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
ImGui::PopID();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Gold row
|
||||||
|
char gbuf[48];
|
||||||
|
formatGold(gold, gbuf, sizeof(gbuf));
|
||||||
|
ImGui::Spacing();
|
||||||
|
if (isMine) {
|
||||||
|
ImGui::Text("Gold offered: %s", gbuf);
|
||||||
|
static char goldInput[32] = "0";
|
||||||
|
ImGui::SetNextItemWidth(120.0f);
|
||||||
|
if (ImGui::InputText("##goldset", goldInput, sizeof(goldInput),
|
||||||
|
ImGuiInputTextFlags_CharsDecimal | ImGuiInputTextFlags_EnterReturnsTrue)) {
|
||||||
|
uint64_t copper = std::strtoull(goldInput, nullptr, 10);
|
||||||
|
gameHandler.setTradeGold(copper);
|
||||||
|
}
|
||||||
|
ImGui::SameLine();
|
||||||
|
ImGui::TextDisabled("(copper, Enter to set)");
|
||||||
|
} else {
|
||||||
|
ImGui::Text("Gold offered: %s", gbuf);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// Two-column layout: my offer | peer offer
|
||||||
|
float colW = ImGui::GetContentRegionAvail().x * 0.5f - 4.0f;
|
||||||
|
ImGui::BeginChild("##myoffer", ImVec2(colW, 240.0f), true);
|
||||||
|
renderSlotColumn("Your offer", mySlots, myGold, true);
|
||||||
|
ImGui::EndChild();
|
||||||
|
|
||||||
|
ImGui::SameLine();
|
||||||
|
|
||||||
|
ImGui::BeginChild("##peroffer", ImVec2(colW, 240.0f), true);
|
||||||
|
renderSlotColumn((peerName + "'s offer").c_str(), peerSlots, peerGold, false);
|
||||||
|
ImGui::EndChild();
|
||||||
|
|
||||||
|
// Buttons
|
||||||
|
ImGui::Spacing();
|
||||||
|
ImGui::Separator();
|
||||||
|
float bw = (ImGui::GetContentRegionAvail().x - ImGui::GetStyle().ItemSpacing.x) * 0.5f;
|
||||||
|
if (ImGui::Button("Accept Trade", ImVec2(bw, 0))) {
|
||||||
|
gameHandler.acceptTrade();
|
||||||
|
}
|
||||||
|
ImGui::SameLine();
|
||||||
|
if (ImGui::Button("Cancel", ImVec2(bw, 0))) {
|
||||||
|
gameHandler.cancelTrade();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
ImGui::End();
|
||||||
|
|
||||||
|
if (!open) {
|
||||||
|
gameHandler.cancelTrade();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
void GameScreen::renderLootRollPopup(game::GameHandler& gameHandler) {
|
void GameScreen::renderLootRollPopup(game::GameHandler& gameHandler) {
|
||||||
if (!gameHandler.hasPendingLootRoll()) return;
|
if (!gameHandler.hasPendingLootRoll()) return;
|
||||||
|
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue