1105 lines
36 KiB
Lua
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()
|