diff --git a/include/game/game_handler.hpp b/include/game/game_handler.hpp index f81e56d8..7a2ddad0 100644 --- a/include/game/game_handler.hpp +++ b/include/game/game_handler.hpp @@ -78,6 +78,26 @@ using WorldConnectFailureCallback = std::function 0 ? (castTimeTotal - castTimeRemaining) / castTimeTotal : 0.0f; } float getCastTimeRemaining() const { return castTimeRemaining; } + // Talents + uint8_t getActiveTalentSpec() const { return activeTalentSpec_; } + uint8_t getUnspentTalentPoints() const { return unspentTalentPoints_[activeTalentSpec_]; } + uint8_t getUnspentTalentPoints(uint8_t spec) const { return spec < 2 ? unspentTalentPoints_[spec] : 0; } + const std::unordered_map& getLearnedTalents() const { return learnedTalents_[activeTalentSpec_]; } + const std::unordered_map& getLearnedTalents(uint8_t spec) const { + static std::unordered_map empty; + return spec < 2 ? learnedTalents_[spec] : empty; + } + uint8_t getTalentRank(uint32_t talentId) const { + auto it = learnedTalents_[activeTalentSpec_].find(talentId); + return (it != learnedTalents_[activeTalentSpec_].end()) ? it->second : 0; + } + void learnTalent(uint32_t talentId, uint32_t requestedRank); + void switchTalentSpec(uint8_t newSpec); + + // Talent DBC access + const TalentEntry* getTalentEntry(uint32_t talentId) const { + auto it = talentCache_.find(talentId); + return (it != talentCache_.end()) ? &it->second : nullptr; + } + const TalentTabEntry* getTalentTabEntry(uint32_t tabId) const { + auto it = talentTabCache_.find(tabId); + return (it != talentTabCache_.end()) ? &it->second : nullptr; + } + const std::unordered_map& getAllTalents() const { return talentCache_; } + const std::unordered_map& getAllTalentTabs() const { return talentTabCache_; } + void loadTalentDbc(); + // Action bar static constexpr int ACTION_BAR_SLOTS = 12; std::array& getActionBar() { return actionBar; } @@ -436,6 +485,10 @@ public: // Player GUID uint64_t getPlayerGuid() const { return playerGuid; } + uint8_t getPlayerClass() const { + const Character* ch = getActiveCharacter(); + return ch ? static_cast(ch->characterClass) : 0; + } void setPlayerGuid(uint64_t guid) { playerGuid = guid; } // Player death state @@ -703,6 +756,9 @@ private: void handleRemovedSpell(network::Packet& packet); void handleUnlearnSpells(network::Packet& packet); + // ---- Talent handlers ---- + void handleTalentsInfo(network::Packet& packet); + // ---- Phase 4 handlers ---- void handleGroupInvite(network::Packet& packet); void handleGroupDecline(network::Packet& packet); @@ -918,6 +974,14 @@ private: bool casting = false; uint32_t currentCastSpellId = 0; float castTimeRemaining = 0.0f; + + // Talents (dual-spec support) + uint8_t activeTalentSpec_ = 0; // Currently active spec (0 or 1) + uint8_t unspentTalentPoints_[2] = {0, 0}; // Unspent points per spec + std::unordered_map learnedTalents_[2]; // Learned talents per spec + std::unordered_map talentCache_; // talentId -> entry + std::unordered_map talentTabCache_; // tabId -> entry + bool talentDbcLoaded_ = false; float castTimeTotal = 0.0f; std::array actionBar{}; std::vector playerAuras; diff --git a/include/game/opcodes.hpp b/include/game/opcodes.hpp index 28cd7d45..0b1439ed 100644 --- a/include/game/opcodes.hpp +++ b/include/game/opcodes.hpp @@ -168,6 +168,11 @@ enum class Opcode : uint16_t { SMSG_SET_FLAT_SPELL_MODIFIER = 0x266, SMSG_SET_PCT_SPELL_MODIFIER = 0x267, + // ---- Talents ---- + SMSG_TALENTS_INFO = 0x4C0, + CMSG_LEARN_TALENT = 0x251, + MSG_TALENT_WIPE_CONFIRM = 0x2AB, + // ---- Phase 4: Group/Party ---- CMSG_GROUP_INVITE = 0x06E, SMSG_GROUP_INVITE = 0x06F, diff --git a/include/game/world_packets.hpp b/include/game/world_packets.hpp index be36ffb2..b32c9a99 100644 --- a/include/game/world_packets.hpp +++ b/include/game/world_packets.hpp @@ -1753,6 +1753,43 @@ public: static network::Packet build(uint64_t trainerGuid, uint32_t spellId); }; +// ============================================================ +// Talents +// ============================================================ + +/** Talent info for a single talent */ +struct TalentInfo { + uint32_t talentId = 0; // Talent.dbc ID + uint8_t currentRank = 0; // 0-5 (0 = not learned) +}; + +/** SMSG_TALENTS_INFO data */ +struct TalentsInfoData { + uint8_t talentSpec = 0; // Active spec (0 or 1 for dual-spec) + uint8_t unspentPoints = 0; // Talent points available + std::vector talents; // Learned talents + + bool isValid() const { return true; } +}; + +/** SMSG_TALENTS_INFO parser */ +class TalentsInfoParser { +public: + static bool parse(network::Packet& packet, TalentsInfoData& data); +}; + +/** CMSG_LEARN_TALENT packet builder */ +class LearnTalentPacket { +public: + static network::Packet build(uint32_t talentId, uint32_t requestedRank); +}; + +/** MSG_TALENT_WIPE_CONFIRM packet builder */ +class TalentWipeConfirmPacket { +public: + static network::Packet build(bool accept); +}; + // ============================================================ // Taxi / Flight Paths // ============================================================ diff --git a/include/ui/game_screen.hpp b/include/ui/game_screen.hpp index 49946fe3..3a927adc 100644 --- a/include/ui/game_screen.hpp +++ b/include/ui/game_screen.hpp @@ -145,6 +145,7 @@ private: // ---- New UI renders ---- void renderActionBar(game::GameHandler& gameHandler); + void renderBagBar(game::GameHandler& gameHandler); void renderXpBar(game::GameHandler& gameHandler); void renderCastBar(game::GameHandler& gameHandler); void renderCombatText(game::GameHandler& gameHandler); @@ -195,6 +196,10 @@ private: int actionBarDragSlot_ = -1; GLuint actionBarDragIcon_ = 0; + // Bag bar textures + GLuint backpackIconTexture_ = 0; + GLuint emptyBagSlotTexture_ = 0; + static std::string getSettingsPath(); // Gender placeholder replacement diff --git a/include/ui/talent_screen.hpp b/include/ui/talent_screen.hpp index e7e36220..55dd429b 100644 --- a/include/ui/talent_screen.hpp +++ b/include/ui/talent_screen.hpp @@ -2,8 +2,13 @@ #include "game/game_handler.hpp" #include +#include +#include +#include namespace wowee { +namespace pipeline { class AssetManager; } + namespace ui { class TalentScreen { @@ -14,8 +19,24 @@ public: void setOpen(bool o) { open = o; } 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 loadSpellDBC(pipeline::AssetManager* assetManager); + void loadSpellIconDBC(pipeline::AssetManager* assetManager); + GLuint getSpellIcon(uint32_t iconId, pipeline::AssetManager* assetManager); + bool open = false; bool nKeyWasDown = false; + + // DBC caches + bool spellDbcLoaded = false; + bool iconDbcLoaded = false; + std::unordered_map spellIconIds; // spellId -> iconId + std::unordered_map spellIconPaths; // iconId -> path + std::unordered_map spellIconCache; // iconId -> texture + std::unordered_map spellTooltips; // spellId -> description }; } // namespace ui diff --git a/src/game/game_handler.cpp b/src/game/game_handler.cpp index 5917466e..8e243f8e 100644 --- a/src/game/game_handler.cpp +++ b/src/game/game_handler.cpp @@ -589,6 +589,11 @@ void GameHandler::handlePacket(network::Packet& packet) { handleUnlearnSpells(packet); break; + // ---- Talents ---- + case Opcode::SMSG_TALENTS_INFO: + handleTalentsInfo(packet); + break; + // ---- Phase 4: Group ---- case Opcode::SMSG_GROUP_INVITE: handleGroupInvite(packet); @@ -4457,6 +4462,96 @@ void GameHandler::handleUnlearnSpells(network::Packet& packet) { } } +// ============================================================ +// Talents +// ============================================================ + +void GameHandler::handleTalentsInfo(network::Packet& packet) { + TalentsInfoData data; + if (!TalentsInfoParser::parse(packet, data)) return; + + // Ensure talent DBCs are loaded + loadTalentDbc(); + + // Validate spec number + if (data.talentSpec > 1) { + LOG_WARNING("Invalid talent spec: ", (int)data.talentSpec); + return; + } + + // Store talents for this spec + unspentTalentPoints_[data.talentSpec] = data.unspentPoints; + + // Clear and rebuild learned talents map for this spec + learnedTalents_[data.talentSpec].clear(); + for (const auto& talent : data.talents) { + if (talent.currentRank > 0) { + learnedTalents_[data.talentSpec][talent.talentId] = talent.currentRank; + } + } + + LOG_INFO("Talents loaded: spec=", (int)data.talentSpec, + " unspent=", (int)unspentTalentPoints_[data.talentSpec], + " learned=", learnedTalents_[data.talentSpec].size()); + + // If this is the first spec received, set it as active + static bool firstSpecReceived = false; + if (!firstSpecReceived) { + firstSpecReceived = true; + activeTalentSpec_ = data.talentSpec; + + // Show message to player about active spec + if (unspentTalentPoints_[data.talentSpec] > 0) { + std::string msg = "You have " + std::to_string(unspentTalentPoints_[data.talentSpec]) + + " unspent talent point"; + if (unspentTalentPoints_[data.talentSpec] > 1) msg += "s"; + msg += " in spec " + std::to_string(data.talentSpec + 1); + addSystemChatMessage(msg); + } + } +} + +void GameHandler::learnTalent(uint32_t talentId, uint32_t requestedRank) { + if (state != WorldState::IN_WORLD || !socket) { + LOG_WARNING("learnTalent: Not in world or no socket connection"); + return; + } + + LOG_INFO("Requesting to learn talent: id=", talentId, " rank=", requestedRank); + + auto packet = LearnTalentPacket::build(talentId, requestedRank); + socket->send(packet); +} + +void GameHandler::switchTalentSpec(uint8_t newSpec) { + if (newSpec > 1) { + LOG_WARNING("Invalid talent spec: ", (int)newSpec); + return; + } + + if (newSpec == activeTalentSpec_) { + LOG_INFO("Already on spec ", (int)newSpec); + return; + } + + // For now, just switch locally. In a real implementation, we'd send + // MSG_TALENT_WIPE_CONFIRM to the server to trigger a spec switch. + // The server would respond with new SMSG_TALENTS_INFO for the new spec. + activeTalentSpec_ = newSpec; + + LOG_INFO("Switched to talent spec ", (int)newSpec, + " (unspent=", (int)unspentTalentPoints_[newSpec], + ", learned=", learnedTalents_[newSpec].size(), ")"); + + std::string msg = "Switched to spec " + std::to_string(newSpec + 1); + if (unspentTalentPoints_[newSpec] > 0) { + msg += " (" + std::to_string(unspentTalentPoints_[newSpec]) + " unspent point"; + if (unspentTalentPoints_[newSpec] > 1) msg += "s"; + msg += ")"; + } + addSystemChatMessage(msg); +} + // ============================================================ // Phase 4: Group/Party // ============================================================ @@ -5195,6 +5290,99 @@ void GameHandler::categorizeTrainerSpells() { LOG_INFO("Trainer: Categorized into ", trainerTabs_.size(), " tabs"); } +void GameHandler::loadTalentDbc() { + if (talentDbcLoaded_) return; + talentDbcLoaded_ = true; + + auto* am = core::Application::getInstance().getAssetManager(); + if (!am || !am->isInitialized()) return; + + // Load Talent.dbc + auto talentDbc = am->loadDBC("Talent.dbc"); + if (talentDbc && talentDbc->isLoaded()) { + // Talent.dbc structure (WoW 3.3.5a): + // 0: TalentID + // 1: TalentTabID + // 2: Row (tier) + // 3: Column + // 4-8: RankID[0-4] (spell IDs for ranks 1-5) + // 9-11: PrereqTalent[0-2] + // 12-14: PrereqRank[0-2] + // (other fields less relevant for basic functionality) + + uint32_t count = talentDbc->getRecordCount(); + for (uint32_t i = 0; i < count; ++i) { + TalentEntry entry; + entry.talentId = talentDbc->getUInt32(i, 0); + if (entry.talentId == 0) continue; + + entry.tabId = talentDbc->getUInt32(i, 1); + entry.row = static_cast(talentDbc->getUInt32(i, 2)); + entry.column = static_cast(talentDbc->getUInt32(i, 3)); + + // Rank spells (1-5 ranks) + for (int r = 0; r < 5; ++r) { + entry.rankSpells[r] = talentDbc->getUInt32(i, 4 + r); + } + + // Prerequisites + for (int p = 0; p < 3; ++p) { + entry.prereqTalent[p] = talentDbc->getUInt32(i, 9 + p); + entry.prereqRank[p] = static_cast(talentDbc->getUInt32(i, 12 + p)); + } + + // Calculate max rank + entry.maxRank = 0; + for (int r = 0; r < 5; ++r) { + if (entry.rankSpells[r] != 0) { + entry.maxRank = r + 1; + } + } + + talentCache_[entry.talentId] = entry; + } + LOG_INFO("Loaded ", talentCache_.size(), " talents from Talent.dbc"); + } else { + LOG_WARNING("Could not load Talent.dbc"); + } + + // Load TalentTab.dbc + auto tabDbc = am->loadDBC("TalentTab.dbc"); + if (tabDbc && tabDbc->isLoaded()) { + // TalentTab.dbc structure (WoW 3.3.5a): + // 0: TalentTabID + // 1-17: Name (16 localized strings + flags = 17 fields) + // 18: SpellIconID + // 19: RaceMask + // 20: ClassMask + // 21: PetTalentMask + // 22: OrderIndex + // 23-39: BackgroundFile (16 localized strings + flags = 17 fields) + + uint32_t count = tabDbc->getRecordCount(); + for (uint32_t i = 0; i < count; ++i) { + TalentTabEntry entry; + entry.tabId = tabDbc->getUInt32(i, 0); + if (entry.tabId == 0) continue; + + entry.name = tabDbc->getString(i, 1); + entry.classMask = tabDbc->getUInt32(i, 20); + entry.orderIndex = static_cast(tabDbc->getUInt32(i, 22)); + entry.backgroundFile = tabDbc->getString(i, 23); + + talentTabCache_[entry.tabId] = entry; + + // Log first few tabs to debug class mask issue + if (talentTabCache_.size() <= 10) { + LOG_INFO(" Tab ", entry.tabId, ": ", entry.name, " (classMask=0x", std::hex, entry.classMask, std::dec, ")"); + } + } + LOG_INFO("Loaded ", talentTabCache_.size(), " talent tabs from TalentTab.dbc"); + } else { + LOG_WARNING("Could not load TalentTab.dbc"); + } +} + static const std::string EMPTY_STRING; const std::string& GameHandler::getSpellName(uint32_t spellId) const { diff --git a/src/game/world_packets.cpp b/src/game/world_packets.cpp index e7bf9501..3b1859c5 100644 --- a/src/game/world_packets.cpp +++ b/src/game/world_packets.cpp @@ -2728,6 +2728,65 @@ network::Packet TrainerBuySpellPacket::build(uint64_t trainerGuid, uint32_t spel return packet; } +// ============================================================ +// Talents +// ============================================================ + +bool TalentsInfoParser::parse(network::Packet& packet, TalentsInfoData& data) { + // WotLK 3.3.5a SMSG_TALENTS_INFO format: + // uint8 talentSpec (0 or 1 for dual-spec) + // uint8 unspentPoints + // uint8 talentCount + // for each talent: + // uint32 talentId + // uint8 currentRank (0-5) + + data = TalentsInfoData{}; + + if (packet.getSize() - packet.getReadPos() < 3) { + LOG_ERROR("TalentsInfoParser: packet too short"); + return false; + } + + data.talentSpec = packet.readUInt8(); + data.unspentPoints = packet.readUInt8(); + uint8_t talentCount = packet.readUInt8(); + + LOG_INFO("SMSG_TALENTS_INFO: spec=", (int)data.talentSpec, + " unspentPoints=", (int)data.unspentPoints, + " talentCount=", (int)talentCount); + + data.talents.reserve(talentCount); + for (uint8_t i = 0; i < talentCount; ++i) { + if (packet.getSize() - packet.getReadPos() < 5) { + LOG_WARNING("TalentsInfoParser: truncated talent data at index ", (int)i); + break; + } + + TalentInfo talent; + talent.talentId = packet.readUInt32(); + talent.currentRank = packet.readUInt8(); + data.talents.push_back(talent); + + LOG_INFO(" Talent: id=", talent.talentId, " rank=", (int)talent.currentRank); + } + + return true; +} + +network::Packet LearnTalentPacket::build(uint32_t talentId, uint32_t requestedRank) { + network::Packet packet(static_cast(Opcode::CMSG_LEARN_TALENT)); + packet.writeUInt32(talentId); + packet.writeUInt32(requestedRank); + return packet; +} + +network::Packet TalentWipeConfirmPacket::build(bool accept) { + network::Packet packet(static_cast(Opcode::MSG_TALENT_WIPE_CONFIRM)); + packet.writeUInt32(accept ? 1 : 0); + return packet; +} + // ============================================================ // Death/Respawn // ============================================================ diff --git a/src/ui/talent_screen.cpp b/src/ui/talent_screen.cpp index bd824e97..ae0fd540 100644 --- a/src/ui/talent_screen.cpp +++ b/src/ui/talent_screen.cpp @@ -1,6 +1,11 @@ #include "ui/talent_screen.hpp" #include "core/input.hpp" #include "core/application.hpp" +#include "core/logger.hpp" +#include "pipeline/asset_manager.hpp" +#include "pipeline/blp_loader.hpp" +#include +#include namespace wowee { namespace ui { @@ -19,8 +24,8 @@ void TalentScreen::render(game::GameHandler& gameHandler) { float screenW = window ? static_cast(window->getWidth()) : 1280.0f; float screenH = window ? static_cast(window->getHeight()) : 720.0f; - float winW = 400.0f; - float winH = 450.0f; + float winW = 600.0f; // Wider for talent grid + float winH = 550.0f; float winX = (screenW - winW) * 0.5f; float winY = (screenH - winH) * 0.5f; @@ -29,33 +34,7 @@ void TalentScreen::render(game::GameHandler& gameHandler) { bool windowOpen = open; if (ImGui::Begin("Talents", &windowOpen)) { - // Placeholder tabs - if (ImGui::BeginTabBar("TalentTabs")) { - if (ImGui::BeginTabItem("Spec 1")) { - ImGui::Spacing(); - ImGui::TextDisabled("Talents coming soon."); - ImGui::Spacing(); - ImGui::TextDisabled("Talent trees will be implemented in a future update."); - - uint32_t level = gameHandler.getPlayerLevel(); - uint32_t talentPoints = (level >= 10) ? (level - 9) : 0; - ImGui::Spacing(); - ImGui::Separator(); - ImGui::Text("Level: %u", level); - ImGui::Text("Talent points available: %u", talentPoints); - - ImGui::EndTabItem(); - } - if (ImGui::BeginTabItem("Spec 2")) { - ImGui::TextDisabled("Talents coming soon."); - ImGui::EndTabItem(); - } - if (ImGui::BeginTabItem("Spec 3")) { - ImGui::TextDisabled("Talents coming soon."); - ImGui::EndTabItem(); - } - ImGui::EndTabBar(); - } + renderTalentTrees(gameHandler); } ImGui::End(); @@ -64,4 +43,477 @@ 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) + 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(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)) + 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 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); + ImGui::Separator(); + + // Render tabs + if (ImGui::BeginTabBar("TalentTabs")) { + for (const auto* tab : classTabs) { + if (ImGui::BeginTabItem(tab->name.c_str())) { + renderTalentTree(gameHandler, tab->tabId); + ImGui::EndTabItem(); + } + } + ImGui::EndTabBar(); + } +} + +void TalentScreen::renderTalentTree(game::GameHandler& gameHandler, uint32_t tabId) { + // Collect all talents for this tab + std::vector talents; + for (const auto& [talentId, talent] : gameHandler.getAllTalents()) { + if (talent.tabId == tabId) { + talents.push_back(&talent); + } + } + + if (talents.empty()) { + ImGui::TextDisabled("No talents in this tree."); + return; + } + + // 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); + } + + const float iconSize = 40.0f; + + 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); + + 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) { + talent = t; + break; + } + } + + if (col > 0) ImGui::SameLine(); + + if (talent) { + renderTalent(gameHandler, *talent); + } else { + // Empty slot + ImGui::InvisibleButton(("empty_" + std::to_string(row) + "_" + std::to_string(col)).c_str(), + ImVec2(iconSize, iconSize)); + } + } + } + + ImGui::EndChild(); +} + +void TalentScreen::renderTalent(game::GameHandler& gameHandler, + const game::GameHandler::TalentEntry& talent) { + auto* assetManager = core::Application::getInstance().getAssetManager(); + + uint8_t currentRank = gameHandler.getTalentRank(talent.talentId); + uint8_t nextRank = currentRank + 1; + + // Check if can learn + bool canLearn = currentRank < talent.maxRank && + gameHandler.getUnspentTalentPoints() > 0; + + // Check prerequisites + bool prereqsMet = true; + for (int i = 0; i < 3; ++i) { + if (talent.prereqTalent[i] != 0) { + uint8_t prereqRank = gameHandler.getTalentRank(talent.prereqTalent[i]); + if (prereqRank < talent.prereqRank[i]) { + prereqsMet = false; + canLearn = false; + break; + } + } + } + + // Check tier requirement (need 5 points in previous tier) + 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 + 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 + } + + const float iconSize = 40.0f; + ImGui::PushID(static_cast(talent.talentId)); + + // Get spell icon + uint32_t spellId = talent.rankSpells[0]; + GLuint iconTex = 0; + if (spellId != 0) { + auto it = spellIconIds.find(spellId); + if (it != spellIconIds.end()) { + iconTex = getSpellIcon(it->second, assetManager); + } + } + + // Use InvisibleButton for click handling + bool clicked = ImGui::InvisibleButton("##talent", ImVec2(iconSize, iconSize)); + bool hovered = ImGui::IsItemHovered(); + + // Draw icon and border + ImVec2 pMin = ImGui::GetItemRectMin(); + ImVec2 pMax = ImGui::GetItemRectMax(); + auto* drawList = ImGui::GetWindowDrawList(); + + // 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); + + // 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); + } + + // Rank indicator overlay + if (talent.maxRank > 1) { + ImVec2 pMin = ImGui::GetItemRectMin(); + ImVec2 pMax = ImGui::GetItemRectMax(); + auto* drawList = ImGui::GetWindowDrawList(); + + char rankText[16]; + 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); + + // Shadow + drawList->AddText(ImVec2(textPos.x + 1, textPos.y + 1), IM_COL32(0, 0, 0, 255), rankText); + // Text + ImU32 rankCol = currentRank == talent.maxRank ? IM_COL32(0, 255, 0, 255) : + currentRank > 0 ? IM_COL32(255, 255, 0, 255) : + IM_COL32(255, 255, 255, 255); + drawList->AddText(textPos, rankCol, rankText); + } + + // Enhanced tooltip + if (hovered) { + ImGui::BeginTooltip(); + + // Spell name + const std::string& spellName = gameHandler.getSpellName(spellId); + if (!spellName.empty()) { + ImGui::TextColored(ImVec4(1.0f, 1.0f, 0.3f, 1.0f), "%s", spellName.c_str()); + } else { + 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); + + // Current rank description + if (currentRank > 0 && 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::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) { + 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); + + 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()); + } + } + } + + // 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(); + ImGui::TextColored(ImVec4(1.0f, 0.3f, 0.3f, 1.0f), + "Requires %u points in this tree (%u/%u)", + requiredPoints, pointsInTree, requiredPoints); + } + } + + // Action hint + 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::EndTooltip(); + } + + // Handle click + if (clicked) { + LOG_INFO("Talent clicked: id=", talent.talentId, " canLearn=", canLearn, " prereqsMet=", prereqsMet, + " currentRank=", static_cast(currentRank), " maxRank=", static_cast(talent.maxRank), + " unspent=", static_cast(gameHandler.getUnspentTalentPoints())); + + if (canLearn && prereqsMet) { + LOG_INFO("Sending CMSG_LEARN_TALENT for talent ", talent.talentId, " rank ", static_cast(nextRank)); + gameHandler.learnTalent(talent.talentId, nextRank); + } else { + if (!canLearn) LOG_WARNING("Cannot learn: canLearn=false"); + if (!prereqsMet) LOG_WARNING("Cannot learn: prereqsMet=false"); + } + } + + ImGui::PopID(); +} + +void TalentScreen::loadSpellDBC(pipeline::AssetManager* assetManager) { + if (spellDbcLoaded) return; + spellDbcLoaded = true; + + 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; + } + + // WoW 3.3.5a Spell.dbc fields: 0=SpellID, 133=SpellIconID, 136=SpellName_enUS, 139=Tooltip_enUS + uint32_t count = dbc->getRecordCount(); + for (uint32_t i = 0; i < count; ++i) { + uint32_t spellId = dbc->getUInt32(i, 0); + if (spellId == 0) continue; + + uint32_t iconId = dbc->getUInt32(i, 133); + spellIconIds[spellId] = iconId; + + std::string tooltip = dbc->getString(i, 139); + if (!tooltip.empty()) { + spellTooltips[spellId] = tooltip; + } + } + + LOG_INFO("Talent screen: Loaded ", spellIconIds.size(), " spell icons from Spell.dbc"); +} + +void TalentScreen::loadSpellIconDBC(pipeline::AssetManager* assetManager) { + if (iconDbcLoaded) return; + iconDbcLoaded = true; + + 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; + } + + for (uint32_t i = 0; i < dbc->getRecordCount(); i++) { + uint32_t id = dbc->getUInt32(i, 0); + std::string path = dbc->getString(i, 1); + if (!path.empty() && id > 0) { + spellIconPaths[id] = path; + } + } + + LOG_INFO("Talent screen: Loaded ", spellIconPaths.size(), " spell icon paths from SpellIcon.dbc"); +} + +GLuint TalentScreen::getSpellIcon(uint32_t iconId, pipeline::AssetManager* assetManager) { + if (iconId == 0 || !assetManager) return 0; + + // Check cache + auto cit = spellIconCache.find(iconId); + if (cit != spellIconCache.end()) return cit->second; + + // Look up icon path + auto pit = spellIconPaths.find(iconId); + if (pit == spellIconPaths.end()) { + spellIconCache[iconId] = 0; + return 0; + } + + // Load BLP file + std::string iconPath = pit->second + ".blp"; + auto blpData = assetManager->readFile(iconPath); + if (blpData.empty()) { + spellIconCache[iconId] = 0; + return 0; + } + + // Decode BLP + auto image = pipeline::BLPLoader::load(blpData); + if (!image.isValid()) { + spellIconCache[iconId] = 0; + return 0; + } + + // Create OpenGL texture + GLuint texId = 0; + glGenTextures(1, &texId); + glBindTexture(GL_TEXTURE_2D, texId); + glTexImage2D(GL_TEXTURE_2D, 0, GL_RGBA8, image.width, image.height, 0, + GL_RGBA, GL_UNSIGNED_BYTE, image.data.data()); + glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR); + glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR); + glBindTexture(GL_TEXTURE_2D, 0); + + spellIconCache[iconId] = texId; + return texId; +} + }} // namespace wowee::ui