Kelsidavis-WoWee/src/ui/talent_screen.cpp
Kelsi e38324619e fix: resolve missing Classic spell icons on action bar and talents
When Classic is active, loadDBC("Spell.dbc") finds the WotLK base DBC
(234 fields) since no binary Classic DBC exists. The Classic layout says
IconID is at field 117, but in the WotLK DBC that field contains
unrelated data (mostly zeros). This caused all spell icon lookups to
fail silently.

Now detects the DBC/layout field count mismatch and falls back to the
WotLK field index 133, which is correct for the base DBC. Classic spell
IDs are a subset of WotLK, so the icon mapping works correctly.
2026-03-17 07:42:01 -07:00

783 lines
29 KiB
C++

#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 <algorithm>
#include <cmath>
namespace wowee { namespace ui {
// WoW class names indexed by class ID (1-11)
static const char* classNames[] = {
"Unknown", "Warrior", "Paladin", "Hunter", "Rogue", "Priest",
"Death Knight", "Shaman", "Mage", "Warlock", "Unknown", "Druid"
};
static const char* getClassName(uint8_t classId) {
return (classId >= 1 && classId <= 11) ? classNames[classId] : "Unknown";
}
void TalentScreen::render(game::GameHandler& gameHandler) {
// 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<float>(window->getWidth()) : 1280.0f;
float screenH = window ? static_cast<float>(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<const game::GameHandler::TalentTabEntry*> classTabs;
for (const auto& [tabId, tab] : gameHandler.getAllTalentTabs()) {
if (tab.classMask & classMask) {
classTabs.push_back(&tab);
}
}
std::sort(classTabs.begin(), classTabs.end(),
[](const auto* a, const auto* b) { return a->orderIndex < b->orderIndex; });
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<const game::GameHandler::TalentEntry*> 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<uint32_t, TalentPos> talentPositions;
// First pass: compute positions
for (const auto* talent : talents) {
float x = gridOrigin.x + talent->column * cellSize + spacing;
float y = gridOrigin.y + talent->row * cellSize + spacing;
ImVec2 center(x + iconSize * 0.5f, y + iconSize * 0.5f);
talentPositions[talent->talentId] = {talent, center};
}
// Draw prerequisite arrows
auto* drawList = ImGui::GetWindowDrawList();
for (const auto* talent : talents) {
for (int i = 0; i < 3; ++i) {
if (talent->prereqTalent[i] == 0) continue;
auto fromIt = talentPositions.find(talent->prereqTalent[i]);
auto toIt = talentPositions.find(talent->talentId);
if (fromIt == talentPositions.end() || toIt == talentPositions.end()) continue;
uint8_t prereqRank = gameHandler.getTalentRank(talent->prereqTalent[i]);
bool met = prereqRank > talent->prereqRank[i]; // 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<int>(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<int>(tint.x * 255), static_cast<int>(tint.y * 255),
static_cast<int>(tint.z * 255), static_cast<int>(tint.w * 255));
dl->AddImage((ImTextureID)(uintptr_t)iconTex,
ImVec2(pMin.x + 2, pMin.y + 2),
ImVec2(pMax.x - 2, pMax.y - 2),
ImVec2(0, 0), ImVec2(1, 1), tintCol);
}
// Border
float borderThick = hovered ? 2.5f : 1.5f;
ImU32 borderCol = IM_COL32(
static_cast<int>(borderColor.x * 255), static_cast<int>(borderColor.y * 255),
static_cast<int>(borderColor.z * 255), static_cast<int>(borderColor.w * 255));
dl->AddRect(pMin, pMax, borderCol, 3.0f, 0, borderThick);
// 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<std::pair<int, bool>> 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