Kelsidavis-WoWee/src/ui/talent_screen.cpp

668 lines
24 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);
}
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();
}
}
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
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);
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];
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
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]) {
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];
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;
// 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