mirror of
https://github.com/Kelsidavis/WoWee.git
synced 2026-04-25 21:03:51 +00:00
Compare commits
13 commits
94e4a0bdb3
...
7982815a67
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
7982815a67 | ||
|
|
0d87a86516 | ||
|
|
da959cfb8f | ||
|
|
889cd86fb0 | ||
|
|
1ae5fe867c | ||
|
|
9906269671 | ||
|
|
ce30cedf4a | ||
|
|
d61b7b385d | ||
|
|
af7fb4242c | ||
|
|
2ab5cf5eb6 | ||
|
|
e220ce888d | ||
|
|
26a685187e | ||
|
|
872b10fe68 |
19 changed files with 1913 additions and 507 deletions
|
|
@ -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();
|
||||
|
|
@ -856,12 +868,28 @@ public:
|
|||
int getSelectedMailIndex() const { return selectedMailIndex_; }
|
||||
void setSelectedMailIndex(int idx) { selectedMailIndex_ = idx; }
|
||||
bool isMailComposeOpen() const { return showMailCompose_; }
|
||||
void openMailCompose() { showMailCompose_ = true; }
|
||||
void closeMailCompose() { showMailCompose_ = false; }
|
||||
void openMailCompose() { showMailCompose_ = true; clearMailAttachments(); }
|
||||
void closeMailCompose() { showMailCompose_ = false; clearMailAttachments(); }
|
||||
bool hasNewMail() const { return hasNewMail_; }
|
||||
void closeMailbox();
|
||||
void sendMail(const std::string& recipient, const std::string& subject,
|
||||
const std::string& body, uint32_t money, uint32_t cod = 0);
|
||||
|
||||
// Mail attachments (max 12 per WotLK)
|
||||
static constexpr int MAIL_MAX_ATTACHMENTS = 12;
|
||||
struct MailAttachSlot {
|
||||
uint64_t itemGuid = 0;
|
||||
game::ItemDef item;
|
||||
uint8_t srcBag = 0xFF; // source container for return
|
||||
uint8_t srcSlot = 0;
|
||||
bool occupied() const { return itemGuid != 0; }
|
||||
};
|
||||
bool attachItemFromBackpack(int backpackIndex);
|
||||
bool attachItemFromBag(int bagIndex, int slotIndex);
|
||||
bool detachMailAttachment(int attachIndex);
|
||||
void clearMailAttachments();
|
||||
const std::array<MailAttachSlot, 12>& getMailAttachments() const { return mailAttachments_; }
|
||||
int getMailAttachmentCount() const;
|
||||
void mailTakeMoney(uint32_t mailId);
|
||||
void mailTakeItem(uint32_t mailId, uint32_t itemIndex);
|
||||
void mailDelete(uint32_t mailId);
|
||||
|
|
@ -1107,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);
|
||||
|
|
@ -1451,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;
|
||||
|
|
@ -1568,6 +1603,7 @@ private:
|
|||
int selectedMailIndex_ = -1;
|
||||
bool showMailCompose_ = false;
|
||||
bool hasNewMail_ = false;
|
||||
std::array<MailAttachSlot, MAIL_MAX_ATTACHMENTS> mailAttachments_{};
|
||||
|
||||
// Bank
|
||||
bool bankOpen_ = false;
|
||||
|
|
@ -1590,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;
|
||||
|
|
|
|||
|
|
@ -217,8 +217,9 @@ public:
|
|||
/** Build CMSG_SEND_MAIL */
|
||||
virtual network::Packet buildSendMail(uint64_t mailboxGuid, const std::string& recipient,
|
||||
const std::string& subject, const std::string& body,
|
||||
uint32_t money, uint32_t cod) {
|
||||
return SendMailPacket::build(mailboxGuid, recipient, subject, body, money, cod);
|
||||
uint32_t money, uint32_t cod,
|
||||
const std::vector<uint64_t>& itemGuids = {}) {
|
||||
return SendMailPacket::build(mailboxGuid, recipient, subject, body, money, cod, itemGuids);
|
||||
}
|
||||
|
||||
/** Parse SMSG_MAIL_LIST_RESULT into a vector of MailMessage */
|
||||
|
|
@ -323,7 +324,8 @@ public:
|
|||
network::Packet buildLeaveChannel(const std::string& channelName) override;
|
||||
network::Packet buildSendMail(uint64_t mailboxGuid, const std::string& recipient,
|
||||
const std::string& subject, const std::string& body,
|
||||
uint32_t money, uint32_t cod) override;
|
||||
uint32_t money, uint32_t cod,
|
||||
const std::vector<uint64_t>& itemGuids = {}) override;
|
||||
bool parseMailList(network::Packet& packet, std::vector<MailMessage>& inbox) override;
|
||||
network::Packet buildMailTakeItem(uint64_t mailboxGuid, uint32_t mailId, uint32_t itemSlot) override;
|
||||
network::Packet buildMailDelete(uint64_t mailboxGuid, uint32_t mailId, uint32_t mailTemplateId) override;
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
@ -2290,7 +2344,8 @@ class SendMailPacket {
|
|||
public:
|
||||
static network::Packet build(uint64_t mailboxGuid, const std::string& recipient,
|
||||
const std::string& subject, const std::string& body,
|
||||
uint32_t money, uint32_t cod);
|
||||
uint32_t money, uint32_t cod,
|
||||
const std::vector<uint64_t>& itemGuids = {});
|
||||
};
|
||||
|
||||
/** CMSG_MAIL_TAKE_MONEY packet builder */
|
||||
|
|
|
|||
|
|
@ -212,6 +212,7 @@ public:
|
|||
* Unload all tiles
|
||||
*/
|
||||
void unloadAll();
|
||||
void softReset(); // Clear tile data without stopping worker threads (non-blocking)
|
||||
|
||||
/**
|
||||
* Precache a set of tiles (for taxi routes, etc.)
|
||||
|
|
|
|||
|
|
@ -172,6 +172,7 @@ private:
|
|||
VkImageView sceneDepthView = VK_NULL_HANDLE;
|
||||
VkExtent2D sceneHistoryExtent = {0, 0};
|
||||
bool sceneHistoryReady = false;
|
||||
mutable uint32_t renderDiagCounter_ = 0;
|
||||
|
||||
// Planar reflection resources
|
||||
static constexpr uint32_t REFLECTION_WIDTH = 512;
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -173,6 +173,8 @@ public:
|
|||
/// Drop the currently held item into a specific equipment slot.
|
||||
/// Returns true if the drop was accepted and consumed.
|
||||
bool dropHeldItemToEquipSlot(game::Inventory& inv, game::EquipSlot slot);
|
||||
/// Drop the currently held item into a bank slot via CMSG_SWAP_ITEM.
|
||||
void dropIntoBankSlot(game::GameHandler& gh, uint8_t dstBag, uint8_t dstSlot);
|
||||
};
|
||||
|
||||
} // namespace ui
|
||||
|
|
|
|||
|
|
@ -18,8 +18,13 @@ struct SpellInfo {
|
|||
uint32_t spellId = 0;
|
||||
std::string name;
|
||||
std::string rank;
|
||||
uint32_t iconId = 0; // SpellIconID
|
||||
uint32_t attributes = 0; // Spell attributes (field 75)
|
||||
std::string description; // Tooltip/description text from Spell.dbc
|
||||
uint32_t iconId = 0; // SpellIconID
|
||||
uint32_t attributes = 0; // Spell attributes (field 4)
|
||||
uint32_t castTimeMs = 0; // Cast time in ms (0 = instant)
|
||||
uint32_t manaCost = 0; // Mana cost
|
||||
uint32_t powerType = 0; // 0=mana, 1=rage, 2=focus, 3=energy
|
||||
uint32_t rangeIndex = 0; // Range index from SpellRange.dbc
|
||||
bool isPassive() const { return (attributes & 0x40) != 0; }
|
||||
};
|
||||
|
||||
|
|
@ -55,19 +60,21 @@ private:
|
|||
// Icon data (loaded from SpellIcon.dbc)
|
||||
bool iconDbLoaded = false;
|
||||
std::unordered_map<uint32_t, std::string> spellIconPaths; // SpellIconID -> path
|
||||
std::unordered_map<uint32_t, VkDescriptorSet> spellIconCache; // SpellIconID -> GL texture
|
||||
std::unordered_map<uint32_t, VkDescriptorSet> spellIconCache; // SpellIconID -> texture
|
||||
|
||||
// Skill line data (loaded from SkillLine.dbc + SkillLineAbility.dbc)
|
||||
bool skillLineDbLoaded = false;
|
||||
std::unordered_map<uint32_t, std::string> skillLineNames; // skillLineID -> name
|
||||
std::unordered_map<uint32_t, uint32_t> skillLineCategories; // skillLineID -> categoryID
|
||||
std::unordered_map<uint32_t, uint32_t> spellToSkillLine; // spellID -> skillLineID
|
||||
std::unordered_map<uint32_t, std::string> skillLineNames;
|
||||
std::unordered_map<uint32_t, uint32_t> skillLineCategories;
|
||||
std::unordered_map<uint32_t, uint32_t> spellToSkillLine;
|
||||
|
||||
// Categorized spell tabs (rebuilt when spell list changes)
|
||||
// ordered map so tabs appear in consistent order
|
||||
// Categorized spell tabs
|
||||
std::vector<SpellTabInfo> spellTabs;
|
||||
size_t lastKnownSpellCount = 0;
|
||||
|
||||
// Search filter
|
||||
char searchFilter_[128] = "";
|
||||
|
||||
// Drag-and-drop from spellbook to action bar
|
||||
bool draggingSpell_ = false;
|
||||
uint32_t dragSpellId_ = 0;
|
||||
|
|
@ -79,6 +86,9 @@ private:
|
|||
void categorizeSpells(const std::unordered_set<uint32_t>& knownSpells);
|
||||
VkDescriptorSet getSpellIcon(uint32_t iconId, pipeline::AssetManager* assetManager);
|
||||
const SpellInfo* getSpellInfo(uint32_t spellId) const;
|
||||
|
||||
// Tooltip rendering helper
|
||||
void renderSpellTooltip(const SpellInfo* info, game::GameHandler& gameHandler);
|
||||
};
|
||||
|
||||
} // namespace ui
|
||||
|
|
|
|||
|
|
@ -20,8 +20,11 @@ public:
|
|||
|
||||
private:
|
||||
void renderTalentTrees(game::GameHandler& gameHandler);
|
||||
void renderTalentTree(game::GameHandler& gameHandler, uint32_t tabId);
|
||||
void renderTalent(game::GameHandler& gameHandler, const game::GameHandler::TalentEntry& talent);
|
||||
void renderTalentTree(game::GameHandler& gameHandler, uint32_t tabId,
|
||||
const std::string& bgFile);
|
||||
void renderTalent(game::GameHandler& gameHandler,
|
||||
const game::GameHandler::TalentEntry& talent,
|
||||
uint32_t pointsInTree);
|
||||
|
||||
void loadSpellDBC(pipeline::AssetManager* assetManager);
|
||||
void loadSpellIconDBC(pipeline::AssetManager* assetManager);
|
||||
|
|
@ -33,10 +36,11 @@ private:
|
|||
// DBC caches
|
||||
bool spellDbcLoaded = false;
|
||||
bool iconDbcLoaded = false;
|
||||
std::unordered_map<uint32_t, uint32_t> spellIconIds; // spellId -> iconId
|
||||
std::unordered_map<uint32_t, uint32_t> spellIconIds; // spellId -> iconId
|
||||
std::unordered_map<uint32_t, std::string> spellIconPaths; // iconId -> path
|
||||
std::unordered_map<uint32_t, VkDescriptorSet> spellIconCache; // iconId -> texture
|
||||
std::unordered_map<uint32_t, std::string> spellTooltips; // spellId -> description
|
||||
std::unordered_map<uint32_t, std::string> spellTooltips; // spellId -> description
|
||||
std::unordered_map<uint32_t, VkDescriptorSet> bgTextureCache_; // tabId -> bg texture
|
||||
};
|
||||
|
||||
} // namespace ui
|
||||
|
|
|
|||
|
|
@ -661,7 +661,11 @@ void Application::logoutToLogin() {
|
|||
if (auto* m2 = renderer->getM2Renderer()) {
|
||||
m2->clear();
|
||||
}
|
||||
// TerrainManager will be re-initialized on next world entry
|
||||
// Clear terrain tile tracking + water surfaces so next world entry starts fresh.
|
||||
// Use softReset() instead of unloadAll() to avoid blocking on worker thread joins.
|
||||
if (auto* terrain = renderer->getTerrainManager()) {
|
||||
terrain->softReset();
|
||||
}
|
||||
if (auto* questMarkers = renderer->getQuestMarkerRenderer()) {
|
||||
questMarkers->clear();
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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]);
|
||||
|
|
@ -12851,10 +12945,114 @@ void GameHandler::sendMail(const std::string& recipient, const std::string& subj
|
|||
LOG_WARNING("sendMail: mailboxGuid_ is 0 (mailbox closed?)");
|
||||
return;
|
||||
}
|
||||
auto packet = packetParsers_->buildSendMail(mailboxGuid_, recipient, subject, body, money, cod);
|
||||
// Collect attached item GUIDs
|
||||
std::vector<uint64_t> itemGuids;
|
||||
for (const auto& att : mailAttachments_) {
|
||||
if (att.occupied()) {
|
||||
itemGuids.push_back(att.itemGuid);
|
||||
}
|
||||
}
|
||||
auto packet = packetParsers_->buildSendMail(mailboxGuid_, recipient, subject, body, money, cod, itemGuids);
|
||||
LOG_INFO("sendMail: to='", recipient, "' subject='", subject, "' money=", money,
|
||||
" mailboxGuid=", mailboxGuid_);
|
||||
" attachments=", itemGuids.size(), " mailboxGuid=", mailboxGuid_);
|
||||
socket->send(packet);
|
||||
clearMailAttachments();
|
||||
}
|
||||
|
||||
bool GameHandler::attachItemFromBackpack(int backpackIndex) {
|
||||
if (backpackIndex < 0 || backpackIndex >= inventory.getBackpackSize()) return false;
|
||||
const auto& slot = inventory.getBackpackSlot(backpackIndex);
|
||||
if (slot.empty()) return false;
|
||||
|
||||
uint64_t itemGuid = backpackSlotGuids_[backpackIndex];
|
||||
if (itemGuid == 0) {
|
||||
itemGuid = resolveOnlineItemGuid(slot.item.itemId);
|
||||
}
|
||||
if (itemGuid == 0) {
|
||||
addSystemChatMessage("Cannot attach: item not found.");
|
||||
return false;
|
||||
}
|
||||
|
||||
// Check not already attached
|
||||
for (const auto& att : mailAttachments_) {
|
||||
if (att.occupied() && att.itemGuid == itemGuid) return false;
|
||||
}
|
||||
|
||||
// Find free attachment slot
|
||||
for (int i = 0; i < MAIL_MAX_ATTACHMENTS; ++i) {
|
||||
if (!mailAttachments_[i].occupied()) {
|
||||
mailAttachments_[i].itemGuid = itemGuid;
|
||||
mailAttachments_[i].item = slot.item;
|
||||
mailAttachments_[i].srcBag = 0xFF;
|
||||
mailAttachments_[i].srcSlot = static_cast<uint8_t>(23 + backpackIndex);
|
||||
LOG_INFO("Mail attach: slot=", i, " item='", slot.item.name, "' guid=0x",
|
||||
std::hex, itemGuid, std::dec, " from backpack[", backpackIndex, "]");
|
||||
return true;
|
||||
}
|
||||
}
|
||||
addSystemChatMessage("Cannot attach: all attachment slots full.");
|
||||
return false;
|
||||
}
|
||||
|
||||
bool GameHandler::attachItemFromBag(int bagIndex, int slotIndex) {
|
||||
if (bagIndex < 0 || bagIndex >= inventory.NUM_BAG_SLOTS) return false;
|
||||
if (slotIndex < 0 || slotIndex >= inventory.getBagSize(bagIndex)) return false;
|
||||
const auto& slot = inventory.getBagSlot(bagIndex, slotIndex);
|
||||
if (slot.empty()) return false;
|
||||
|
||||
uint64_t itemGuid = 0;
|
||||
uint64_t bagGuid = equipSlotGuids_[19 + bagIndex];
|
||||
if (bagGuid != 0) {
|
||||
auto it = containerContents_.find(bagGuid);
|
||||
if (it != containerContents_.end() && slotIndex < static_cast<int>(it->second.numSlots)) {
|
||||
itemGuid = it->second.slotGuids[slotIndex];
|
||||
}
|
||||
}
|
||||
if (itemGuid == 0) {
|
||||
itemGuid = resolveOnlineItemGuid(slot.item.itemId);
|
||||
}
|
||||
if (itemGuid == 0) {
|
||||
addSystemChatMessage("Cannot attach: item not found.");
|
||||
return false;
|
||||
}
|
||||
|
||||
for (const auto& att : mailAttachments_) {
|
||||
if (att.occupied() && att.itemGuid == itemGuid) return false;
|
||||
}
|
||||
|
||||
for (int i = 0; i < MAIL_MAX_ATTACHMENTS; ++i) {
|
||||
if (!mailAttachments_[i].occupied()) {
|
||||
mailAttachments_[i].itemGuid = itemGuid;
|
||||
mailAttachments_[i].item = slot.item;
|
||||
mailAttachments_[i].srcBag = static_cast<uint8_t>(19 + bagIndex);
|
||||
mailAttachments_[i].srcSlot = static_cast<uint8_t>(slotIndex);
|
||||
LOG_INFO("Mail attach: slot=", i, " item='", slot.item.name, "' guid=0x",
|
||||
std::hex, itemGuid, std::dec, " from bag[", bagIndex, "][", slotIndex, "]");
|
||||
return true;
|
||||
}
|
||||
}
|
||||
addSystemChatMessage("Cannot attach: all attachment slots full.");
|
||||
return false;
|
||||
}
|
||||
|
||||
bool GameHandler::detachMailAttachment(int attachIndex) {
|
||||
if (attachIndex < 0 || attachIndex >= MAIL_MAX_ATTACHMENTS) return false;
|
||||
if (!mailAttachments_[attachIndex].occupied()) return false;
|
||||
LOG_INFO("Mail detach: slot=", attachIndex, " item='", mailAttachments_[attachIndex].item.name, "'");
|
||||
mailAttachments_[attachIndex] = MailAttachSlot{};
|
||||
return true;
|
||||
}
|
||||
|
||||
void GameHandler::clearMailAttachments() {
|
||||
for (auto& att : mailAttachments_) att = MailAttachSlot{};
|
||||
}
|
||||
|
||||
int GameHandler::getMailAttachmentCount() const {
|
||||
int count = 0;
|
||||
for (const auto& att : mailAttachments_) {
|
||||
if (att.occupied()) ++count;
|
||||
}
|
||||
return count;
|
||||
}
|
||||
|
||||
void GameHandler::mailTakeMoney(uint32_t mailId) {
|
||||
|
|
@ -13201,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,
|
||||
|
|
@ -13333,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",
|
||||
|
|
|
|||
|
|
@ -728,7 +728,8 @@ network::Packet ClassicPacketParsers::buildSendMail(uint64_t mailboxGuid,
|
|||
const std::string& recipient,
|
||||
const std::string& subject,
|
||||
const std::string& body,
|
||||
uint32_t money, uint32_t cod) {
|
||||
uint32_t money, uint32_t cod,
|
||||
const std::vector<uint64_t>& itemGuids) {
|
||||
network::Packet packet(wireOpcode(Opcode::CMSG_SEND_MAIL));
|
||||
packet.writeUInt64(mailboxGuid);
|
||||
packet.writeString(recipient);
|
||||
|
|
@ -736,7 +737,9 @@ network::Packet ClassicPacketParsers::buildSendMail(uint64_t mailboxGuid,
|
|||
packet.writeString(body);
|
||||
packet.writeUInt32(0); // stationery
|
||||
packet.writeUInt32(0); // unknown
|
||||
packet.writeUInt64(0); // item GUID (0 = no attachment, single item only in Vanilla)
|
||||
// Vanilla supports only one item attachment (single uint64 GUID)
|
||||
uint64_t singleItemGuid = itemGuids.empty() ? 0 : itemGuids[0];
|
||||
packet.writeUInt64(singleItemGuid);
|
||||
packet.writeUInt32(money);
|
||||
packet.writeUInt32(cod);
|
||||
packet.writeUInt64(0); // unk3 (clients > 1.9.4)
|
||||
|
|
|
|||
|
|
@ -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());
|
||||
|
|
@ -3939,7 +4018,8 @@ network::Packet GetMailListPacket::build(uint64_t mailboxGuid) {
|
|||
|
||||
network::Packet SendMailPacket::build(uint64_t mailboxGuid, const std::string& recipient,
|
||||
const std::string& subject, const std::string& body,
|
||||
uint32_t money, uint32_t cod) {
|
||||
uint32_t money, uint32_t cod,
|
||||
const std::vector<uint64_t>& itemGuids) {
|
||||
// WotLK 3.3.5a format
|
||||
network::Packet packet(wireOpcode(Opcode::CMSG_SEND_MAIL));
|
||||
packet.writeUInt64(mailboxGuid);
|
||||
|
|
@ -3948,7 +4028,12 @@ network::Packet SendMailPacket::build(uint64_t mailboxGuid, const std::string& r
|
|||
packet.writeString(body);
|
||||
packet.writeUInt32(0); // stationery
|
||||
packet.writeUInt32(0); // unknown
|
||||
packet.writeUInt8(0); // attachment count (0 = no attachments)
|
||||
uint8_t attachCount = static_cast<uint8_t>(itemGuids.size());
|
||||
packet.writeUInt8(attachCount);
|
||||
for (uint8_t i = 0; i < attachCount; ++i) {
|
||||
packet.writeUInt8(i); // attachment slot index
|
||||
packet.writeUInt64(itemGuids[i]);
|
||||
}
|
||||
packet.writeUInt32(money);
|
||||
packet.writeUInt32(cod);
|
||||
return packet;
|
||||
|
|
|
|||
|
|
@ -705,7 +705,15 @@ bool TerrainManager::advanceFinalization(FinalizingTile& ft) {
|
|||
// Load water immediately after terrain (same frame) — water is now
|
||||
// deduplicated to ~1-2 merged surfaces per tile, so this is fast.
|
||||
if (waterRenderer) {
|
||||
size_t beforeSurfaces = waterRenderer->getSurfaceCount();
|
||||
waterRenderer->loadFromTerrain(pending->terrain, true, x, y);
|
||||
size_t afterSurfaces = waterRenderer->getSurfaceCount();
|
||||
if (afterSurfaces > beforeSurfaces) {
|
||||
LOG_INFO("Water: tile [", x, ",", y, "] added ", afterSurfaces - beforeSurfaces,
|
||||
" surfaces (total: ", afterSurfaces, ")");
|
||||
}
|
||||
} else {
|
||||
LOG_WARNING("Water: waterRenderer is null during tile [", x, ",", y, "] finalization!");
|
||||
}
|
||||
|
||||
// Ensure M2 renderer has asset manager
|
||||
|
|
@ -1230,13 +1238,35 @@ void TerrainManager::unloadTile(int x, int y) {
|
|||
}
|
||||
|
||||
void TerrainManager::unloadAll() {
|
||||
// Stop worker threads
|
||||
// Signal worker threads to stop and wait briefly for them to finish.
|
||||
// Workers may be mid-prepareTile (reading MPQ / parsing ADT) which can
|
||||
// take seconds, so use a short deadline and detach any stragglers.
|
||||
if (workerRunning.load()) {
|
||||
workerRunning.store(false);
|
||||
queueCV.notify_all();
|
||||
|
||||
auto deadline = std::chrono::steady_clock::now() + std::chrono::milliseconds(500);
|
||||
for (auto& t : workerThreads) {
|
||||
if (t.joinable()) {
|
||||
t.join();
|
||||
if (!t.joinable()) continue;
|
||||
// Try a timed wait via polling — std::thread has no timed join.
|
||||
bool joined = false;
|
||||
while (std::chrono::steady_clock::now() < deadline) {
|
||||
// Check if thread finished by trying a native timed join
|
||||
#ifdef __linux__
|
||||
struct timespec ts;
|
||||
clock_gettime(CLOCK_REALTIME, &ts);
|
||||
ts.tv_nsec += 50000000; // 50ms
|
||||
if (ts.tv_nsec >= 1000000000) { ts.tv_sec++; ts.tv_nsec -= 1000000000; }
|
||||
if (pthread_timedjoin_np(t.native_handle(), nullptr, &ts) == 0) {
|
||||
joined = true;
|
||||
break;
|
||||
}
|
||||
#else
|
||||
std::this_thread::sleep_for(std::chrono::milliseconds(50));
|
||||
#endif
|
||||
}
|
||||
if (!joined && t.joinable()) {
|
||||
t.detach();
|
||||
}
|
||||
}
|
||||
workerThreads.clear();
|
||||
|
|
@ -1288,6 +1318,32 @@ void TerrainManager::unloadAll() {
|
|||
}
|
||||
}
|
||||
|
||||
void TerrainManager::softReset() {
|
||||
// Clear queues (workers may still be running — they'll find empty queues)
|
||||
{
|
||||
std::lock_guard<std::mutex> lock(queueMutex);
|
||||
loadQueue.clear();
|
||||
while (!readyQueue.empty()) readyQueue.pop();
|
||||
}
|
||||
pendingTiles.clear();
|
||||
finalizingTiles_.clear();
|
||||
placedDoodadIds.clear();
|
||||
|
||||
LOG_INFO("Soft-resetting terrain (clearing tiles + water, workers stay alive)");
|
||||
loadedTiles.clear();
|
||||
failedTiles.clear();
|
||||
|
||||
currentTile = {-1, -1};
|
||||
lastStreamTile = {-1, -1};
|
||||
|
||||
if (terrainRenderer) {
|
||||
terrainRenderer->clear();
|
||||
}
|
||||
if (waterRenderer) {
|
||||
waterRenderer->clear();
|
||||
}
|
||||
}
|
||||
|
||||
TileCoord TerrainManager::worldToTile(float glX, float glY) const {
|
||||
auto [tileX, tileY] = core::coords::worldToTile(glX, glY);
|
||||
return {tileX, tileY};
|
||||
|
|
|
|||
|
|
@ -76,6 +76,7 @@ bool WaterRenderer::initialize(VkContext* ctx, VkDescriptorSetLayout perFrameLay
|
|||
|
||||
VkDescriptorPoolCreateInfo poolInfo{};
|
||||
poolInfo.sType = VK_STRUCTURE_TYPE_DESCRIPTOR_POOL_CREATE_INFO;
|
||||
poolInfo.flags = VK_DESCRIPTOR_POOL_CREATE_FREE_DESCRIPTOR_SET_BIT;
|
||||
poolInfo.maxSets = MAX_WATER_SETS;
|
||||
poolInfo.poolSizeCount = 1;
|
||||
poolInfo.pPoolSizes = &poolSize;
|
||||
|
|
@ -541,6 +542,8 @@ void WaterRenderer::updateMaterialUBO(WaterSurface& surface) {
|
|||
write.pBufferInfo = &bufInfo;
|
||||
|
||||
vkUpdateDescriptorSets(vkCtx->getDevice(), 1, &write, 0, nullptr);
|
||||
} else {
|
||||
LOG_WARNING("Water: failed to allocate material descriptor set (pool exhaustion?)");
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -802,8 +805,10 @@ void WaterRenderer::loadFromTerrain(const pipeline::ADTTerrain& terrain, bool ap
|
|||
totalSurfaces++;
|
||||
}
|
||||
|
||||
LOG_DEBUG("Water: Loaded ", totalSurfaces, " surfaces from tile [", tileX, ",", tileY,
|
||||
"] (", mergeGroups.size(), " groups), total surfaces: ", surfaces.size());
|
||||
if (totalSurfaces > 0) {
|
||||
LOG_INFO("Water: Loaded ", totalSurfaces, " surfaces from tile [", tileX, ",", tileY,
|
||||
"] (", mergeGroups.size(), " groups), total surfaces: ", surfaces.size());
|
||||
}
|
||||
}
|
||||
|
||||
void WaterRenderer::removeTile(int tileX, int tileY) {
|
||||
|
|
@ -936,8 +941,21 @@ void WaterRenderer::clear() {
|
|||
void WaterRenderer::render(VkCommandBuffer cmd, VkDescriptorSet perFrameSet,
|
||||
const Camera& /*camera*/, float /*time*/, bool use1x) {
|
||||
VkPipeline pipeline = (use1x && water1xPipeline) ? water1xPipeline : waterPipeline;
|
||||
if (!renderingEnabled || surfaces.empty() || !pipeline) return;
|
||||
if (!sceneSet) return;
|
||||
if (!renderingEnabled || surfaces.empty() || !pipeline) {
|
||||
if (renderDiagCounter_++ % 300 == 0 && !surfaces.empty()) {
|
||||
LOG_WARNING("Water: render skipped — enabled=", renderingEnabled,
|
||||
" surfaces=", surfaces.size(),
|
||||
" pipeline=", (pipeline ? "ok" : "null"),
|
||||
" use1x=", use1x);
|
||||
}
|
||||
return;
|
||||
}
|
||||
if (!sceneSet) {
|
||||
if (renderDiagCounter_++ % 300 == 0) {
|
||||
LOG_WARNING("Water: render skipped — sceneSet is null, surfaces=", surfaces.size());
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
vkCmdBindPipeline(cmd, VK_PIPELINE_BIND_POINT_GRAPHICS, pipeline);
|
||||
|
||||
|
|
@ -1251,6 +1269,9 @@ void WaterRenderer::destroyWaterMesh(WaterSurface& surface) {
|
|||
destroyBuffer(allocator, ab);
|
||||
surface.materialUBO = VK_NULL_HANDLE;
|
||||
}
|
||||
if (surface.materialSet && materialDescPool) {
|
||||
vkFreeDescriptorSets(vkCtx->getDevice(), materialDescPool, 1, &surface.materialSet);
|
||||
}
|
||||
surface.materialSet = VK_NULL_HANDLE;
|
||||
}
|
||||
|
||||
|
|
|
|||
File diff suppressed because it is too large
Load diff
|
|
@ -543,6 +543,25 @@ bool InventoryScreen::dropHeldItemToEquipSlot(game::Inventory& inv, game::EquipS
|
|||
return !holdingItem;
|
||||
}
|
||||
|
||||
void InventoryScreen::dropIntoBankSlot(game::GameHandler& /*gh*/, uint8_t dstBag, uint8_t dstSlot) {
|
||||
if (!holdingItem || !gameHandler_) return;
|
||||
uint8_t srcBag = 0xFF;
|
||||
uint8_t srcSlot = 0;
|
||||
if (heldSource == HeldSource::BACKPACK && heldBackpackIndex >= 0) {
|
||||
srcSlot = static_cast<uint8_t>(23 + heldBackpackIndex);
|
||||
} else if (heldSource == HeldSource::BAG) {
|
||||
srcBag = static_cast<uint8_t>(19 + heldBagIndex);
|
||||
srcSlot = static_cast<uint8_t>(heldBagSlotIndex);
|
||||
} else if (heldSource == HeldSource::EQUIPMENT) {
|
||||
srcSlot = static_cast<uint8_t>(heldEquipSlot);
|
||||
} else {
|
||||
return;
|
||||
}
|
||||
gameHandler_->swapContainerItems(srcBag, srcSlot, dstBag, dstSlot);
|
||||
holdingItem = false;
|
||||
inventoryDirty = true;
|
||||
}
|
||||
|
||||
bool InventoryScreen::beginPickupFromEquipSlot(game::Inventory& inv, game::EquipSlot slot) {
|
||||
if (holdingItem) return false;
|
||||
const auto& eq = inv.getEquipSlot(slot);
|
||||
|
|
@ -1402,13 +1421,22 @@ void InventoryScreen::renderItemSlot(game::Inventory& inventory, const game::Ite
|
|||
}
|
||||
}
|
||||
|
||||
// Right-click: vendor sell (if vendor mode) or auto-equip/use
|
||||
// Right-click: bank deposit (if bank open), vendor sell (if vendor mode), or auto-equip/use
|
||||
if (ImGui::IsItemClicked(ImGuiMouseButton_Right) && !holdingItem && gameHandler_) {
|
||||
LOG_INFO("Right-click slot: kind=", (int)kind,
|
||||
" backpackIndex=", backpackIndex,
|
||||
" bagIndex=", bagIndex, " bagSlotIndex=", bagSlotIndex,
|
||||
" vendorMode=", vendorMode_);
|
||||
if (vendorMode_ && kind == SlotKind::BACKPACK && backpackIndex >= 0) {
|
||||
" vendorMode=", vendorMode_,
|
||||
" bankOpen=", gameHandler_->isBankOpen());
|
||||
if (gameHandler_->isMailComposeOpen() && kind == SlotKind::BACKPACK && backpackIndex >= 0) {
|
||||
gameHandler_->attachItemFromBackpack(backpackIndex);
|
||||
} else if (gameHandler_->isMailComposeOpen() && kind == SlotKind::BACKPACK && isBagSlot) {
|
||||
gameHandler_->attachItemFromBag(bagIndex, bagSlotIndex);
|
||||
} else if (gameHandler_->isBankOpen() && kind == SlotKind::BACKPACK && backpackIndex >= 0) {
|
||||
gameHandler_->depositItem(0xFF, static_cast<uint8_t>(23 + backpackIndex));
|
||||
} else if (gameHandler_->isBankOpen() && kind == SlotKind::BACKPACK && isBagSlot) {
|
||||
gameHandler_->depositItem(static_cast<uint8_t>(19 + bagIndex), static_cast<uint8_t>(bagSlotIndex));
|
||||
} else if (vendorMode_ && kind == SlotKind::BACKPACK && backpackIndex >= 0) {
|
||||
gameHandler_->sellItemBySlot(backpackIndex);
|
||||
} else if (vendorMode_ && kind == SlotKind::BACKPACK && isBagSlot) {
|
||||
gameHandler_->sellItemInBag(bagIndex, bagSlotIndex);
|
||||
|
|
|
|||
|
|
@ -9,9 +9,29 @@
|
|||
#include "core/logger.hpp"
|
||||
#include <algorithm>
|
||||
#include <map>
|
||||
#include <cctype>
|
||||
|
||||
namespace wowee { namespace ui {
|
||||
|
||||
// Case-insensitive substring match
|
||||
static bool containsCI(const std::string& haystack, const char* needle) {
|
||||
if (!needle || !needle[0]) return true;
|
||||
size_t needleLen = strlen(needle);
|
||||
if (needleLen > haystack.size()) return false;
|
||||
for (size_t i = 0; i <= haystack.size() - needleLen; i++) {
|
||||
bool match = true;
|
||||
for (size_t j = 0; j < needleLen; j++) {
|
||||
if (std::tolower(static_cast<unsigned char>(haystack[i + j])) !=
|
||||
std::tolower(static_cast<unsigned char>(needle[j]))) {
|
||||
match = false;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (match) return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
void SpellbookScreen::loadSpellDBC(pipeline::AssetManager* assetManager) {
|
||||
if (dbcLoadAttempted) return;
|
||||
dbcLoadAttempted = true;
|
||||
|
|
@ -30,12 +50,11 @@ void SpellbookScreen::loadSpellDBC(pipeline::AssetManager* assetManager) {
|
|||
return;
|
||||
}
|
||||
|
||||
// Try expansion-specific layout first, then fall back to WotLK hardcoded indices
|
||||
// if the DBC is from WotLK MPQs but the active expansion uses different field offsets.
|
||||
const auto* spellL = pipeline::getActiveDBCLayout() ? pipeline::getActiveDBCLayout()->getLayout("Spell") : nullptr;
|
||||
|
||||
auto tryLoad = [&](uint32_t idField, uint32_t attrField, uint32_t iconField,
|
||||
uint32_t nameField, uint32_t rankField, const char* label) {
|
||||
uint32_t nameField, uint32_t rankField, uint32_t tooltipField,
|
||||
const char* label) {
|
||||
spellData.clear();
|
||||
uint32_t count = dbc->getRecordCount();
|
||||
for (uint32_t i = 0; i < count; ++i) {
|
||||
|
|
@ -48,6 +67,7 @@ void SpellbookScreen::loadSpellDBC(pipeline::AssetManager* assetManager) {
|
|||
info.iconId = dbc->getUInt32(i, iconField);
|
||||
info.name = dbc->getString(i, nameField);
|
||||
info.rank = dbc->getString(i, rankField);
|
||||
info.description = dbc->getString(i, tooltipField);
|
||||
|
||||
if (!info.name.empty()) {
|
||||
spellData[spellId] = std::move(info);
|
||||
|
|
@ -56,17 +76,17 @@ void SpellbookScreen::loadSpellDBC(pipeline::AssetManager* assetManager) {
|
|||
LOG_INFO("Spellbook: Loaded ", spellData.size(), " spells from Spell.dbc (", label, ")");
|
||||
};
|
||||
|
||||
// Try active expansion layout
|
||||
if (spellL) {
|
||||
uint32_t tooltipField = 139;
|
||||
// Try to get Tooltip field from layout, fall back to 139
|
||||
try { tooltipField = (*spellL)["Tooltip"]; } catch (...) {}
|
||||
tryLoad((*spellL)["ID"], (*spellL)["Attributes"], (*spellL)["IconID"],
|
||||
(*spellL)["Name"], (*spellL)["Rank"], "expansion layout");
|
||||
(*spellL)["Name"], (*spellL)["Rank"], tooltipField, "expansion layout");
|
||||
}
|
||||
|
||||
// If layout failed or loaded 0 spells, try WotLK hardcoded indices
|
||||
// (binary DBC may be from WotLK MPQs regardless of active expansion)
|
||||
if (spellData.empty() && fieldCount >= 200) {
|
||||
LOG_INFO("Spellbook: Retrying with WotLK field indices (DBC has ", fieldCount, " fields)");
|
||||
tryLoad(0, 4, 133, 136, 153, "WotLK fallback");
|
||||
tryLoad(0, 4, 133, 136, 153, 139, "WotLK fallback");
|
||||
}
|
||||
|
||||
dbcLoaded = !spellData.empty();
|
||||
|
|
@ -88,10 +108,7 @@ void SpellbookScreen::loadSpellIconDBC(pipeline::AssetManager* assetManager) {
|
|||
if (!assetManager || !assetManager->isInitialized()) return;
|
||||
|
||||
auto dbc = assetManager->loadDBC("SpellIcon.dbc");
|
||||
if (!dbc || !dbc->isLoaded()) {
|
||||
LOG_WARNING("Spellbook: Could not load SpellIcon.dbc");
|
||||
return;
|
||||
}
|
||||
if (!dbc || !dbc->isLoaded()) return;
|
||||
|
||||
const auto* iconL = pipeline::getActiveDBCLayout() ? pipeline::getActiveDBCLayout()->getLayout("SpellIcon") : nullptr;
|
||||
for (uint32_t i = 0; i < dbc->getRecordCount(); i++) {
|
||||
|
|
@ -101,8 +118,6 @@ void SpellbookScreen::loadSpellIconDBC(pipeline::AssetManager* assetManager) {
|
|||
spellIconPaths[id] = path;
|
||||
}
|
||||
}
|
||||
|
||||
LOG_INFO("Spellbook: Loaded ", spellIconPaths.size(), " spell icon paths");
|
||||
}
|
||||
|
||||
void SpellbookScreen::loadSkillLineDBCs(pipeline::AssetManager* assetManager) {
|
||||
|
|
@ -111,7 +126,6 @@ void SpellbookScreen::loadSkillLineDBCs(pipeline::AssetManager* assetManager) {
|
|||
|
||||
if (!assetManager || !assetManager->isInitialized()) return;
|
||||
|
||||
// Load SkillLine.dbc: field 0 = ID, field 1 = categoryID, field 3 = name_enUS
|
||||
auto skillLineDbc = assetManager->loadDBC("SkillLine.dbc");
|
||||
const auto* slL = pipeline::getActiveDBCLayout() ? pipeline::getActiveDBCLayout()->getLayout("SkillLine") : nullptr;
|
||||
if (skillLineDbc && skillLineDbc->isLoaded()) {
|
||||
|
|
@ -124,12 +138,8 @@ void SpellbookScreen::loadSkillLineDBCs(pipeline::AssetManager* assetManager) {
|
|||
skillLineCategories[id] = category;
|
||||
}
|
||||
}
|
||||
LOG_INFO("Spellbook: Loaded ", skillLineNames.size(), " skill lines");
|
||||
} else {
|
||||
LOG_WARNING("Spellbook: Could not load SkillLine.dbc");
|
||||
}
|
||||
|
||||
// Load SkillLineAbility.dbc: field 0 = ID, field 1 = skillLineID, field 2 = spellID
|
||||
auto slaDbc = assetManager->loadDBC("SkillLineAbility.dbc");
|
||||
const auto* slaL = pipeline::getActiveDBCLayout() ? pipeline::getActiveDBCLayout()->getLayout("SkillLineAbility") : nullptr;
|
||||
if (slaDbc && slaDbc->isLoaded()) {
|
||||
|
|
@ -140,20 +150,26 @@ void SpellbookScreen::loadSkillLineDBCs(pipeline::AssetManager* assetManager) {
|
|||
spellToSkillLine[spellId] = skillLineId;
|
||||
}
|
||||
}
|
||||
LOG_INFO("Spellbook: Loaded ", spellToSkillLine.size(), " skill line abilities");
|
||||
} else {
|
||||
LOG_WARNING("Spellbook: Could not load SkillLineAbility.dbc");
|
||||
}
|
||||
}
|
||||
|
||||
void SpellbookScreen::categorizeSpells(const std::unordered_set<uint32_t>& knownSpells) {
|
||||
spellTabs.clear();
|
||||
|
||||
// Only SkillLine category 7 ("Class") gets its own tab (the 3 specialties).
|
||||
// Everything else (weapons, professions, racials, general utilities) → General.
|
||||
static constexpr uint32_t SKILLLINE_CATEGORY_CLASS = 7;
|
||||
// SkillLine.dbc category IDs
|
||||
static constexpr uint32_t CAT_CLASS = 7; // Class abilities (spec trees)
|
||||
static constexpr uint32_t CAT_PROFESSION = 11; // Primary professions
|
||||
static constexpr uint32_t CAT_SECONDARY = 9; // Secondary skills (Cooking, First Aid, Fishing, Riding, Companions)
|
||||
|
||||
std::map<uint32_t, std::vector<const SpellInfo*>> specialtySpells;
|
||||
// Special skill line IDs within category 9 that get their own tabs
|
||||
static constexpr uint32_t SKILLLINE_RIDING = 762; // Mounts
|
||||
static constexpr uint32_t SKILLLINE_COMPANIONS = 778; // Vanity/companion pets
|
||||
|
||||
// Buckets
|
||||
std::map<uint32_t, std::vector<const SpellInfo*>> specSpells; // class spec trees
|
||||
std::map<uint32_t, std::vector<const SpellInfo*>> profSpells; // professions + secondary
|
||||
std::vector<const SpellInfo*> mountSpells;
|
||||
std::vector<const SpellInfo*> companionSpells;
|
||||
std::vector<const SpellInfo*> generalSpells;
|
||||
|
||||
for (uint32_t spellId : knownSpells) {
|
||||
|
|
@ -165,11 +181,41 @@ void SpellbookScreen::categorizeSpells(const std::unordered_set<uint32_t>& known
|
|||
auto slIt = spellToSkillLine.find(spellId);
|
||||
if (slIt != spellToSkillLine.end()) {
|
||||
uint32_t skillLineId = slIt->second;
|
||||
auto catIt = skillLineCategories.find(skillLineId);
|
||||
if (catIt != skillLineCategories.end() && catIt->second == SKILLLINE_CATEGORY_CLASS) {
|
||||
specialtySpells[skillLineId].push_back(info);
|
||||
|
||||
// Mounts: Riding skill line (762)
|
||||
if (skillLineId == SKILLLINE_RIDING) {
|
||||
mountSpells.push_back(info);
|
||||
continue;
|
||||
}
|
||||
|
||||
// Companions: vanity pets skill line (778)
|
||||
if (skillLineId == SKILLLINE_COMPANIONS) {
|
||||
companionSpells.push_back(info);
|
||||
continue;
|
||||
}
|
||||
|
||||
auto catIt = skillLineCategories.find(skillLineId);
|
||||
if (catIt != skillLineCategories.end()) {
|
||||
uint32_t cat = catIt->second;
|
||||
|
||||
// Class spec abilities
|
||||
if (cat == CAT_CLASS) {
|
||||
specSpells[skillLineId].push_back(info);
|
||||
continue;
|
||||
}
|
||||
|
||||
// Primary professions
|
||||
if (cat == CAT_PROFESSION) {
|
||||
profSpells[skillLineId].push_back(info);
|
||||
continue;
|
||||
}
|
||||
|
||||
// Secondary skills (Cooking, First Aid, Fishing)
|
||||
if (cat == CAT_SECONDARY) {
|
||||
profSpells[skillLineId].push_back(info);
|
||||
continue;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
generalSpells.push_back(info);
|
||||
|
|
@ -177,28 +223,47 @@ void SpellbookScreen::categorizeSpells(const std::unordered_set<uint32_t>& known
|
|||
|
||||
auto byName = [](const SpellInfo* a, const SpellInfo* b) { return a->name < b->name; };
|
||||
|
||||
// Specialty tabs sorted alphabetically by skill line name
|
||||
std::vector<std::pair<std::string, std::vector<const SpellInfo*>>> named;
|
||||
for (auto& [skillLineId, spells] : specialtySpells) {
|
||||
auto nameIt = skillLineNames.find(skillLineId);
|
||||
std::string tabName = (nameIt != skillLineNames.end()) ? nameIt->second
|
||||
: "Specialty";
|
||||
std::sort(spells.begin(), spells.end(), byName);
|
||||
named.push_back({std::move(tabName), std::move(spells)});
|
||||
}
|
||||
std::sort(named.begin(), named.end(),
|
||||
[](const auto& a, const auto& b) { return a.first < b.first; });
|
||||
// Helper: add sorted skill-line-grouped tabs
|
||||
auto addGroupedTabs = [&](std::map<uint32_t, std::vector<const SpellInfo*>>& groups,
|
||||
const char* fallbackName) {
|
||||
std::vector<std::pair<std::string, std::vector<const SpellInfo*>>> named;
|
||||
for (auto& [skillLineId, spells] : groups) {
|
||||
auto nameIt = skillLineNames.find(skillLineId);
|
||||
std::string tabName = (nameIt != skillLineNames.end()) ? nameIt->second : fallbackName;
|
||||
std::sort(spells.begin(), spells.end(), byName);
|
||||
named.push_back({std::move(tabName), std::move(spells)});
|
||||
}
|
||||
std::sort(named.begin(), named.end(),
|
||||
[](const auto& a, const auto& b) { return a.first < b.first; });
|
||||
for (auto& [name, spells] : named) {
|
||||
spellTabs.push_back({std::move(name), std::move(spells)});
|
||||
}
|
||||
};
|
||||
|
||||
for (auto& [name, spells] : named) {
|
||||
spellTabs.push_back({std::move(name), std::move(spells)});
|
||||
}
|
||||
// 1. Class spec tabs
|
||||
addGroupedTabs(specSpells, "Spec");
|
||||
|
||||
// General tab last
|
||||
// 2. General tab
|
||||
if (!generalSpells.empty()) {
|
||||
std::sort(generalSpells.begin(), generalSpells.end(), byName);
|
||||
spellTabs.push_back({"General", std::move(generalSpells)});
|
||||
}
|
||||
|
||||
// 3. Professions tabs
|
||||
addGroupedTabs(profSpells, "Profession");
|
||||
|
||||
// 4. Mounts tab
|
||||
if (!mountSpells.empty()) {
|
||||
std::sort(mountSpells.begin(), mountSpells.end(), byName);
|
||||
spellTabs.push_back({"Mounts", std::move(mountSpells)});
|
||||
}
|
||||
|
||||
// 5. Companions tab
|
||||
if (!companionSpells.empty()) {
|
||||
std::sort(companionSpells.begin(), companionSpells.end(), byName);
|
||||
spellTabs.push_back({"Companions", std::move(companionSpells)});
|
||||
}
|
||||
|
||||
lastKnownSpellCount = knownSpells.size();
|
||||
}
|
||||
|
||||
|
|
@ -244,6 +309,47 @@ const SpellInfo* SpellbookScreen::getSpellInfo(uint32_t spellId) const {
|
|||
return (it != spellData.end()) ? &it->second : nullptr;
|
||||
}
|
||||
|
||||
void SpellbookScreen::renderSpellTooltip(const SpellInfo* info, game::GameHandler& gameHandler) {
|
||||
ImGui::BeginTooltip();
|
||||
ImGui::PushTextWrapPos(320.0f);
|
||||
|
||||
// Spell name in yellow
|
||||
ImGui::TextColored(ImVec4(1.0f, 1.0f, 0.3f, 1.0f), "%s", info->name.c_str());
|
||||
|
||||
// Rank in gray
|
||||
if (!info->rank.empty()) {
|
||||
ImGui::SameLine();
|
||||
ImGui::TextColored(ImVec4(0.6f, 0.6f, 0.6f, 1.0f), "(%s)", info->rank.c_str());
|
||||
}
|
||||
|
||||
// Passive indicator
|
||||
if (info->isPassive()) {
|
||||
ImGui::TextColored(ImVec4(1.0f, 1.0f, 0.0f, 1.0f), "Passive");
|
||||
}
|
||||
|
||||
// Cooldown if active
|
||||
float cd = gameHandler.getSpellCooldown(info->spellId);
|
||||
if (cd > 0.0f) {
|
||||
ImGui::TextColored(ImVec4(1.0f, 0.3f, 0.3f, 1.0f), "Cooldown: %.1fs", cd);
|
||||
}
|
||||
|
||||
// Description
|
||||
if (!info->description.empty()) {
|
||||
ImGui::Spacing();
|
||||
ImGui::TextWrapped("%s", info->description.c_str());
|
||||
}
|
||||
|
||||
// Usage hints
|
||||
if (!info->isPassive()) {
|
||||
ImGui::Spacing();
|
||||
ImGui::TextColored(ImVec4(0.3f, 1.0f, 0.3f, 1.0f), "Drag to action bar");
|
||||
ImGui::TextColored(ImVec4(0.3f, 1.0f, 0.3f, 1.0f), "Double-click to cast");
|
||||
}
|
||||
|
||||
ImGui::PopTextWrapPos();
|
||||
ImGui::EndTooltip();
|
||||
}
|
||||
|
||||
void SpellbookScreen::render(game::GameHandler& gameHandler, pipeline::AssetManager* assetManager) {
|
||||
// P key toggle (edge-triggered)
|
||||
bool wantsTextInput = ImGui::GetIO().WantTextInput;
|
||||
|
|
@ -272,88 +378,156 @@ void SpellbookScreen::render(game::GameHandler& gameHandler, pipeline::AssetMana
|
|||
float screenW = window ? static_cast<float>(window->getWidth()) : 1280.0f;
|
||||
float screenH = window ? static_cast<float>(window->getHeight()) : 720.0f;
|
||||
|
||||
float bookW = 360.0f;
|
||||
float bookH = std::min(520.0f, screenH - 120.0f);
|
||||
float bookW = 380.0f;
|
||||
float bookH = std::min(560.0f, screenH - 100.0f);
|
||||
float bookX = screenW - bookW - 10.0f;
|
||||
float bookY = 80.0f;
|
||||
|
||||
ImGui::SetNextWindowPos(ImVec2(bookX, bookY), ImGuiCond_FirstUseEver);
|
||||
ImGui::SetNextWindowSize(ImVec2(bookW, bookH), ImGuiCond_FirstUseEver);
|
||||
ImGui::SetNextWindowSizeConstraints(ImVec2(280, 200), ImVec2(screenW, screenH));
|
||||
ImGui::SetNextWindowSizeConstraints(ImVec2(300, 250), ImVec2(screenW, screenH));
|
||||
|
||||
bool windowOpen = open;
|
||||
ImGui::PushStyleVar(ImGuiStyleVar_WindowPadding, ImVec2(8, 8));
|
||||
if (ImGui::Begin("Spellbook", &windowOpen)) {
|
||||
// Clamp window position to stay on screen
|
||||
ImVec2 winPos = ImGui::GetWindowPos();
|
||||
ImVec2 winSize = ImGui::GetWindowSize();
|
||||
float clampedX = std::max(0.0f, std::min(winPos.x, screenW - winSize.x));
|
||||
float clampedY = std::max(0.0f, std::min(winPos.y, screenH - winSize.y));
|
||||
if (clampedX != winPos.x || clampedY != winPos.y) {
|
||||
ImGui::SetWindowPos(ImVec2(clampedX, clampedY));
|
||||
}
|
||||
// Search bar
|
||||
ImGui::SetNextItemWidth(-1);
|
||||
ImGui::InputTextWithHint("##search", "Search spells...", searchFilter_, sizeof(searchFilter_));
|
||||
|
||||
ImGui::Spacing();
|
||||
|
||||
// Tab bar
|
||||
if (ImGui::BeginTabBar("SpellbookTabs")) {
|
||||
for (size_t tabIdx = 0; tabIdx < spellTabs.size(); tabIdx++) {
|
||||
const auto& tab = spellTabs[tabIdx];
|
||||
|
||||
char tabLabel[64];
|
||||
snprintf(tabLabel, sizeof(tabLabel), "%s (%zu)",
|
||||
tab.name.c_str(), tab.spells.size());
|
||||
// Count visible spells (respecting search filter)
|
||||
int visibleCount = 0;
|
||||
for (const SpellInfo* info : tab.spells) {
|
||||
if (containsCI(info->name, searchFilter_)) visibleCount++;
|
||||
}
|
||||
|
||||
char tabLabel[128];
|
||||
snprintf(tabLabel, sizeof(tabLabel), "%s (%d)###sbtab%zu",
|
||||
tab.name.c_str(), visibleCount, tabIdx);
|
||||
|
||||
if (ImGui::BeginTabItem(tabLabel)) {
|
||||
if (tab.spells.empty()) {
|
||||
ImGui::TextDisabled("No spells in this category.");
|
||||
if (visibleCount == 0) {
|
||||
if (searchFilter_[0])
|
||||
ImGui::TextDisabled("No matching spells.");
|
||||
else
|
||||
ImGui::TextDisabled("No spells in this category.");
|
||||
}
|
||||
|
||||
ImGui::BeginChild("SpellList", ImVec2(0, 0), true);
|
||||
|
||||
float iconSize = 32.0f;
|
||||
const float iconSize = 36.0f;
|
||||
const float rowHeight = iconSize + 4.0f;
|
||||
|
||||
for (const SpellInfo* info : tab.spells) {
|
||||
// Apply search filter
|
||||
if (!containsCI(info->name, searchFilter_)) continue;
|
||||
|
||||
ImGui::PushID(static_cast<int>(info->spellId));
|
||||
|
||||
float cd = gameHandler.getSpellCooldown(info->spellId);
|
||||
bool onCooldown = cd > 0.0f;
|
||||
bool isPassive = info->isPassive();
|
||||
bool isDim = isPassive || onCooldown;
|
||||
|
||||
VkDescriptorSet iconTex = getSpellIcon(info->iconId, assetManager);
|
||||
|
||||
// Selectable consumes clicks properly (prevents window drag)
|
||||
// Row selectable
|
||||
ImGui::Selectable("##row", false,
|
||||
ImGuiSelectableFlags_AllowDoubleClick, ImVec2(0, iconSize));
|
||||
ImGuiSelectableFlags_AllowDoubleClick, ImVec2(0, rowHeight));
|
||||
bool rowHovered = ImGui::IsItemHovered();
|
||||
bool rowClicked = ImGui::IsItemClicked(0);
|
||||
ImVec2 rMin = ImGui::GetItemRectMin();
|
||||
ImVec2 rMax = ImGui::GetItemRectMax();
|
||||
auto* dl = ImGui::GetWindowDrawList();
|
||||
|
||||
// Draw icon on top of selectable
|
||||
// Hover highlight
|
||||
if (rowHovered) {
|
||||
dl->AddRectFilled(rMin, rMax, IM_COL32(255, 255, 255, 15), 3.0f);
|
||||
}
|
||||
|
||||
// Icon background
|
||||
ImVec2 iconMin = rMin;
|
||||
ImVec2 iconMax(rMin.x + iconSize, rMin.y + iconSize);
|
||||
dl->AddRectFilled(iconMin, iconMax, IM_COL32(25, 25, 35, 200), 3.0f);
|
||||
|
||||
// Icon
|
||||
if (iconTex) {
|
||||
ImU32 tint = (isPassive || onCooldown) ? IM_COL32(150, 150, 150, 255) : IM_COL32(255, 255, 255, 255);
|
||||
dl->AddImage((ImTextureID)(uintptr_t)iconTex,
|
||||
rMin, ImVec2(rMin.x + iconSize, rMin.y + iconSize));
|
||||
} else {
|
||||
dl->AddRectFilled(rMin,
|
||||
ImVec2(rMin.x + iconSize, rMin.y + iconSize),
|
||||
IM_COL32(60, 60, 80, 255));
|
||||
ImVec2(iconMin.x + 1, iconMin.y + 1),
|
||||
ImVec2(iconMax.x - 1, iconMax.y - 1),
|
||||
ImVec2(0, 0), ImVec2(1, 1), tint);
|
||||
}
|
||||
|
||||
// Draw name and rank text
|
||||
ImU32 textCol = isDim ? IM_COL32(153, 153, 153, 255)
|
||||
: ImGui::GetColorU32(ImGuiCol_Text);
|
||||
ImU32 dimCol = ImGui::GetColorU32(ImGuiCol_TextDisabled);
|
||||
float textX = rMin.x + iconSize + 4.0f;
|
||||
dl->AddText(ImVec2(textX, rMin.y), textCol, info->name.c_str());
|
||||
if (!info->rank.empty()) {
|
||||
dl->AddText(ImVec2(textX, rMin.y + ImGui::GetTextLineHeight()),
|
||||
dimCol, info->rank.c_str());
|
||||
// Icon border
|
||||
ImU32 borderCol;
|
||||
if (isPassive) {
|
||||
borderCol = IM_COL32(180, 180, 50, 200); // Yellow for passive
|
||||
} else if (onCooldown) {
|
||||
char cdBuf[32];
|
||||
snprintf(cdBuf, sizeof(cdBuf), "%.1fs cooldown", cd);
|
||||
dl->AddText(ImVec2(textX, rMin.y + ImGui::GetTextLineHeight()),
|
||||
dimCol, cdBuf);
|
||||
borderCol = IM_COL32(120, 40, 40, 200); // Red for cooldown
|
||||
} else {
|
||||
borderCol = IM_COL32(100, 100, 120, 200); // Default border
|
||||
}
|
||||
dl->AddRect(iconMin, iconMax, borderCol, 3.0f, 0, 1.5f);
|
||||
|
||||
// Cooldown overlay on icon
|
||||
if (onCooldown) {
|
||||
// Darkened sweep
|
||||
dl->AddRectFilled(iconMin, iconMax, IM_COL32(0, 0, 0, 120), 3.0f);
|
||||
// Cooldown text centered on icon
|
||||
char cdBuf[16];
|
||||
snprintf(cdBuf, sizeof(cdBuf), "%.0f", cd);
|
||||
ImVec2 cdSize = ImGui::CalcTextSize(cdBuf);
|
||||
ImVec2 cdPos(iconMin.x + (iconSize - cdSize.x) * 0.5f,
|
||||
iconMin.y + (iconSize - cdSize.y) * 0.5f);
|
||||
dl->AddText(ImVec2(cdPos.x + 1, cdPos.y + 1), IM_COL32(0, 0, 0, 255), cdBuf);
|
||||
dl->AddText(cdPos, IM_COL32(255, 80, 80, 255), cdBuf);
|
||||
}
|
||||
|
||||
// Spell name
|
||||
float textX = rMin.x + iconSize + 8.0f;
|
||||
float nameY = rMin.y + 2.0f;
|
||||
|
||||
ImU32 nameCol;
|
||||
if (isPassive) {
|
||||
nameCol = IM_COL32(255, 255, 130, 255); // Yellow-ish for passive
|
||||
} else if (onCooldown) {
|
||||
nameCol = IM_COL32(150, 150, 150, 255);
|
||||
} else {
|
||||
nameCol = IM_COL32(255, 255, 255, 255);
|
||||
}
|
||||
dl->AddText(ImVec2(textX, nameY), nameCol, info->name.c_str());
|
||||
|
||||
// Second line: rank or passive/cooldown indicator
|
||||
float subY = nameY + ImGui::GetTextLineHeight() + 1.0f;
|
||||
if (!info->rank.empty()) {
|
||||
dl->AddText(ImVec2(textX, subY),
|
||||
IM_COL32(150, 150, 150, 255), info->rank.c_str());
|
||||
}
|
||||
if (isPassive) {
|
||||
float afterRank = textX;
|
||||
if (!info->rank.empty()) {
|
||||
afterRank += ImGui::CalcTextSize(info->rank.c_str()).x + 8.0f;
|
||||
}
|
||||
dl->AddText(ImVec2(afterRank, subY),
|
||||
IM_COL32(200, 200, 80, 200), "Passive");
|
||||
} else if (onCooldown) {
|
||||
float afterRank = textX;
|
||||
if (!info->rank.empty()) {
|
||||
afterRank += ImGui::CalcTextSize(info->rank.c_str()).x + 8.0f;
|
||||
}
|
||||
char cdText[32];
|
||||
snprintf(cdText, sizeof(cdText), "%.1fs", cd);
|
||||
dl->AddText(ImVec2(afterRank, subY),
|
||||
IM_COL32(255, 100, 100, 200), cdText);
|
||||
}
|
||||
|
||||
// Interaction
|
||||
if (rowHovered) {
|
||||
// Start drag on click (not passive)
|
||||
if (rowClicked && !isPassive) {
|
||||
|
|
@ -362,31 +536,18 @@ void SpellbookScreen::render(game::GameHandler& gameHandler, pipeline::AssetMana
|
|||
dragSpellIconTex_ = iconTex;
|
||||
}
|
||||
|
||||
// Double-click to cast
|
||||
if (ImGui::IsMouseDoubleClicked(0) && !isPassive && !onCooldown) {
|
||||
draggingSpell_ = false;
|
||||
dragSpellId_ = 0;
|
||||
dragSpellIconTex_ = 0;
|
||||
dragSpellIconTex_ = VK_NULL_HANDLE;
|
||||
uint64_t target = gameHandler.hasTarget() ? gameHandler.getTargetGuid() : 0;
|
||||
gameHandler.castSpell(info->spellId, target);
|
||||
}
|
||||
|
||||
// Tooltip (only when not dragging)
|
||||
if (!draggingSpell_) {
|
||||
ImGui::BeginTooltip();
|
||||
ImGui::Text("%s", info->name.c_str());
|
||||
if (!info->rank.empty()) {
|
||||
ImGui::TextDisabled("%s", info->rank.c_str());
|
||||
}
|
||||
ImGui::TextDisabled("Spell ID: %u", info->spellId);
|
||||
if (isPassive) {
|
||||
ImGui::TextDisabled("Passive");
|
||||
} else {
|
||||
ImGui::TextDisabled("Drag to action bar to assign");
|
||||
if (!onCooldown) {
|
||||
ImGui::TextDisabled("Double-click to cast");
|
||||
}
|
||||
}
|
||||
ImGui::EndTooltip();
|
||||
renderSpellTooltip(info, gameHandler);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -402,6 +563,7 @@ void SpellbookScreen::render(game::GameHandler& gameHandler, pipeline::AssetMana
|
|||
}
|
||||
}
|
||||
ImGui::End();
|
||||
ImGui::PopStyleVar();
|
||||
|
||||
if (!windowOpen) {
|
||||
open = false;
|
||||
|
|
@ -410,7 +572,7 @@ void SpellbookScreen::render(game::GameHandler& gameHandler, pipeline::AssetMana
|
|||
// Render dragged spell icon at cursor
|
||||
if (draggingSpell_ && dragSpellId_ != 0) {
|
||||
ImVec2 mousePos = ImGui::GetMousePos();
|
||||
float dragSize = 32.0f;
|
||||
float dragSize = 36.0f;
|
||||
if (dragSpellIconTex_) {
|
||||
ImGui::GetForegroundDrawList()->AddImage(
|
||||
(ImTextureID)(uintptr_t)dragSpellIconTex_,
|
||||
|
|
@ -420,14 +582,13 @@ void SpellbookScreen::render(game::GameHandler& gameHandler, pipeline::AssetMana
|
|||
ImGui::GetForegroundDrawList()->AddRectFilled(
|
||||
ImVec2(mousePos.x - dragSize * 0.5f, mousePos.y - dragSize * 0.5f),
|
||||
ImVec2(mousePos.x + dragSize * 0.5f, mousePos.y + dragSize * 0.5f),
|
||||
IM_COL32(80, 80, 120, 180));
|
||||
IM_COL32(80, 80, 120, 180), 3.0f);
|
||||
}
|
||||
|
||||
// Cancel drag on mouse release (action bar consumes it before this if dropped on a slot)
|
||||
if (ImGui::IsMouseReleased(ImGuiMouseButton_Left)) {
|
||||
draggingSpell_ = false;
|
||||
dragSpellId_ = 0;
|
||||
dragSpellIconTex_ = 0;
|
||||
dragSpellIconTex_ = VK_NULL_HANDLE;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -7,9 +7,20 @@
|
|||
#include "pipeline/blp_loader.hpp"
|
||||
#include "pipeline/dbc_layout.hpp"
|
||||
#include <algorithm>
|
||||
#include <cmath>
|
||||
|
||||
namespace wowee { namespace ui {
|
||||
|
||||
// WoW class names indexed by class ID (1-11)
|
||||
static const char* classNames[] = {
|
||||
"Unknown", "Warrior", "Paladin", "Hunter", "Rogue", "Priest",
|
||||
"Death Knight", "Shaman", "Mage", "Warlock", "Unknown", "Druid"
|
||||
};
|
||||
|
||||
static const char* getClassName(uint8_t classId) {
|
||||
return (classId >= 1 && classId <= 11) ? classNames[classId] : "Unknown";
|
||||
}
|
||||
|
||||
void TalentScreen::render(game::GameHandler& gameHandler) {
|
||||
// N key toggle (edge-triggered)
|
||||
bool wantsTextInput = ImGui::GetIO().WantTextInput;
|
||||
|
|
@ -25,19 +36,28 @@ void TalentScreen::render(game::GameHandler& gameHandler) {
|
|||
float screenW = window ? static_cast<float>(window->getWidth()) : 1280.0f;
|
||||
float screenH = window ? static_cast<float>(window->getHeight()) : 720.0f;
|
||||
|
||||
float winW = 600.0f; // Wider for talent grid
|
||||
float winH = 550.0f;
|
||||
float winW = 680.0f;
|
||||
float winH = 600.0f;
|
||||
float winX = (screenW - winW) * 0.5f;
|
||||
float winY = (screenH - winH) * 0.5f;
|
||||
|
||||
ImGui::SetNextWindowPos(ImVec2(winX, winY), ImGuiCond_FirstUseEver);
|
||||
ImGui::SetNextWindowSize(ImVec2(winW, winH), ImGuiCond_FirstUseEver);
|
||||
|
||||
// Build title with point distribution
|
||||
uint8_t playerClass = gameHandler.getPlayerClass();
|
||||
std::string title = "Talents";
|
||||
if (playerClass > 0) {
|
||||
title = std::string(getClassName(playerClass)) + " Talents";
|
||||
}
|
||||
|
||||
bool windowOpen = open;
|
||||
if (ImGui::Begin("Talents", &windowOpen)) {
|
||||
ImGui::PushStyleVar(ImGuiStyleVar_WindowPadding, ImVec2(8, 8));
|
||||
if (ImGui::Begin(title.c_str(), &windowOpen)) {
|
||||
renderTalentTrees(gameHandler);
|
||||
}
|
||||
ImGui::End();
|
||||
ImGui::PopStyleVar();
|
||||
|
||||
if (!windowOpen) {
|
||||
open = false;
|
||||
|
|
@ -47,93 +67,95 @@ void TalentScreen::render(game::GameHandler& gameHandler) {
|
|||
void TalentScreen::renderTalentTrees(game::GameHandler& gameHandler) {
|
||||
auto* assetManager = core::Application::getInstance().getAssetManager();
|
||||
|
||||
// Ensure talent DBCs are loaded (even if server hasn't sent SMSG_TALENTS_INFO)
|
||||
// Ensure talent DBCs are loaded once
|
||||
static bool dbcLoadAttempted = false;
|
||||
if (!dbcLoadAttempted) {
|
||||
dbcLoadAttempted = true;
|
||||
gameHandler.loadTalentDbc();
|
||||
loadSpellDBC(assetManager);
|
||||
loadSpellIconDBC(assetManager);
|
||||
LOG_INFO("Talent window opened, DBC load triggered");
|
||||
}
|
||||
|
||||
uint8_t playerClass = gameHandler.getPlayerClass();
|
||||
LOG_INFO("Talent window: playerClass=", static_cast<int>(playerClass));
|
||||
|
||||
// Active spec indicator and switcher
|
||||
uint8_t activeSpec = gameHandler.getActiveTalentSpec();
|
||||
ImGui::Text("Active Spec: %u", activeSpec + 1);
|
||||
ImGui::SameLine();
|
||||
|
||||
// Spec buttons
|
||||
if (ImGui::SmallButton("Spec 1")) {
|
||||
gameHandler.switchTalentSpec(0);
|
||||
}
|
||||
ImGui::SameLine();
|
||||
if (ImGui::SmallButton("Spec 2")) {
|
||||
gameHandler.switchTalentSpec(1);
|
||||
}
|
||||
ImGui::SameLine();
|
||||
|
||||
// Show unspent points for both specs
|
||||
ImGui::Text("| Unspent: Spec1=%u Spec2=%u",
|
||||
gameHandler.getUnspentTalentPoints(0),
|
||||
gameHandler.getUnspentTalentPoints(1));
|
||||
|
||||
ImGui::Separator();
|
||||
|
||||
// Debug info
|
||||
ImGui::Text("Player Class: %u", playerClass);
|
||||
ImGui::Text("Total Talent Tabs: %zu", gameHandler.getAllTalentTabs().size());
|
||||
ImGui::Text("Total Talents: %zu", gameHandler.getAllTalents().size());
|
||||
ImGui::Separator();
|
||||
|
||||
if (playerClass == 0) {
|
||||
ImGui::TextDisabled("Class information not available.");
|
||||
LOG_WARNING("Talent window: getPlayerClass() returned 0");
|
||||
return;
|
||||
}
|
||||
|
||||
// Get talent tabs for this class (class mask: 1 << (class - 1))
|
||||
// Get talent tabs for this class, sorted by orderIndex
|
||||
uint32_t classMask = 1u << (playerClass - 1);
|
||||
LOG_INFO("Talent window: classMask=0x", std::hex, classMask, std::dec);
|
||||
|
||||
// Collect talent tabs for this class, sorted by orderIndex
|
||||
std::vector<const game::GameHandler::TalentTabEntry*> classTabs;
|
||||
for (const auto& [tabId, tab] : gameHandler.getAllTalentTabs()) {
|
||||
if (tab.classMask & classMask) {
|
||||
classTabs.push_back(&tab);
|
||||
}
|
||||
}
|
||||
|
||||
std::sort(classTabs.begin(), classTabs.end(),
|
||||
[](const auto* a, const auto* b) { return a->orderIndex < b->orderIndex; });
|
||||
|
||||
LOG_INFO("Talent window: found ", classTabs.size(), " tabs for class mask 0x", std::hex, classMask, std::dec);
|
||||
|
||||
ImGui::Text("Class Mask: 0x%X", classMask);
|
||||
ImGui::Text("Tabs for this class: %zu", classTabs.size());
|
||||
|
||||
if (classTabs.empty()) {
|
||||
ImGui::TextDisabled("No talent trees available for your class.");
|
||||
ImGui::Spacing();
|
||||
ImGui::TextDisabled("Available tabs:");
|
||||
for (const auto& [tabId, tab] : gameHandler.getAllTalentTabs()) {
|
||||
ImGui::Text(" Tab %u: %s (mask: 0x%X)", tabId, tab.name.c_str(), tab.classMask);
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
// Display points
|
||||
uint8_t unspentPoints = gameHandler.getUnspentTalentPoints();
|
||||
ImGui::Text("Unspent Points: %u", unspentPoints);
|
||||
// Compute points-per-tree for display
|
||||
uint32_t treeTotals[3] = {0, 0, 0};
|
||||
for (size_t ti = 0; ti < classTabs.size() && ti < 3; ti++) {
|
||||
for (const auto& [tid, rank] : gameHandler.getLearnedTalents()) {
|
||||
const auto* t = gameHandler.getTalentEntry(tid);
|
||||
if (t && t->tabId == classTabs[ti]->tabId) {
|
||||
treeTotals[ti] += rank;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Header: spec switcher + unspent points + point distribution
|
||||
uint8_t activeSpec = gameHandler.getActiveTalentSpec();
|
||||
uint8_t unspent = gameHandler.getUnspentTalentPoints();
|
||||
|
||||
// Spec buttons
|
||||
for (uint8_t s = 0; s < 2; s++) {
|
||||
if (s > 0) ImGui::SameLine();
|
||||
bool isActive = (s == activeSpec);
|
||||
if (isActive) {
|
||||
ImGui::PushStyleColor(ImGuiCol_Button, ImVec4(0.2f, 0.5f, 0.8f, 1.0f));
|
||||
ImGui::PushStyleColor(ImGuiCol_ButtonHovered, ImVec4(0.3f, 0.6f, 0.9f, 1.0f));
|
||||
}
|
||||
char specLabel[32];
|
||||
snprintf(specLabel, sizeof(specLabel), "Spec %u", s + 1);
|
||||
if (ImGui::Button(specLabel, ImVec2(70, 0))) {
|
||||
if (!isActive) gameHandler.switchTalentSpec(s);
|
||||
}
|
||||
if (isActive) ImGui::PopStyleColor(2);
|
||||
}
|
||||
|
||||
// Point distribution
|
||||
ImGui::SameLine(0, 20);
|
||||
if (classTabs.size() >= 3) {
|
||||
ImGui::Text("(%u / %u / %u)", treeTotals[0], treeTotals[1], treeTotals[2]);
|
||||
}
|
||||
|
||||
// Unspent points
|
||||
ImGui::SameLine(0, 20);
|
||||
if (unspent > 0) {
|
||||
ImGui::TextColored(ImVec4(0.3f, 1.0f, 0.3f, 1.0f), "%u point%s available",
|
||||
unspent, unspent > 1 ? "s" : "");
|
||||
} else {
|
||||
ImGui::TextColored(ImVec4(0.5f, 0.5f, 0.5f, 1.0f), "No points available");
|
||||
}
|
||||
|
||||
ImGui::Separator();
|
||||
|
||||
// Render tabs
|
||||
// Render tabs with point counts in tab labels
|
||||
if (ImGui::BeginTabBar("TalentTabs")) {
|
||||
for (const auto* tab : classTabs) {
|
||||
if (ImGui::BeginTabItem(tab->name.c_str())) {
|
||||
renderTalentTree(gameHandler, tab->tabId);
|
||||
for (size_t ti = 0; ti < classTabs.size(); ti++) {
|
||||
const auto* tab = classTabs[ti];
|
||||
char tabLabel[128];
|
||||
uint32_t pts = (ti < 3) ? treeTotals[ti] : 0;
|
||||
snprintf(tabLabel, sizeof(tabLabel), "%s (%u)###tab%u", tab->name.c_str(), pts, tab->tabId);
|
||||
|
||||
if (ImGui::BeginTabItem(tabLabel)) {
|
||||
renderTalentTree(gameHandler, tab->tabId, tab->backgroundFile);
|
||||
ImGui::EndTabItem();
|
||||
}
|
||||
}
|
||||
|
|
@ -141,7 +163,10 @@ void TalentScreen::renderTalentTrees(game::GameHandler& gameHandler) {
|
|||
}
|
||||
}
|
||||
|
||||
void TalentScreen::renderTalentTree(game::GameHandler& gameHandler, uint32_t tabId) {
|
||||
void TalentScreen::renderTalentTree(game::GameHandler& gameHandler, uint32_t tabId,
|
||||
const std::string& bgFile) {
|
||||
auto* assetManager = core::Application::getInstance().getAssetManager();
|
||||
|
||||
// Collect all talents for this tab
|
||||
std::vector<const game::GameHandler::TalentEntry*> talents;
|
||||
for (const auto& [talentId, talent] : gameHandler.getAllTalents()) {
|
||||
|
|
@ -155,25 +180,132 @@ void TalentScreen::renderTalentTree(game::GameHandler& gameHandler, uint32_t tab
|
|||
return;
|
||||
}
|
||||
|
||||
// Sort talents by row then column for consistent rendering
|
||||
std::sort(talents.begin(), talents.end(), [](const auto* a, const auto* b) {
|
||||
if (a->row != b->row) return a->row < b->row;
|
||||
return a->column < b->column;
|
||||
});
|
||||
|
||||
// Find grid dimensions
|
||||
uint8_t maxRow = 0, maxCol = 0;
|
||||
for (const auto* talent : talents) {
|
||||
maxRow = std::max(maxRow, talent->row);
|
||||
maxCol = std::max(maxCol, talent->column);
|
||||
}
|
||||
// WoW talent grids are always 4 columns wide
|
||||
if (maxCol < 3) maxCol = 3;
|
||||
|
||||
const float iconSize = 40.0f;
|
||||
const float spacing = 8.0f;
|
||||
const float cellSize = iconSize + spacing;
|
||||
const float gridWidth = (maxCol + 1) * cellSize + spacing;
|
||||
const float gridHeight = (maxRow + 1) * cellSize + spacing;
|
||||
|
||||
// Points in this tree
|
||||
uint32_t pointsInTree = 0;
|
||||
for (const auto& [tid, rank] : gameHandler.getLearnedTalents()) {
|
||||
const auto* t = gameHandler.getTalentEntry(tid);
|
||||
if (t && t->tabId == tabId) {
|
||||
pointsInTree += rank;
|
||||
}
|
||||
}
|
||||
|
||||
// Center the grid
|
||||
float availW = ImGui::GetContentRegionAvail().x;
|
||||
float offsetX = std::max(0.0f, (availW - gridWidth) * 0.5f);
|
||||
|
||||
ImGui::BeginChild("TalentGrid", ImVec2(0, 0), false);
|
||||
|
||||
// Render grid
|
||||
for (uint8_t row = 0; row <= maxRow; ++row) {
|
||||
// Row label
|
||||
ImGui::Text("Tier %u", row);
|
||||
ImGui::SameLine(80);
|
||||
ImVec2 gridOrigin = ImGui::GetCursorScreenPos();
|
||||
gridOrigin.x += offsetX;
|
||||
|
||||
// Draw background texture if available
|
||||
if (!bgFile.empty() && assetManager) {
|
||||
VkDescriptorSet bgTex = VK_NULL_HANDLE;
|
||||
auto bgIt = bgTextureCache_.find(tabId);
|
||||
if (bgIt != bgTextureCache_.end()) {
|
||||
bgTex = bgIt->second;
|
||||
} else {
|
||||
// Try to load the background texture
|
||||
std::string bgPath = bgFile;
|
||||
// Normalize path separators
|
||||
for (auto& c : bgPath) { if (c == '\\') c = '/'; }
|
||||
bgPath += ".blp";
|
||||
auto blpData = assetManager->readFile(bgPath);
|
||||
if (!blpData.empty()) {
|
||||
auto image = pipeline::BLPLoader::load(blpData);
|
||||
if (image.isValid()) {
|
||||
auto* window = core::Application::getInstance().getWindow();
|
||||
auto* vkCtx = window ? window->getVkContext() : nullptr;
|
||||
if (vkCtx) {
|
||||
bgTex = vkCtx->uploadImGuiTexture(image.data.data(), image.width, image.height);
|
||||
}
|
||||
}
|
||||
}
|
||||
bgTextureCache_[tabId] = bgTex;
|
||||
}
|
||||
|
||||
if (bgTex) {
|
||||
auto* drawList = ImGui::GetWindowDrawList();
|
||||
float bgW = gridWidth + spacing * 2;
|
||||
float bgH = gridHeight + spacing * 2;
|
||||
drawList->AddImage((ImTextureID)(uintptr_t)bgTex,
|
||||
ImVec2(gridOrigin.x - spacing, gridOrigin.y - spacing),
|
||||
ImVec2(gridOrigin.x + bgW - spacing, gridOrigin.y + bgH - spacing),
|
||||
ImVec2(0, 0), ImVec2(1, 1),
|
||||
IM_COL32(255, 255, 255, 60)); // Subtle background
|
||||
}
|
||||
}
|
||||
|
||||
// Build a position lookup for prerequisite arrows
|
||||
struct TalentPos {
|
||||
const game::GameHandler::TalentEntry* talent;
|
||||
ImVec2 center;
|
||||
};
|
||||
std::unordered_map<uint32_t, TalentPos> talentPositions;
|
||||
|
||||
// First pass: compute positions
|
||||
for (const auto* talent : talents) {
|
||||
float x = gridOrigin.x + talent->column * cellSize + spacing;
|
||||
float y = gridOrigin.y + talent->row * cellSize + spacing;
|
||||
ImVec2 center(x + iconSize * 0.5f, y + iconSize * 0.5f);
|
||||
talentPositions[talent->talentId] = {talent, center};
|
||||
}
|
||||
|
||||
// Draw prerequisite arrows
|
||||
auto* drawList = ImGui::GetWindowDrawList();
|
||||
for (const auto* talent : talents) {
|
||||
for (int i = 0; i < 3; ++i) {
|
||||
if (talent->prereqTalent[i] == 0) continue;
|
||||
auto fromIt = talentPositions.find(talent->prereqTalent[i]);
|
||||
auto toIt = talentPositions.find(talent->talentId);
|
||||
if (fromIt == talentPositions.end() || toIt == talentPositions.end()) continue;
|
||||
|
||||
uint8_t prereqRank = gameHandler.getTalentRank(talent->prereqTalent[i]);
|
||||
bool met = prereqRank >= talent->prereqRank[i];
|
||||
ImU32 lineCol = met ? IM_COL32(100, 220, 100, 200) : IM_COL32(120, 120, 120, 150);
|
||||
|
||||
ImVec2 from = fromIt->second.center;
|
||||
ImVec2 to = toIt->second.center;
|
||||
|
||||
// Draw line from bottom of prerequisite to top of dependent
|
||||
ImVec2 lineStart(from.x, from.y + iconSize * 0.5f);
|
||||
ImVec2 lineEnd(to.x, to.y - iconSize * 0.5f);
|
||||
drawList->AddLine(lineStart, lineEnd, lineCol, 2.0f);
|
||||
|
||||
// Arrow head
|
||||
float arrowSize = 5.0f;
|
||||
drawList->AddTriangleFilled(
|
||||
ImVec2(lineEnd.x, lineEnd.y),
|
||||
ImVec2(lineEnd.x - arrowSize, lineEnd.y - arrowSize * 1.5f),
|
||||
ImVec2(lineEnd.x + arrowSize, lineEnd.y - arrowSize * 1.5f),
|
||||
lineCol);
|
||||
}
|
||||
}
|
||||
|
||||
// Render talent icons
|
||||
for (uint8_t row = 0; row <= maxRow; ++row) {
|
||||
for (uint8_t col = 0; col <= maxCol; ++col) {
|
||||
// Find talent at this position
|
||||
const game::GameHandler::TalentEntry* talent = nullptr;
|
||||
for (const auto* t : talents) {
|
||||
if (t->row == row && t->column == col) {
|
||||
|
|
@ -182,23 +314,31 @@ void TalentScreen::renderTalentTree(game::GameHandler& gameHandler, uint32_t tab
|
|||
}
|
||||
}
|
||||
|
||||
if (col > 0) ImGui::SameLine();
|
||||
float x = gridOrigin.x + col * cellSize + spacing;
|
||||
float y = gridOrigin.y + row * cellSize + spacing;
|
||||
|
||||
ImGui::SetCursorScreenPos(ImVec2(x, y));
|
||||
|
||||
if (talent) {
|
||||
renderTalent(gameHandler, *talent);
|
||||
renderTalent(gameHandler, *talent, pointsInTree);
|
||||
} else {
|
||||
// Empty slot
|
||||
ImGui::InvisibleButton(("empty_" + std::to_string(row) + "_" + std::to_string(col)).c_str(),
|
||||
ImVec2(iconSize, iconSize));
|
||||
// Empty cell — invisible placeholder
|
||||
ImGui::InvisibleButton(("e_" + std::to_string(row) + "_" + std::to_string(col)).c_str(),
|
||||
ImVec2(iconSize, iconSize));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Reserve space for the full grid so scrolling works
|
||||
ImGui::SetCursorScreenPos(ImVec2(gridOrigin.x, gridOrigin.y + gridHeight));
|
||||
ImGui::Dummy(ImVec2(gridWidth, 0));
|
||||
|
||||
ImGui::EndChild();
|
||||
}
|
||||
|
||||
void TalentScreen::renderTalent(game::GameHandler& gameHandler,
|
||||
const game::GameHandler::TalentEntry& talent) {
|
||||
const game::GameHandler::TalentEntry& talent,
|
||||
uint32_t pointsInTree) {
|
||||
auto* assetManager = core::Application::getInstance().getAssetManager();
|
||||
|
||||
uint8_t currentRank = gameHandler.getTalentRank(talent.talentId);
|
||||
|
|
@ -220,38 +360,35 @@ void TalentScreen::renderTalent(game::GameHandler& gameHandler,
|
|||
}
|
||||
}
|
||||
|
||||
// Check tier requirement (need 5 points in previous tier)
|
||||
// Check tier requirement (need row*5 points in tree)
|
||||
if (talent.row > 0) {
|
||||
// Count points spent in this tree
|
||||
uint32_t pointsInTree = 0;
|
||||
for (const auto& [tid, rank] : gameHandler.getLearnedTalents()) {
|
||||
const auto* t = gameHandler.getTalentEntry(tid);
|
||||
if (t && t->tabId == talent.tabId) {
|
||||
pointsInTree += rank;
|
||||
}
|
||||
}
|
||||
|
||||
uint32_t requiredPoints = talent.row * 5;
|
||||
if (pointsInTree < requiredPoints) {
|
||||
canLearn = false;
|
||||
}
|
||||
}
|
||||
|
||||
// Determine state color and tint
|
||||
// Determine visual state
|
||||
enum TalentState { MAXED, PARTIAL, AVAILABLE, LOCKED };
|
||||
TalentState state;
|
||||
if (currentRank >= talent.maxRank) {
|
||||
state = MAXED;
|
||||
} else if (currentRank > 0) {
|
||||
state = PARTIAL;
|
||||
} else if (canLearn && prereqsMet) {
|
||||
state = AVAILABLE;
|
||||
} else {
|
||||
state = LOCKED;
|
||||
}
|
||||
|
||||
// Colors per state
|
||||
ImVec4 borderColor;
|
||||
ImVec4 tint;
|
||||
if (currentRank == talent.maxRank) {
|
||||
borderColor = ImVec4(0.3f, 0.9f, 0.3f, 1.0f); // Green border (maxed)
|
||||
tint = ImVec4(1.0f, 1.0f, 1.0f, 1.0f); // Full color
|
||||
} else if (currentRank > 0) {
|
||||
borderColor = ImVec4(1.0f, 0.9f, 0.3f, 1.0f); // Yellow border (partial)
|
||||
tint = ImVec4(1.0f, 1.0f, 1.0f, 1.0f); // Full color
|
||||
} else if (canLearn && prereqsMet) {
|
||||
borderColor = ImVec4(1.0f, 1.0f, 1.0f, 1.0f); // White border (available)
|
||||
tint = ImVec4(1.0f, 1.0f, 1.0f, 1.0f); // Full color
|
||||
} else {
|
||||
borderColor = ImVec4(0.5f, 0.5f, 0.5f, 1.0f); // Gray border (locked)
|
||||
tint = ImVec4(0.5f, 0.5f, 0.5f, 1.0f); // Desaturated
|
||||
switch (state) {
|
||||
case MAXED: borderColor = ImVec4(0.2f, 0.9f, 0.2f, 1.0f); tint = ImVec4(1,1,1,1); break;
|
||||
case PARTIAL: borderColor = ImVec4(0.2f, 0.8f, 0.2f, 1.0f); tint = ImVec4(1,1,1,1); break;
|
||||
case AVAILABLE:borderColor = ImVec4(1.0f, 1.0f, 1.0f, 0.8f); tint = ImVec4(1,1,1,1); break;
|
||||
case LOCKED: borderColor = ImVec4(0.4f, 0.4f, 0.4f, 0.8f); tint = ImVec4(0.4f,0.4f,0.4f,1); break;
|
||||
}
|
||||
|
||||
const float iconSize = 40.0f;
|
||||
|
|
@ -267,60 +404,76 @@ void TalentScreen::renderTalent(game::GameHandler& gameHandler,
|
|||
}
|
||||
}
|
||||
|
||||
// Use InvisibleButton for click handling
|
||||
bool clicked = ImGui::InvisibleButton("##talent", ImVec2(iconSize, iconSize));
|
||||
// Click target
|
||||
bool clicked = ImGui::InvisibleButton("##t", ImVec2(iconSize, iconSize));
|
||||
bool hovered = ImGui::IsItemHovered();
|
||||
|
||||
// Draw icon and border
|
||||
ImVec2 pMin = ImGui::GetItemRectMin();
|
||||
ImVec2 pMax = ImGui::GetItemRectMax();
|
||||
auto* drawList = ImGui::GetWindowDrawList();
|
||||
auto* dl = ImGui::GetWindowDrawList();
|
||||
|
||||
// Background fill
|
||||
ImU32 bgCol;
|
||||
if (state == LOCKED) {
|
||||
bgCol = IM_COL32(20, 20, 25, 200);
|
||||
} else {
|
||||
bgCol = IM_COL32(30, 30, 40, 200);
|
||||
}
|
||||
dl->AddRectFilled(pMin, pMax, bgCol, 3.0f);
|
||||
|
||||
// Icon
|
||||
if (iconTex) {
|
||||
ImU32 tintCol = IM_COL32(
|
||||
static_cast<int>(tint.x * 255), static_cast<int>(tint.y * 255),
|
||||
static_cast<int>(tint.z * 255), static_cast<int>(tint.w * 255));
|
||||
dl->AddImage((ImTextureID)(uintptr_t)iconTex,
|
||||
ImVec2(pMin.x + 2, pMin.y + 2),
|
||||
ImVec2(pMax.x - 2, pMax.y - 2),
|
||||
ImVec2(0, 0), ImVec2(1, 1), tintCol);
|
||||
}
|
||||
|
||||
// Border
|
||||
float borderThickness = hovered ? 3.0f : 2.0f;
|
||||
ImU32 borderCol = IM_COL32(borderColor.x * 255, borderColor.y * 255, borderColor.z * 255, 255);
|
||||
drawList->AddRect(pMin, pMax, borderCol, 0.0f, 0, borderThickness);
|
||||
float borderThick = hovered ? 2.5f : 1.5f;
|
||||
ImU32 borderCol = IM_COL32(
|
||||
static_cast<int>(borderColor.x * 255), static_cast<int>(borderColor.y * 255),
|
||||
static_cast<int>(borderColor.z * 255), static_cast<int>(borderColor.w * 255));
|
||||
dl->AddRect(pMin, pMax, borderCol, 3.0f, 0, borderThick);
|
||||
|
||||
// Icon or colored background
|
||||
if (iconTex) {
|
||||
ImU32 tintCol = IM_COL32(tint.x * 255, tint.y * 255, tint.z * 255, tint.w * 255);
|
||||
drawList->AddImage((ImTextureID)(uintptr_t)iconTex,
|
||||
ImVec2(pMin.x + 2, pMin.y + 2),
|
||||
ImVec2(pMax.x - 2, pMax.y - 2),
|
||||
ImVec2(0, 0), ImVec2(1, 1), tintCol);
|
||||
} else {
|
||||
ImU32 bgCol = IM_COL32(borderColor.x * 80, borderColor.y * 80, borderColor.z * 80, 255);
|
||||
drawList->AddRectFilled(ImVec2(pMin.x + 2, pMin.y + 2),
|
||||
ImVec2(pMax.x - 2, pMax.y - 2), bgCol);
|
||||
// Hover glow
|
||||
if (hovered && state != LOCKED) {
|
||||
dl->AddRect(ImVec2(pMin.x - 1, pMin.y - 1), ImVec2(pMax.x + 1, pMax.y + 1),
|
||||
IM_COL32(255, 255, 255, 60), 3.0f, 0, 1.0f);
|
||||
}
|
||||
|
||||
// Rank indicator overlay
|
||||
if (talent.maxRank > 1) {
|
||||
ImVec2 pMax = ImGui::GetItemRectMax();
|
||||
auto* drawList = ImGui::GetWindowDrawList();
|
||||
|
||||
// Display rank: if learned, show (rank+1) since ranks are 0-indexed
|
||||
const auto& learned = gameHandler.getLearnedTalents();
|
||||
uint8_t displayRank = (learned.find(talent.talentId) != learned.end()) ? currentRank + 1 : 0;
|
||||
|
||||
// Rank counter (bottom-right corner)
|
||||
{
|
||||
char rankText[16];
|
||||
snprintf(rankText, sizeof(rankText), "%u/%u", displayRank, talent.maxRank);
|
||||
|
||||
snprintf(rankText, sizeof(rankText), "%u/%u", currentRank, talent.maxRank);
|
||||
ImVec2 textSize = ImGui::CalcTextSize(rankText);
|
||||
ImVec2 textPos(pMax.x - textSize.x - 2, pMax.y - textSize.y - 2);
|
||||
ImVec2 textPos(pMax.x - textSize.x - 2, pMax.y - textSize.y - 1);
|
||||
|
||||
// Shadow
|
||||
drawList->AddText(ImVec2(textPos.x + 1, textPos.y + 1), IM_COL32(0, 0, 0, 255), rankText);
|
||||
// Text
|
||||
ImU32 rankCol = displayRank == talent.maxRank ? IM_COL32(0, 255, 0, 255) :
|
||||
displayRank > 0 ? IM_COL32(255, 255, 0, 255) :
|
||||
IM_COL32(255, 255, 255, 255);
|
||||
drawList->AddText(textPos, rankCol, rankText);
|
||||
// Background pill for readability
|
||||
dl->AddRectFilled(ImVec2(textPos.x - 2, textPos.y - 1),
|
||||
ImVec2(pMax.x, pMax.y),
|
||||
IM_COL32(0, 0, 0, 180), 2.0f);
|
||||
|
||||
// Text shadow
|
||||
dl->AddText(ImVec2(textPos.x + 1, textPos.y + 1), IM_COL32(0, 0, 0, 255), rankText);
|
||||
|
||||
// Rank text color
|
||||
ImU32 rankCol;
|
||||
switch (state) {
|
||||
case MAXED: rankCol = IM_COL32(80, 255, 80, 255); break;
|
||||
case PARTIAL: rankCol = IM_COL32(80, 255, 80, 255); break;
|
||||
default: rankCol = IM_COL32(200, 200, 200, 255); break;
|
||||
}
|
||||
dl->AddText(textPos, rankCol, rankText);
|
||||
}
|
||||
|
||||
// Enhanced tooltip
|
||||
// Tooltip
|
||||
if (hovered) {
|
||||
ImGui::BeginTooltip();
|
||||
ImGui::PushTextWrapPos(320.0f);
|
||||
|
||||
// Spell name
|
||||
const std::string& spellName = gameHandler.getSpellName(spellId);
|
||||
|
|
@ -330,60 +483,55 @@ void TalentScreen::renderTalent(game::GameHandler& gameHandler,
|
|||
ImGui::TextColored(ImVec4(1.0f, 1.0f, 0.3f, 1.0f), "Talent #%u", talent.talentId);
|
||||
}
|
||||
|
||||
// Rank
|
||||
ImGui::TextColored(borderColor, "Rank %u/%u", currentRank, talent.maxRank);
|
||||
// Rank display
|
||||
ImVec4 rankColor;
|
||||
switch (state) {
|
||||
case MAXED: rankColor = ImVec4(0.3f, 0.9f, 0.3f, 1); break;
|
||||
case PARTIAL: rankColor = ImVec4(0.3f, 0.9f, 0.3f, 1); break;
|
||||
default: rankColor = ImVec4(0.7f, 0.7f, 0.7f, 1); break;
|
||||
}
|
||||
ImGui::TextColored(rankColor, "Rank %u/%u", currentRank, talent.maxRank);
|
||||
|
||||
// Current rank description
|
||||
if (currentRank > 0 && talent.rankSpells[currentRank - 1] != 0) {
|
||||
if (currentRank > 0 && currentRank <= 5 && talent.rankSpells[currentRank - 1] != 0) {
|
||||
auto tooltipIt = spellTooltips.find(talent.rankSpells[currentRank - 1]);
|
||||
if (tooltipIt != spellTooltips.end() && !tooltipIt->second.empty()) {
|
||||
ImGui::PushTextWrapPos(ImGui::GetCursorPos().x + 300.0f);
|
||||
ImGui::Spacing();
|
||||
ImGui::TextColored(ImVec4(1.0f, 0.82f, 0.0f, 1.0f), "Current:");
|
||||
ImGui::TextWrapped("%s", tooltipIt->second.c_str());
|
||||
ImGui::PopTextWrapPos();
|
||||
}
|
||||
}
|
||||
|
||||
// Next rank description
|
||||
if (currentRank < talent.maxRank && talent.rankSpells[currentRank] != 0) {
|
||||
if (currentRank < talent.maxRank && currentRank < 5 && talent.rankSpells[currentRank] != 0) {
|
||||
auto tooltipIt = spellTooltips.find(talent.rankSpells[currentRank]);
|
||||
if (tooltipIt != spellTooltips.end() && !tooltipIt->second.empty()) {
|
||||
ImGui::Spacing();
|
||||
ImGui::PushTextWrapPos(ImGui::GetCursorPos().x + 300.0f);
|
||||
ImGui::TextColored(ImVec4(0.3f, 1.0f, 0.3f, 1.0f), "Next Rank:");
|
||||
ImGui::TextWrapped("%s", tooltipIt->second.c_str());
|
||||
ImGui::PopTextWrapPos();
|
||||
}
|
||||
}
|
||||
|
||||
// Prerequisites
|
||||
for (int i = 0; i < 3; ++i) {
|
||||
if (talent.prereqTalent[i] != 0) {
|
||||
const auto* prereq = gameHandler.getTalentEntry(talent.prereqTalent[i]);
|
||||
if (prereq && prereq->rankSpells[0] != 0) {
|
||||
uint8_t prereqCurrentRank = gameHandler.getTalentRank(talent.prereqTalent[i]);
|
||||
bool met = prereqCurrentRank >= talent.prereqRank[i];
|
||||
ImVec4 prereqColor = met ? ImVec4(0.3f, 0.9f, 0.3f, 1.0f) : ImVec4(1.0f, 0.3f, 0.3f, 1.0f);
|
||||
if (talent.prereqTalent[i] == 0) continue;
|
||||
const auto* prereq = gameHandler.getTalentEntry(talent.prereqTalent[i]);
|
||||
if (!prereq || prereq->rankSpells[0] == 0) continue;
|
||||
|
||||
const std::string& prereqName = gameHandler.getSpellName(prereq->rankSpells[0]);
|
||||
ImGui::Spacing();
|
||||
ImGui::TextColored(prereqColor, "Requires %u point%s in %s",
|
||||
talent.prereqRank[i],
|
||||
talent.prereqRank[i] > 1 ? "s" : "",
|
||||
prereqName.empty() ? "prerequisite" : prereqName.c_str());
|
||||
}
|
||||
}
|
||||
uint8_t prereqCurrentRank = gameHandler.getTalentRank(talent.prereqTalent[i]);
|
||||
bool met = prereqCurrentRank >= talent.prereqRank[i];
|
||||
ImVec4 pColor = met ? ImVec4(0.3f, 0.9f, 0.3f, 1) : ImVec4(1.0f, 0.3f, 0.3f, 1);
|
||||
|
||||
const std::string& prereqName = gameHandler.getSpellName(prereq->rankSpells[0]);
|
||||
ImGui::Spacing();
|
||||
ImGui::TextColored(pColor, "Requires %u point%s in %s",
|
||||
talent.prereqRank[i],
|
||||
talent.prereqRank[i] > 1 ? "s" : "",
|
||||
prereqName.empty() ? "prerequisite" : prereqName.c_str());
|
||||
}
|
||||
|
||||
// Tier requirement
|
||||
if (talent.row > 0 && currentRank == 0) {
|
||||
uint32_t pointsInTree = 0;
|
||||
for (const auto& [tid, rank] : gameHandler.getLearnedTalents()) {
|
||||
const auto* t = gameHandler.getTalentEntry(tid);
|
||||
if (t && t->tabId == talent.tabId) {
|
||||
pointsInTree += rank;
|
||||
}
|
||||
}
|
||||
uint32_t requiredPoints = talent.row * 5;
|
||||
if (pointsInTree < requiredPoints) {
|
||||
ImGui::Spacing();
|
||||
|
|
@ -397,38 +545,22 @@ void TalentScreen::renderTalent(game::GameHandler& gameHandler,
|
|||
if (canLearn && prereqsMet) {
|
||||
ImGui::Spacing();
|
||||
ImGui::TextColored(ImVec4(0.3f, 1.0f, 0.3f, 1.0f), "Click to learn");
|
||||
} else if (currentRank >= talent.maxRank) {
|
||||
ImGui::Spacing();
|
||||
ImGui::TextColored(ImVec4(0.3f, 0.9f, 0.3f, 1.0f), "Maxed");
|
||||
}
|
||||
|
||||
ImGui::PopTextWrapPos();
|
||||
ImGui::EndTooltip();
|
||||
}
|
||||
|
||||
// Handle click
|
||||
if (clicked) {
|
||||
LOG_INFO("Talent clicked: id=", talent.talentId, " canLearn=", canLearn, " prereqsMet=", prereqsMet,
|
||||
" currentRank=", static_cast<int>(currentRank), " maxRank=", static_cast<int>(talent.maxRank),
|
||||
" unspent=", static_cast<int>(gameHandler.getUnspentTalentPoints()));
|
||||
|
||||
if (canLearn && prereqsMet) {
|
||||
// Rank is 0-indexed: first point = rank 0, second = rank 1, etc.
|
||||
// Check if talent is already learned
|
||||
const auto& learned = gameHandler.getLearnedTalents();
|
||||
uint8_t desiredRank;
|
||||
if (learned.find(talent.talentId) == learned.end()) {
|
||||
// Not learned yet, learn first rank (0)
|
||||
desiredRank = 0;
|
||||
} else {
|
||||
// Already learned, upgrade to next rank
|
||||
desiredRank = currentRank + 1;
|
||||
}
|
||||
LOG_INFO("Sending CMSG_LEARN_TALENT for talent ", talent.talentId, " rank ", static_cast<int>(desiredRank), " (0-indexed)");
|
||||
gameHandler.learnTalent(talent.talentId, desiredRank);
|
||||
if (clicked && canLearn && prereqsMet) {
|
||||
const auto& learned = gameHandler.getLearnedTalents();
|
||||
uint8_t desiredRank;
|
||||
if (learned.find(talent.talentId) == learned.end()) {
|
||||
desiredRank = 0; // First rank (0-indexed on wire)
|
||||
} else {
|
||||
if (!canLearn) LOG_WARNING("Cannot learn: canLearn=false");
|
||||
if (!prereqsMet) LOG_WARNING("Cannot learn: prereqsMet=false");
|
||||
desiredRank = currentRank; // currentRank is already the next 0-indexed rank to learn
|
||||
}
|
||||
gameHandler.learnTalent(talent.talentId, desiredRank);
|
||||
}
|
||||
|
||||
ImGui::PopID();
|
||||
|
|
@ -441,12 +573,8 @@ void TalentScreen::loadSpellDBC(pipeline::AssetManager* assetManager) {
|
|||
if (!assetManager || !assetManager->isInitialized()) return;
|
||||
|
||||
auto dbc = assetManager->loadDBC("Spell.dbc");
|
||||
if (!dbc || !dbc->isLoaded()) {
|
||||
LOG_WARNING("Talent screen: Could not load Spell.dbc");
|
||||
return;
|
||||
}
|
||||
if (!dbc || !dbc->isLoaded()) return;
|
||||
|
||||
// WoW 3.3.5a Spell.dbc fields: 0=SpellID, 133=SpellIconID, 136=SpellName_enUS, 139=Tooltip_enUS
|
||||
const auto* spellL = pipeline::getActiveDBCLayout() ? pipeline::getActiveDBCLayout()->getLayout("Spell") : nullptr;
|
||||
uint32_t count = dbc->getRecordCount();
|
||||
for (uint32_t i = 0; i < count; ++i) {
|
||||
|
|
@ -461,8 +589,6 @@ void TalentScreen::loadSpellDBC(pipeline::AssetManager* assetManager) {
|
|||
spellTooltips[spellId] = tooltip;
|
||||
}
|
||||
}
|
||||
|
||||
LOG_INFO("Talent screen: Loaded ", spellIconIds.size(), " spell icons from Spell.dbc");
|
||||
}
|
||||
|
||||
void TalentScreen::loadSpellIconDBC(pipeline::AssetManager* assetManager) {
|
||||
|
|
@ -472,10 +598,7 @@ void TalentScreen::loadSpellIconDBC(pipeline::AssetManager* assetManager) {
|
|||
if (!assetManager || !assetManager->isInitialized()) return;
|
||||
|
||||
auto dbc = assetManager->loadDBC("SpellIcon.dbc");
|
||||
if (!dbc || !dbc->isLoaded()) {
|
||||
LOG_WARNING("Talent screen: Could not load SpellIcon.dbc");
|
||||
return;
|
||||
}
|
||||
if (!dbc || !dbc->isLoaded()) return;
|
||||
|
||||
const auto* iconL = pipeline::getActiveDBCLayout() ? pipeline::getActiveDBCLayout()->getLayout("SpellIcon") : nullptr;
|
||||
for (uint32_t i = 0; i < dbc->getRecordCount(); i++) {
|
||||
|
|
@ -485,8 +608,6 @@ void TalentScreen::loadSpellIconDBC(pipeline::AssetManager* assetManager) {
|
|||
spellIconPaths[id] = path;
|
||||
}
|
||||
}
|
||||
|
||||
LOG_INFO("Talent screen: Loaded ", spellIconPaths.size(), " spell icon paths from SpellIcon.dbc");
|
||||
}
|
||||
|
||||
VkDescriptorSet TalentScreen::getSpellIcon(uint32_t iconId, pipeline::AssetManager* assetManager) {
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue