Files
HailMaryGuildTools/Modules/Tracker/SpellDatabase.lua
Torsten Brendgen fc5a8aa361 initial commit
2026-04-10 21:30:31 +02:00

1105 lines
36 KiB
Lua

-- 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()