From 836a629513f91d09683a912c575f2f6c581760cc Mon Sep 17 00:00:00 2001 From: Kelsi Date: Fri, 6 Feb 2026 20:40:17 -0800 Subject: [PATCH] Organize spellbook tabs by skill line specialty using SkillLine.dbc and SkillLineAbility.dbc --- include/ui/spellbook_screen.hpp | 22 +++--- src/ui/spellbook_screen.cpp | 120 +++++++++++++++++++++++--------- 2 files changed, 103 insertions(+), 39 deletions(-) diff --git a/include/ui/spellbook_screen.hpp b/include/ui/spellbook_screen.hpp index 28b20db7..a4a98c93 100644 --- a/include/ui/spellbook_screen.hpp +++ b/include/ui/spellbook_screen.hpp @@ -5,6 +5,7 @@ #include #include #include +#include #include namespace wowee { @@ -22,7 +23,10 @@ struct SpellInfo { bool isPassive() const { return (attributes & 0x40) != 0; } }; -enum class SpellTab { GENERAL, ACTIVE, PASSIVE }; +struct SpellTabInfo { + std::string name; + std::vector spells; +}; class SpellbookScreen { public: @@ -50,14 +54,15 @@ private: std::unordered_map spellIconPaths; // SpellIconID -> path std::unordered_map spellIconCache; // SpellIconID -> GL texture - // Categorized spell lists (rebuilt when spell list changes) - std::vector generalSpells; - std::vector activeSpells; - std::vector passiveSpells; - size_t lastKnownSpellCount = 0; + // Skill line data (loaded from SkillLine.dbc + SkillLineAbility.dbc) + bool skillLineDbLoaded = false; + std::unordered_map skillLineNames; // skillLineID -> name + std::unordered_map spellToSkillLine; // spellID -> skillLineID - // Tab state - SpellTab currentTab = SpellTab::GENERAL; + // Categorized spell tabs (rebuilt when spell list changes) + // ordered map so tabs appear in consistent order + std::vector spellTabs; + size_t lastKnownSpellCount = 0; // Drag-and-drop from spellbook to action bar bool draggingSpell_ = false; @@ -66,6 +71,7 @@ private: void loadSpellDBC(pipeline::AssetManager* assetManager); void loadSpellIconDBC(pipeline::AssetManager* assetManager); + void loadSkillLineDBCs(pipeline::AssetManager* assetManager); void categorizeSpells(const std::vector& knownSpells); GLuint getSpellIcon(uint32_t iconId, pipeline::AssetManager* assetManager); const SpellInfo* getSpellInfo(uint32_t spellId) const; diff --git a/src/ui/spellbook_screen.cpp b/src/ui/spellbook_screen.cpp index d01e2d98..2f623f14 100644 --- a/src/ui/spellbook_screen.cpp +++ b/src/ui/spellbook_screen.cpp @@ -6,6 +6,7 @@ #include "pipeline/blp_loader.hpp" #include "core/logger.hpp" #include +#include namespace wowee { namespace ui { @@ -87,30 +88,92 @@ void SpellbookScreen::loadSpellIconDBC(pipeline::AssetManager* assetManager) { LOG_INFO("Spellbook: Loaded ", spellIconPaths.size(), " spell icon paths"); } +void SpellbookScreen::loadSkillLineDBCs(pipeline::AssetManager* assetManager) { + if (skillLineDbLoaded) return; + skillLineDbLoaded = true; + + if (!assetManager || !assetManager->isInitialized()) return; + + // Load SkillLine.dbc: field 0 = ID, field 1 = categoryID, field 3 = name_enUS + auto skillLineDbc = assetManager->loadDBC("SkillLine.dbc"); + if (skillLineDbc && skillLineDbc->isLoaded()) { + for (uint32_t i = 0; i < skillLineDbc->getRecordCount(); i++) { + uint32_t id = skillLineDbc->getUInt32(i, 0); + std::string name = skillLineDbc->getString(i, 3); + if (id > 0 && !name.empty()) { + skillLineNames[id] = name; + } + } + LOG_INFO("Spellbook: Loaded ", skillLineNames.size(), " skill lines"); + } else { + LOG_WARNING("Spellbook: Could not load SkillLine.dbc"); + } + + // Load SkillLineAbility.dbc: field 0 = ID, field 1 = skillLineID, field 2 = spellID + auto slaDbc = assetManager->loadDBC("SkillLineAbility.dbc"); + if (slaDbc && slaDbc->isLoaded()) { + for (uint32_t i = 0; i < slaDbc->getRecordCount(); i++) { + uint32_t skillLineId = slaDbc->getUInt32(i, 1); + uint32_t spellId = slaDbc->getUInt32(i, 2); + if (spellId > 0 && skillLineId > 0) { + spellToSkillLine[spellId] = skillLineId; + } + } + LOG_INFO("Spellbook: Loaded ", spellToSkillLine.size(), " skill line abilities"); + } else { + LOG_WARNING("Spellbook: Could not load SkillLineAbility.dbc"); + } +} + void SpellbookScreen::categorizeSpells(const std::vector& knownSpells) { - generalSpells.clear(); - activeSpells.clear(); - passiveSpells.clear(); + spellTabs.clear(); + + // Group spells by skill line, preserving order of first appearance + std::map> skillLineSpells; + std::vector generalSpells; for (uint32_t spellId : knownSpells) { auto it = spellData.find(spellId); if (it == spellData.end()) continue; const SpellInfo* info = &it->second; + if (isGeneralSpell(spellId)) { generalSpells.push_back(info); - } else if (info->isPassive()) { - passiveSpells.push_back(info); + continue; + } + + auto slIt = spellToSkillLine.find(spellId); + if (slIt != spellToSkillLine.end()) { + skillLineSpells[slIt->second].push_back(info); } else { - activeSpells.push_back(info); + generalSpells.push_back(info); } } - // Sort each tab alphabetically auto byName = [](const SpellInfo* a, const SpellInfo* b) { return a->name < b->name; }; - std::sort(generalSpells.begin(), generalSpells.end(), byName); - std::sort(activeSpells.begin(), activeSpells.end(), byName); - std::sort(passiveSpells.begin(), passiveSpells.end(), byName); + + // General tab first + if (!generalSpells.empty()) { + std::sort(generalSpells.begin(), generalSpells.end(), byName); + spellTabs.push_back({"General", std::move(generalSpells)}); + } + + // Skill line tabs sorted by name + std::vector>> named; + for (auto& [skillLineId, spells] : skillLineSpells) { + auto nameIt = skillLineNames.find(skillLineId); + std::string tabName = (nameIt != skillLineNames.end()) ? nameIt->second + : "Unknown (" + std::to_string(skillLineId) + ")"; + std::sort(spells.begin(), spells.end(), byName); + named.push_back({std::move(tabName), std::move(spells)}); + } + std::sort(named.begin(), named.end(), + [](const auto& a, const auto& b) { return a.first < b.first; }); + + for (auto& [name, spells] : named) { + spellTabs.push_back({std::move(name), std::move(spells)}); + } lastKnownSpellCount = knownSpells.size(); } @@ -175,6 +238,7 @@ void SpellbookScreen::render(game::GameHandler& gameHandler, pipeline::AssetMana if (!dbcLoadAttempted) { loadSpellDBC(assetManager); loadSpellIconDBC(assetManager); + loadSkillLineDBCs(assetManager); } // Rebuild categories if spell list changed @@ -209,26 +273,29 @@ void SpellbookScreen::render(game::GameHandler& gameHandler, pipeline::AssetMana // Tab bar if (ImGui::BeginTabBar("SpellbookTabs")) { - auto renderTab = [&](const char* label, SpellTab tab, const std::vector& spellList) { - if (ImGui::BeginTabItem(label)) { - currentTab = tab; + for (size_t tabIdx = 0; tabIdx < spellTabs.size(); tabIdx++) { + const auto& tab = spellTabs[tabIdx]; - if (spellList.empty()) { + char tabLabel[64]; + snprintf(tabLabel, sizeof(tabLabel), "%s (%zu)", + tab.name.c_str(), tab.spells.size()); + + if (ImGui::BeginTabItem(tabLabel)) { + if (tab.spells.empty()) { ImGui::TextDisabled("No spells in this category."); } - // Spell list with icons ImGui::BeginChild("SpellList", ImVec2(0, 0), true); float iconSize = 32.0f; - bool isPassiveTab = (tab == SpellTab::PASSIVE); - for (const SpellInfo* info : spellList) { + for (const SpellInfo* info : tab.spells) { ImGui::PushID(static_cast(info->spellId)); float cd = gameHandler.getSpellCooldown(info->spellId); bool onCooldown = cd > 0.0f; - bool isDim = isPassiveTab || onCooldown; + bool isPassive = info->isPassive(); + bool isDim = isPassive || onCooldown; GLuint iconTex = getSpellIcon(info->iconId, assetManager); @@ -268,13 +335,13 @@ void SpellbookScreen::render(game::GameHandler& gameHandler, pipeline::AssetMana if (rowHovered) { // Start drag on click (not passive) - if (rowClicked && !isPassiveTab) { + if (rowClicked && !isPassive) { draggingSpell_ = true; dragSpellId_ = info->spellId; dragSpellIconTex_ = iconTex; } - if (ImGui::IsMouseDoubleClicked(0) && !isPassiveTab && !onCooldown) { + if (ImGui::IsMouseDoubleClicked(0) && !isPassive && !onCooldown) { draggingSpell_ = false; dragSpellId_ = 0; dragSpellIconTex_ = 0; @@ -290,7 +357,7 @@ void SpellbookScreen::render(game::GameHandler& gameHandler, pipeline::AssetMana ImGui::TextDisabled("%s", info->rank.c_str()); } ImGui::TextDisabled("Spell ID: %u", info->spellId); - if (isPassiveTab) { + if (isPassive) { ImGui::TextDisabled("Passive"); } else { ImGui::TextDisabled("Drag to action bar to assign"); @@ -308,16 +375,7 @@ void SpellbookScreen::render(game::GameHandler& gameHandler, pipeline::AssetMana ImGui::EndChild(); ImGui::EndTabItem(); } - }; - - char generalLabel[32], activeLabel[32], passiveLabel[32]; - snprintf(generalLabel, sizeof(generalLabel), "General (%zu)", generalSpells.size()); - snprintf(activeLabel, sizeof(activeLabel), "Active (%zu)", activeSpells.size()); - snprintf(passiveLabel, sizeof(passiveLabel), "Passive (%zu)", passiveSpells.size()); - - renderTab(generalLabel, SpellTab::GENERAL, generalSpells); - renderTab(activeLabel, SpellTab::ACTIVE, activeSpells); - renderTab(passiveLabel, SpellTab::PASSIVE, passiveSpells); + } ImGui::EndTabBar(); }