Kelsidavis-WoWee/src/ui/talent_screen.cpp

812 lines
30 KiB
C++
Raw Normal View History

#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();
}
// Talent learn confirmation popup
if (talentConfirmOpen_) {
ImGui::OpenPopup("Learn Talent?##talent_confirm");
talentConfirmOpen_ = false;
}
if (ImGui::BeginPopupModal("Learn Talent?##talent_confirm", nullptr,
ImGuiWindowFlags_AlwaysAutoResize | ImGuiWindowFlags_NoMove)) {
ImGui::TextColored(ImVec4(1.0f, 1.0f, 0.3f, 1.0f), "%s", pendingTalentName_.c_str());
ImGui::Text("Rank %u", pendingTalentRank_ + 1);
ImGui::Spacing();
ImGui::TextWrapped("Spend a talent point?");
ImGui::Spacing();
if (ImGui::Button("Learn", ImVec2(80, 0))) {
gameHandler.learnTalent(pendingTalentId_, pendingTalentRank_);
ImGui::CloseCurrentPopup();
}
ImGui::SameLine();
if (ImGui::Button("Cancel", ImVec2(80, 0))) {
ImGui::CloseCurrentPopup();
}
ImGui::EndPopup();
}
}
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 — open confirmation dialog instead of learning directly
if (clicked && canLearn && prereqsMet) {
talentConfirmOpen_ = true;
pendingTalentId_ = talent.talentId;
pendingTalentRank_ = currentRank;
uint32_t nextSpell = (currentRank < 5) ? talent.rankSpells[currentRank] : 0;
pendingTalentName_ = nextSpell ? gameHandler.getSpellName(nextSpell) : "";
if (pendingTalentName_.empty())
pendingTalentName_ = spellId ? gameHandler.getSpellName(spellId) : "Talent";
}
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