577 lines
20 KiB
Lua
577 lines
20 KiB
Lua
local ADDON_NAME = "HailMaryGuildTools"
|
|
local HMGT = LibStub("AceAddon-3.0"):GetAddon(ADDON_NAME)
|
|
if not HMGT then return end
|
|
|
|
HMGT.TrackerCore = HMGT.TrackerCore or {}
|
|
|
|
HMGT.TRACKER_PRESET_DEFINITIONS = HMGT.TRACKER_PRESET_DEFINITIONS or {
|
|
interruptTracker = {
|
|
moduleName = "InterruptTracker",
|
|
dbKey = "interruptTracker",
|
|
trackerType = "normal",
|
|
trackerKey = "interruptTracker",
|
|
categories = { "interrupt" },
|
|
defaultName = function(L)
|
|
return (L and L["IT_NAME"]) or "Interrupts"
|
|
end,
|
|
},
|
|
raidCooldownTracker = {
|
|
moduleName = "RaidCooldownTracker",
|
|
dbKey = "raidCooldownTracker",
|
|
trackerType = "normal",
|
|
trackerKey = "raidCooldownTracker",
|
|
categories = { "lust", "defensive", "healing", "tank", "utility", "offensive", "cc", "interrupt" },
|
|
defaultName = function(L)
|
|
return (L and L["RCD_NAME"]) or "Raid Cooldowns"
|
|
end,
|
|
},
|
|
groupCooldownTracker = {
|
|
moduleName = "GroupCooldownTracker",
|
|
dbKey = "groupCooldownTracker",
|
|
trackerType = "group",
|
|
trackerKey = "groupCooldownTracker",
|
|
categories = { "tank", "defensive", "healing", "cc", "utility", "offensive", "lust", "interrupt" },
|
|
includeSelfFrame = false,
|
|
showChargesOnIcon = true,
|
|
defaultName = function(L)
|
|
return (L and L["GCD_NAME"]) or "Cooldowns"
|
|
end,
|
|
},
|
|
}
|
|
|
|
function HMGT:GetTrackerPresetDefinitions()
|
|
return self.TRACKER_PRESET_DEFINITIONS or {}
|
|
end
|
|
|
|
function HMGT:GetTrackerPresetDefinition(key)
|
|
local definitions = self:GetTrackerPresetDefinitions()
|
|
return definitions and definitions[tostring(key or "")]
|
|
end
|
|
|
|
function HMGT:GetTrackerPresetDefinitionByModule(moduleName)
|
|
local target = tostring(moduleName or "")
|
|
for _, definition in pairs(self:GetTrackerPresetDefinitions()) do
|
|
if tostring(definition.moduleName or "") == target then
|
|
return definition
|
|
end
|
|
end
|
|
return nil
|
|
end
|
|
|
|
function HMGT:GetTrackerTypeOptions()
|
|
local L = self.L
|
|
return {
|
|
normal = (L and L["OPT_TRACKER_TYPE_NORMAL"]) or "Normal tracker",
|
|
group = (L and L["OPT_TRACKER_TYPE_GROUP"]) or "Group-based tracker",
|
|
}
|
|
end
|
|
|
|
function HMGT:BuildTrackerConfigFromPreset(presetKey, trackerId, overrides)
|
|
local definition = self:GetTrackerPresetDefinition(presetKey)
|
|
local config = overrides or {}
|
|
if not definition then
|
|
return self:CreateTrackerConfig(trackerId, config)
|
|
end
|
|
|
|
local base = {
|
|
name = type(definition.defaultName) == "function" and definition.defaultName(self.L) or tostring(definition.defaultName or ""),
|
|
trackerType = definition.trackerType,
|
|
trackerKey = definition.trackerKey,
|
|
categories = definition.categories,
|
|
includeSelfFrame = definition.includeSelfFrame,
|
|
showChargesOnIcon = definition.showChargesOnIcon,
|
|
}
|
|
|
|
for key, value in pairs(config) do
|
|
base[key] = value
|
|
end
|
|
|
|
return self:CreateTrackerConfig(trackerId, base)
|
|
end
|
|
|
|
local function EntryNeedsVisualTicker(entry)
|
|
if type(entry) ~= "table" then
|
|
return false
|
|
end
|
|
|
|
local remaining = tonumber(entry.remaining) or 0
|
|
if remaining > 0 then
|
|
return true
|
|
end
|
|
|
|
local maxCharges = tonumber(entry.maxCharges) or 0
|
|
local currentCharges = tonumber(entry.currentCharges)
|
|
if maxCharges > 0 and currentCharges ~= nil and currentCharges < maxCharges then
|
|
return true
|
|
end
|
|
|
|
return false
|
|
end
|
|
|
|
function HMGT:IsGroupTrackerConfig(tracker)
|
|
return type(tracker) == "table" and tracker.trackerType == "group"
|
|
end
|
|
|
|
function HMGT:GetTrackerSpellPool(categories)
|
|
if HMGT_SpellData and type(HMGT_SpellData.GetSpellPoolForCategories) == "function" then
|
|
return HMGT_SpellData.GetSpellPoolForCategories(categories)
|
|
end
|
|
return {}
|
|
end
|
|
|
|
function HMGT:GetTrackerSpellsForPlayer(classToken, specIndex, categories)
|
|
if HMGT_SpellData and type(HMGT_SpellData.GetSpellsForCategories) == "function" then
|
|
return HMGT_SpellData.GetSpellsForCategories(classToken, specIndex, categories)
|
|
end
|
|
return {}
|
|
end
|
|
|
|
function HMGT:GetTrackerPlayers(tracker)
|
|
local players = {}
|
|
|
|
local ownName = self:NormalizePlayerName(UnitName("player"))
|
|
local ownClass = select(2, UnitClass("player"))
|
|
local includeOwnPlayer = true
|
|
if self:IsGroupTrackerConfig(tracker) then
|
|
includeOwnPlayer = tracker.includeSelfFrame == true
|
|
end
|
|
if includeOwnPlayer then
|
|
players[#players + 1] = {
|
|
name = ownName,
|
|
class = ownClass,
|
|
isOwn = true,
|
|
unitId = "player",
|
|
}
|
|
end
|
|
|
|
if IsInRaid() then
|
|
for i = 1, GetNumGroupMembers() do
|
|
local unitId = "raid" .. i
|
|
local name = self:NormalizePlayerName(UnitName(unitId))
|
|
local class = select(2, UnitClass(unitId))
|
|
if name and name ~= ownName then
|
|
players[#players + 1] = {
|
|
name = name,
|
|
class = class,
|
|
unitId = unitId,
|
|
}
|
|
end
|
|
end
|
|
elseif IsInGroup() then
|
|
for i = 1, GetNumGroupMembers() - 1 do
|
|
local unitId = "party" .. i
|
|
local name = self:NormalizePlayerName(UnitName(unitId))
|
|
local class = select(2, UnitClass(unitId))
|
|
if name and name ~= ownName then
|
|
players[#players + 1] = {
|
|
name = name,
|
|
class = class,
|
|
unitId = unitId,
|
|
}
|
|
end
|
|
end
|
|
end
|
|
|
|
return players
|
|
end
|
|
|
|
function HMGT:CollectTrackerEntriesForPlayer(tracker, playerInfo)
|
|
local entries = {}
|
|
if type(tracker) ~= "table" or type(playerInfo) ~= "table" then
|
|
return entries
|
|
end
|
|
|
|
local playerName = playerInfo.name
|
|
if not playerName then
|
|
return entries
|
|
end
|
|
|
|
local pData = self.playerData[playerName]
|
|
local classToken = pData and pData.class or playerInfo.class
|
|
if not classToken then
|
|
return entries
|
|
end
|
|
|
|
local specIndex
|
|
if playerInfo.isOwn then
|
|
specIndex = GetSpecialization()
|
|
if not specIndex or specIndex == 0 then
|
|
return entries
|
|
end
|
|
else
|
|
specIndex = pData and pData.specIndex or nil
|
|
if not specIndex or tonumber(specIndex) <= 0 then
|
|
return entries
|
|
end
|
|
end
|
|
|
|
local talents = pData and pData.talents or {}
|
|
local spells = self:GetTrackerSpellsForPlayer(classToken, specIndex, tracker.categories)
|
|
for _, spellEntry in ipairs(spells) do
|
|
if tracker.enabledSpells[spellEntry.spellId] ~= false then
|
|
local remaining, total, currentCharges, maxCharges = self:GetCooldownInfo(playerName, spellEntry.spellId)
|
|
local effectiveCd = HMGT_SpellData.GetEffectiveCooldown(spellEntry, talents)
|
|
local isAvailabilitySpell = self:IsAvailabilitySpell(spellEntry)
|
|
local include = self:ShouldDisplayEntry(tracker, remaining, currentCharges, maxCharges, spellEntry)
|
|
local spellKnown = self:IsTrackedSpellKnownForPlayer(playerName, spellEntry.spellId)
|
|
local hasPartialCharges = (tonumber(maxCharges) or 0) > 0
|
|
and (tonumber(currentCharges) or tonumber(maxCharges) or 0) < (tonumber(maxCharges) or 0)
|
|
local hasActiveCd = ((remaining or 0) > 0) or hasPartialCharges
|
|
|
|
if not spellKnown and not hasActiveCd then
|
|
include = false
|
|
end
|
|
if isAvailabilitySpell and not spellKnown then
|
|
include = false
|
|
end
|
|
if not playerInfo.isOwn and isAvailabilitySpell and not self:HasAvailabilityState(playerName, spellEntry.spellId) then
|
|
include = false
|
|
end
|
|
|
|
if include then
|
|
entries[#entries + 1] = {
|
|
playerName = playerName,
|
|
class = classToken,
|
|
spellEntry = spellEntry,
|
|
remaining = remaining,
|
|
total = total > 0 and total or effectiveCd,
|
|
currentCharges = currentCharges,
|
|
maxCharges = maxCharges,
|
|
}
|
|
end
|
|
end
|
|
end
|
|
|
|
return entries
|
|
end
|
|
|
|
local function CopyEntriesForPreview(entries, playerName)
|
|
local copies = {}
|
|
for _, entry in ipairs(entries or {}) do
|
|
local nextEntry = {}
|
|
for key, value in pairs(entry) do
|
|
nextEntry[key] = value
|
|
end
|
|
nextEntry.playerName = playerName
|
|
copies[#copies + 1] = nextEntry
|
|
end
|
|
return copies
|
|
end
|
|
|
|
function HMGT:BuildPartyPreviewEntries(entries, resolveUnitAnchorFrame)
|
|
local byPlayer = {}
|
|
local order = {}
|
|
local unitByPlayer = {}
|
|
for index = 1, 4 do
|
|
local unitId = "party" .. index
|
|
if not resolveUnitAnchorFrame or resolveUnitAnchorFrame(unitId) then
|
|
local playerName = string.format("Party %d", index)
|
|
local playerEntries = CopyEntriesForPreview(entries, playerName)
|
|
if #playerEntries > 0 then
|
|
byPlayer[playerName] = playerEntries
|
|
order[#order + 1] = playerName
|
|
unitByPlayer[playerName] = unitId
|
|
end
|
|
end
|
|
end
|
|
return byPlayer, order, unitByPlayer, #order > 0
|
|
end
|
|
|
|
function HMGT:CollectTrackerEntries(tracker)
|
|
local entries = {}
|
|
local players = self:GetTrackerPlayers(tracker)
|
|
for _, playerInfo in ipairs(players) do
|
|
local playerEntries = self:CollectTrackerEntriesForPlayer(tracker, playerInfo)
|
|
for _, entry in ipairs(playerEntries) do
|
|
entries[#entries + 1] = entry
|
|
end
|
|
end
|
|
return entries
|
|
end
|
|
|
|
function HMGT:GetDemoEntries(trackerKey, database, settings)
|
|
local pool = {}
|
|
local poolByClass = {}
|
|
for _, entry in ipairs(database or {}) do
|
|
if settings.enabledSpells[entry.spellId] ~= false then
|
|
pool[#pool + 1] = entry
|
|
for _, classToken in ipairs(entry.classes or {}) do
|
|
poolByClass[classToken] = poolByClass[classToken] or {}
|
|
poolByClass[classToken][#poolByClass[classToken] + 1] = entry
|
|
end
|
|
end
|
|
end
|
|
if #pool == 0 then
|
|
return {}
|
|
end
|
|
|
|
local classKeys = {}
|
|
for classToken in pairs(poolByClass) do
|
|
classKeys[#classKeys + 1] = classToken
|
|
end
|
|
if #classKeys == 0 then
|
|
classKeys[1] = "WARRIOR"
|
|
end
|
|
|
|
local count = settings.showBar and math.min(8, #pool) or math.min(12, #pool)
|
|
local names = { "Alice", "Bob", "Clara", "Duke", "Elli", "Fynn", "Gina", "Hektor", "Ivo", "Jana", "Kira", "Lio" }
|
|
|
|
local spellIds = {}
|
|
for _, entry in ipairs(pool) do
|
|
spellIds[#spellIds + 1] = tostring(entry.spellId)
|
|
end
|
|
table.sort(spellIds)
|
|
local signature = table.concat(spellIds, ",") .. "|" .. tostring(settings.showBar and 1 or 0) .. "|" .. tostring(count)
|
|
|
|
local now = GetTime()
|
|
local cache = self.demoModeData[trackerKey]
|
|
if (not cache) or cache.signature ~= signature or (not cache.entries) or #cache.entries ~= count then
|
|
local cachedEntries = {}
|
|
for index = 1, count do
|
|
local classToken = classKeys[math.random(1, #classKeys)]
|
|
local classPool = poolByClass[classToken]
|
|
local spellEntry = (classPool and classPool[math.random(1, #classPool)]) or pool[math.random(1, #pool)]
|
|
local duration = math.max(
|
|
1,
|
|
tonumber(HMGT_SpellData.GetBaseCooldown and HMGT_SpellData.GetBaseCooldown(spellEntry)) or tonumber(spellEntry.cooldown) or 60
|
|
)
|
|
local offset = math.random() * math.min(duration * 0.85, duration - 0.1)
|
|
cachedEntries[#cachedEntries + 1] = {
|
|
playerName = names[((index - 1) % #names) + 1],
|
|
class = classToken or ((spellEntry.classes and spellEntry.classes[1]) or "WARRIOR"),
|
|
spellEntry = spellEntry,
|
|
total = duration,
|
|
cycleStart = now - offset,
|
|
currentCharges = nil,
|
|
maxCharges = nil,
|
|
}
|
|
end
|
|
cache = {
|
|
signature = signature,
|
|
entries = cachedEntries,
|
|
}
|
|
self.demoModeData[trackerKey] = cache
|
|
end
|
|
|
|
local entries = {}
|
|
for _, entry in ipairs(cache.entries) do
|
|
local total = math.max(1, tonumber(entry.total) or 1)
|
|
local elapsed = math.max(0, now - (entry.cycleStart or now))
|
|
local phase = math.fmod(elapsed, total)
|
|
local remaining = total - phase
|
|
if elapsed > 0 and phase < 0.05 then
|
|
remaining = 0
|
|
end
|
|
entries[#entries + 1] = {
|
|
playerName = entry.playerName,
|
|
class = entry.class,
|
|
spellEntry = entry.spellEntry,
|
|
remaining = remaining,
|
|
total = total,
|
|
currentCharges = entry.currentCharges,
|
|
maxCharges = entry.maxCharges,
|
|
}
|
|
end
|
|
|
|
return entries
|
|
end
|
|
|
|
function HMGT:CollectTrackerTestEntries(tracker)
|
|
local playerName = self:NormalizePlayerName(UnitName("player")) or "Player"
|
|
local classToken = select(2, UnitClass("player"))
|
|
if not classToken then
|
|
return {}
|
|
end
|
|
|
|
local entries = {}
|
|
local pData = self.playerData[playerName]
|
|
local talents = pData and pData.talents or {}
|
|
local spells = self:GetTrackerSpellsForPlayer(classToken, GetSpecialization() or 0, tracker.categories)
|
|
for _, spellEntry in ipairs(spells) do
|
|
if tracker.enabledSpells[spellEntry.spellId] ~= false then
|
|
local remaining, total, currentCharges, maxCharges = self:GetCooldownInfo(playerName, spellEntry.spellId)
|
|
local effectiveCd = HMGT_SpellData.GetEffectiveCooldown(spellEntry, talents)
|
|
local isAvailabilitySpell = self:IsAvailabilitySpell(spellEntry)
|
|
local spellKnown = self:IsTrackedSpellKnownForPlayer(playerName, spellEntry.spellId)
|
|
local hasPartialCharges = (tonumber(maxCharges) or 0) > 0
|
|
and (tonumber(currentCharges) or tonumber(maxCharges) or 0) < (tonumber(maxCharges) or 0)
|
|
local hasActiveCd = ((remaining or 0) > 0) or hasPartialCharges
|
|
local hasAvailabilityState = isAvailabilitySpell and self:HasAvailabilityState(playerName, spellEntry.spellId)
|
|
if spellKnown or hasActiveCd or hasAvailabilityState then
|
|
entries[#entries + 1] = {
|
|
playerName = playerName,
|
|
class = classToken,
|
|
spellEntry = spellEntry,
|
|
remaining = remaining,
|
|
total = total > 0 and total or effectiveCd,
|
|
currentCharges = currentCharges,
|
|
maxCharges = maxCharges,
|
|
}
|
|
end
|
|
end
|
|
end
|
|
return entries
|
|
end
|
|
|
|
function HMGT:BuildEntriesForTracker(tracker, trackerKey)
|
|
local key = trackerKey or tostring(tonumber(tracker and tracker.id) or 0)
|
|
if tracker and tracker.testMode then
|
|
return self:CollectTrackerTestEntries(tracker), true
|
|
end
|
|
if tracker and tracker.demoMode then
|
|
return self:GetDemoEntries(key, self:GetTrackerSpellPool(tracker.categories), tracker), true
|
|
end
|
|
if not tracker or not tracker.enabled or not self:IsVisibleForCurrentGroup(tracker) then
|
|
return {}, false
|
|
end
|
|
return self:CollectTrackerEntries(tracker), true
|
|
end
|
|
|
|
function HMGT:BuildEntriesByPlayerForTracker(tracker, trackerKey, resolveUnitAnchorFrame)
|
|
local key = trackerKey or tostring(tonumber(tracker and tracker.id) or 0)
|
|
local ownName = self:NormalizePlayerName(UnitName("player")) or "Player"
|
|
if tracker.testMode then
|
|
local entries = self:CollectTrackerTestEntries(tracker)
|
|
if self:IsGroupTrackerConfig(tracker) and tracker.attachToPartyFrame == true then
|
|
return self:BuildPartyPreviewEntries(entries, resolveUnitAnchorFrame)
|
|
end
|
|
local byPlayer, order, unitByPlayer = {}, {}, {}
|
|
if #entries > 0 then
|
|
byPlayer[ownName] = entries
|
|
order[1] = ownName
|
|
unitByPlayer[ownName] = "player"
|
|
end
|
|
return byPlayer, order, unitByPlayer, true
|
|
end
|
|
if tracker.demoMode then
|
|
local entries = self:GetDemoEntries(key, self:GetTrackerSpellPool(tracker.categories), tracker)
|
|
if self:IsGroupTrackerConfig(tracker) and tracker.attachToPartyFrame == true then
|
|
return self:BuildPartyPreviewEntries(entries, resolveUnitAnchorFrame)
|
|
end
|
|
for _, entry in ipairs(entries) do
|
|
entry.playerName = ownName
|
|
end
|
|
local byPlayer, order, unitByPlayer = {}, {}, {}
|
|
if #entries > 0 then
|
|
byPlayer[ownName] = entries
|
|
order[1] = ownName
|
|
unitByPlayer[ownName] = "player"
|
|
end
|
|
return byPlayer, order, unitByPlayer, true
|
|
end
|
|
if not tracker.enabled or not self:IsVisibleForCurrentGroup(tracker) then
|
|
return {}, {}, {}, false
|
|
end
|
|
if IsInRaid() or not IsInGroup() then
|
|
return {}, {}, {}, false
|
|
end
|
|
local byPlayer, order, unitByPlayer = {}, {}, {}
|
|
for _, playerInfo in ipairs(self:GetTrackerPlayers(tracker)) do
|
|
local entries = self:CollectTrackerEntriesForPlayer(tracker, playerInfo)
|
|
if #entries > 0 then
|
|
local playerName = playerInfo.name
|
|
byPlayer[playerName] = entries
|
|
order[#order + 1] = playerName
|
|
unitByPlayer[playerName] = playerInfo.unitId
|
|
end
|
|
end
|
|
return byPlayer, order, unitByPlayer, true
|
|
end
|
|
|
|
function HMGT:FinalizeTrackerEntries(tracker, entries, trackerKey)
|
|
local result = entries or {}
|
|
if self.FilterDisplayEntries then
|
|
result = self:FilterDisplayEntries(tracker, result) or result
|
|
end
|
|
if self.SortDisplayEntries then
|
|
self:SortDisplayEntries(result, trackerKey)
|
|
end
|
|
|
|
local shouldTick = false
|
|
for _, entry in ipairs(result) do
|
|
if EntryNeedsVisualTicker(entry) then
|
|
shouldTick = true
|
|
break
|
|
end
|
|
end
|
|
|
|
return result, shouldTick
|
|
end
|
|
|
|
function HMGT:TriggerTrackerUpdate(reason)
|
|
local function normalizeReason(value)
|
|
if value == true then
|
|
return "trackers"
|
|
elseif value == "trackers" or value == "layout" or value == "visual" then
|
|
return value
|
|
end
|
|
return "full"
|
|
end
|
|
|
|
local function mergeReasons(current, incoming)
|
|
local priority = {
|
|
visual = 1,
|
|
layout = 2,
|
|
trackers = 3,
|
|
full = 4,
|
|
}
|
|
current = normalizeReason(current)
|
|
incoming = normalizeReason(incoming)
|
|
if (priority[incoming] or 4) >= (priority[current] or 4) then
|
|
return incoming
|
|
end
|
|
return current
|
|
end
|
|
|
|
self._trackerUpdateMinDelay = self._trackerUpdateMinDelay or 0.08
|
|
self._trackerUpdatePending = true
|
|
self._trackerUpdateReason = mergeReasons(self._trackerUpdateReason, reason)
|
|
if HMGT.TrackerManager then
|
|
local normalizedReason = normalizeReason(reason)
|
|
if normalizedReason == "trackers" then
|
|
HMGT.TrackerManager:MarkTrackersDirty()
|
|
elseif normalizedReason == "layout" then
|
|
HMGT.TrackerManager:MarkLayoutDirty()
|
|
end
|
|
end
|
|
if self._updateScheduled then return end
|
|
|
|
local now = GetTime()
|
|
local last = self._lastTrackerUpdateAt or 0
|
|
local delay = math.max(0, self._trackerUpdateMinDelay - (now - last))
|
|
self._updateScheduled = true
|
|
|
|
self:ScheduleTimer(function()
|
|
self._updateScheduled = nil
|
|
if not self._trackerUpdatePending then return end
|
|
self._trackerUpdatePending = nil
|
|
self._lastTrackerUpdateAt = GetTime()
|
|
local pendingReason = self._trackerUpdateReason
|
|
self._trackerUpdateReason = nil
|
|
|
|
local function profileModule(name, fn)
|
|
if not fn then return end
|
|
local t0 = debugprofilestop and debugprofilestop() or nil
|
|
fn()
|
|
local t1 = debugprofilestop and debugprofilestop() or nil
|
|
if t0 and t1 then
|
|
local mod = HMGT[name]
|
|
local count = mod and mod.lastEntryCount or 0
|
|
self:DebugScoped("verbose", "TrackerUI", "UIUpdate %s took %.2fms entries=%s", tostring(name), t1 - t0, tostring(count))
|
|
end
|
|
end
|
|
|
|
profileModule("TrackerManager", HMGT.TrackerManager and function()
|
|
if pendingReason == "visual" and HMGT.TrackerManager.RefreshVisibleVisuals then
|
|
HMGT.TrackerManager:RefreshVisibleVisuals()
|
|
else
|
|
HMGT.TrackerManager:UpdateDisplay()
|
|
end
|
|
end or nil)
|
|
|
|
if self._trackerUpdatePending then
|
|
self:TriggerTrackerUpdate()
|
|
end
|
|
end, delay)
|
|
end
|