-- Core/SpellData.lua -- Shared spell metadata and helpers. HMGT_SpellData = HMGT_SpellData or {} local ADDON_NAME = "HailMaryGuildTools" local AceLocale = LibStub and LibStub("AceLocale-3.0", true) local L = AceLocale and AceLocale:GetLocale(ADDON_NAME, true) or nil local function GetCategoryDisplayName(category) if not L and AceLocale then L = AceLocale:GetLocale(ADDON_NAME, true) end return (L and L["CAT_" .. tostring(category)]) or tostring(category) end HMGT_SpellData.Interrupts = HMGT_SpellData.Interrupts or {} HMGT_SpellData.RaidCooldowns = HMGT_SpellData.RaidCooldowns or {} HMGT_SpellData.GroupCooldowns = HMGT_SpellData.GroupCooldowns or {} HMGT_SpellData.Relations = HMGT_SpellData.Relations or {} -- Legacy fallback. New data should use HMGT_SpellData.Relations instead. HMGT_SpellData.CooldownReducers = HMGT_SpellData.CooldownReducers or {} local NormalizeSpellEntry local NormalizeRelation local function CopyArray(source) if type(source) ~= "table" then return nil end local result = {} for i = 1, #source do result[i] = source[i] end return result end local function NormalizeIdList(source) if type(source) == "table" then return CopyArray(source) end local single = tonumber(source) if single and single > 0 then return { single } end return nil end local function NormalizeModOperation(op) local value = tostring(op or "set") if value == "reduce" then return "reduceByPercent" end return value end local function NormalizeModTarget(target) if target == nil or target == "" then return "cooldown" end local value = tostring(target) if value == "chargecd" then return "chargeCooldown" end if value == "interruptreduce" then return "eventReduce" end return value end local function NormalizePowerType(powerType) local value = tostring(powerType or ""):upper() if value == "" then return nil end return value end local function NormalizeTalentMod(mod) if type(mod) ~= "table" then return nil end local talentSpellId = tonumber(mod.talentSpellId or mod.talentId or mod[1]) or 0 local value = tonumber(mod.value or mod.amount or mod[2]) or 0 local op = NormalizeModOperation(mod.op or mod.modType or mod[3] or "set") local target = NormalizeModTarget(mod.target or mod[4]) local targetSpellId = tonumber(mod.targetSpellId or mod[5]) local onSuccess = mod.onSuccess if onSuccess == nil then onSuccess = mod[6] end return { talentSpellId = talentSpellId, value = value, op = op, target = target, targetSpellId = targetSpellId, onSuccess = onSuccess, } end local function NormalizeScalarModList(mods) if type(mods) ~= "table" then return nil end local result = {} for _, mod in ipairs(mods) do local normalized = NormalizeTalentMod(mod) if normalized then result[#result + 1] = normalized end end if #result == 0 then return nil end return result end local function NormalizeAvailabilitySource(source, legacyAvailability) local data = type(source) == "table" and source or {} local legacy = type(legacyAvailability) == "table" and legacyAvailability or {} return { type = tostring(data.type or legacy.type or ""), auraSpellId = tonumber(data.auraSpellId or legacy.auraSpellId), fallbackSpellCountId = tonumber( data.fallbackSpellCountId or data.progressSpellId or legacy.fallbackSpellCountId or legacy.progressSpellId ), progressSpellId = tonumber(data.progressSpellId or legacy.progressSpellId), } end local function NormalizeSpellState(state, entry) local legacyAvailability = type(entry and entry.availability) == "table" and entry.availability or nil local data = type(state) == "table" and state or nil local kind if data and type(data.kind) == "string" and data.kind ~= "" then kind = tostring(data.kind) elseif legacyAvailability then kind = "availability" else kind = "cooldown" end if kind == "stacks" then kind = "availability" end if kind == "availability" then local required = tonumber( (data and (data.required or data.max or data.threshold)) or (legacyAvailability and legacyAvailability.required) ) or 0 local source = NormalizeAvailabilitySource(data and data.source, legacyAvailability) return { kind = "availability", required = math.max(1, math.floor(required + 0.5)), source = source, } end local cooldown = tonumber((data and (data.cooldown or data.duration)) or (entry and entry.cooldown)) or 0 local charges = tonumber((data and (data.charges or data.maxCharges)) or (entry and entry.maxCharges)) local chargeCooldown = tonumber( (data and (data.chargeCooldown or data.chargecd or data.chargeDuration or data.cooldown or data.duration)) or (entry and (entry.chargeCooldown or entry.cooldown)) ) or 0 local normalized = { kind = "cooldown", cooldown = math.max(0, cooldown), } if charges and charges > 1 then normalized.charges = math.max(1, math.floor(charges + 0.5)) end if chargeCooldown > 0 then normalized.chargeCooldown = math.max(0, chargeCooldown) end return normalized end local function BuildLegacyAvailabilityConfig(state) local source = state and state.source or {} return { type = source.type, auraSpellId = source.auraSpellId, fallbackSpellCountId = source.fallbackSpellCountId, progressSpellId = source.progressSpellId, required = tonumber(state and state.required) or 0, } end local function NormalizeSpellMods(entry) local result = {} if type(entry.mods) == "table" then for _, mod in ipairs(entry.mods) do local normalized = NormalizeTalentMod(mod) if normalized and normalized.target ~= "eventReduce" then result[#result + 1] = normalized end end end if type(entry.talentMods) == "table" then for _, mod in ipairs(entry.talentMods) do local normalized = NormalizeTalentMod(mod) if normalized and normalized.target ~= "eventReduce" then result[#result + 1] = normalized end end end return result end local function NormalizeRelationEffect(effect, defaultTargetSpellId) if type(effect) ~= "table" then return nil end return { type = tostring(effect.type or effect.action or "reduceCooldown"), targetSpellId = tonumber(effect.targetSpellId or effect.spellId or defaultTargetSpellId), amount = tonumber(effect.amount or effect.value) or 0, talentSpellId = tonumber(effect.talentSpellId or effect.talentId or effect.talentRequired), requireInterruptSuccess = effect.requireInterruptSuccess, observe = effect.observe, proc = effect.proc, amountMods = NormalizeScalarModList(effect.amountMods or effect.mods or effect.talentMods), } end local function BuildRelationKey(relation) if type(relation) ~= "table" then return "" end local parts = { tostring(relation.when or "cast"), tostring(tonumber(relation.triggerSpellId) or 0), tostring(tonumber(relation.talentRequired) or 0), tostring(relation.powerType or ""), tostring(tonumber(relation.amountPerTrigger) or 0), } for _, effect in ipairs(relation.effects or {}) do parts[#parts + 1] = string.format( "%s:%d:%.3f:%d", tostring(effect.type or ""), tonumber(effect.targetSpellId) or 0, tonumber(effect.amount) or 0, tonumber(effect.talentSpellId) or 0 ) end return table.concat(parts, "|") end NormalizeRelation = function(relation, defaults) if type(relation) ~= "table" then return nil end local defaultTriggerSpellId = defaults and defaults.triggerSpellId or nil local defaultClasses = defaults and defaults.classes or nil local defaultSpecs = defaults and defaults.specs or nil local rawEffects = relation.effects if type(rawEffects) ~= "table" or rawEffects[1] == nil then rawEffects = { { type = relation.type or relation.action or "reduceCooldown", targetSpellId = relation.targetSpellId, amount = relation.amount, talentSpellId = relation.talentSpellId, requireInterruptSuccess = relation.requireInterruptSuccess, observe = relation.observe, proc = relation.proc, amountMods = relation.amountMods or relation.mods or relation.talentMods, }, } end local effects = {} for _, effect in ipairs(rawEffects) do local normalized = NormalizeRelationEffect(effect, relation.targetSpellId or defaultTriggerSpellId) if normalized and normalized.targetSpellId and normalized.amount > 0 then effects[#effects + 1] = normalized end end if #effects == 0 then return nil end local when = tostring(relation.when or relation.event or "cast") local normalized = { triggerSpellId = tonumber(relation.triggerSpellId or relation.sourceSpellId or defaultTriggerSpellId), classes = CopyArray(relation.classes or defaultClasses), specs = CopyArray(relation.specs or defaultSpecs), when = when, talentRequired = tonumber(relation.talentRequired or relation.talentSpellId), talentExcluded = NormalizeIdList(relation.talentExcluded or relation.excludedTalents), powerType = NormalizePowerType(relation.powerType or relation.resourceType), amountPerTrigger = tonumber(relation.amountPerTrigger or relation.powerAmount or relation.amountRequired), observe = relation.observe, proc = relation.proc, effects = effects, key = nil, } normalized.key = BuildRelationKey(normalized) return normalized end local function NormalizeSpellRelations(entry) local result = {} local defaults = { triggerSpellId = entry.spellId, classes = entry.classes, specs = entry.specs, } if type(entry.relations) == "table" then for _, relation in ipairs(entry.relations) do local normalized = NormalizeRelation(relation, defaults) if normalized then result[#result + 1] = normalized end end end if type(entry.talentMods) == "table" then for _, mod in ipairs(entry.talentMods) do local normalized = NormalizeTalentMod(mod) if normalized and normalized.target == "eventReduce" then local when = (normalized.onSuccess == false) and "cast" or "interruptSuccess" result[#result + 1] = { triggerSpellId = entry.spellId, classes = CopyArray(entry.classes), specs = CopyArray(entry.specs), when = when, effects = { { type = "reduceCooldown", targetSpellId = normalized.targetSpellId or entry.spellId, amount = normalized.value, talentSpellId = normalized.talentSpellId, }, }, } end end end return result end NormalizeSpellEntry = function(entry) if type(entry) ~= "table" then return entry end entry.classes = CopyArray(entry.classes) or {} entry.specs = CopyArray(entry.specs) entry.category = entry.category or "utility" entry.enabled = (entry.enabled ~= false) entry.state = NormalizeSpellState(entry.state, entry) entry.mods = NormalizeSpellMods(entry) entry.relations = NormalizeSpellRelations(entry) entry.talentMods = nil if entry.state.kind == "cooldown" then entry.cooldown = tonumber(entry.state.cooldown) or 0 entry.maxCharges = tonumber(entry.state.charges) entry.chargeCooldown = tonumber(entry.state.chargeCooldown) or tonumber(entry.cooldown) or 0 entry.availability = nil else entry.cooldown = 0 entry.maxCharges = nil entry.chargeCooldown = nil entry.availability = BuildLegacyAvailabilityConfig(entry.state) end return entry end local function Spell(spellId, name, cooldownOrData, classes, specs, talentMods, category, extraData) local entry = { spellId = tonumber(spellId) or 0, name = name, } if type(cooldownOrData) == "table" and classes == nil and specs == nil and talentMods == nil and category == nil and extraData == nil then for key, value in pairs(cooldownOrData) do entry[key] = value end else entry.state = { kind = "cooldown", cooldown = tonumber(cooldownOrData) or 0, } entry.classes = classes entry.specs = specs entry.talentMods = talentMods entry.category = category if type(extraData) == "table" then for key, value in pairs(extraData) do entry[key] = value end end end return NormalizeSpellEntry(entry) end local function Relation(data) return NormalizeRelation(data) end HMGT_SpellData.Spell = Spell HMGT_SpellData.Relation = Relation HMGT_SpellData.NormalizeSpellEntry = NormalizeSpellEntry HMGT_SpellData.NormalizeRelation = NormalizeRelation HMGT_SpellData.ClassOrder = { "WARRIOR", "PALADIN", "HUNTER", "ROGUE", "PRIEST", "DEATHKNIGHT", "SHAMAN", "MAGE", "WARLOCK", "MONK", "DRUID", "DEMONHUNTER", "EVOKER", } HMGT_SpellData.ClassColor = { WARRIOR = "ffc79c6e", PALADIN = "fff48cba", HUNTER = "ffabd473", ROGUE = "fffff569", PRIEST = "ffffffff", DEATHKNIGHT = "ffc41e3a", SHAMAN = "ff0070de", MAGE = "ff69ccf0", WARLOCK = "ff9482c9", MONK = "ff00ff96", DRUID = "ffff7d0a", DEMONHUNTER = "ffa330c9", EVOKER = "ff33937f", } HMGT_SpellData.CategoryOrder = { "interrupt", "raid", "lust", "offensive", "defensive", "tank", "healing", "utility", "cc", } local function NormalizeTrackerCategoryTag(tag) local value = tostring(tag or ""):lower() if value == "" then return nil end return value end local function AppendTrackerTag(target, seen, tag) local normalized = NormalizeTrackerCategoryTag(tag) if not normalized or seen[normalized] then return end seen[normalized] = true target[#target + 1] = normalized end local function NormalizeTrackerTags(entry, sourceTag) local tags = {} local seen = {} if type(entry and entry.trackerTags) == "table" then for _, tag in ipairs(entry.trackerTags) do AppendTrackerTag(tags, seen, tag) end end AppendTrackerTag(tags, seen, entry and entry.category) if sourceTag == "Interrupts" then AppendTrackerTag(tags, seen, "interrupt") elseif sourceTag == "RaidCooldowns" then AppendTrackerTag(tags, seen, "raid") end entry.trackerTags = tags end local function BuildTrackerCategorySet(categories) if type(categories) ~= "table" then return nil end local set = {} for _, category in ipairs(categories) do local normalized = NormalizeTrackerCategoryTag(category) if normalized then set[normalized] = true end end if next(set) == nil then return nil end return set end local function BuildTrackerCategorySignature(categories) local set = BuildTrackerCategorySet(categories) if not set then return "all" end local values = {} for category in pairs(set) do values[#values + 1] = category end table.sort(values) return table.concat(values, "|") end local _classIdByToken = {} local function GetClassIDByToken(classToken) if not classToken or classToken == "" then return nil end local cached = _classIdByToken[classToken] if cached ~= nil then return cached or nil end if type(GetClassInfo) ~= "function" then _classIdByToken[classToken] = false return nil end for classID = 1, 20 do local _, token = GetClassInfo(classID) if token == classToken then _classIdByToken[classToken] = classID return classID end end _classIdByToken[classToken] = false return nil end function HMGT_SpellData.NormalizeSpecIndex(classToken, specIndex) local s = tonumber(specIndex) if not s or s <= 0 then return 0 end if s <= 4 then return s end if type(GetSpecializationInfoForClassID) ~= "function" then return s end local classID = GetClassIDByToken(classToken) if not classID then return s end local numSpecs = 4 if type(GetNumSpecializationsForClassID) == "function" then numSpecs = tonumber(GetNumSpecializationsForClassID(classID)) or 4 end for idx = 1, math.max(1, numSpecs) do local specID = GetSpecializationInfoForClassID(classID, idx) if tonumber(specID) == s then return idx end end return s end local function IsTalentModActive(knownTalents, talentSpellId) return talentSpellId == nil or tonumber(talentSpellId) == 0 or (knownTalents and knownTalents[tonumber(talentSpellId)] == true) end local function ApplySingleMod(baseValue, modValue, modType) local current = tonumber(baseValue) or 0 local value = tonumber(modValue) or 0 local mt = tostring(modType or "") if mt == "set" then return value elseif mt == "multiply" then return current * value elseif mt == "reduceByValue" then return current - value elseif mt == "reduceByPercent" then return current * (1 - value / 100) end return current end local function ApplyTargetMods(base, mods, knownTalents, target) local value = tonumber(base) or 0 if type(mods) ~= "table" then return math.max(0, value) end local normalizedTarget = NormalizeModTarget(target) for _, mod in ipairs(mods) do if mod and NormalizeModTarget(mod.target) == normalizedTarget and IsTalentModActive(knownTalents, mod.talentSpellId) then value = ApplySingleMod(value, mod.value, mod.op) end end return math.max(0, value) end local function ApplyScalarMods(base, mods, knownTalents) local value = tonumber(base) or 0 if type(mods) ~= "table" then return math.max(0, value) end for _, mod in ipairs(mods) do if mod and IsTalentModActive(knownTalents, mod.talentSpellId) then value = ApplySingleMod(value, mod.value, mod.op) end end return math.max(0, value) end function HMGT_SpellData.GetSpellStateDefinition(spellEntry) if not spellEntry then return nil end if type(spellEntry.state) ~= "table" then NormalizeSpellEntry(spellEntry) end return spellEntry.state end function HMGT_SpellData.GetStateKind(spellEntry) local state = HMGT_SpellData.GetSpellStateDefinition(spellEntry) return state and state.kind or nil end function HMGT_SpellData.GetBaseCooldown(spellEntry) local state = HMGT_SpellData.GetSpellStateDefinition(spellEntry) if not state or state.kind ~= "cooldown" then return 0 end return math.max(0, tonumber(state.cooldown) or 0) end function HMGT_SpellData.GetBaseChargeInfo(spellEntry) local state = HMGT_SpellData.GetSpellStateDefinition(spellEntry) if not state or state.kind ~= "cooldown" then return 1, 0 end local charges = tonumber(state.charges) or 1 local chargeCooldown = tonumber(state.chargeCooldown or state.cooldown) or 0 charges = math.max(1, math.floor(charges + 0.5)) chargeCooldown = math.max(0, chargeCooldown) return charges, chargeCooldown end function HMGT_SpellData.GetAvailabilityConfig(spellEntry) local state = HMGT_SpellData.GetSpellStateDefinition(spellEntry) if not state or state.kind ~= "availability" then return nil end return BuildLegacyAvailabilityConfig(state) end function HMGT_SpellData.GetEffectiveAvailabilityRequired(spellEntry, knownTalents) local state = HMGT_SpellData.GetSpellStateDefinition(spellEntry) if not state or state.kind ~= "availability" then return 0 end local required = ApplyTargetMods(state.required or 0, spellEntry.mods, knownTalents, "required") if required <= 0 then return 0 end return math.max(1, math.floor(required + 0.5)) end function HMGT_SpellData.GetEffectiveCooldown(spellEntry, knownTalents) local state = HMGT_SpellData.GetSpellStateDefinition(spellEntry) if not state or state.kind ~= "cooldown" then return 0 end return ApplyTargetMods(state.cooldown or 0, spellEntry.mods, knownTalents, "cooldown") end function HMGT_SpellData.GetEffectiveChargeInfo(spellEntry, knownTalents, baseMaxCharges, baseChargeDuration) local state = HMGT_SpellData.GetSpellStateDefinition(spellEntry) local maxCharges local chargeDur if state and state.kind == "cooldown" then maxCharges = tonumber(baseMaxCharges) or tonumber(state.charges) or 1 chargeDur = tonumber(baseChargeDuration) or tonumber(state.chargeCooldown or state.cooldown) or 0 maxCharges = ApplyTargetMods(maxCharges, spellEntry.mods, knownTalents, "charges") chargeDur = ApplyTargetMods(chargeDur, spellEntry.mods, knownTalents, "chargeCooldown") else maxCharges = tonumber(baseMaxCharges) or 1 chargeDur = tonumber(baseChargeDuration) or 0 end maxCharges = math.max(1, math.floor((tonumber(maxCharges) or 1) + 0.5)) chargeDur = math.max(0, tonumber(chargeDur) or 0) return maxCharges, chargeDur end local function MatchesClassAndSpec(classToken, normalizedSpec, classes, specs) local classOk = true if type(classes) == "table" and #classes > 0 then classOk = false for _, c in ipairs(classes) do if c == classToken then classOk = true break end end end local specOk = true if classOk and type(specs) == "table" and #specs > 0 then specOk = false for _, s in ipairs(specs) do if tonumber(s) == tonumber(normalizedSpec) then specOk = true break end end end return classOk and specOk end local function ResolveReducerObservation(relation, effect) if type(effect and effect.observe) == "table" then return effect.observe end if type(relation and relation.observe) == "table" then return relation.observe end local proc = effect and effect.proc or relation and relation.proc if type(proc) == "table" and tostring(proc.mode) == "observed" then return { type = "refreshTargetState", delay = tonumber(proc.delay) or 0.12, } end return nil end local function RelationMatchesEvent(relation, eventKey, context) if type(relation) ~= "table" then return false end local when = tostring(relation.when or "cast") if when ~= tostring(eventKey or "") then return false end if when == "cast" or when == "interruptSuccess" then return tonumber(relation.triggerSpellId) == tonumber(context and context.triggerSpellId) end if when == "powerSpent" then local relationPowerType = NormalizePowerType(relation.powerType) local contextPowerType = NormalizePowerType(context and context.powerType) if relationPowerType and relationPowerType ~= contextPowerType then return false end return (tonumber(relation.amountPerTrigger) or 0) > 0 end return false end local function AppendRelationReducers(result, relation, classToken, normalizedSpec, eventKey, context, knownTalents) if not RelationMatchesEvent(relation, eventKey, context) then return end if not MatchesClassAndSpec(classToken, normalizedSpec, relation.classes, relation.specs) then return end if relation.talentRequired and not IsTalentModActive(knownTalents, relation.talentRequired) then return end if type(relation.talentExcluded) == "table" then for _, talentSpellId in ipairs(relation.talentExcluded) do if IsTalentModActive(knownTalents, talentSpellId) then return end end end local relationRequiresSuccess = tostring(relation.when or "cast") == "interruptSuccess" for _, effect in ipairs(relation.effects or {}) do if effect.type == "reduceCooldown" and tonumber(effect.targetSpellId) and IsTalentModActive(knownTalents, effect.talentSpellId) then local amount = ApplyScalarMods(effect.amount or 0, effect.amountMods, knownTalents) if amount > 0 then local requireInterruptSuccess = effect.requireInterruptSuccess if requireInterruptSuccess == nil then requireInterruptSuccess = relationRequiresSuccess end result[#result + 1] = { triggerSpellId = tonumber(relation.triggerSpellId or context and context.triggerSpellId) or 0, targetSpellId = tonumber(effect.targetSpellId), amount = amount, requireInterruptSuccess = requireInterruptSuccess and true or false, observe = ResolveReducerObservation(relation, effect), relationKey = relation.key, powerType = relation.powerType, amountPerTrigger = tonumber(relation.amountPerTrigger) or 0, } end end end end function HMGT_SpellData.GetCooldownReducersForEvent(classToken, specIndex, eventKey, context, knownTalents) local normalizedSpec = HMGT_SpellData.NormalizeSpecIndex(classToken, specIndex) local result = {} local eventName = tostring(eventKey or "") for _, relation in ipairs(HMGT_SpellData.Relations or {}) do AppendRelationReducers(result, relation, classToken, normalizedSpec, eventName, context, knownTalents) end local triggerSpellId = tonumber(context and context.triggerSpellId) if triggerSpellId and (eventName == "cast" or eventName == "interruptSuccess") then local triggerEntry = HMGT_SpellData.InterruptLookup[triggerSpellId] or HMGT_SpellData.CooldownLookup[triggerSpellId] if triggerEntry and type(triggerEntry.relations) == "table" then for _, relation in ipairs(triggerEntry.relations) do AppendRelationReducers(result, relation, classToken, normalizedSpec, eventName, context, knownTalents) end end end if eventName == "cast" and triggerSpellId then for _, reducer in ipairs(HMGT_SpellData.CooldownReducers or {}) do if tonumber(reducer.triggerSpellId) == triggerSpellId then local classOk = MatchesClassAndSpec(classToken, normalizedSpec, reducer.classes, reducer.specs) local talentOk = true if classOk and tonumber(reducer.talentRequired) then talentOk = IsTalentModActive(knownTalents, reducer.talentRequired) end if classOk and talentOk then local amount = ApplyScalarMods(reducer.amount or 0, reducer.talentMods, knownTalents) if amount > 0 and tonumber(reducer.targetSpellId) then result[#result + 1] = { triggerSpellId = triggerSpellId, targetSpellId = tonumber(reducer.targetSpellId), amount = amount, requireInterruptSuccess = reducer.requireInterruptSuccess and true or false, observe = reducer.observe, } end end end end end return result end function HMGT_SpellData.GetCooldownReducersForCast(classToken, specIndex, triggerSpellId, knownTalents) local sid = tonumber(triggerSpellId) if not sid then return {} end return HMGT_SpellData.GetCooldownReducersForEvent( classToken, specIndex, "cast", { triggerSpellId = sid }, knownTalents ) end function HMGT_SpellData.GetCooldownReducersForPowerSpend(classToken, specIndex, powerType, knownTalents) local token = NormalizePowerType(powerType) if not token then return {} end return HMGT_SpellData.GetCooldownReducersForEvent( classToken, specIndex, "powerSpent", { powerType = token }, knownTalents ) end function HMGT_SpellData.GetTrackedPowerTypes(classToken, specIndex, knownTalents) local normalizedSpec = HMGT_SpellData.NormalizeSpecIndex(classToken, specIndex) local tracked = {} for _, relation in ipairs(HMGT_SpellData.Relations or {}) do if tostring(relation.when or "") == "powerSpent" and MatchesClassAndSpec(classToken, normalizedSpec, relation.classes, relation.specs) and (not relation.talentRequired or IsTalentModActive(knownTalents, relation.talentRequired)) then local powerType = NormalizePowerType(relation.powerType) if powerType then tracked[powerType] = true end end end return tracked end function HMGT_SpellData.FindSpell(spellId, database) if type(database) ~= "table" then return nil end for _, entry in ipairs(database) do if entry.spellId == spellId then return entry end end return nil end function HMGT_SpellData.GetSpellsForSpec(classToken, specIndex, database) if type(database) ~= "table" then return {} end local normalizedSpec = HMGT_SpellData.NormalizeSpecIndex(classToken, specIndex) local allSpecs = (not normalizedSpec or normalizedSpec == 0) local cacheKey = string.format("%s:%s:%s", tostring(classToken or ""), tostring(normalizedSpec or 0), allSpecs and "all" or "spec") database._hmgtSpecCache = database._hmgtSpecCache or {} local cached = database._hmgtSpecCache[cacheKey] if cached then return cached end local result = {} for _, entry in ipairs(database) do local classMatch = false for _, c in ipairs(entry.classes) do if c == classToken then classMatch = true; break end end if classMatch then if allSpecs or not entry.specs then table.insert(result, entry) else for _, s in ipairs(entry.specs) do if tonumber(s) == tonumber(normalizedSpec) then table.insert(result, entry) break end end end end end database._hmgtSpecCache[cacheKey] = result return result end local _iconCache = {} local _fallbackIcon = "Interface\\Icons\\INV_Misc_QuestionMark" function HMGT_SpellData.GetSpellIcon(spellId) if _iconCache[spellId] ~= nil then return _iconCache[spellId] end local icon = C_Spell.GetSpellTexture(spellId) _iconCache[spellId] = icon or _fallbackIcon return _iconCache[spellId] end function HMGT_SpellData.EntryMatchesCategories(entry, categories) if type(entry) ~= "table" then return false end local set = BuildTrackerCategorySet(categories) if not set then return true end for _, tag in ipairs(entry.trackerTags or {}) do if set[tostring(tag)] then return true end end return false end function HMGT_SpellData.GetAllSpells() return HMGT_SpellData.AllSpells or {} end function HMGT_SpellData.GetTrackerCategoryValues() local values = {} for _, category in ipairs(HMGT_SpellData.CategoryOrder or {}) do values[category] = GetCategoryDisplayName(category) end return values end function HMGT_SpellData.GetSpellPoolForCategories(categories) HMGT_SpellData._trackerCategoryCache = HMGT_SpellData._trackerCategoryCache or {} local signature = BuildTrackerCategorySignature(categories) local cached = HMGT_SpellData._trackerCategoryCache[signature] if cached then return cached end local result = {} for _, entry in ipairs(HMGT_SpellData.GetAllSpells()) do if HMGT_SpellData.EntryMatchesCategories(entry, categories) then result[#result + 1] = entry end end HMGT_SpellData._trackerCategoryCache[signature] = result return result end function HMGT_SpellData.GetSpellsForCategories(classToken, specIndex, categories) return HMGT_SpellData.GetSpellsForSpec( classToken, specIndex, HMGT_SpellData.GetSpellPoolForCategories(categories) ) end function HMGT_SpellData.RebuildLookups() for i, entry in ipairs(HMGT_SpellData.Interrupts or {}) do HMGT_SpellData.Interrupts[i] = NormalizeSpellEntry(entry) NormalizeTrackerTags(HMGT_SpellData.Interrupts[i], "Interrupts") end for i, entry in ipairs(HMGT_SpellData.RaidCooldowns or {}) do HMGT_SpellData.RaidCooldowns[i] = NormalizeSpellEntry(entry) NormalizeTrackerTags(HMGT_SpellData.RaidCooldowns[i], "RaidCooldowns") end for i, entry in ipairs(HMGT_SpellData.GroupCooldowns or {}) do HMGT_SpellData.GroupCooldowns[i] = NormalizeSpellEntry(entry) NormalizeTrackerTags(HMGT_SpellData.GroupCooldowns[i], "GroupCooldowns") end local normalizedRelations = {} for _, relation in ipairs(HMGT_SpellData.Relations or {}) do local normalized = NormalizeRelation(relation) if normalized then normalizedRelations[#normalizedRelations + 1] = normalized end end HMGT_SpellData.Relations = normalizedRelations if type(HMGT_SpellData.Interrupts) == "table" then HMGT_SpellData.Interrupts._hmgtSpecCache = nil end if type(HMGT_SpellData.RaidCooldowns) == "table" then HMGT_SpellData.RaidCooldowns._hmgtSpecCache = nil end if type(HMGT_SpellData.GroupCooldowns) == "table" then HMGT_SpellData.GroupCooldowns._hmgtSpecCache = nil end HMGT_SpellData._trackerCategoryCache = nil HMGT_SpellData.InterruptLookup = {} for _, entry in ipairs(HMGT_SpellData.Interrupts or {}) do entry._hmgtDataset = "Interrupts" HMGT_SpellData.InterruptLookup[entry.spellId] = entry end HMGT_SpellData.CooldownLookup = {} for _, entry in ipairs(HMGT_SpellData.RaidCooldowns or {}) do entry._hmgtDataset = "RaidCooldowns" HMGT_SpellData.CooldownLookup[entry.spellId] = entry end for _, entry in ipairs(HMGT_SpellData.GroupCooldowns or {}) do entry._hmgtDataset = "GroupCooldowns" HMGT_SpellData.CooldownLookup[entry.spellId] = entry end HMGT_SpellData.AllSpells = {} for _, entry in ipairs(HMGT_SpellData.Interrupts or {}) do HMGT_SpellData.AllSpells[#HMGT_SpellData.AllSpells + 1] = entry end for _, entry in ipairs(HMGT_SpellData.RaidCooldowns or {}) do HMGT_SpellData.AllSpells[#HMGT_SpellData.AllSpells + 1] = entry end for _, entry in ipairs(HMGT_SpellData.GroupCooldowns or {}) do HMGT_SpellData.AllSpells[#HMGT_SpellData.AllSpells + 1] = entry end end HMGT_SpellData.RebuildLookups()