diff --git a/include/game/game_handler.hpp b/include/game/game_handler.hpp index 9eb2b867..5bb40efa 100644 --- a/include/game/game_handler.hpp +++ b/include/game/game_handler.hpp @@ -830,6 +830,14 @@ public: void togglePetSpellAutocast(uint32_t spellId); const std::unordered_set& getKnownSpells() const { return knownSpells; } + // Spell book tabs — groups known spells by class skill line for Lua API + struct SpellBookTab { + std::string name; + std::string texture; // icon path + std::vector spellIds; // spells in this tab + }; + const std::vector& getSpellBookTabs(); + // ---- Pet Stable ---- struct StabledPet { uint32_t petNumber = 0; // server-side pet number (used for unstable/swap) @@ -3443,6 +3451,8 @@ private: std::unordered_map skillLineNames_; std::unordered_map skillLineCategories_; std::unordered_map spellToSkillLine_; // spellID -> skillLineID + std::vector spellBookTabs_; + bool spellBookTabsDirty_ = true; bool skillLineDbcLoaded_ = false; bool skillLineAbilityLoaded_ = false; static constexpr size_t PLAYER_EXPLORED_ZONES_COUNT = 128; diff --git a/src/addons/lua_engine.cpp b/src/addons/lua_engine.cpp index 3ab3a3c9..691ced5a 100644 --- a/src/addons/lua_engine.cpp +++ b/src/addons/lua_engine.cpp @@ -1396,6 +1396,86 @@ static int lua_IsSpellKnown(lua_State* L) { return 1; } +// --- Spell Book Tab API --- + +// GetNumSpellTabs() → count +static int lua_GetNumSpellTabs(lua_State* L) { + auto* gh = getGameHandler(L); + if (!gh) { lua_pushnumber(L, 0); return 1; } + lua_pushnumber(L, gh->getSpellBookTabs().size()); + return 1; +} + +// GetSpellTabInfo(tabIndex) → name, texture, offset, numSpells +// tabIndex is 1-based; offset is 1-based global spell book slot +static int lua_GetSpellTabInfo(lua_State* L) { + auto* gh = getGameHandler(L); + int tabIdx = static_cast(luaL_checknumber(L, 1)); + if (!gh || tabIdx < 1) { + lua_pushnil(L); return 1; + } + const auto& tabs = gh->getSpellBookTabs(); + if (tabIdx > static_cast(tabs.size())) { + lua_pushnil(L); return 1; + } + // Compute offset: sum of spells in all preceding tabs (1-based) + int offset = 0; + for (int i = 0; i < tabIdx - 1; ++i) + offset += static_cast(tabs[i].spellIds.size()); + const auto& tab = tabs[tabIdx - 1]; + lua_pushstring(L, tab.name.c_str()); // name + lua_pushstring(L, tab.texture.c_str()); // texture + lua_pushnumber(L, offset); // offset (0-based for WoW compat) + lua_pushnumber(L, tab.spellIds.size()); // numSpells + return 4; +} + +// GetSpellBookItemInfo(slot, bookType) → "SPELL", spellId +// slot is 1-based global spell book index +static int lua_GetSpellBookItemInfo(lua_State* L) { + auto* gh = getGameHandler(L); + int slot = static_cast(luaL_checknumber(L, 1)); + if (!gh || slot < 1) { + lua_pushstring(L, "SPELL"); + lua_pushnumber(L, 0); + return 2; + } + const auto& tabs = gh->getSpellBookTabs(); + int idx = slot; // 1-based + for (const auto& tab : tabs) { + if (idx <= static_cast(tab.spellIds.size())) { + lua_pushstring(L, "SPELL"); + lua_pushnumber(L, tab.spellIds[idx - 1]); + return 2; + } + idx -= static_cast(tab.spellIds.size()); + } + lua_pushstring(L, "SPELL"); + lua_pushnumber(L, 0); + return 2; +} + +// GetSpellBookItemName(slot, bookType) → name, subName +static int lua_GetSpellBookItemName(lua_State* L) { + auto* gh = getGameHandler(L); + int slot = static_cast(luaL_checknumber(L, 1)); + if (!gh || slot < 1) { lua_pushnil(L); return 1; } + const auto& tabs = gh->getSpellBookTabs(); + int idx = slot; + for (const auto& tab : tabs) { + if (idx <= static_cast(tab.spellIds.size())) { + uint32_t spellId = tab.spellIds[idx - 1]; + const std::string& name = gh->getSpellName(spellId); + lua_pushstring(L, name.empty() ? "Unknown" : name.c_str()); + lua_pushstring(L, ""); // subName/rank + return 2; + } + idx -= static_cast(tab.spellIds.size()); + } + lua_pushnil(L); + return 1; +} + static int lua_GetSpellCooldown(lua_State* L) { auto* gh = getGameHandler(L); if (!gh) { lua_pushnumber(L, 0); lua_pushnumber(L, 0); return 2; } @@ -3915,6 +3995,10 @@ void LuaEngine::registerCoreAPI() { {"IsAddonMessagePrefixRegistered", lua_IsAddonMessagePrefixRegistered}, {"CastSpellByName", lua_CastSpellByName}, {"IsSpellKnown", lua_IsSpellKnown}, + {"GetNumSpellTabs", lua_GetNumSpellTabs}, + {"GetSpellTabInfo", lua_GetSpellTabInfo}, + {"GetSpellBookItemInfo", lua_GetSpellBookItemInfo}, + {"GetSpellBookItemName", lua_GetSpellBookItemName}, {"GetSpellCooldown", lua_GetSpellCooldown}, {"GetSpellPowerCost", lua_GetSpellPowerCost}, {"IsSpellInRange", lua_IsSpellInRange}, diff --git a/src/game/game_handler.cpp b/src/game/game_handler.cpp index 58fbf032..66c116bf 100644 --- a/src/game/game_handler.cpp +++ b/src/game/game_handler.cpp @@ -23033,6 +23033,62 @@ void GameHandler::loadSkillLineAbilityDbc() { } } +const std::vector& GameHandler::getSpellBookTabs() { + // Rebuild when spell count changes (learns/unlearns) + static size_t lastSpellCount = 0; + if (lastSpellCount == knownSpells.size() && !spellBookTabsDirty_) + return spellBookTabs_; + lastSpellCount = knownSpells.size(); + spellBookTabsDirty_ = false; + spellBookTabs_.clear(); + + static constexpr uint32_t SKILLLINE_CATEGORY_CLASS = 7; + + // Group known spells by class skill line + std::map> bySkillLine; + std::vector general; + + for (uint32_t spellId : knownSpells) { + auto slIt = spellToSkillLine_.find(spellId); + if (slIt != spellToSkillLine_.end()) { + uint32_t skillLineId = slIt->second; + auto catIt = skillLineCategories_.find(skillLineId); + if (catIt != skillLineCategories_.end() && catIt->second == SKILLLINE_CATEGORY_CLASS) { + bySkillLine[skillLineId].push_back(spellId); + continue; + } + } + general.push_back(spellId); + } + + // Sort spells within each group by name + auto byName = [this](uint32_t a, uint32_t b) { + return getSpellName(a) < getSpellName(b); + }; + + // "General" tab first (spells not in a class skill line) + if (!general.empty()) { + std::sort(general.begin(), general.end(), byName); + spellBookTabs_.push_back({"General", "Interface\\Icons\\INV_Misc_Book_09", std::move(general)}); + } + + // Class skill line tabs, sorted by name + std::vector>> named; + for (auto& [skillLineId, spells] : bySkillLine) { + auto nameIt = skillLineNames_.find(skillLineId); + std::string tabName = (nameIt != skillLineNames_.end()) ? nameIt->second : "Unknown"; + std::sort(spells.begin(), spells.end(), byName); + named.emplace_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) { + spellBookTabs_.push_back({std::move(name), "Interface\\Icons\\INV_Misc_Book_09", std::move(spells)}); + } + + return spellBookTabs_; +} + void GameHandler::categorizeTrainerSpells() { trainerTabs_.clear();