mirror of
https://github.com/Kelsidavis/WoWee.git
synced 2026-05-03 08:03:50 +00:00
Fix trainer buy spell and add specialization tabs
Fix SMSG_BINDPOINTUPDATE opcode from 0x1B3 to 0x155 — the old value collided with SMSG_TRAINER_BUY_SUCCEEDED, causing buy responses to be misinterpreted as bindpoint updates. Add specialization tabs using SkillLineAbility.dbc to group spells by class spec (category 7).
This commit is contained in:
parent
64a19657a7
commit
f15dba80ba
4 changed files with 174 additions and 42 deletions
|
|
@ -544,6 +544,13 @@ public:
|
||||||
void closeTrainer();
|
void closeTrainer();
|
||||||
const std::string& getSpellName(uint32_t spellId) const;
|
const std::string& getSpellName(uint32_t spellId) const;
|
||||||
const std::string& getSpellRank(uint32_t spellId) const;
|
const std::string& getSpellRank(uint32_t spellId) const;
|
||||||
|
const std::string& getSkillLineName(uint32_t spellId) const;
|
||||||
|
|
||||||
|
struct TrainerTab {
|
||||||
|
std::string name;
|
||||||
|
std::vector<const TrainerSpell*> spells;
|
||||||
|
};
|
||||||
|
const std::vector<TrainerTab>& getTrainerTabs() const { return trainerTabs_; }
|
||||||
const ItemQueryResponseData* getItemInfo(uint32_t itemId) const {
|
const ItemQueryResponseData* getItemInfo(uint32_t itemId) const {
|
||||||
auto it = itemInfoCache_.find(itemId);
|
auto it = itemInfoCache_.find(itemId);
|
||||||
return (it != itemInfoCache_.end()) ? &it->second : nullptr;
|
return (it != itemInfoCache_.end()) ? &it->second : nullptr;
|
||||||
|
|
@ -962,8 +969,10 @@ private:
|
||||||
struct SpellNameEntry { std::string name; std::string rank; };
|
struct SpellNameEntry { std::string name; std::string rank; };
|
||||||
std::unordered_map<uint32_t, SpellNameEntry> spellNameCache_;
|
std::unordered_map<uint32_t, SpellNameEntry> spellNameCache_;
|
||||||
bool spellNameCacheLoaded_ = false;
|
bool spellNameCacheLoaded_ = false;
|
||||||
|
std::vector<TrainerTab> trainerTabs_;
|
||||||
void handleTrainerList(network::Packet& packet);
|
void handleTrainerList(network::Packet& packet);
|
||||||
void loadSpellNameCache();
|
void loadSpellNameCache();
|
||||||
|
void categorizeTrainerSpells();
|
||||||
|
|
||||||
// Callbacks
|
// Callbacks
|
||||||
WorldConnectSuccessCallback onSuccess;
|
WorldConnectSuccessCallback onSuccess;
|
||||||
|
|
@ -985,8 +994,11 @@ private:
|
||||||
std::map<uint32_t, PlayerSkill> playerSkills_;
|
std::map<uint32_t, PlayerSkill> playerSkills_;
|
||||||
std::unordered_map<uint32_t, std::string> skillLineNames_;
|
std::unordered_map<uint32_t, std::string> skillLineNames_;
|
||||||
std::unordered_map<uint32_t, uint32_t> skillLineCategories_;
|
std::unordered_map<uint32_t, uint32_t> skillLineCategories_;
|
||||||
|
std::unordered_map<uint32_t, uint32_t> spellToSkillLine_; // spellID -> skillLineID
|
||||||
bool skillLineDbcLoaded_ = false;
|
bool skillLineDbcLoaded_ = false;
|
||||||
|
bool skillLineAbilityLoaded_ = false;
|
||||||
void loadSkillLineDbc();
|
void loadSkillLineDbc();
|
||||||
|
void loadSkillLineAbilityDbc();
|
||||||
void extractSkillFields(const std::map<uint16_t, uint32_t>& fields);
|
void extractSkillFields(const std::map<uint16_t, uint32_t>& fields);
|
||||||
|
|
||||||
NpcDeathCallback npcDeathCallback_;
|
NpcDeathCallback npcDeathCallback_;
|
||||||
|
|
|
||||||
|
|
@ -277,7 +277,8 @@ enum class Opcode : uint16_t {
|
||||||
// ---- Battleground ----
|
// ---- Battleground ----
|
||||||
SMSG_BATTLEFIELD_PORT_DENIED = 0x014B,
|
SMSG_BATTLEFIELD_PORT_DENIED = 0x014B,
|
||||||
SMSG_REMOVED_FROM_PVP_QUEUE = 0x0170,
|
SMSG_REMOVED_FROM_PVP_QUEUE = 0x0170,
|
||||||
SMSG_BINDPOINTUPDATE = 0x01B3,
|
SMSG_TRAINER_BUY_SUCCEEDED = 0x01B3,
|
||||||
|
SMSG_BINDPOINTUPDATE = 0x0155,
|
||||||
CMSG_BATTLEFIELD_LIST = 0x023C,
|
CMSG_BATTLEFIELD_LIST = 0x023C,
|
||||||
SMSG_BATTLEFIELD_LIST = 0x023D,
|
SMSG_BATTLEFIELD_LIST = 0x023D,
|
||||||
CMSG_BATTLEFIELD_JOIN = 0x023E,
|
CMSG_BATTLEFIELD_JOIN = 0x023E,
|
||||||
|
|
|
||||||
|
|
@ -654,6 +654,17 @@ void GameHandler::handlePacket(network::Packet& packet) {
|
||||||
case Opcode::SMSG_TRAINER_LIST:
|
case Opcode::SMSG_TRAINER_LIST:
|
||||||
handleTrainerList(packet);
|
handleTrainerList(packet);
|
||||||
break;
|
break;
|
||||||
|
case Opcode::SMSG_TRAINER_BUY_SUCCEEDED: {
|
||||||
|
uint64_t guid = packet.readUInt64();
|
||||||
|
uint32_t spellId = packet.readUInt32();
|
||||||
|
(void)guid;
|
||||||
|
const std::string& name = getSpellName(spellId);
|
||||||
|
if (!name.empty())
|
||||||
|
addSystemChatMessage("You have learned " + name + ".");
|
||||||
|
else
|
||||||
|
addSystemChatMessage("Spell learned.");
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
// Silently ignore common packets we don't handle yet
|
// Silently ignore common packets we don't handle yet
|
||||||
case Opcode::SMSG_FEATURE_SYSTEM_STATUS:
|
case Opcode::SMSG_FEATURE_SYSTEM_STATUS:
|
||||||
|
|
@ -4562,8 +4573,11 @@ void GameHandler::handleTrainerList(network::Packet& packet) {
|
||||||
trainerWindowOpen_ = true;
|
trainerWindowOpen_ = true;
|
||||||
gossipWindowOpen = false;
|
gossipWindowOpen = false;
|
||||||
|
|
||||||
// Ensure spell name cache is populated
|
// Ensure caches are populated
|
||||||
loadSpellNameCache();
|
loadSpellNameCache();
|
||||||
|
loadSkillLineDbc();
|
||||||
|
loadSkillLineAbilityDbc();
|
||||||
|
categorizeTrainerSpells();
|
||||||
}
|
}
|
||||||
|
|
||||||
void GameHandler::trainSpell(uint32_t spellId) {
|
void GameHandler::trainSpell(uint32_t spellId) {
|
||||||
|
|
@ -4575,6 +4589,7 @@ void GameHandler::trainSpell(uint32_t spellId) {
|
||||||
void GameHandler::closeTrainer() {
|
void GameHandler::closeTrainer() {
|
||||||
trainerWindowOpen_ = false;
|
trainerWindowOpen_ = false;
|
||||||
currentTrainerList_ = TrainerListData{};
|
currentTrainerList_ = TrainerListData{};
|
||||||
|
trainerTabs_.clear();
|
||||||
}
|
}
|
||||||
|
|
||||||
void GameHandler::loadSpellNameCache() {
|
void GameHandler::loadSpellNameCache() {
|
||||||
|
|
@ -4609,6 +4624,78 @@ void GameHandler::loadSpellNameCache() {
|
||||||
LOG_INFO("Trainer: Loaded ", spellNameCache_.size(), " spell names from Spell.dbc");
|
LOG_INFO("Trainer: Loaded ", spellNameCache_.size(), " spell names from Spell.dbc");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void GameHandler::loadSkillLineAbilityDbc() {
|
||||||
|
if (skillLineAbilityLoaded_) return;
|
||||||
|
skillLineAbilityLoaded_ = true;
|
||||||
|
|
||||||
|
auto* am = core::Application::getInstance().getAssetManager();
|
||||||
|
if (!am || !am->isInitialized()) return;
|
||||||
|
|
||||||
|
// SkillLineAbility.dbc: field 1=skillLineID, field 2=spellID
|
||||||
|
auto slaDbc = am->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("Trainer: Loaded ", spellToSkillLine_.size(), " skill line abilities");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void GameHandler::categorizeTrainerSpells() {
|
||||||
|
trainerTabs_.clear();
|
||||||
|
|
||||||
|
static constexpr uint32_t SKILLLINE_CATEGORY_CLASS = 7;
|
||||||
|
|
||||||
|
// Group spells by skill line (category 7 = class spec tabs)
|
||||||
|
std::map<uint32_t, std::vector<const TrainerSpell*>> specialtySpells;
|
||||||
|
std::vector<const TrainerSpell*> generalSpells;
|
||||||
|
|
||||||
|
for (const auto& spell : currentTrainerList_.spells) {
|
||||||
|
auto slIt = spellToSkillLine_.find(spell.spellId);
|
||||||
|
if (slIt != spellToSkillLine_.end()) {
|
||||||
|
uint32_t skillLineId = slIt->second;
|
||||||
|
auto catIt = skillLineCategories_.find(skillLineId);
|
||||||
|
if (catIt != skillLineCategories_.end() && catIt->second == SKILLLINE_CATEGORY_CLASS) {
|
||||||
|
specialtySpells[skillLineId].push_back(&spell);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
generalSpells.push_back(&spell);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Sort by spell name within each group
|
||||||
|
auto byName = [this](const TrainerSpell* a, const TrainerSpell* b) {
|
||||||
|
return getSpellName(a->spellId) < getSpellName(b->spellId);
|
||||||
|
};
|
||||||
|
|
||||||
|
// Build named tabs sorted alphabetically
|
||||||
|
std::vector<std::pair<std::string, std::vector<const TrainerSpell*>>> named;
|
||||||
|
for (auto& [skillLineId, spells] : specialtySpells) {
|
||||||
|
auto nameIt = skillLineNames_.find(skillLineId);
|
||||||
|
std::string tabName = (nameIt != skillLineNames_.end()) ? nameIt->second : "Specialty";
|
||||||
|
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) {
|
||||||
|
trainerTabs_.push_back({std::move(name), std::move(spells)});
|
||||||
|
}
|
||||||
|
|
||||||
|
// General tab last
|
||||||
|
if (!generalSpells.empty()) {
|
||||||
|
std::sort(generalSpells.begin(), generalSpells.end(), byName);
|
||||||
|
trainerTabs_.push_back({"General", std::move(generalSpells)});
|
||||||
|
}
|
||||||
|
|
||||||
|
LOG_INFO("Trainer: Categorized into ", trainerTabs_.size(), " tabs");
|
||||||
|
}
|
||||||
|
|
||||||
static const std::string EMPTY_STRING;
|
static const std::string EMPTY_STRING;
|
||||||
|
|
||||||
const std::string& GameHandler::getSpellName(uint32_t spellId) const {
|
const std::string& GameHandler::getSpellName(uint32_t spellId) const {
|
||||||
|
|
@ -4621,6 +4708,13 @@ const std::string& GameHandler::getSpellRank(uint32_t spellId) const {
|
||||||
return (it != spellNameCache_.end()) ? it->second.rank : EMPTY_STRING;
|
return (it != spellNameCache_.end()) ? it->second.rank : EMPTY_STRING;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const std::string& GameHandler::getSkillLineName(uint32_t spellId) const {
|
||||||
|
auto slIt = spellToSkillLine_.find(spellId);
|
||||||
|
if (slIt == spellToSkillLine_.end()) return EMPTY_STRING;
|
||||||
|
auto nameIt = skillLineNames_.find(slIt->second);
|
||||||
|
return (nameIt != skillLineNames_.end()) ? nameIt->second : EMPTY_STRING;
|
||||||
|
}
|
||||||
|
|
||||||
// ============================================================
|
// ============================================================
|
||||||
// Single-player local combat
|
// Single-player local combat
|
||||||
// ============================================================
|
// ============================================================
|
||||||
|
|
|
||||||
|
|
@ -3569,25 +3569,18 @@ void GameScreen::renderTrainerWindow(game::GameHandler& gameHandler) {
|
||||||
return std::find(knownSpells.begin(), knownSpells.end(), id) != knownSpells.end();
|
return std::find(knownSpells.begin(), knownSpells.end(), id) != knownSpells.end();
|
||||||
};
|
};
|
||||||
|
|
||||||
if (ImGui::BeginTable("TrainerTable", 4,
|
// Renders spell rows into the current table
|
||||||
ImGuiTableFlags_Borders | ImGuiTableFlags_RowBg | ImGuiTableFlags_ScrollY)) {
|
auto renderSpellRows = [&](const std::vector<const game::TrainerSpell*>& spells) {
|
||||||
ImGui::TableSetupColumn("Spell", ImGuiTableColumnFlags_WidthStretch);
|
for (const auto* spell : spells) {
|
||||||
ImGui::TableSetupColumn("Level", ImGuiTableColumnFlags_WidthFixed, 40.0f);
|
|
||||||
ImGui::TableSetupColumn("Cost", ImGuiTableColumnFlags_WidthFixed, 120.0f);
|
|
||||||
ImGui::TableSetupColumn("##action", ImGuiTableColumnFlags_WidthFixed, 55.0f);
|
|
||||||
ImGui::TableHeadersRow();
|
|
||||||
|
|
||||||
for (const auto& spell : trainer.spells) {
|
|
||||||
ImGui::TableNextRow();
|
ImGui::TableNextRow();
|
||||||
ImGui::PushID(static_cast<int>(spell.spellId));
|
ImGui::PushID(static_cast<int>(spell->spellId));
|
||||||
|
|
||||||
// State color: 0=known(green), 1=available(white), 2=unavailable(gray)
|
|
||||||
ImVec4 color;
|
ImVec4 color;
|
||||||
const char* statusLabel;
|
const char* statusLabel;
|
||||||
if (spell.state == 0 || isKnown(spell.spellId)) {
|
if (spell->state == 0 || isKnown(spell->spellId)) {
|
||||||
color = ImVec4(0.3f, 0.9f, 0.3f, 1.0f);
|
color = ImVec4(0.3f, 0.9f, 0.3f, 1.0f);
|
||||||
statusLabel = "Known";
|
statusLabel = "Known";
|
||||||
} else if (spell.state == 1) {
|
} else if (spell->state == 1) {
|
||||||
color = ImVec4(1.0f, 1.0f, 1.0f, 1.0f);
|
color = ImVec4(1.0f, 1.0f, 1.0f, 1.0f);
|
||||||
statusLabel = "Available";
|
statusLabel = "Available";
|
||||||
} else {
|
} else {
|
||||||
|
|
@ -3597,19 +3590,17 @@ void GameScreen::renderTrainerWindow(game::GameHandler& gameHandler) {
|
||||||
|
|
||||||
// Spell name
|
// Spell name
|
||||||
ImGui::TableSetColumnIndex(0);
|
ImGui::TableSetColumnIndex(0);
|
||||||
const std::string& name = gameHandler.getSpellName(spell.spellId);
|
const std::string& name = gameHandler.getSpellName(spell->spellId);
|
||||||
const std::string& rank = gameHandler.getSpellRank(spell.spellId);
|
const std::string& rank = gameHandler.getSpellRank(spell->spellId);
|
||||||
if (!name.empty()) {
|
if (!name.empty()) {
|
||||||
if (!rank.empty()) {
|
if (!rank.empty())
|
||||||
ImGui::TextColored(color, "%s (%s)", name.c_str(), rank.c_str());
|
ImGui::TextColored(color, "%s (%s)", name.c_str(), rank.c_str());
|
||||||
} else {
|
else
|
||||||
ImGui::TextColored(color, "%s", name.c_str());
|
ImGui::TextColored(color, "%s", name.c_str());
|
||||||
}
|
|
||||||
} else {
|
} else {
|
||||||
ImGui::TextColored(color, "Spell #%u", spell.spellId);
|
ImGui::TextColored(color, "Spell #%u", spell->spellId);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Tooltip
|
|
||||||
if (ImGui::IsItemHovered()) {
|
if (ImGui::IsItemHovered()) {
|
||||||
ImGui::BeginTooltip();
|
ImGui::BeginTooltip();
|
||||||
if (!name.empty()) {
|
if (!name.empty()) {
|
||||||
|
|
@ -3617,27 +3608,27 @@ void GameScreen::renderTrainerWindow(game::GameHandler& gameHandler) {
|
||||||
if (!rank.empty()) ImGui::TextColored(ImVec4(0.7f, 0.7f, 0.7f, 1.0f), "%s", rank.c_str());
|
if (!rank.empty()) ImGui::TextColored(ImVec4(0.7f, 0.7f, 0.7f, 1.0f), "%s", rank.c_str());
|
||||||
}
|
}
|
||||||
ImGui::Text("Status: %s", statusLabel);
|
ImGui::Text("Status: %s", statusLabel);
|
||||||
if (spell.reqLevel > 0) ImGui::Text("Required Level: %u", spell.reqLevel);
|
if (spell->reqLevel > 0) ImGui::Text("Required Level: %u", spell->reqLevel);
|
||||||
if (spell.reqSkill > 0) ImGui::Text("Required Skill: %u (value %u)", spell.reqSkill, spell.reqSkillValue);
|
if (spell->reqSkill > 0) ImGui::Text("Required Skill: %u (value %u)", spell->reqSkill, spell->reqSkillValue);
|
||||||
if (spell.chainNode1 > 0) {
|
if (spell->chainNode1 > 0) {
|
||||||
const std::string& prereq = gameHandler.getSpellName(spell.chainNode1);
|
const std::string& prereq = gameHandler.getSpellName(spell->chainNode1);
|
||||||
if (!prereq.empty()) ImGui::Text("Requires: %s", prereq.c_str());
|
if (!prereq.empty()) ImGui::Text("Requires: %s", prereq.c_str());
|
||||||
else ImGui::Text("Requires: Spell #%u", spell.chainNode1);
|
else ImGui::Text("Requires: Spell #%u", spell->chainNode1);
|
||||||
}
|
}
|
||||||
ImGui::EndTooltip();
|
ImGui::EndTooltip();
|
||||||
}
|
}
|
||||||
|
|
||||||
// Level
|
// Level
|
||||||
ImGui::TableSetColumnIndex(1);
|
ImGui::TableSetColumnIndex(1);
|
||||||
ImGui::TextColored(color, "%u", spell.reqLevel);
|
ImGui::TextColored(color, "%u", spell->reqLevel);
|
||||||
|
|
||||||
// Cost
|
// Cost
|
||||||
ImGui::TableSetColumnIndex(2);
|
ImGui::TableSetColumnIndex(2);
|
||||||
if (spell.spellCost > 0) {
|
if (spell->spellCost > 0) {
|
||||||
uint32_t g = spell.spellCost / 10000;
|
uint32_t g = spell->spellCost / 10000;
|
||||||
uint32_t s = (spell.spellCost / 100) % 100;
|
uint32_t s = (spell->spellCost / 100) % 100;
|
||||||
uint32_t c = spell.spellCost % 100;
|
uint32_t c = spell->spellCost % 100;
|
||||||
bool canAfford = money >= spell.spellCost;
|
bool canAfford = money >= spell->spellCost;
|
||||||
ImVec4 costColor = canAfford ? color : ImVec4(1.0f, 0.3f, 0.3f, 1.0f);
|
ImVec4 costColor = canAfford ? color : ImVec4(1.0f, 0.3f, 0.3f, 1.0f);
|
||||||
ImGui::TextColored(costColor, "%ug %us %uc", g, s, c);
|
ImGui::TextColored(costColor, "%ug %us %uc", g, s, c);
|
||||||
} else {
|
} else {
|
||||||
|
|
@ -3646,21 +3637,55 @@ void GameScreen::renderTrainerWindow(game::GameHandler& gameHandler) {
|
||||||
|
|
||||||
// Train button
|
// Train button
|
||||||
ImGui::TableSetColumnIndex(3);
|
ImGui::TableSetColumnIndex(3);
|
||||||
bool canTrain = (spell.state == 1) && (money >= spell.spellCost);
|
bool canTrain = (spell->state == 1) && (money >= spell->spellCost);
|
||||||
if (!canTrain) {
|
if (!canTrain) ImGui::BeginDisabled();
|
||||||
ImGui::BeginDisabled();
|
|
||||||
}
|
|
||||||
if (ImGui::SmallButton("Train")) {
|
if (ImGui::SmallButton("Train")) {
|
||||||
gameHandler.trainSpell(spell.spellId);
|
gameHandler.trainSpell(spell->spellId);
|
||||||
}
|
|
||||||
if (!canTrain) {
|
|
||||||
ImGui::EndDisabled();
|
|
||||||
}
|
}
|
||||||
|
if (!canTrain) ImGui::EndDisabled();
|
||||||
|
|
||||||
ImGui::PopID();
|
ImGui::PopID();
|
||||||
}
|
}
|
||||||
|
};
|
||||||
|
|
||||||
ImGui::EndTable();
|
auto renderSpellTable = [&](const char* tableId, const std::vector<const game::TrainerSpell*>& spells) {
|
||||||
|
if (ImGui::BeginTable(tableId, 4,
|
||||||
|
ImGuiTableFlags_Borders | ImGuiTableFlags_RowBg | ImGuiTableFlags_ScrollY)) {
|
||||||
|
ImGui::TableSetupColumn("Spell", ImGuiTableColumnFlags_WidthStretch);
|
||||||
|
ImGui::TableSetupColumn("Level", ImGuiTableColumnFlags_WidthFixed, 40.0f);
|
||||||
|
ImGui::TableSetupColumn("Cost", ImGuiTableColumnFlags_WidthFixed, 120.0f);
|
||||||
|
ImGui::TableSetupColumn("##action", ImGuiTableColumnFlags_WidthFixed, 55.0f);
|
||||||
|
ImGui::TableHeadersRow();
|
||||||
|
renderSpellRows(spells);
|
||||||
|
ImGui::EndTable();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const auto& tabs = gameHandler.getTrainerTabs();
|
||||||
|
if (tabs.size() > 1) {
|
||||||
|
// Multiple tabs - show tab bar
|
||||||
|
if (ImGui::BeginTabBar("TrainerTabs")) {
|
||||||
|
for (size_t i = 0; i < tabs.size(); i++) {
|
||||||
|
char tabLabel[64];
|
||||||
|
snprintf(tabLabel, sizeof(tabLabel), "%s (%zu)",
|
||||||
|
tabs[i].name.c_str(), tabs[i].spells.size());
|
||||||
|
|
||||||
|
if (ImGui::BeginTabItem(tabLabel)) {
|
||||||
|
char tableId[32];
|
||||||
|
snprintf(tableId, sizeof(tableId), "TT%zu", i);
|
||||||
|
renderSpellTable(tableId, tabs[i].spells);
|
||||||
|
ImGui::EndTabItem();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
ImGui::EndTabBar();
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// Single tab or no categorization - flat list
|
||||||
|
std::vector<const game::TrainerSpell*> allSpells;
|
||||||
|
for (const auto& spell : trainer.spells) {
|
||||||
|
allSpells.push_back(&spell);
|
||||||
|
}
|
||||||
|
renderSpellTable("TrainerTable", allSpells);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue