#include "ui/talent_screen.hpp" #include "ui/keybinding_manager.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) { // Talents toggle via keybinding (edge-triggered) // Customizable key (default: N) from KeybindingManager bool talentsDown = KeybindingManager::getInstance().isActionPressed( KeybindingManager::Action::TOGGLE_TALENTS, false); if (talentsDown && !nKeyWasDown) { open = !open; } nKeyWasDown = talentsDown; 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); loadGlyphPropertiesDBC(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(); } } // Glyphs tab (WotLK only — visible when any glyph slot is populated or DBC data loaded) if (!glyphProperties_.empty() || [&]() { const auto& g = gameHandler.getGlyphs(); for (auto id : g) if (id != 0) return true; return false; }()) { if (ImGui::BeginTabItem("Glyphs")) { renderGlyphs(gameHandler); 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 — use int to avoid uint8_t wrap-around infinite loops int maxRow = 0, maxCol = 0; for (const auto* talent : talents) { maxRow = std::max(maxRow, (int)talent->row); maxCol = std::max(maxCol, (int)talent->column); } // Sanity-cap to prevent runaway loops from corrupt/unexpected DBC data maxRow = std::min(maxRow, 15); maxCol = std::min(maxCol, 15); // 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 = (float)(maxCol + 1) * cellSize + spacing; const float gridHeight = (float)(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); char childId[32]; snprintf(childId, sizeof(childId), "TalentGrid_%u", tabId); ImGui::BeginChild(childId, 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 { // Only load the background if icon uploads aren't saturating this frame. // Background is cosmetic; skip if we're already loading icons this frame. std::string bgPath = bgFile; 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); } } } // Cache even if null to avoid retrying every frame on missing files 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]; // storage 1-indexed, DBC 0-indexed 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 (int row = 0; row <= maxRow; ++row) { for (int 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 char emptyId[32]; snprintf(emptyId, sizeof(emptyId), "e_%u_%u_%u", tabId, row, col); ImGui::InvisibleButton(emptyId, 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]) { // storage 1-indexed, DBC 0-indexed 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]; // storage 1-indexed, DBC 0-indexed 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(); const uint8_t reqRankDisplay = talent.prereqRank[i] + 1u; // DBC 0-indexed → display 1-indexed ImGui::TextColored(pColor, "Requires %u point%s in %s", reqRankDisplay, reqRankDisplay > 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 — currentRank is 1-indexed (0=not learned, 1=rank1, ...) // CMSG_LEARN_TALENT requestedRank must equal current count of learned ranks (same value) if (clicked && canLearn && prereqsMet) { gameHandler.learnTalent(talent.talentId, currentRank); } 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 fieldCount = dbc->getFieldCount(); // Detect DBC/layout mismatch: Classic layout expects ~173 fields but we may // load the WotLK base DBC (234 fields). Use WotLK field indices in that case. uint32_t idField = 0, iconField = 133, tooltipField = 139; if (spellL) { uint32_t layoutIcon = (*spellL)["IconID"]; if (layoutIcon < fieldCount && fieldCount <= layoutIcon + 20) { idField = (*spellL)["ID"]; iconField = layoutIcon; try { tooltipField = (*spellL)["Tooltip"]; } catch (...) {} } } uint32_t count = dbc->getRecordCount(); for (uint32_t i = 0; i < count; ++i) { uint32_t spellId = dbc->getUInt32(i, idField); if (spellId == 0) continue; uint32_t iconId = dbc->getUInt32(i, iconField); spellIconIds[spellId] = iconId; std::string tooltip = dbc->getString(i, tooltipField); 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; } } } void TalentScreen::loadGlyphPropertiesDBC(pipeline::AssetManager* assetManager) { if (glyphDbcLoaded) return; glyphDbcLoaded = true; if (!assetManager || !assetManager->isInitialized()) return; auto dbc = assetManager->loadDBC("GlyphProperties.dbc"); if (!dbc || !dbc->isLoaded()) return; // GlyphProperties.dbc: field 0=ID, field 1=SpellID, field 2=GlyphSlotFlags (1=minor), field 3=SpellIconID for (uint32_t i = 0; i < dbc->getRecordCount(); i++) { uint32_t id = dbc->getUInt32(i, 0); uint32_t spellId = dbc->getUInt32(i, 1); uint32_t flags = dbc->getUInt32(i, 2); if (id == 0) continue; GlyphInfo info; info.spellId = spellId; info.isMajor = (flags == 0); // flag 0 = major, flag 1 = minor glyphProperties_[id] = info; } } void TalentScreen::renderGlyphs(game::GameHandler& gameHandler) { auto* assetManager = core::Application::getInstance().getAssetManager(); const auto& glyphs = gameHandler.getGlyphs(); ImGui::Spacing(); ImGui::TextColored(ImVec4(1.0f, 0.85f, 0.0f, 1.0f), "Major Glyphs"); ImGui::Separator(); // WotLK: 6 glyph slots total. Slots 0,2,4 are major by convention from the server, // but we check GlyphProperties.dbc flags when available. // Display all 6 slots grouped: show major (non-minor) first, then minor. std::vector> majorSlots, minorSlots; for (int i = 0; i < game::GameHandler::MAX_GLYPH_SLOTS; i++) { uint16_t glyphId = glyphs[i]; bool isMajor = true; if (glyphId != 0) { auto git = glyphProperties_.find(glyphId); if (git != glyphProperties_.end()) isMajor = git->second.isMajor; else isMajor = (i % 2 == 0); // fallback: even slots = major } else { isMajor = (i % 2 == 0); // empty slots follow same pattern } if (isMajor) majorSlots.push_back({i, true}); else minorSlots.push_back({i, false}); } auto renderGlyphSlot = [&](int slotIdx) { uint16_t glyphId = glyphs[slotIdx]; char label[64]; if (glyphId == 0) { snprintf(label, sizeof(label), "Slot %d [Empty]", slotIdx + 1); ImGui::TextDisabled("%s", label); return; } uint32_t spellId = 0; uint32_t iconId = 0; auto git = glyphProperties_.find(glyphId); if (git != glyphProperties_.end()) { spellId = git->second.spellId; auto iit = spellIconIds.find(spellId); if (iit != spellIconIds.end()) iconId = iit->second; } // Icon (24x24) VkDescriptorSet icon = getSpellIcon(iconId, assetManager); if (icon != VK_NULL_HANDLE) { ImGui::Image((ImTextureID)(uintptr_t)icon, ImVec2(24, 24)); ImGui::SameLine(0, 6); } else { ImGui::Dummy(ImVec2(24, 24)); ImGui::SameLine(0, 6); } // Spell name const std::string& name = spellId ? gameHandler.getSpellName(spellId) : ""; if (!name.empty()) { ImGui::TextColored(ImVec4(0.9f, 0.9f, 0.9f, 1.0f), "%s", name.c_str()); } else { ImGui::TextColored(ImVec4(0.7f, 0.7f, 0.7f, 1.0f), "Glyph #%u", (uint32_t)glyphId); } }; for (auto& [idx, major] : majorSlots) renderGlyphSlot(idx); ImGui::Spacing(); ImGui::TextColored(ImVec4(0.6f, 0.8f, 1.0f, 1.0f), "Minor Glyphs"); ImGui::Separator(); for (auto& [idx, major] : minorSlots) renderGlyphSlot(idx); } 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; // Rate-limit texture uploads to avoid multi-hundred-ms stalls when switching // to a tab whose icons are not yet cached (each upload is a blocking GPU op). // Allow at most 4 new icon loads per frame; the rest show a blank icon and // load on the next frame, spreading the cost across ~5 frames. static int loadsThisFrame = 0; static int lastImGuiFrame = -1; int curFrame = ImGui::GetFrameCount(); if (curFrame != lastImGuiFrame) { loadsThisFrame = 0; lastImGuiFrame = curFrame; } if (loadsThisFrame >= 4) return VK_NULL_HANDLE; // defer, don't cache null ++loadsThisFrame; 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