#include "ui/talent_screen.hpp" #include "core/input.hpp" #include "core/application.hpp" #include "core/logger.hpp" #include "rendering/vk_context.hpp" #include "pipeline/asset_manager.hpp" #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; bool nDown = !wantsTextInput && core::Input::getInstance().isKeyPressed(SDL_SCANCODE_N); if (nDown && !nKeyWasDown) { open = !open; } nKeyWasDown = nDown; if (!open) return; auto* window = core::Application::getInstance().getWindow(); float screenW = window ? static_cast(window->getWidth()) : 1280.0f; float screenH = window ? static_cast(window->getHeight()) : 720.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; ImGui::PushStyleVar(ImGuiStyleVar_WindowPadding, ImVec2(8, 8)); if (ImGui::Begin(title.c_str(), &windowOpen)) { renderTalentTrees(gameHandler); } ImGui::End(); ImGui::PopStyleVar(); if (!windowOpen) { open = false; } } void TalentScreen::renderTalentTrees(game::GameHandler& gameHandler) { auto* assetManager = core::Application::getInstance().getAssetManager(); // Ensure talent DBCs are loaded once static bool dbcLoadAttempted = false; if (!dbcLoadAttempted) { dbcLoadAttempted = true; gameHandler.loadTalentDbc(); loadSpellDBC(assetManager); loadSpellIconDBC(assetManager); } uint8_t playerClass = gameHandler.getPlayerClass(); if (playerClass == 0) { ImGui::TextDisabled("Class information not available."); return; } // Get talent tabs for this class, sorted by orderIndex uint32_t classMask = 1u << (playerClass - 1); 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; }); if (classTabs.empty()) { ImGui::TextDisabled("No talent trees available for your class."); return; } // 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 with point counts in tab labels if (ImGui::BeginTabBar("TalentTabs")) { 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(); } } ImGui::EndTabBar(); } } 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()) { if (talent.tabId == tabId) { talents.push_back(&talent); } } if (talents.empty()) { ImGui::TextDisabled("No talents in this tree."); 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); 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) { const game::GameHandler::TalentEntry* talent = nullptr; for (const auto* t : talents) { if (t->row == row && t->column == col) { talent = t; break; } } float x = gridOrigin.x + col * cellSize + spacing; float y = gridOrigin.y + row * cellSize + spacing; ImGui::SetCursorScreenPos(ImVec2(x, y)); if (talent) { renderTalent(gameHandler, *talent, pointsInTree); } else { // 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, uint32_t pointsInTree) { auto* assetManager = core::Application::getInstance().getAssetManager(); uint8_t currentRank = gameHandler.getTalentRank(talent.talentId); // 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 row*5 points in tree) if (talent.row > 0) { uint32_t requiredPoints = talent.row * 5; if (pointsInTree < requiredPoints) { canLearn = false; } } // 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; 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; ImGui::PushID(static_cast(talent.talentId)); // Get spell icon uint32_t spellId = talent.rankSpells[0]; VkDescriptorSet iconTex = VK_NULL_HANDLE; if (spellId != 0) { auto it = spellIconIds.find(spellId); if (it != spellIconIds.end()) { iconTex = getSpellIcon(it->second, assetManager); } } // Click target bool clicked = ImGui::InvisibleButton("##t", ImVec2(iconSize, iconSize)); bool hovered = ImGui::IsItemHovered(); ImVec2 pMin = ImGui::GetItemRectMin(); ImVec2 pMax = ImGui::GetItemRectMax(); 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 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); // 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 counter (bottom-right corner) { 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 - 1); // 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); } // Tooltip if (hovered) { ImGui::BeginTooltip(); ImGui::PushTextWrapPos(320.0f); // 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 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 && currentRank <= 5 && talent.rankSpells[currentRank - 1] != 0) { auto tooltipIt = spellTooltips.find(talent.rankSpells[currentRank - 1]); if (tooltipIt != spellTooltips.end() && !tooltipIt->second.empty()) { ImGui::Spacing(); ImGui::TextColored(ImVec4(1.0f, 0.82f, 0.0f, 1.0f), "Current:"); ImGui::TextWrapped("%s", tooltipIt->second.c_str()); } } // Next rank description 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::TextColored(ImVec4(0.3f, 1.0f, 0.3f, 1.0f), "Next Rank:"); ImGui::TextWrapped("%s", tooltipIt->second.c_str()); } } // Prerequisites for (int i = 0; i < 3; ++i) { if (talent.prereqTalent[i] == 0) continue; const auto* prereq = gameHandler.getTalentEntry(talent.prereqTalent[i]); if (!prereq || prereq->rankSpells[0] == 0) continue; 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 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"); } ImGui::PopTextWrapPos(); ImGui::EndTooltip(); } // Handle click 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 { desiredRank = currentRank; // currentRank is already the next 0-indexed rank to learn } gameHandler.learnTalent(talent.talentId, desiredRank); } 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()) return; const auto* spellL = pipeline::getActiveDBCLayout() ? pipeline::getActiveDBCLayout()->getLayout("Spell") : nullptr; uint32_t count = dbc->getRecordCount(); for (uint32_t i = 0; i < count; ++i) { uint32_t spellId = dbc->getUInt32(i, spellL ? (*spellL)["ID"] : 0); if (spellId == 0) continue; uint32_t iconId = dbc->getUInt32(i, spellL ? (*spellL)["IconID"] : 133); spellIconIds[spellId] = iconId; std::string tooltip = dbc->getString(i, spellL ? (*spellL)["Tooltip"] : 139); if (!tooltip.empty()) { spellTooltips[spellId] = tooltip; } } } 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()) return; const auto* iconL = pipeline::getActiveDBCLayout() ? pipeline::getActiveDBCLayout()->getLayout("SpellIcon") : nullptr; for (uint32_t i = 0; i < dbc->getRecordCount(); i++) { uint32_t id = dbc->getUInt32(i, iconL ? (*iconL)["ID"] : 0); std::string path = dbc->getString(i, iconL ? (*iconL)["Path"] : 1); if (!path.empty() && id > 0) { spellIconPaths[id] = path; } } } VkDescriptorSet TalentScreen::getSpellIcon(uint32_t iconId, pipeline::AssetManager* assetManager) { if (iconId == 0 || !assetManager) return VK_NULL_HANDLE; auto cit = spellIconCache.find(iconId); if (cit != spellIconCache.end()) return cit->second; auto pit = spellIconPaths.find(iconId); if (pit == spellIconPaths.end()) { spellIconCache[iconId] = VK_NULL_HANDLE; return VK_NULL_HANDLE; } std::string iconPath = pit->second + ".blp"; auto blpData = assetManager->readFile(iconPath); if (blpData.empty()) { spellIconCache[iconId] = VK_NULL_HANDLE; return VK_NULL_HANDLE; } auto image = pipeline::BLPLoader::load(blpData); if (!image.isValid()) { spellIconCache[iconId] = VK_NULL_HANDLE; return VK_NULL_HANDLE; } auto* window = core::Application::getInstance().getWindow(); auto* vkCtx = window ? window->getVkContext() : nullptr; if (!vkCtx) { spellIconCache[iconId] = VK_NULL_HANDLE; return VK_NULL_HANDLE; } VkDescriptorSet ds = vkCtx->uploadImGuiTexture(image.data.data(), image.width, image.height); spellIconCache[iconId] = ds; return ds; } }} // namespace wowee::ui