From da959cfb8f662356033a82838733010f7c0c029a Mon Sep 17 00:00:00 2001 From: Kelsi Date: Wed, 25 Feb 2026 14:55:40 -0800 Subject: [PATCH] Revamp talent and spellbook UIs with proper visuals and functionality Talent screen: - Remove all debug text and per-frame LOG_INFO spam - Show class name in window title (e.g. "Warrior Talents") - Display point distribution in header (0/31/20) and per-tab counts - Highlighted active spec button with styled spec switcher - Load and render tree background textures from TalentTab.dbc - Draw prerequisite arrows with arrowheads (green=met, gray=unmet) - Fix rank display (was showing rank+1, now correct 1-indexed values) - Rank counter with dark background pill for readability - Hover glow effect, rounded corners, centered grid layout - Wider window (680x600) for 4-column WoW talent grid Spellbook: - Add search/filter bar for finding spells by name - Add spell descriptions from Spell.dbc tooltip field - Rich tooltips with name, rank, passive indicator, cooldown, description - Visual icon borders: yellow=passive, red=cooldown, default=active - Cooldown overlay on icon with countdown number - Hover highlight on spell rows - Tab counts update to reflect search filter results - Rounded corners on icons and hover states - Extracted renderSpellTooltip helper for consistent tooltip rendering --- include/ui/spellbook_screen.hpp | 26 +- include/ui/talent_screen.hpp | 12 +- src/ui/spellbook_screen.cpp | 272 +++++++++++------ src/ui/talent_screen.cpp | 517 ++++++++++++++++++++------------ 4 files changed, 530 insertions(+), 297 deletions(-) diff --git a/include/ui/spellbook_screen.hpp b/include/ui/spellbook_screen.hpp index b77a2ece..537e862c 100644 --- a/include/ui/spellbook_screen.hpp +++ b/include/ui/spellbook_screen.hpp @@ -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 spellIconPaths; // SpellIconID -> path - std::unordered_map spellIconCache; // SpellIconID -> GL texture + std::unordered_map spellIconCache; // SpellIconID -> texture // Skill line data (loaded from SkillLine.dbc + SkillLineAbility.dbc) bool skillLineDbLoaded = false; - std::unordered_map skillLineNames; // skillLineID -> name - std::unordered_map skillLineCategories; // skillLineID -> categoryID - std::unordered_map spellToSkillLine; // spellID -> skillLineID + std::unordered_map skillLineNames; + std::unordered_map skillLineCategories; + std::unordered_map spellToSkillLine; - // Categorized spell tabs (rebuilt when spell list changes) - // ordered map so tabs appear in consistent order + // Categorized spell tabs std::vector 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& 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 diff --git a/include/ui/talent_screen.hpp b/include/ui/talent_screen.hpp index 792e7706..18bbe152 100644 --- a/include/ui/talent_screen.hpp +++ b/include/ui/talent_screen.hpp @@ -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 spellIconIds; // spellId -> iconId + std::unordered_map spellIconIds; // spellId -> iconId std::unordered_map spellIconPaths; // iconId -> path std::unordered_map spellIconCache; // iconId -> texture - std::unordered_map spellTooltips; // spellId -> description + std::unordered_map spellTooltips; // spellId -> description + std::unordered_map bgTextureCache_; // tabId -> bg texture }; } // namespace ui diff --git a/src/ui/spellbook_screen.cpp b/src/ui/spellbook_screen.cpp index 9fec4524..243c4f2e 100644 --- a/src/ui/spellbook_screen.cpp +++ b/src/ui/spellbook_screen.cpp @@ -9,9 +9,29 @@ #include "core/logger.hpp" #include #include +#include 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(haystack[i + j])) != + std::tolower(static_cast(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,17 +150,12 @@ 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& 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; std::map> specialtySpells; @@ -177,12 +182,10 @@ void SpellbookScreen::categorizeSpells(const std::unordered_set& known auto byName = [](const SpellInfo* a, const SpellInfo* b) { return a->name < b->name; }; - // Specialty tabs sorted alphabetically by skill line name std::vector>> named; for (auto& [skillLineId, spells] : specialtySpells) { auto nameIt = skillLineNames.find(skillLineId); - std::string tabName = (nameIt != skillLineNames.end()) ? nameIt->second - : "Specialty"; + 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)}); } @@ -193,7 +196,6 @@ void SpellbookScreen::categorizeSpells(const std::unordered_set& known spellTabs.push_back({std::move(name), std::move(spells)}); } - // General tab last if (!generalSpells.empty()) { std::sort(generalSpells.begin(), generalSpells.end(), byName); spellTabs.push_back({"General", std::move(generalSpells)}); @@ -244,6 +246,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 +315,156 @@ void SpellbookScreen::render(game::GameHandler& gameHandler, pipeline::AssetMana float screenW = window ? static_cast(window->getWidth()) : 1280.0f; float screenH = window ? static_cast(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(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 +473,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 +500,7 @@ void SpellbookScreen::render(game::GameHandler& gameHandler, pipeline::AssetMana } } ImGui::End(); + ImGui::PopStyleVar(); if (!windowOpen) { open = false; @@ -410,7 +509,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 +519,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; } } } diff --git a/src/ui/talent_screen.cpp b/src/ui/talent_screen.cpp index 62f49593..eeff7c41 100644 --- a/src/ui/talent_screen.cpp +++ b/src/ui/talent_screen.cpp @@ -7,9 +7,20 @@ #include "pipeline/blp_loader.hpp" #include "pipeline/dbc_layout.hpp" #include +#include 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(window->getWidth()) : 1280.0f; float screenH = window ? static_cast(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(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 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 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 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(tint.x * 255), static_cast(tint.y * 255), + static_cast(tint.z * 255), static_cast(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(borderColor.x * 255), static_cast(borderColor.y * 255), + static_cast(borderColor.z * 255), static_cast(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(currentRank), " maxRank=", static_cast(talent.maxRank), - " unspent=", static_cast(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(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) {