feat: display spell school in spellbook tooltip

Load SchoolMask (TBC/WotLK bitmask) or SchoolEnum (Classic/Turtle 0-6
enum, converted to mask via 1<<N) from Spell.dbc into SpellInfo.

renderSpellTooltip now shows the spell school name (Holy/Fire/Nature/
Frost/Shadow/Arcane) in the appropriate school color between the spell
rank and resource cost. Physical school is suppressed as it is the
implied default. Multi-school spells display both names separated by /.

WotLK DBC fallback path uses field 225 for SchoolMask.
This commit is contained in:
Kelsi 2026-03-10 19:22:48 -07:00
parent 458a95ae8e
commit bae3477c94
2 changed files with 48 additions and 1 deletions

View file

@ -25,6 +25,7 @@ struct SpellInfo {
uint32_t manaCost = 0; // Mana cost uint32_t manaCost = 0; // Mana cost
uint32_t powerType = 0; // 0=mana, 1=rage, 2=focus, 3=energy uint32_t powerType = 0; // 0=mana, 1=rage, 2=focus, 3=energy
uint32_t rangeIndex = 0; // Range index from SpellRange.dbc uint32_t rangeIndex = 0; // Range index from SpellRange.dbc
uint32_t schoolMask = 0; // School bitmask (1=phys,2=holy,4=fire,8=nature,16=frost,32=shadow,64=arcane)
bool isPassive() const { return (attributes & 0x40) != 0; } bool isPassive() const { return (attributes & 0x40) != 0; }
}; };

View file

@ -92,6 +92,11 @@ void SpellbookScreen::loadSpellDBC(pipeline::AssetManager* assetManager) {
} }
} }
// schoolField / isSchoolEnum are declared before the lambda so the WotLK fallback path
// can override them before the second tryLoad call.
uint32_t schoolField_ = UINT32_MAX;
bool isSchoolEnum_ = false;
auto tryLoad = [&](uint32_t idField, uint32_t attrField, uint32_t iconField, auto tryLoad = [&](uint32_t idField, uint32_t attrField, uint32_t iconField,
uint32_t nameField, uint32_t rankField, uint32_t tooltipField, uint32_t nameField, uint32_t rankField, uint32_t tooltipField,
uint32_t powerTypeField, uint32_t manaCostField, uint32_t powerTypeField, uint32_t manaCostField,
@ -128,6 +133,12 @@ void SpellbookScreen::loadSpellDBC(pipeline::AssetManager* assetManager) {
if (rangeIt != rangeMap.end()) info.rangeIndex = static_cast<uint32_t>(rangeIt->second); if (rangeIt != rangeMap.end()) info.rangeIndex = static_cast<uint32_t>(rangeIt->second);
} }
} }
if (schoolField_ < fc) {
uint32_t raw = dbc->getUInt32(i, schoolField_);
// Classic/Turtle use a 0-6 school enum; TBC/WotLK use a bitmask.
// enum→mask: schoolEnum N maps to bit (1u << N), e.g. 0→1 (physical), 4→16 (frost).
info.schoolMask = isSchoolEnum_ ? (raw <= 6 ? (1u << raw) : 0u) : raw;
}
if (!info.name.empty()) { if (!info.name.empty()) {
spellData[spellId] = std::move(info); spellData[spellId] = std::move(info);
@ -149,6 +160,13 @@ void SpellbookScreen::loadSpellDBC(pipeline::AssetManager* assetManager) {
try { manaCostField = (*spellL)["ManaCost"]; } catch (...) {} try { manaCostField = (*spellL)["ManaCost"]; } catch (...) {}
try { castTimeIdxField = (*spellL)["CastingTimeIndex"]; } catch (...) {} try { castTimeIdxField = (*spellL)["CastingTimeIndex"]; } catch (...) {}
try { rangeIdxField = (*spellL)["RangeIndex"]; } catch (...) {} try { rangeIdxField = (*spellL)["RangeIndex"]; } catch (...) {}
// Try SchoolMask (TBC/WotLK bitmask) then SchoolEnum (Classic/Turtle 0-6 value)
schoolField_ = UINT32_MAX;
isSchoolEnum_ = false;
try { schoolField_ = (*spellL)["SchoolMask"]; } catch (...) {}
if (schoolField_ == UINT32_MAX) {
try { schoolField_ = (*spellL)["SchoolEnum"]; isSchoolEnum_ = true; } catch (...) {}
}
tryLoad((*spellL)["ID"], (*spellL)["Attributes"], (*spellL)["IconID"], tryLoad((*spellL)["ID"], (*spellL)["Attributes"], (*spellL)["IconID"],
(*spellL)["Name"], (*spellL)["Rank"], tooltipField, (*spellL)["Name"], (*spellL)["Rank"], tooltipField,
powerTypeField, manaCostField, castTimeIdxField, rangeIdxField, powerTypeField, manaCostField, castTimeIdxField, rangeIdxField,
@ -157,7 +175,9 @@ void SpellbookScreen::loadSpellDBC(pipeline::AssetManager* assetManager) {
if (spellData.empty() && fieldCount >= 200) { if (spellData.empty() && fieldCount >= 200) {
LOG_INFO("Spellbook: Retrying with WotLK field indices (DBC has ", fieldCount, " fields)"); LOG_INFO("Spellbook: Retrying with WotLK field indices (DBC has ", fieldCount, " fields)");
// WotLK Spell.dbc field indices (verified against 3.3.5a schema) // WotLK Spell.dbc field indices (verified against 3.3.5a schema); SchoolMask at field 225
schoolField_ = 225;
isSchoolEnum_ = false;
tryLoad(0, 4, 133, 136, 153, 139, 14, 39, 47, 49, "WotLK fallback"); tryLoad(0, 4, 133, 136, 153, 139, 14, 39, 47, 49, "WotLK fallback");
} }
@ -435,6 +455,32 @@ void SpellbookScreen::renderSpellTooltip(const SpellInfo* info, game::GameHandle
ImGui::TextColored(ImVec4(1.0f, 1.0f, 0.0f, 1.0f), "Passive"); ImGui::TextColored(ImVec4(1.0f, 1.0f, 0.0f, 1.0f), "Passive");
} }
// Spell school — only show for non-physical schools (physical is the default/implicit)
if (info->schoolMask != 0 && info->schoolMask != 1 /*physical*/) {
struct SchoolEntry { uint32_t mask; const char* name; ImVec4 color; };
static constexpr SchoolEntry kSchools[] = {
{ 2, "Holy", { 1.0f, 1.0f, 0.6f, 1.0f } },
{ 4, "Fire", { 1.0f, 0.5f, 0.1f, 1.0f } },
{ 8, "Nature", { 0.4f, 0.9f, 0.3f, 1.0f } },
{ 16, "Frost", { 0.5f, 0.8f, 1.0f, 1.0f } },
{ 32, "Shadow", { 0.7f, 0.4f, 1.0f, 1.0f } },
{ 64, "Arcane", { 0.9f, 0.5f, 1.0f, 1.0f } },
};
bool first = true;
for (const auto& s : kSchools) {
if (info->schoolMask & s.mask) {
if (!first) ImGui::SameLine(0, 0);
if (first) {
ImGui::TextColored(s.color, "%s", s.name);
first = false;
} else {
ImGui::SameLine(0, 2);
ImGui::TextColored(s.color, "/%s", s.name);
}
}
}
}
// Resource cost + cast time on same row (WoW style) // Resource cost + cast time on same row (WoW style)
if (!info->isPassive()) { if (!info->isPassive()) {
// Left: resource cost // Left: resource cost