mirror of
https://github.com/Kelsidavis/WoWee.git
synced 2026-03-22 23:30:14 +00:00
Clicking a learnable talent now opens a modal confirmation popup showing the spell name and rank, preventing accidental talent spending.
811 lines
30 KiB
C++
811 lines
30 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();
|
|
}
|
|
|
|
// 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
|