Improve auction house UI with search filters, pagination, sell picker, and tooltips

- Add item class/subclass category filters (Weapon, Armor, etc.) with correct WoW 3.3.5a IDs
- Add sell item picker dropdown with icons and Create Auction button
- Add pagination (Prev/Next) for browse results with filter preservation
- Add Buy/Bid action buttons to Bids tab
- Add item icons and stat tooltips on hover across all three tabs
- Add Enter-to-search from name field and search delay countdown
- Parse SMSG_AUCTION_OWNER/BIDDER_NOTIFICATION into chat messages
- Auto-refresh browse results after bid/buyout using saved search params
- Clamp level range inputs to 0-80
This commit is contained in:
Kelsi 2026-02-25 14:44:44 -08:00
parent 9906269671
commit 1ae5fe867c
6 changed files with 945 additions and 157 deletions

View file

@ -355,6 +355,11 @@ public:
void acceptGuildInvite();
void declineGuildInvite();
void queryGuildInfo(uint32_t guildId);
void createGuild(const std::string& guildName);
void addGuildRank(const std::string& rankName);
void deleteGuildRank();
void requestPetitionShowlist(uint64_t npcGuid);
void buyPetition(uint64_t npcGuid, const std::string& guildName);
// Guild state accessors
bool isInGuild() const {
@ -369,6 +374,13 @@ public:
bool hasPendingGuildInvite() const { return pendingGuildInvite_; }
const std::string& getPendingGuildInviterName() const { return pendingGuildInviterName_; }
const std::string& getPendingGuildInviteGuildName() const { return pendingGuildInviteGuildName_; }
const GuildInfoData& getGuildInfoData() const { return guildInfoData_; }
const GuildQueryResponseData& getGuildQueryData() const { return guildQueryData_; }
bool hasGuildInfoData() const { return guildInfoData_.isValid(); }
bool hasPetitionShowlist() const { return showPetitionDialog_; }
void clearPetitionDialog() { showPetitionDialog_ = false; }
uint32_t getPetitionCost() const { return petitionCost_; }
uint64_t getPetitionNpcGuid() const { return petitionNpcGuid_; }
// Ready check
void initiateReadyCheck();
@ -1123,6 +1135,8 @@ private:
void handleGuildEvent(network::Packet& packet);
void handleGuildInvite(network::Packet& packet);
void handleGuildCommandResult(network::Packet& packet);
void handlePetitionShowlist(network::Packet& packet);
void handleTurnInPetitionResults(network::Packet& packet);
// ---- Character creation handler ----
void handleCharCreateResponse(network::Packet& packet);
@ -1467,10 +1481,15 @@ private:
std::string guildName_;
std::vector<std::string> guildRankNames_;
GuildRosterData guildRoster_;
GuildInfoData guildInfoData_;
GuildQueryResponseData guildQueryData_;
bool hasGuildRoster_ = false;
bool pendingGuildInvite_ = false;
std::string pendingGuildInviterName_;
std::string pendingGuildInviteGuildName_;
bool showPetitionDialog_ = false;
uint32_t petitionCost_ = 0;
uint64_t petitionNpcGuid_ = 0;
uint64_t activeCharacterGuid_ = 0;
Race playerRace_ = Race::HUMAN;
@ -1607,6 +1626,18 @@ private:
AuctionListResult auctionBidderResults_;
int auctionActiveTab_ = 0; // 0=Browse, 1=Bids, 2=Auctions
float auctionSearchDelayTimer_ = 0.0f;
// Last search params for re-query (pagination, auto-refresh after bid/buyout)
struct AuctionSearchParams {
std::string name;
uint8_t levelMin = 0, levelMax = 0;
uint32_t quality = 0xFFFFFFFF;
uint32_t itemClass = 0xFFFFFFFF;
uint32_t itemSubClass = 0xFFFFFFFF;
uint32_t invTypeMask = 0;
uint8_t usableOnly = 0;
uint32_t offset = 0;
};
AuctionSearchParams lastAuctionSearch_;
// Routing: which result vector to populate from next SMSG_AUCTION_LIST_RESULT
enum class AuctionResultTarget { BROWSE, OWNER, BIDDER };
AuctionResultTarget pendingAuctionTarget_ = AuctionResultTarget::BROWSE;

View file

@ -1050,6 +1050,60 @@ public:
static network::Packet build();
};
/** CMSG_GUILD_CREATE packet builder */
class GuildCreatePacket {
public:
static network::Packet build(const std::string& guildName);
};
/** CMSG_GUILD_ADD_RANK packet builder */
class GuildAddRankPacket {
public:
static network::Packet build(const std::string& rankName);
};
/** CMSG_GUILD_DEL_RANK packet builder (empty body) */
class GuildDelRankPacket {
public:
static network::Packet build();
};
/** CMSG_PETITION_SHOWLIST packet builder */
class PetitionShowlistPacket {
public:
static network::Packet build(uint64_t npcGuid);
};
/** CMSG_PETITION_BUY packet builder */
class PetitionBuyPacket {
public:
static network::Packet build(uint64_t npcGuid, const std::string& guildName);
};
/** SMSG_PETITION_SHOWLIST data */
struct PetitionShowlistData {
uint64_t npcGuid = 0;
uint32_t itemId = 0;
uint32_t displayId = 0;
uint32_t cost = 0;
uint32_t charterType = 0;
uint32_t requiredSigs = 0;
bool isValid() const { return npcGuid != 0; }
};
/** SMSG_PETITION_SHOWLIST parser */
class PetitionShowlistParser {
public:
static bool parse(network::Packet& packet, PetitionShowlistData& data);
};
/** SMSG_TURN_IN_PETITION_RESULTS parser */
class TurnInPetitionResultsParser {
public:
static bool parse(network::Packet& packet, uint32_t& result);
};
// Guild event type constants
namespace GuildEvent {
constexpr uint8_t PROMOTION = 0;

View file

@ -68,6 +68,12 @@ private:
bool showGuildNoteEdit_ = false;
bool editingOfficerNote_ = false;
char guildNoteEditBuffer_[256] = {0};
int guildRosterTab_ = 0; // 0=Roster, 1=Guild Info
char guildMotdEditBuffer_[256] = {0};
bool showMotdEdit_ = false;
char petitionNameBuffer_[64] = {0};
char addRankNameBuffer_[64] = {0};
bool showAddRankModal_ = false;
bool refocusChatInput = false;
bool vendorBagsOpened_ = false; // Track if bags were auto-opened for current vendor session
bool chatWindowLocked = true;
@ -284,6 +290,10 @@ private:
int auctionSellBid_[3] = {0, 0, 0}; // gold, silver, copper
int auctionSellBuyout_[3] = {0, 0, 0}; // gold, silver, copper
int auctionSelectedItem_ = -1;
int auctionSellSlotIndex_ = -1; // Selected backpack slot for selling
uint32_t auctionBrowseOffset_ = 0; // Pagination offset for browse results
int auctionItemClass_ = -1; // Item class filter (-1 = All)
int auctionItemSubClass_ = -1; // Item subclass filter (-1 = All)
// Guild bank money input
int guildBankMoneyInput_[3] = {0, 0, 0}; // gold, silver, copper

View file

@ -1774,6 +1774,12 @@ void GameHandler::handlePacket(network::Packet& packet) {
case Opcode::SMSG_GUILD_COMMAND_RESULT:
handleGuildCommandResult(packet);
break;
case Opcode::SMSG_PETITION_SHOWLIST:
handlePetitionShowlist(packet);
break;
case Opcode::SMSG_TURN_IN_PETITION_RESULTS:
handleTurnInPetitionResults(packet);
break;
// ---- Phase 5: Loot/Gossip/Vendor ----
case Opcode::SMSG_LOOT_RESPONSE:
@ -2781,11 +2787,36 @@ void GameHandler::handlePacket(network::Packet& packet) {
case Opcode::SMSG_AUCTION_COMMAND_RESULT:
handleAuctionCommandResult(packet);
break;
case Opcode::SMSG_AUCTION_OWNER_NOTIFICATION:
case Opcode::SMSG_AUCTION_BIDDER_NOTIFICATION:
// Auction notification payloads are informational; ignore until UI support lands.
case Opcode::SMSG_AUCTION_OWNER_NOTIFICATION: {
// auctionId(u32) + action(u32) + error(u32) + itemEntry(u32) + ...
if (packet.getSize() - packet.getReadPos() >= 16) {
uint32_t auctionId = packet.readUInt32();
uint32_t action = packet.readUInt32();
uint32_t error = packet.readUInt32();
uint32_t itemEntry = packet.readUInt32();
(void)auctionId; (void)action; (void)error;
ensureItemInfo(itemEntry);
auto* info = getItemInfo(itemEntry);
std::string itemName = info ? info->name : ("Item #" + std::to_string(itemEntry));
addSystemChatMessage("Your auction of " + itemName + " has sold!");
}
packet.setReadPos(packet.getSize());
break;
}
case Opcode::SMSG_AUCTION_BIDDER_NOTIFICATION: {
// auctionId(u32) + itemEntry(u32) + ...
if (packet.getSize() - packet.getReadPos() >= 8) {
uint32_t auctionId = packet.readUInt32();
uint32_t itemEntry = packet.readUInt32();
(void)auctionId;
ensureItemInfo(itemEntry);
auto* info = getItemInfo(itemEntry);
std::string itemName = info ? info->name : ("Item #" + std::to_string(itemEntry));
addSystemChatMessage("You have been outbid on " + itemName + ".");
}
packet.setReadPos(packet.getSize());
break;
}
case Opcode::SMSG_TAXINODE_STATUS:
// Node status cache not implemented yet.
packet.setReadPos(packet.getSize());
@ -9568,10 +9599,72 @@ void GameHandler::queryGuildInfo(uint32_t guildId) {
LOG_INFO("Querying guild info: guildId=", guildId);
}
void GameHandler::createGuild(const std::string& guildName) {
if (state != WorldState::IN_WORLD || !socket) return;
auto packet = GuildCreatePacket::build(guildName);
socket->send(packet);
LOG_INFO("Creating guild: ", guildName);
}
void GameHandler::addGuildRank(const std::string& rankName) {
if (state != WorldState::IN_WORLD || !socket) return;
auto packet = GuildAddRankPacket::build(rankName);
socket->send(packet);
LOG_INFO("Adding guild rank: ", rankName);
// Refresh roster to update rank list
requestGuildRoster();
}
void GameHandler::deleteGuildRank() {
if (state != WorldState::IN_WORLD || !socket) return;
auto packet = GuildDelRankPacket::build();
socket->send(packet);
LOG_INFO("Deleting last guild rank");
// Refresh roster to update rank list
requestGuildRoster();
}
void GameHandler::requestPetitionShowlist(uint64_t npcGuid) {
if (state != WorldState::IN_WORLD || !socket) return;
auto packet = PetitionShowlistPacket::build(npcGuid);
socket->send(packet);
}
void GameHandler::buyPetition(uint64_t npcGuid, const std::string& guildName) {
if (state != WorldState::IN_WORLD || !socket) return;
auto packet = PetitionBuyPacket::build(npcGuid, guildName);
socket->send(packet);
LOG_INFO("Buying guild petition: ", guildName);
}
void GameHandler::handlePetitionShowlist(network::Packet& packet) {
PetitionShowlistData data;
if (!PetitionShowlistParser::parse(packet, data)) return;
petitionNpcGuid_ = data.npcGuid;
petitionCost_ = data.cost;
showPetitionDialog_ = true;
LOG_INFO("Petition showlist: cost=", data.cost);
}
void GameHandler::handleTurnInPetitionResults(network::Packet& packet) {
uint32_t result = 0;
if (!TurnInPetitionResultsParser::parse(packet, result)) return;
switch (result) {
case 0: addSystemChatMessage("Guild created successfully!"); break;
case 1: addSystemChatMessage("Guild creation failed: already in a guild."); break;
case 2: addSystemChatMessage("Guild creation failed: not enough signatures."); break;
case 3: addSystemChatMessage("Guild creation failed: name already taken."); break;
default: addSystemChatMessage("Guild creation failed (error " + std::to_string(result) + ")."); break;
}
}
void GameHandler::handleGuildInfo(network::Packet& packet) {
GuildInfoData data;
if (!GuildInfoParser::parse(packet, data)) return;
guildInfoData_ = data;
addSystemChatMessage("Guild: " + data.guildName + " (" +
std::to_string(data.numMembers) + " members, " +
std::to_string(data.numAccounts) + " accounts)");
@ -9591,6 +9684,7 @@ void GameHandler::handleGuildQueryResponse(network::Packet& packet) {
if (!packetParsers_->parseGuildQueryResponse(packet, data)) return;
guildName_ = data.guildName;
guildQueryData_ = data;
guildRankNames_.clear();
for (uint32_t i = 0; i < 10; ++i) {
guildRankNames_.push_back(data.rankNames[i]);
@ -13305,6 +13399,8 @@ void GameHandler::auctionSearch(const std::string& name, uint8_t levelMin, uint8
addSystemChatMessage("Please wait before searching again.");
return;
}
// Save search params for pagination and auto-refresh
lastAuctionSearch_ = {name, levelMin, levelMax, quality, itemClass, itemSubClass, invTypeMask, usableOnly, offset};
pendingAuctionTarget_ = AuctionResultTarget::BROWSE;
auto pkt = AuctionListItemsPacket::build(auctioneerGuid_, offset, name,
levelMin, levelMax, invTypeMask,
@ -13437,9 +13533,16 @@ void GameHandler::handleAuctionCommandResult(network::Packet& packet) {
if (result.errorCode == 0) {
std::string msg = std::string("Auction ") + actionName + " successful.";
addSystemChatMessage(msg);
// Refresh appropriate list
if (result.action == 0) auctionListOwnerItems();
else if (result.action == 1) auctionListOwnerItems();
// Refresh appropriate lists
if (result.action == 0) auctionListOwnerItems(); // create
else if (result.action == 1) auctionListOwnerItems(); // cancel
else if (result.action == 2 || result.action == 3) { // bid or buyout
auctionListBidderItems();
// Re-query browse results with the same filters the user last searched with
const auto& s = lastAuctionSearch_;
auctionSearch(s.name, s.levelMin, s.levelMax, s.quality,
s.itemClass, s.itemSubClass, s.invTypeMask, s.usableOnly, s.offset);
}
} else {
const char* errors[] = {"OK", "Inventory", "Not enough money", "Item not found",
"Higher bid", "Increment", "Not enough items",

View file

@ -1843,6 +1843,85 @@ network::Packet GuildDeclineInvitationPacket::build() {
return packet;
}
network::Packet GuildCreatePacket::build(const std::string& guildName) {
network::Packet packet(wireOpcode(Opcode::CMSG_GUILD_CREATE));
packet.writeString(guildName);
LOG_DEBUG("Built CMSG_GUILD_CREATE: ", guildName);
return packet;
}
network::Packet GuildAddRankPacket::build(const std::string& rankName) {
network::Packet packet(wireOpcode(Opcode::CMSG_GUILD_ADD_RANK));
packet.writeString(rankName);
LOG_DEBUG("Built CMSG_GUILD_ADD_RANK: ", rankName);
return packet;
}
network::Packet GuildDelRankPacket::build() {
network::Packet packet(wireOpcode(Opcode::CMSG_GUILD_DEL_RANK));
LOG_DEBUG("Built CMSG_GUILD_DEL_RANK");
return packet;
}
network::Packet PetitionShowlistPacket::build(uint64_t npcGuid) {
network::Packet packet(wireOpcode(Opcode::CMSG_PETITION_SHOWLIST));
packet.writeUInt64(npcGuid);
LOG_DEBUG("Built CMSG_PETITION_SHOWLIST: guid=", npcGuid);
return packet;
}
network::Packet PetitionBuyPacket::build(uint64_t npcGuid, const std::string& guildName) {
network::Packet packet(wireOpcode(Opcode::CMSG_PETITION_BUY));
packet.writeUInt64(npcGuid); // NPC GUID
packet.writeUInt32(0); // unk
packet.writeUInt64(0); // unk
packet.writeString(guildName); // guild name
packet.writeUInt32(0); // body text (empty)
packet.writeUInt32(0); // min sigs
packet.writeUInt32(0); // max sigs
packet.writeUInt32(0); // unk
packet.writeUInt32(0); // unk
packet.writeUInt32(0); // unk
packet.writeUInt32(0); // unk
packet.writeUInt16(0); // unk
packet.writeUInt32(0); // unk
packet.writeUInt32(0); // unk index
packet.writeUInt32(0); // unk
LOG_DEBUG("Built CMSG_PETITION_BUY: npcGuid=", npcGuid, " name=", guildName);
return packet;
}
bool PetitionShowlistParser::parse(network::Packet& packet, PetitionShowlistData& data) {
if (packet.getSize() < 12) {
LOG_ERROR("SMSG_PETITION_SHOWLIST too small: ", packet.getSize());
return false;
}
data.npcGuid = packet.readUInt64();
uint32_t count = packet.readUInt32();
if (count > 0) {
data.itemId = packet.readUInt32();
data.displayId = packet.readUInt32();
data.cost = packet.readUInt32();
// Skip unused fields if present
if ((packet.getSize() - packet.getReadPos()) >= 8) {
data.charterType = packet.readUInt32();
data.requiredSigs = packet.readUInt32();
}
}
LOG_INFO("Parsed SMSG_PETITION_SHOWLIST: npcGuid=", data.npcGuid, " cost=", data.cost);
return true;
}
bool TurnInPetitionResultsParser::parse(network::Packet& packet, uint32_t& result) {
if (packet.getSize() < 4) {
LOG_ERROR("SMSG_TURN_IN_PETITION_RESULTS too small: ", packet.getSize());
return false;
}
result = packet.readUInt32();
LOG_INFO("Parsed SMSG_TURN_IN_PETITION_RESULTS: result=", result);
return true;
}
bool GuildQueryResponseParser::parse(network::Packet& packet, GuildQueryResponseData& data) {
if (packet.getSize() < 8) {
LOG_ERROR("SMSG_GUILD_QUERY_RESPONSE too small: ", packet.getSize());

View file

@ -2357,6 +2357,24 @@ void GameScreen::sendChatMessage(game::GameHandler& gameHandler) {
return;
}
// /gcreate command
if (cmdLower == "gcreate" || cmdLower == "guildcreate") {
if (spacePos != std::string::npos) {
std::string guildName = command.substr(spacePos + 1);
gameHandler.createGuild(guildName);
chatInputBuffer[0] = '\0';
return;
}
game::MessageChatData msg;
msg.type = game::ChatType::SYSTEM;
msg.language = game::ChatLanguage::UNIVERSAL;
msg.message = "Usage: /gcreate <guild name>";
gameHandler.addLocalChatMessage(msg);
chatInputBuffer[0] = '\0';
return;
}
// /gdisband command
if (cmdLower == "gdisband" || cmdLower == "guilddisband") {
gameHandler.disbandGuild();
@ -4300,11 +4318,50 @@ void GameScreen::renderGuildRoster(game::GameHandler& gameHandler) {
}
}
gameHandler.requestGuildRoster();
gameHandler.requestGuildInfo();
}
}
// Petition creation dialog (shown when NPC sends SMSG_PETITION_SHOWLIST)
if (gameHandler.hasPetitionShowlist()) {
ImGui::OpenPopup("CreateGuildPetition");
gameHandler.clearPetitionDialog();
}
if (ImGui::BeginPopupModal("CreateGuildPetition", nullptr, ImGuiWindowFlags_AlwaysAutoResize)) {
ImGui::Text("Create Guild Charter");
ImGui::Separator();
uint32_t cost = gameHandler.getPetitionCost();
uint32_t gold = cost / 10000;
uint32_t silver = (cost % 10000) / 100;
uint32_t copper = cost % 100;
ImGui::Text("Cost: %ug %us %uc", gold, silver, copper);
ImGui::Spacing();
ImGui::Text("Guild Name:");
ImGui::InputText("##petitionname", petitionNameBuffer_, sizeof(petitionNameBuffer_));
ImGui::Spacing();
if (ImGui::Button("Create", ImVec2(120, 0))) {
if (petitionNameBuffer_[0] != '\0') {
gameHandler.buyPetition(gameHandler.getPetitionNpcGuid(), petitionNameBuffer_);
petitionNameBuffer_[0] = '\0';
ImGui::CloseCurrentPopup();
}
}
ImGui::SameLine();
if (ImGui::Button("Cancel", ImVec2(120, 0))) {
petitionNameBuffer_[0] = '\0';
ImGui::CloseCurrentPopup();
}
ImGui::EndPopup();
}
if (!showGuildRoster_) return;
// Get zone manager for name lookup
game::ZoneManager* zoneManager = nullptr;
if (auto* renderer = core::Application::getInstance().getRenderer()) {
zoneManager = renderer->getZoneManager();
}
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;
@ -4312,164 +4369,311 @@ void GameScreen::renderGuildRoster(game::GameHandler& gameHandler) {
ImGui::SetNextWindowPos(ImVec2(screenW / 2 - 375, screenH / 2 - 250), ImGuiCond_Once);
ImGui::SetNextWindowSize(ImVec2(750, 500), ImGuiCond_Once);
std::string title = gameHandler.isInGuild() ? (gameHandler.getGuildName() + " - Roster") : "Guild Roster";
std::string title = gameHandler.isInGuild() ? (gameHandler.getGuildName() + " - Guild") : "Guild";
bool open = showGuildRoster_;
if (ImGui::Begin(title.c_str(), &open, ImGuiWindowFlags_NoCollapse)) {
if (!gameHandler.hasGuildRoster()) {
ImGui::Text("Loading roster...");
} else {
const auto& roster = gameHandler.getGuildRoster();
// Tab bar: Roster | Guild Info
if (ImGui::BeginTabBar("GuildTabs")) {
if (ImGui::BeginTabItem("Roster")) {
guildRosterTab_ = 0;
if (!gameHandler.hasGuildRoster()) {
ImGui::Text("Loading roster...");
} else {
const auto& roster = gameHandler.getGuildRoster();
// MOTD
if (!roster.motd.empty()) {
ImGui::TextColored(ImVec4(0.0f, 1.0f, 0.0f, 1.0f), "MOTD: %s", roster.motd.c_str());
ImGui::Separator();
}
// Count online
int onlineCount = 0;
for (const auto& m : roster.members) {
if (m.online) ++onlineCount;
}
ImGui::Text("%d members (%d online)", (int)roster.members.size(), onlineCount);
ImGui::Separator();
const auto& rankNames = gameHandler.getGuildRankNames();
// Table
if (ImGui::BeginTable("GuildRoster", 7,
ImGuiTableFlags_ScrollY | ImGuiTableFlags_RowBg | ImGuiTableFlags_BordersInnerV |
ImGuiTableFlags_Sortable)) {
ImGui::TableSetupColumn("Name", ImGuiTableColumnFlags_DefaultSort);
ImGui::TableSetupColumn("Rank");
ImGui::TableSetupColumn("Level", ImGuiTableColumnFlags_WidthFixed, 40.0f);
ImGui::TableSetupColumn("Class", ImGuiTableColumnFlags_WidthFixed, 70.0f);
ImGui::TableSetupColumn("Zone", ImGuiTableColumnFlags_WidthFixed, 80.0f);
ImGui::TableSetupColumn("Note");
ImGui::TableSetupColumn("Officer Note");
ImGui::TableHeadersRow();
// Online members first, then offline
auto sortedMembers = roster.members;
std::sort(sortedMembers.begin(), sortedMembers.end(), [](const auto& a, const auto& b) {
if (a.online != b.online) return a.online > b.online;
return a.name < b.name;
});
static const char* classNames[] = {
"Unknown", "Warrior", "Paladin", "Hunter", "Rogue",
"Priest", "Death Knight", "Shaman", "Mage", "Warlock",
"", "Druid"
};
for (const auto& m : sortedMembers) {
ImGui::TableNextRow();
ImVec4 textColor = m.online ? ImVec4(1.0f, 1.0f, 1.0f, 1.0f)
: ImVec4(0.5f, 0.5f, 0.5f, 1.0f);
ImGui::TableNextColumn();
ImGui::TextColored(textColor, "%s", m.name.c_str());
// Right-click context menu
if (ImGui::IsItemClicked(ImGuiMouseButton_Right)) {
selectedGuildMember_ = m.name;
ImGui::OpenPopup("GuildMemberContext");
// MOTD
if (!roster.motd.empty()) {
ImGui::TextColored(ImVec4(0.0f, 1.0f, 0.0f, 1.0f), "MOTD: %s", roster.motd.c_str());
ImGui::Separator();
}
ImGui::TableNextColumn();
// Show rank name instead of index
if (m.rankIndex < rankNames.size()) {
ImGui::TextColored(textColor, "%s", rankNames[m.rankIndex].c_str());
} else {
ImGui::TextColored(textColor, "Rank %u", m.rankIndex);
// Count online
int onlineCount = 0;
for (const auto& m : roster.members) {
if (m.online) ++onlineCount;
}
ImGui::Text("%d members (%d online)", (int)roster.members.size(), onlineCount);
ImGui::Separator();
ImGui::TableNextColumn();
ImGui::TextColored(textColor, "%u", m.level);
const auto& rankNames = gameHandler.getGuildRankNames();
ImGui::TableNextColumn();
const char* className = (m.classId < 12) ? classNames[m.classId] : "Unknown";
ImGui::TextColored(textColor, "%s", className);
// Table
if (ImGui::BeginTable("GuildRoster", 7,
ImGuiTableFlags_ScrollY | ImGuiTableFlags_RowBg | ImGuiTableFlags_BordersInnerV |
ImGuiTableFlags_Sortable)) {
ImGui::TableSetupColumn("Name", ImGuiTableColumnFlags_DefaultSort);
ImGui::TableSetupColumn("Rank");
ImGui::TableSetupColumn("Level", ImGuiTableColumnFlags_WidthFixed, 40.0f);
ImGui::TableSetupColumn("Class", ImGuiTableColumnFlags_WidthFixed, 70.0f);
ImGui::TableSetupColumn("Zone", ImGuiTableColumnFlags_WidthFixed, 120.0f);
ImGui::TableSetupColumn("Note");
ImGui::TableSetupColumn("Officer Note");
ImGui::TableHeadersRow();
ImGui::TableNextColumn();
ImGui::TextColored(textColor, "%u", m.zoneId);
// Online members first, then offline
auto sortedMembers = roster.members;
std::sort(sortedMembers.begin(), sortedMembers.end(), [](const auto& a, const auto& b) {
if (a.online != b.online) return a.online > b.online;
return a.name < b.name;
});
ImGui::TableNextColumn();
ImGui::TextColored(textColor, "%s", m.publicNote.c_str());
static const char* classNames[] = {
"Unknown", "Warrior", "Paladin", "Hunter", "Rogue",
"Priest", "Death Knight", "Shaman", "Mage", "Warlock",
"", "Druid"
};
ImGui::TableNextColumn();
ImGui::TextColored(textColor, "%s", m.officerNote.c_str());
}
ImGui::EndTable();
}
for (const auto& m : sortedMembers) {
ImGui::TableNextRow();
ImVec4 textColor = m.online ? ImVec4(1.0f, 1.0f, 1.0f, 1.0f)
: ImVec4(0.5f, 0.5f, 0.5f, 1.0f);
// Context menu popup
if (ImGui::BeginPopup("GuildMemberContext")) {
ImGui::Text("%s", selectedGuildMember_.c_str());
ImGui::Separator();
if (ImGui::MenuItem("Promote")) {
gameHandler.promoteGuildMember(selectedGuildMember_);
}
if (ImGui::MenuItem("Demote")) {
gameHandler.demoteGuildMember(selectedGuildMember_);
}
if (ImGui::MenuItem("Kick")) {
gameHandler.kickGuildMember(selectedGuildMember_);
}
ImGui::Separator();
if (ImGui::MenuItem("Set Public Note...")) {
showGuildNoteEdit_ = true;
editingOfficerNote_ = false;
guildNoteEditBuffer_[0] = '\0';
// Pre-fill with existing note
for (const auto& mem : roster.members) {
if (mem.name == selectedGuildMember_) {
snprintf(guildNoteEditBuffer_, sizeof(guildNoteEditBuffer_), "%s", mem.publicNote.c_str());
break;
ImGui::TableNextColumn();
ImGui::TextColored(textColor, "%s", m.name.c_str());
// Right-click context menu
if (ImGui::IsItemClicked(ImGuiMouseButton_Right)) {
selectedGuildMember_ = m.name;
ImGui::OpenPopup("GuildMemberContext");
}
ImGui::TableNextColumn();
// Show rank name instead of index
if (m.rankIndex < rankNames.size()) {
ImGui::TextColored(textColor, "%s", rankNames[m.rankIndex].c_str());
} else {
ImGui::TextColored(textColor, "Rank %u", m.rankIndex);
}
ImGui::TableNextColumn();
ImGui::TextColored(textColor, "%u", m.level);
ImGui::TableNextColumn();
const char* className = (m.classId < 12) ? classNames[m.classId] : "Unknown";
ImGui::TextColored(textColor, "%s", className);
ImGui::TableNextColumn();
// Zone name lookup
if (zoneManager) {
const auto* zoneInfo = zoneManager->getZoneInfo(m.zoneId);
if (zoneInfo && !zoneInfo->name.empty()) {
ImGui::TextColored(textColor, "%s", zoneInfo->name.c_str());
} else {
ImGui::TextColored(textColor, "%u", m.zoneId);
}
} else {
ImGui::TextColored(textColor, "%u", m.zoneId);
}
ImGui::TableNextColumn();
ImGui::TextColored(textColor, "%s", m.publicNote.c_str());
ImGui::TableNextColumn();
ImGui::TextColored(textColor, "%s", m.officerNote.c_str());
}
ImGui::EndTable();
}
}
if (ImGui::MenuItem("Set Officer Note...")) {
showGuildNoteEdit_ = true;
editingOfficerNote_ = true;
guildNoteEditBuffer_[0] = '\0';
for (const auto& mem : roster.members) {
if (mem.name == selectedGuildMember_) {
snprintf(guildNoteEditBuffer_, sizeof(guildNoteEditBuffer_), "%s", mem.officerNote.c_str());
break;
// Context menu popup
if (ImGui::BeginPopup("GuildMemberContext")) {
ImGui::Text("%s", selectedGuildMember_.c_str());
ImGui::Separator();
if (ImGui::MenuItem("Promote")) {
gameHandler.promoteGuildMember(selectedGuildMember_);
}
if (ImGui::MenuItem("Demote")) {
gameHandler.demoteGuildMember(selectedGuildMember_);
}
if (ImGui::MenuItem("Kick")) {
gameHandler.kickGuildMember(selectedGuildMember_);
}
ImGui::Separator();
if (ImGui::MenuItem("Set Public Note...")) {
showGuildNoteEdit_ = true;
editingOfficerNote_ = false;
guildNoteEditBuffer_[0] = '\0';
// Pre-fill with existing note
for (const auto& mem : roster.members) {
if (mem.name == selectedGuildMember_) {
snprintf(guildNoteEditBuffer_, sizeof(guildNoteEditBuffer_), "%s", mem.publicNote.c_str());
break;
}
}
}
if (ImGui::MenuItem("Set Officer Note...")) {
showGuildNoteEdit_ = true;
editingOfficerNote_ = true;
guildNoteEditBuffer_[0] = '\0';
for (const auto& mem : roster.members) {
if (mem.name == selectedGuildMember_) {
snprintf(guildNoteEditBuffer_, sizeof(guildNoteEditBuffer_), "%s", mem.officerNote.c_str());
break;
}
}
}
ImGui::Separator();
if (ImGui::MenuItem("Set as Leader")) {
gameHandler.setGuildLeader(selectedGuildMember_);
}
ImGui::EndPopup();
}
// Note edit modal
if (showGuildNoteEdit_) {
ImGui::OpenPopup("EditGuildNote");
showGuildNoteEdit_ = false;
}
if (ImGui::BeginPopupModal("EditGuildNote", nullptr, ImGuiWindowFlags_AlwaysAutoResize)) {
ImGui::Text("%s %s for %s:",
editingOfficerNote_ ? "Officer" : "Public", "Note", selectedGuildMember_.c_str());
ImGui::InputText("##guildnote", guildNoteEditBuffer_, sizeof(guildNoteEditBuffer_));
if (ImGui::Button("Save")) {
if (editingOfficerNote_) {
gameHandler.setGuildOfficerNote(selectedGuildMember_, guildNoteEditBuffer_);
} else {
gameHandler.setGuildPublicNote(selectedGuildMember_, guildNoteEditBuffer_);
}
ImGui::CloseCurrentPopup();
}
ImGui::SameLine();
if (ImGui::Button("Cancel")) {
ImGui::CloseCurrentPopup();
}
ImGui::EndPopup();
}
}
ImGui::Separator();
if (ImGui::MenuItem("Set as Leader")) {
gameHandler.setGuildLeader(selectedGuildMember_);
}
ImGui::EndPopup();
ImGui::EndTabItem();
}
// Note edit modal
if (showGuildNoteEdit_) {
ImGui::OpenPopup("EditGuildNote");
showGuildNoteEdit_ = false;
}
if (ImGui::BeginPopupModal("EditGuildNote", nullptr, ImGuiWindowFlags_AlwaysAutoResize)) {
ImGui::Text("%s %s for %s:",
editingOfficerNote_ ? "Officer" : "Public", "Note", selectedGuildMember_.c_str());
ImGui::InputText("##guildnote", guildNoteEditBuffer_, sizeof(guildNoteEditBuffer_));
if (ImGui::Button("Save")) {
if (editingOfficerNote_) {
gameHandler.setGuildOfficerNote(selectedGuildMember_, guildNoteEditBuffer_);
} else {
gameHandler.setGuildPublicNote(selectedGuildMember_, guildNoteEditBuffer_);
if (ImGui::BeginTabItem("Guild Info")) {
guildRosterTab_ = 1;
const auto& infoData = gameHandler.getGuildInfoData();
const auto& queryData = gameHandler.getGuildQueryData();
const auto& roster = gameHandler.getGuildRoster();
const auto& rankNames = gameHandler.getGuildRankNames();
// Guild name (large, gold)
ImGui::PushFont(nullptr); // default font
ImGui::TextColored(ImVec4(1.0f, 0.82f, 0.0f, 1.0f), "<%s>", gameHandler.getGuildName().c_str());
ImGui::PopFont();
ImGui::Separator();
// Creation date
if (infoData.isValid()) {
ImGui::Text("Created: %u/%u/%u", infoData.creationDay, infoData.creationMonth, infoData.creationYear);
ImGui::Text("Members: %u | Accounts: %u", infoData.numMembers, infoData.numAccounts);
}
ImGui::Spacing();
// Guild description / info text
if (!roster.guildInfo.empty()) {
ImGui::TextColored(ImVec4(0.8f, 0.8f, 0.8f, 1.0f), "Description:");
ImGui::TextWrapped("%s", roster.guildInfo.c_str());
}
ImGui::Spacing();
// MOTD with edit button
ImGui::TextColored(ImVec4(0.0f, 1.0f, 0.0f, 1.0f), "MOTD:");
ImGui::SameLine();
if (!roster.motd.empty()) {
ImGui::TextWrapped("%s", roster.motd.c_str());
} else {
ImGui::TextColored(ImVec4(0.5f, 0.5f, 0.5f, 1.0f), "(not set)");
}
if (ImGui::Button("Set MOTD")) {
showMotdEdit_ = true;
snprintf(guildMotdEditBuffer_, sizeof(guildMotdEditBuffer_), "%s", roster.motd.c_str());
}
ImGui::Spacing();
// MOTD edit modal
if (showMotdEdit_) {
ImGui::OpenPopup("EditMotd");
showMotdEdit_ = false;
}
if (ImGui::BeginPopupModal("EditMotd", nullptr, ImGuiWindowFlags_AlwaysAutoResize)) {
ImGui::Text("Set Message of the Day:");
ImGui::InputText("##motdinput", guildMotdEditBuffer_, sizeof(guildMotdEditBuffer_));
if (ImGui::Button("Save", ImVec2(120, 0))) {
gameHandler.setGuildMotd(guildMotdEditBuffer_);
ImGui::CloseCurrentPopup();
}
ImGui::CloseCurrentPopup();
ImGui::SameLine();
if (ImGui::Button("Cancel", ImVec2(120, 0))) {
ImGui::CloseCurrentPopup();
}
ImGui::EndPopup();
}
// Emblem info
if (queryData.isValid()) {
ImGui::Separator();
ImGui::Text("Emblem: Style %u, Color %u | Border: Style %u, Color %u | BG: %u",
queryData.emblemStyle, queryData.emblemColor,
queryData.borderStyle, queryData.borderColor, queryData.backgroundColor);
}
// Rank list
ImGui::Separator();
ImGui::TextColored(ImVec4(1.0f, 0.82f, 0.0f, 1.0f), "Ranks:");
for (size_t i = 0; i < rankNames.size(); ++i) {
if (rankNames[i].empty()) continue;
// Show rank permission summary from roster data
if (i < roster.ranks.size()) {
uint32_t rights = roster.ranks[i].rights;
std::string perms;
if (rights & 0x01) perms += "Invite ";
if (rights & 0x02) perms += "Remove ";
if (rights & 0x40) perms += "Promote ";
if (rights & 0x80) perms += "Demote ";
if (rights & 0x04) perms += "OChat ";
if (rights & 0x10) perms += "MOTD ";
ImGui::Text(" %zu. %s", i + 1, rankNames[i].c_str());
if (!perms.empty()) {
ImGui::SameLine();
ImGui::TextColored(ImVec4(0.5f, 0.5f, 0.5f, 1.0f), "[%s]", perms.c_str());
}
} else {
ImGui::Text(" %zu. %s", i + 1, rankNames[i].c_str());
}
}
// Rank management buttons
ImGui::Spacing();
if (ImGui::Button("Add Rank")) {
showAddRankModal_ = true;
addRankNameBuffer_[0] = '\0';
}
ImGui::SameLine();
if (ImGui::Button("Cancel")) {
ImGui::CloseCurrentPopup();
if (ImGui::Button("Delete Last Rank")) {
gameHandler.deleteGuildRank();
}
ImGui::EndPopup();
// Add rank modal
if (showAddRankModal_) {
ImGui::OpenPopup("AddGuildRank");
showAddRankModal_ = false;
}
if (ImGui::BeginPopupModal("AddGuildRank", nullptr, ImGuiWindowFlags_AlwaysAutoResize)) {
ImGui::Text("New Rank Name:");
ImGui::InputText("##rankname", addRankNameBuffer_, sizeof(addRankNameBuffer_));
if (ImGui::Button("Add", ImVec2(120, 0))) {
if (addRankNameBuffer_[0] != '\0') {
gameHandler.addGuildRank(addRankNameBuffer_);
ImGui::CloseCurrentPopup();
}
}
ImGui::SameLine();
if (ImGui::Button("Cancel", ImVec2(120, 0))) {
ImGui::CloseCurrentPopup();
}
ImGui::EndPopup();
}
ImGui::EndTabItem();
}
ImGui::EndTabBar();
}
}
ImGui::End();
@ -7793,8 +7997,74 @@ void GameScreen::renderAuctionHouseWindow(game::GameHandler& gameHandler) {
if (tab == 0) {
// Browse tab - Search filters
// --- Helper: resolve current UI filter state into wire-format search params ---
// WoW 3.3.5a item class IDs:
// 0=Consumable, 1=Container, 2=Weapon, 3=Gem, 4=Armor,
// 7=Projectile/TradeGoods, 9=Recipe, 11=Quiver, 15=Miscellaneous
struct AHClassMapping { const char* label; uint32_t classId; };
static const AHClassMapping classMappings[] = {
{"All", 0xFFFFFFFF},
{"Weapon", 2},
{"Armor", 4},
{"Container", 1},
{"Consumable", 0},
{"Trade Goods", 7},
{"Gem", 3},
{"Recipe", 9},
{"Quiver", 11},
{"Miscellaneous", 15},
};
static constexpr int NUM_CLASSES = 10;
// Weapon subclass IDs (WoW 3.3.5a)
struct AHSubMapping { const char* label; uint32_t subId; };
static const AHSubMapping weaponSubs[] = {
{"All", 0xFFFFFFFF}, {"Axe (1H)", 0}, {"Axe (2H)", 1}, {"Bow", 2},
{"Gun", 3}, {"Mace (1H)", 4}, {"Mace (2H)", 5}, {"Polearm", 6},
{"Sword (1H)", 7}, {"Sword (2H)", 8}, {"Staff", 10},
{"Fist Weapon", 13}, {"Dagger", 15}, {"Thrown", 16},
{"Crossbow", 18}, {"Wand", 19},
};
static constexpr int NUM_WEAPON_SUBS = 16;
// Armor subclass IDs
static const AHSubMapping armorSubs[] = {
{"All", 0xFFFFFFFF}, {"Cloth", 1}, {"Leather", 2}, {"Mail", 3},
{"Plate", 4}, {"Shield", 6}, {"Miscellaneous", 0},
};
static constexpr int NUM_ARMOR_SUBS = 7;
auto getSearchClassId = [&]() -> uint32_t {
if (auctionItemClass_ < 0 || auctionItemClass_ >= NUM_CLASSES) return 0xFFFFFFFF;
return classMappings[auctionItemClass_].classId;
};
auto getSearchSubClassId = [&]() -> uint32_t {
if (auctionItemSubClass_ < 0) return 0xFFFFFFFF;
uint32_t cid = getSearchClassId();
if (cid == 2 && auctionItemSubClass_ < NUM_WEAPON_SUBS)
return weaponSubs[auctionItemSubClass_].subId;
if (cid == 4 && auctionItemSubClass_ < NUM_ARMOR_SUBS)
return armorSubs[auctionItemSubClass_].subId;
return 0xFFFFFFFF;
};
auto doSearch = [&](uint32_t offset) {
auctionBrowseOffset_ = offset;
auctionLevelMin_ = std::clamp(auctionLevelMin_, 0, 80);
auctionLevelMax_ = std::clamp(auctionLevelMax_, 0, 80);
uint32_t q = auctionQuality_ > 0 ? static_cast<uint32_t>(auctionQuality_ - 1) : 0xFFFFFFFF;
gameHandler.auctionSearch(auctionSearchName_,
static_cast<uint8_t>(auctionLevelMin_),
static_cast<uint8_t>(auctionLevelMax_),
q, getSearchClassId(), getSearchSubClassId(), 0, 0, offset);
};
// Row 1: Name + Level range
ImGui::SetNextItemWidth(200);
ImGui::InputText("Name", auctionSearchName_, sizeof(auctionSearchName_));
bool enterPressed = ImGui::InputText("Name", auctionSearchName_, sizeof(auctionSearchName_),
ImGuiInputTextFlags_EnterReturnsTrue);
ImGui::SameLine();
ImGui::SetNextItemWidth(50);
ImGui::InputInt("Min Lv", &auctionLevelMin_, 0);
@ -7802,23 +8072,49 @@ void GameScreen::renderAuctionHouseWindow(game::GameHandler& gameHandler) {
ImGui::SetNextItemWidth(50);
ImGui::InputInt("Max Lv", &auctionLevelMax_, 0);
// Row 2: Quality + Category + Subcategory + Search button
const char* qualities[] = {"All", "Poor", "Common", "Uncommon", "Rare", "Epic", "Legendary"};
ImGui::SetNextItemWidth(100);
ImGui::Combo("Quality", &auctionQuality_, qualities, 7);
ImGui::SameLine();
// Build class label list from mappings
const char* classLabels[NUM_CLASSES];
for (int c = 0; c < NUM_CLASSES; c++) classLabels[c] = classMappings[c].label;
ImGui::SetNextItemWidth(120);
int classIdx = auctionItemClass_ < 0 ? 0 : auctionItemClass_;
if (ImGui::Combo("Category", &classIdx, classLabels, NUM_CLASSES)) {
if (classIdx != auctionItemClass_) auctionItemSubClass_ = -1;
auctionItemClass_ = classIdx;
}
// Subcategory (only for Weapon and Armor)
uint32_t curClassId = getSearchClassId();
if (curClassId == 2 || curClassId == 4) {
const AHSubMapping* subs = (curClassId == 2) ? weaponSubs : armorSubs;
int numSubs = (curClassId == 2) ? NUM_WEAPON_SUBS : NUM_ARMOR_SUBS;
const char* subLabels[20];
for (int s = 0; s < numSubs && s < 20; s++) subLabels[s] = subs[s].label;
int subIdx = auctionItemSubClass_ + 1; // -1 → 0 ("All")
if (subIdx < 0 || subIdx >= numSubs) subIdx = 0;
ImGui::SameLine();
ImGui::SetNextItemWidth(110);
if (ImGui::Combo("Subcat", &subIdx, subLabels, numSubs)) {
auctionItemSubClass_ = subIdx - 1; // 0 → -1 ("All")
}
}
ImGui::SameLine();
float delay = gameHandler.getAuctionSearchDelay();
if (delay > 0.0f) {
char delayBuf[32];
snprintf(delayBuf, sizeof(delayBuf), "Search (%.0fs)", delay);
ImGui::BeginDisabled();
ImGui::Button("Search...");
ImGui::Button(delayBuf);
ImGui::EndDisabled();
} else {
if (ImGui::Button("Search")) {
uint32_t q = auctionQuality_ > 0 ? static_cast<uint32_t>(auctionQuality_ - 1) : 0xFFFFFFFF;
gameHandler.auctionSearch(auctionSearchName_,
static_cast<uint8_t>(auctionLevelMin_),
static_cast<uint8_t>(auctionLevelMax_),
q, 0xFFFFFFFF, 0xFFFFFFFF, 0, 0);
if (ImGui::Button("Search") || enterPressed) {
doSearch(0);
}
}
@ -7826,9 +8122,34 @@ void GameScreen::renderAuctionHouseWindow(game::GameHandler& gameHandler) {
// Results table
const auto& results = gameHandler.getAuctionBrowseResults();
constexpr uint32_t AH_PAGE_SIZE = 50;
ImGui::Text("%zu results (of %u total)", results.auctions.size(), results.totalCount);
if (ImGui::BeginChild("AuctionResults", ImVec2(0, -80), true)) {
// Pagination
if (results.totalCount > AH_PAGE_SIZE) {
ImGui::SameLine();
uint32_t page = auctionBrowseOffset_ / AH_PAGE_SIZE + 1;
uint32_t totalPages = (results.totalCount + AH_PAGE_SIZE - 1) / AH_PAGE_SIZE;
if (auctionBrowseOffset_ == 0) ImGui::BeginDisabled();
if (ImGui::SmallButton("< Prev")) {
uint32_t newOff = (auctionBrowseOffset_ >= AH_PAGE_SIZE) ? auctionBrowseOffset_ - AH_PAGE_SIZE : 0;
doSearch(newOff);
}
if (auctionBrowseOffset_ == 0) ImGui::EndDisabled();
ImGui::SameLine();
ImGui::Text("Page %u/%u", page, totalPages);
ImGui::SameLine();
if (auctionBrowseOffset_ + AH_PAGE_SIZE >= results.totalCount) ImGui::BeginDisabled();
if (ImGui::SmallButton("Next >")) {
doSearch(auctionBrowseOffset_ + AH_PAGE_SIZE);
}
if (auctionBrowseOffset_ + AH_PAGE_SIZE >= results.totalCount) ImGui::EndDisabled();
}
if (ImGui::BeginChild("AuctionResults", ImVec2(0, -110), true)) {
if (ImGui::BeginTable("AuctionTable", 6, ImGuiTableFlags_Borders | ImGuiTableFlags_RowBg | ImGuiTableFlags_ScrollY)) {
ImGui::TableSetupColumn("Item", ImGuiTableColumnFlags_WidthStretch);
ImGui::TableSetupColumn("Qty", ImGuiTableColumnFlags_WidthFixed, 40);
@ -7847,7 +8168,47 @@ void GameScreen::renderAuctionHouseWindow(game::GameHandler& gameHandler) {
ImGui::TableNextRow();
ImGui::TableSetColumnIndex(0);
// Item icon
if (info && info->valid && info->displayInfoId != 0) {
VkDescriptorSet iconTex = inventoryScreen.getItemIcon(info->displayInfoId);
if (iconTex) {
ImGui::Image((void*)(intptr_t)iconTex, ImVec2(16, 16));
ImGui::SameLine();
}
}
ImGui::TextColored(qc, "%s", name.c_str());
// Item tooltip on hover
if (ImGui::IsItemHovered() && info && info->valid) {
ImGui::BeginTooltip();
ImGui::TextColored(qc, "%s", info->name.c_str());
if (info->inventoryType > 0) {
if (!info->subclassName.empty())
ImGui::TextColored(ImVec4(0.7f, 0.7f, 0.7f, 1), "%s", info->subclassName.c_str());
}
if (info->armor > 0) ImGui::Text("%d Armor", info->armor);
if (info->damageMax > 0.0f && info->delayMs > 0) {
float speed = static_cast<float>(info->delayMs) / 1000.0f;
ImGui::Text("%.0f - %.0f Damage Speed %.2f", info->damageMin, info->damageMax, speed);
}
ImVec4 green(0.0f, 1.0f, 0.0f, 1.0f);
std::string bonusLine;
auto appendStat = [](std::string& out, int32_t val, const char* n) {
if (val <= 0) return;
if (!out.empty()) out += " ";
out += "+" + std::to_string(val) + " " + n;
};
appendStat(bonusLine, info->strength, "Str");
appendStat(bonusLine, info->agility, "Agi");
appendStat(bonusLine, info->stamina, "Sta");
appendStat(bonusLine, info->intellect, "Int");
appendStat(bonusLine, info->spirit, "Spi");
if (!bonusLine.empty()) ImGui::TextColored(green, "%s", bonusLine.c_str());
if (info->sellPrice > 0) {
ImGui::TextColored(ImVec4(1, 0.84f, 0, 1), "Sell: %ug %us %uc",
info->sellPrice / 10000, (info->sellPrice / 100) % 100, info->sellPrice % 100);
}
ImGui::EndTooltip();
}
ImGui::TableSetColumnIndex(1);
ImGui::Text("%u", auction.stackCount);
@ -7894,8 +8255,52 @@ void GameScreen::renderAuctionHouseWindow(game::GameHandler& gameHandler) {
// Sell section
ImGui::Separator();
ImGui::Text("Sell:");
ImGui::SameLine();
ImGui::Text("Sell Item:");
// Item picker from backpack
{
auto& inv = gameHandler.getInventory();
// Build list of non-empty backpack slots
std::string preview = (auctionSellSlotIndex_ >= 0)
? ([&]() -> std::string {
const auto& slot = inv.getBackpackSlot(auctionSellSlotIndex_);
if (!slot.empty()) {
std::string s = slot.item.name;
if (slot.item.stackCount > 1) s += " x" + std::to_string(slot.item.stackCount);
return s;
}
return "Select item...";
})()
: "Select item...";
ImGui::SetNextItemWidth(250);
if (ImGui::BeginCombo("##sellitem", preview.c_str())) {
for (int i = 0; i < game::Inventory::BACKPACK_SLOTS; i++) {
const auto& slot = inv.getBackpackSlot(i);
if (slot.empty()) continue;
ImGui::PushID(i + 9000);
// Item icon
if (slot.item.displayInfoId != 0) {
VkDescriptorSet sIcon = inventoryScreen.getItemIcon(slot.item.displayInfoId);
if (sIcon) {
ImGui::Image((void*)(intptr_t)sIcon, ImVec2(16, 16));
ImGui::SameLine();
}
}
std::string label = slot.item.name;
if (slot.item.stackCount > 1) label += " x" + std::to_string(slot.item.stackCount);
ImVec4 iqc = InventoryScreen::getQualityColor(slot.item.quality);
ImGui::PushStyleColor(ImGuiCol_Text, iqc);
if (ImGui::Selectable(label.c_str(), auctionSellSlotIndex_ == i)) {
auctionSellSlotIndex_ = i;
}
ImGui::PopStyleColor();
ImGui::PopID();
}
ImGui::EndCombo();
}
}
ImGui::Text("Bid:");
ImGui::SameLine();
ImGui::SetNextItemWidth(50);
@ -7907,7 +8312,7 @@ void GameScreen::renderAuctionHouseWindow(game::GameHandler& gameHandler) {
ImGui::SetNextItemWidth(35);
ImGui::InputInt("##sbc", &auctionSellBid_[2], 0); ImGui::SameLine(); ImGui::Text("c");
ImGui::Text(" "); ImGui::SameLine();
ImGui::SameLine(0, 20);
ImGui::Text("Buyout:");
ImGui::SameLine();
ImGui::SetNextItemWidth(50);
@ -7920,31 +8325,92 @@ void GameScreen::renderAuctionHouseWindow(game::GameHandler& gameHandler) {
ImGui::InputInt("##sboc", &auctionSellBuyout_[2], 0); ImGui::SameLine(); ImGui::Text("c");
const char* durations[] = {"12 hours", "24 hours", "48 hours"};
ImGui::SameLine();
ImGui::SetNextItemWidth(90);
ImGui::Combo("##dur", &auctionSellDuration_, durations, 3);
ImGui::SameLine();
// Create Auction button
bool canCreate = auctionSellSlotIndex_ >= 0 &&
!gameHandler.getInventory().getBackpackSlot(auctionSellSlotIndex_).empty() &&
(auctionSellBid_[0] > 0 || auctionSellBid_[1] > 0 || auctionSellBid_[2] > 0);
if (!canCreate) ImGui::BeginDisabled();
if (ImGui::Button("Create Auction")) {
uint32_t bidCopper = static_cast<uint32_t>(auctionSellBid_[0]) * 10000
+ static_cast<uint32_t>(auctionSellBid_[1]) * 100
+ static_cast<uint32_t>(auctionSellBid_[2]);
uint32_t buyoutCopper = static_cast<uint32_t>(auctionSellBuyout_[0]) * 10000
+ static_cast<uint32_t>(auctionSellBuyout_[1]) * 100
+ static_cast<uint32_t>(auctionSellBuyout_[2]);
const uint32_t durationMins[] = {720, 1440, 2880};
uint32_t dur = durationMins[auctionSellDuration_];
uint64_t itemGuid = gameHandler.getBackpackItemGuid(auctionSellSlotIndex_);
const auto& slot = gameHandler.getInventory().getBackpackSlot(auctionSellSlotIndex_);
uint32_t stackCount = slot.item.stackCount;
if (itemGuid != 0) {
gameHandler.auctionSellItem(itemGuid, stackCount, bidCopper, buyoutCopper, dur);
// Clear sell inputs
auctionSellSlotIndex_ = -1;
auctionSellBid_[0] = auctionSellBid_[1] = auctionSellBid_[2] = 0;
auctionSellBuyout_[0] = auctionSellBuyout_[1] = auctionSellBuyout_[2] = 0;
}
}
if (!canCreate) ImGui::EndDisabled();
} else if (tab == 1) {
// Bids tab
const auto& results = gameHandler.getAuctionBidderResults();
ImGui::Text("Your Bids: %zu items", results.auctions.size());
if (ImGui::BeginTable("BidTable", 5, ImGuiTableFlags_Borders | ImGuiTableFlags_RowBg)) {
if (ImGui::BeginTable("BidTable", 6, ImGuiTableFlags_Borders | ImGuiTableFlags_RowBg)) {
ImGui::TableSetupColumn("Item", ImGuiTableColumnFlags_WidthStretch);
ImGui::TableSetupColumn("Qty", ImGuiTableColumnFlags_WidthFixed, 40);
ImGui::TableSetupColumn("Your Bid", ImGuiTableColumnFlags_WidthFixed, 90);
ImGui::TableSetupColumn("Buyout", ImGuiTableColumnFlags_WidthFixed, 90);
ImGui::TableSetupColumn("Time", ImGuiTableColumnFlags_WidthFixed, 60);
ImGui::TableSetupColumn("##act", ImGuiTableColumnFlags_WidthFixed, 60);
ImGui::TableHeadersRow();
for (const auto& a : results.auctions) {
for (size_t bi = 0; bi < results.auctions.size(); bi++) {
const auto& a = results.auctions[bi];
auto* info = gameHandler.getItemInfo(a.itemEntry);
std::string name = info ? info->name : ("Item #" + std::to_string(a.itemEntry));
game::ItemQuality quality = info ? static_cast<game::ItemQuality>(info->quality) : game::ItemQuality::COMMON;
ImVec4 bqc = InventoryScreen::getQualityColor(quality);
ImGui::TableNextRow();
ImGui::TableSetColumnIndex(0);
ImGui::TextColored(InventoryScreen::getQualityColor(quality), "%s", name.c_str());
if (info && info->valid && info->displayInfoId != 0) {
VkDescriptorSet bIcon = inventoryScreen.getItemIcon(info->displayInfoId);
if (bIcon) {
ImGui::Image((void*)(intptr_t)bIcon, ImVec2(16, 16));
ImGui::SameLine();
}
}
ImGui::TextColored(bqc, "%s", name.c_str());
// Tooltip
if (ImGui::IsItemHovered() && info && info->valid) {
ImGui::BeginTooltip();
ImGui::TextColored(bqc, "%s", info->name.c_str());
if (info->armor > 0) ImGui::Text("%d Armor", info->armor);
if (info->damageMax > 0.0f && info->delayMs > 0) {
float speed = static_cast<float>(info->delayMs) / 1000.0f;
ImGui::Text("%.0f - %.0f Damage Speed %.2f", info->damageMin, info->damageMax, speed);
}
std::string bl;
auto appS = [](std::string& o, int32_t v, const char* n) {
if (v <= 0) return;
if (!o.empty()) o += " ";
o += "+" + std::to_string(v) + " " + n;
};
appS(bl, info->strength, "Str"); appS(bl, info->agility, "Agi");
appS(bl, info->stamina, "Sta"); appS(bl, info->intellect, "Int");
appS(bl, info->spirit, "Spi");
if (!bl.empty()) ImGui::TextColored(ImVec4(0,1,0,1), "%s", bl.c_str());
if (info->sellPrice > 0)
ImGui::TextColored(ImVec4(1,0.84f,0,1), "Sell: %ug %us %uc",
info->sellPrice/10000, (info->sellPrice/100)%100, info->sellPrice%100);
ImGui::EndTooltip();
}
ImGui::TableSetColumnIndex(1);
ImGui::Text("%u", a.stackCount);
ImGui::TableSetColumnIndex(2);
@ -7959,6 +8425,20 @@ void GameScreen::renderAuctionHouseWindow(game::GameHandler& gameHandler) {
if (mins > 720) ImGui::Text("Long");
else if (mins > 120) ImGui::Text("Medium");
else ImGui::TextColored(ImVec4(1, 0.3f, 0.3f, 1), "Short");
ImGui::TableSetColumnIndex(5);
ImGui::PushID(static_cast<int>(bi) + 7500);
if (a.buyoutPrice > 0 && ImGui::SmallButton("Buy")) {
gameHandler.auctionBuyout(a.auctionId, a.buyoutPrice);
}
if (a.buyoutPrice > 0) ImGui::SameLine();
if (ImGui::SmallButton("Bid")) {
uint32_t bidAmt = a.currentBid > 0
? a.currentBid + a.minBidIncrement
: a.startBid;
gameHandler.auctionPlaceBid(a.auctionId, bidAmt);
}
ImGui::PopID();
}
ImGui::EndTable();
}
@ -7984,7 +8464,38 @@ void GameScreen::renderAuctionHouseWindow(game::GameHandler& gameHandler) {
ImGui::TableNextRow();
ImGui::TableSetColumnIndex(0);
ImGui::TextColored(InventoryScreen::getQualityColor(quality), "%s", name.c_str());
ImVec4 oqc = InventoryScreen::getQualityColor(quality);
if (info && info->valid && info->displayInfoId != 0) {
VkDescriptorSet oIcon = inventoryScreen.getItemIcon(info->displayInfoId);
if (oIcon) {
ImGui::Image((void*)(intptr_t)oIcon, ImVec2(16, 16));
ImGui::SameLine();
}
}
ImGui::TextColored(oqc, "%s", name.c_str());
if (ImGui::IsItemHovered() && info && info->valid) {
ImGui::BeginTooltip();
ImGui::TextColored(oqc, "%s", info->name.c_str());
if (info->armor > 0) ImGui::Text("%d Armor", info->armor);
if (info->damageMax > 0.0f && info->delayMs > 0) {
float speed = static_cast<float>(info->delayMs) / 1000.0f;
ImGui::Text("%.0f - %.0f Damage Speed %.2f", info->damageMin, info->damageMax, speed);
}
std::string ol;
auto appO = [](std::string& o, int32_t v, const char* n) {
if (v <= 0) return;
if (!o.empty()) o += " ";
o += "+" + std::to_string(v) + " " + n;
};
appO(ol, info->strength, "Str"); appO(ol, info->agility, "Agi");
appO(ol, info->stamina, "Sta"); appO(ol, info->intellect, "Int");
appO(ol, info->spirit, "Spi");
if (!ol.empty()) ImGui::TextColored(ImVec4(0,1,0,1), "%s", ol.c_str());
if (info->sellPrice > 0)
ImGui::TextColored(ImVec4(1,0.84f,0,1), "Sell: %ug %us %uc",
info->sellPrice/10000, (info->sellPrice/100)%100, info->sellPrice%100);
ImGui::EndTooltip();
}
ImGui::TableSetColumnIndex(1);
ImGui::Text("%u", a.stackCount);
ImGui::TableSetColumnIndex(2);