initial commit
This commit is contained in:
472
Modules/BuffEndingAnnouncer/BuffEndingAnnouncer.lua
Normal file
472
Modules/BuffEndingAnnouncer/BuffEndingAnnouncer.lua
Normal file
@@ -0,0 +1,472 @@
|
||||
-- Modules/BuffEndingAnnouncer/BuffEndingAnnouncer.lua
|
||||
-- Announces tracked buffs in SAY shortly before they expire.
|
||||
|
||||
local ADDON_NAME = "HailMaryGuildTools"
|
||||
local HMGT = LibStub("AceAddon-3.0"):GetAddon(ADDON_NAME)
|
||||
if not HMGT then return end
|
||||
|
||||
local L = LibStub("AceLocale-3.0"):GetLocale(ADDON_NAME, true) or {}
|
||||
|
||||
local BEA = HMGT:NewModule("BuffEndingAnnouncer")
|
||||
HMGT.BuffEndingAnnouncer = BEA
|
||||
|
||||
BEA.runtimeEnabled = false
|
||||
BEA.eventFrame = nil
|
||||
BEA.ticker = nil
|
||||
BEA.lastAnnouncedSecond = {}
|
||||
BEA.recentOwnCastAt = {}
|
||||
|
||||
local function NormalizeThreshold(value, fallback)
|
||||
local threshold = tonumber(value)
|
||||
if not threshold then
|
||||
threshold = tonumber(fallback) or 5
|
||||
end
|
||||
threshold = math.floor(threshold + 0.5)
|
||||
if threshold < 1 then threshold = 1 end
|
||||
if threshold > 30 then threshold = 30 end
|
||||
return threshold
|
||||
end
|
||||
|
||||
local function GetSpellName(spellId)
|
||||
local sid = tonumber(spellId)
|
||||
if not sid or sid <= 0 then return nil end
|
||||
if C_Spell and type(C_Spell.GetSpellName) == "function" then
|
||||
local name = C_Spell.GetSpellName(sid)
|
||||
if type(name) == "string" and name ~= "" then
|
||||
return name
|
||||
end
|
||||
end
|
||||
if type(GetSpellInfo) == "function" then
|
||||
local name = GetSpellInfo(sid)
|
||||
if type(name) == "string" and name ~= "" then
|
||||
return name
|
||||
end
|
||||
end
|
||||
return nil
|
||||
end
|
||||
|
||||
local function IsAuraSourcePlayer(sourceUnit)
|
||||
if not sourceUnit then return nil end
|
||||
if sourceUnit == "player" then return true end
|
||||
if type(UnitIsUnit) == "function" and type(UnitExists) == "function" and UnitExists(sourceUnit) then
|
||||
return UnitIsUnit(sourceUnit, "player") and true or false
|
||||
end
|
||||
return false
|
||||
end
|
||||
|
||||
local function GetPlayerBuffExpiration(spellId)
|
||||
local sid = tonumber(spellId)
|
||||
if not sid or sid <= 0 then return nil, nil, nil, nil end
|
||||
|
||||
if C_UnitAuras and type(C_UnitAuras.GetPlayerAuraBySpellID) == "function" then
|
||||
local aura = C_UnitAuras.GetPlayerAuraBySpellID(sid)
|
||||
if aura then
|
||||
local exp = tonumber(aura.expirationTime) or 0
|
||||
if exp > 0 then
|
||||
local isOwnCaster = IsAuraSourcePlayer(aura.sourceUnit)
|
||||
if isOwnCaster == nil and aura.isFromPlayerOrPlayerPet ~= nil then
|
||||
isOwnCaster = aura.isFromPlayerOrPlayerPet and true or false
|
||||
end
|
||||
return exp, tonumber(aura.duration) or 0, aura.name, isOwnCaster
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
if AuraUtil and type(AuraUtil.FindAuraBySpellID) == "function" then
|
||||
local name, _, _, _, duration, expirationTime, sourceUnit, _, _, _, _, _, castByPlayer =
|
||||
AuraUtil.FindAuraBySpellID(sid, "player", "HELPFUL")
|
||||
local exp = tonumber(expirationTime) or 0
|
||||
if name and exp > 0 then
|
||||
local isOwnCaster = IsAuraSourcePlayer(sourceUnit)
|
||||
if isOwnCaster == nil and castByPlayer ~= nil then
|
||||
isOwnCaster = castByPlayer and true or false
|
||||
end
|
||||
return exp, tonumber(duration) or 0, name, isOwnCaster
|
||||
end
|
||||
end
|
||||
|
||||
return nil, nil, nil, nil
|
||||
end
|
||||
|
||||
local function GetPlayerChannelExpiration(spellId)
|
||||
local sid = tonumber(spellId)
|
||||
if not sid or sid <= 0 then return nil, nil, nil, nil end
|
||||
|
||||
if type(UnitChannelInfo) ~= "function" then
|
||||
return nil, nil, nil, nil
|
||||
end
|
||||
|
||||
local name, _, _, startTimeMS, endTimeMS, _, _, channelSpellId = UnitChannelInfo("player")
|
||||
local activeSpellId = tonumber(channelSpellId)
|
||||
if activeSpellId ~= sid then
|
||||
return nil, nil, nil, nil
|
||||
end
|
||||
|
||||
local startTime = tonumber(startTimeMS) or 0
|
||||
local endTime = tonumber(endTimeMS) or 0
|
||||
if endTime <= 0 or endTime <= startTime then
|
||||
return nil, nil, nil, nil
|
||||
end
|
||||
|
||||
return endTime / 1000, (endTime - startTime) / 1000, name, true
|
||||
end
|
||||
|
||||
local function GetTrackedSpellExpiration(spellId)
|
||||
local expirationTime, duration, name, isOwnCaster = GetPlayerBuffExpiration(spellId)
|
||||
if expirationTime and expirationTime > 0 then
|
||||
return expirationTime, duration, name, isOwnCaster, "aura"
|
||||
end
|
||||
|
||||
expirationTime, duration, name, isOwnCaster = GetPlayerChannelExpiration(spellId)
|
||||
if expirationTime and expirationTime > 0 then
|
||||
return expirationTime, duration, name, isOwnCaster, "channel"
|
||||
end
|
||||
|
||||
return nil, nil, nil, nil, nil
|
||||
end
|
||||
|
||||
function BEA:GetSettings()
|
||||
local p = HMGT.db and HMGT.db.profile
|
||||
if not p then return nil end
|
||||
p.buffEndingAnnouncer = p.buffEndingAnnouncer or {}
|
||||
p.buffEndingAnnouncer.announceAtSec = NormalizeThreshold(p.buffEndingAnnouncer.announceAtSec, 5)
|
||||
p.buffEndingAnnouncer.trackedBuffs = p.buffEndingAnnouncer.trackedBuffs or {}
|
||||
return p.buffEndingAnnouncer
|
||||
end
|
||||
|
||||
function BEA:GetDefaultThreshold()
|
||||
local s = self:GetSettings()
|
||||
return NormalizeThreshold(s and s.announceAtSec, 5)
|
||||
end
|
||||
|
||||
function BEA:GetTrackedBuffThreshold(spellId)
|
||||
local sid = tonumber(spellId)
|
||||
if not sid or sid <= 0 then return nil end
|
||||
|
||||
local settings = self:GetSettings()
|
||||
local tracked = settings and settings.trackedBuffs
|
||||
local value = tracked and tracked[sid]
|
||||
if value == nil then
|
||||
return nil
|
||||
end
|
||||
|
||||
if type(value) == "table" then
|
||||
value = value.threshold
|
||||
end
|
||||
if value == true then
|
||||
value = self:GetDefaultThreshold()
|
||||
end
|
||||
|
||||
return NormalizeThreshold(value, self:GetDefaultThreshold())
|
||||
end
|
||||
|
||||
function BEA:SetTrackedBuffThreshold(spellId, threshold)
|
||||
local sid = tonumber(spellId)
|
||||
if not sid or sid <= 0 then
|
||||
return false
|
||||
end
|
||||
|
||||
local s = self:GetSettings()
|
||||
s.trackedBuffs[sid] = NormalizeThreshold(threshold, self:GetDefaultThreshold())
|
||||
self.lastAnnouncedSecond[sid] = nil
|
||||
self:Refresh()
|
||||
return true
|
||||
end
|
||||
|
||||
function BEA:GetTrackedBuffEntries()
|
||||
local entries = {}
|
||||
for _, sid in ipairs(self:GetTrackedBuffSpellIds()) do
|
||||
entries[#entries + 1] = {
|
||||
spellId = sid,
|
||||
name = GetSpellName(sid) or ("Spell " .. tostring(sid)),
|
||||
threshold = self:GetTrackedBuffThreshold(sid) or self:GetDefaultThreshold(),
|
||||
}
|
||||
end
|
||||
return entries
|
||||
end
|
||||
|
||||
function BEA:GetTrackedBuffSpellIds()
|
||||
local ids = {}
|
||||
local settings = self:GetSettings()
|
||||
local tracked = (settings and settings.trackedBuffs) or {}
|
||||
for sid, value in pairs(tracked) do
|
||||
local id = tonumber(sid)
|
||||
if id and id > 0 and value ~= nil and value ~= false then
|
||||
ids[#ids + 1] = id
|
||||
end
|
||||
end
|
||||
table.sort(ids, function(a, b)
|
||||
local nameA = tostring(GetSpellName(a) or a):lower()
|
||||
local nameB = tostring(GetSpellName(b) or b):lower()
|
||||
if nameA == nameB then
|
||||
return a < b
|
||||
end
|
||||
return nameA < nameB
|
||||
end)
|
||||
return ids
|
||||
end
|
||||
|
||||
function BEA:ResetAnnouncements()
|
||||
for sid in pairs(self.lastAnnouncedSecond) do
|
||||
self.lastAnnouncedSecond[sid] = nil
|
||||
end
|
||||
end
|
||||
|
||||
function BEA:ResetRecentOwnCasts()
|
||||
for sid in pairs(self.recentOwnCastAt) do
|
||||
self.recentOwnCastAt[sid] = nil
|
||||
end
|
||||
end
|
||||
|
||||
function BEA:StopTicker()
|
||||
if self.ticker then
|
||||
self.ticker:Cancel()
|
||||
self.ticker = nil
|
||||
end
|
||||
end
|
||||
|
||||
function BEA:HasTrackedBuffsConfigured()
|
||||
return #self:GetTrackedBuffSpellIds() > 0
|
||||
end
|
||||
|
||||
function BEA:UpdateRuntimeEventRegistrations()
|
||||
if not self.eventFrame then
|
||||
return
|
||||
end
|
||||
|
||||
self.eventFrame:UnregisterEvent("UNIT_AURA")
|
||||
self.eventFrame:UnregisterEvent("UNIT_SPELLCAST_SUCCEEDED")
|
||||
self.eventFrame:UnregisterEvent("UNIT_SPELLCAST_CHANNEL_START")
|
||||
self.eventFrame:UnregisterEvent("UNIT_SPELLCAST_CHANNEL_UPDATE")
|
||||
self.eventFrame:UnregisterEvent("UNIT_SPELLCAST_CHANNEL_STOP")
|
||||
|
||||
if not self.runtimeEnabled or not self:HasTrackedBuffsConfigured() then
|
||||
return
|
||||
end
|
||||
|
||||
self.eventFrame:RegisterUnitEvent("UNIT_AURA", "player")
|
||||
self.eventFrame:RegisterUnitEvent("UNIT_SPELLCAST_SUCCEEDED", "player")
|
||||
self.eventFrame:RegisterUnitEvent("UNIT_SPELLCAST_CHANNEL_START", "player")
|
||||
self.eventFrame:RegisterUnitEvent("UNIT_SPELLCAST_CHANNEL_UPDATE", "player")
|
||||
self.eventFrame:RegisterUnitEvent("UNIT_SPELLCAST_CHANNEL_STOP", "player")
|
||||
end
|
||||
|
||||
function BEA:EnsureTicker()
|
||||
if self.ticker then return end
|
||||
self.ticker = C_Timer.NewTicker(0.1, function()
|
||||
self:OnTicker()
|
||||
end)
|
||||
end
|
||||
|
||||
function BEA:IsProfileEnabled()
|
||||
local s = self:GetSettings()
|
||||
return s and s.enabled == true
|
||||
end
|
||||
|
||||
function BEA:BuildAnnouncement(spellName, secondsLeft)
|
||||
local template = L["BEA_MSG_TEMPLATE"] or "%s ending in %d"
|
||||
return string.format(template, tostring(spellName or "?"), tonumber(secondsLeft) or 0)
|
||||
end
|
||||
|
||||
function BEA:IsOwnBuffCaster(spellId, isOwnCaster, duration, now)
|
||||
if isOwnCaster == true then
|
||||
return true
|
||||
end
|
||||
if isOwnCaster == false then
|
||||
return false
|
||||
end
|
||||
|
||||
-- Fallback when aura source information is missing:
|
||||
-- only trust a very recent own cast of the tracked spell.
|
||||
local castAt = self.recentOwnCastAt[tonumber(spellId)]
|
||||
if not castAt then
|
||||
return false
|
||||
end
|
||||
|
||||
local maxAge = tonumber(duration) or 0
|
||||
if maxAge < 3 then maxAge = 3 end
|
||||
if maxAge > 12 then maxAge = 12 end
|
||||
return (tonumber(now) or GetTime()) - castAt <= maxAge
|
||||
end
|
||||
|
||||
function BEA:EvaluateTrackedBuffs()
|
||||
if not self:IsProfileEnabled() then
|
||||
self:ResetAnnouncements()
|
||||
return false
|
||||
end
|
||||
|
||||
local ids = self:GetTrackedBuffSpellIds()
|
||||
if #ids == 0 then
|
||||
self:ResetAnnouncements()
|
||||
return false
|
||||
end
|
||||
|
||||
local now = GetTime()
|
||||
local hasAnyTrackedBuff = false
|
||||
|
||||
for _, sid in ipairs(ids) do
|
||||
local threshold = self:GetTrackedBuffThreshold(sid)
|
||||
local expirationTime, duration, auraName, isOwnCaster, stateKind = GetTrackedSpellExpiration(sid)
|
||||
if expirationTime and expirationTime > now then
|
||||
local isOwnSource = (stateKind == "channel") or self:IsOwnBuffCaster(sid, isOwnCaster, duration, now)
|
||||
if isOwnSource then
|
||||
hasAnyTrackedBuff = true
|
||||
local remaining = expirationTime - now
|
||||
if threshold and remaining > 0 and remaining <= threshold then
|
||||
local second = math.ceil(remaining - 0.0001)
|
||||
if second < 1 then second = 1 end
|
||||
if self.lastAnnouncedSecond[sid] ~= second then
|
||||
local msg = self:BuildAnnouncement(auraName or GetSpellName(sid) or ("Spell " .. tostring(sid)), second)
|
||||
SendChatMessage(msg, "SAY")
|
||||
self.lastAnnouncedSecond[sid] = second
|
||||
end
|
||||
else
|
||||
self.lastAnnouncedSecond[sid] = nil
|
||||
end
|
||||
else
|
||||
self.lastAnnouncedSecond[sid] = nil
|
||||
end
|
||||
else
|
||||
self.lastAnnouncedSecond[sid] = nil
|
||||
end
|
||||
|
||||
local castAt = self.recentOwnCastAt[sid]
|
||||
if castAt and (now - castAt) > 30 then
|
||||
self.recentOwnCastAt[sid] = nil
|
||||
end
|
||||
end
|
||||
|
||||
return hasAnyTrackedBuff
|
||||
end
|
||||
|
||||
function BEA:Refresh()
|
||||
if not self.runtimeEnabled then return end
|
||||
self:UpdateRuntimeEventRegistrations()
|
||||
local active = self:EvaluateTrackedBuffs()
|
||||
if active then
|
||||
self:EnsureTicker()
|
||||
else
|
||||
self:StopTicker()
|
||||
end
|
||||
end
|
||||
|
||||
function BEA:OnTicker()
|
||||
if not self.runtimeEnabled then
|
||||
self:StopTicker()
|
||||
return
|
||||
end
|
||||
local active = self:EvaluateTrackedBuffs()
|
||||
if not active then
|
||||
self:StopTicker()
|
||||
end
|
||||
end
|
||||
|
||||
function BEA:OnEvent(event, ...)
|
||||
if event == "UNIT_AURA" then
|
||||
local unit = ...
|
||||
if unit ~= "player" then
|
||||
return
|
||||
end
|
||||
elseif event == "UNIT_SPELLCAST_SUCCEEDED" then
|
||||
local unit, _, spellId = ...
|
||||
if unit == "player" then
|
||||
local sid = tonumber(spellId)
|
||||
if sid and sid > 0 then
|
||||
local s = self:GetSettings()
|
||||
if s and s.trackedBuffs and s.trackedBuffs[sid] then
|
||||
self.recentOwnCastAt[sid] = GetTime()
|
||||
end
|
||||
end
|
||||
end
|
||||
elseif event == "UNIT_SPELLCAST_CHANNEL_START" or event == "UNIT_SPELLCAST_CHANNEL_UPDATE" or event == "UNIT_SPELLCAST_CHANNEL_STOP" then
|
||||
local unit = ...
|
||||
if unit ~= "player" then
|
||||
return
|
||||
end
|
||||
elseif event == "PLAYER_ENTERING_WORLD" or event == "PLAYER_DEAD" then
|
||||
self:ResetAnnouncements()
|
||||
self:ResetRecentOwnCasts()
|
||||
end
|
||||
self:Refresh()
|
||||
end
|
||||
|
||||
function BEA:StartRuntime()
|
||||
if not self:IsProfileEnabled() then return end
|
||||
if self.runtimeEnabled then
|
||||
self:Refresh()
|
||||
return
|
||||
end
|
||||
|
||||
self.runtimeEnabled = true
|
||||
if not self.eventFrame then
|
||||
self.eventFrame = CreateFrame("Frame")
|
||||
self.eventFrame:SetScript("OnEvent", function(_, event, ...)
|
||||
self:OnEvent(event, ...)
|
||||
end)
|
||||
end
|
||||
self.eventFrame:RegisterEvent("PLAYER_ENTERING_WORLD")
|
||||
self.eventFrame:RegisterEvent("PLAYER_DEAD")
|
||||
self:Refresh()
|
||||
end
|
||||
|
||||
function BEA:StopRuntime()
|
||||
if self.eventFrame then
|
||||
self.eventFrame:UnregisterEvent("UNIT_AURA")
|
||||
self.eventFrame:UnregisterEvent("UNIT_SPELLCAST_SUCCEEDED")
|
||||
self.eventFrame:UnregisterEvent("UNIT_SPELLCAST_CHANNEL_START")
|
||||
self.eventFrame:UnregisterEvent("UNIT_SPELLCAST_CHANNEL_UPDATE")
|
||||
self.eventFrame:UnregisterEvent("UNIT_SPELLCAST_CHANNEL_STOP")
|
||||
self.eventFrame:UnregisterEvent("PLAYER_ENTERING_WORLD")
|
||||
self.eventFrame:UnregisterEvent("PLAYER_DEAD")
|
||||
end
|
||||
self.runtimeEnabled = false
|
||||
self:StopTicker()
|
||||
self:ResetAnnouncements()
|
||||
self:ResetRecentOwnCasts()
|
||||
end
|
||||
|
||||
function BEA:OnInitialize()
|
||||
HMGT.BuffEndingAnnouncer = self
|
||||
end
|
||||
|
||||
function BEA:OnEnable()
|
||||
self:StartRuntime()
|
||||
end
|
||||
|
||||
function BEA:OnDisable()
|
||||
self:StopRuntime()
|
||||
end
|
||||
|
||||
function BEA:AddTrackedBuff(spellId, threshold)
|
||||
local sid = tonumber(spellId)
|
||||
if not sid or sid <= 0 then
|
||||
return false, "invalid"
|
||||
end
|
||||
local name = GetSpellName(sid)
|
||||
if not name then
|
||||
return false, "invalid"
|
||||
end
|
||||
|
||||
local s = self:GetSettings()
|
||||
s.trackedBuffs[sid] = NormalizeThreshold(threshold, self:GetDefaultThreshold())
|
||||
self.lastAnnouncedSecond[sid] = nil
|
||||
self:Refresh()
|
||||
return true, name
|
||||
end
|
||||
|
||||
function BEA:RemoveTrackedBuff(spellId)
|
||||
local sid = tonumber(spellId)
|
||||
if not sid or sid <= 0 then
|
||||
return false, "invalid"
|
||||
end
|
||||
|
||||
local s = self:GetSettings()
|
||||
if not s.trackedBuffs[sid] then
|
||||
return false, "missing"
|
||||
end
|
||||
|
||||
s.trackedBuffs[sid] = nil
|
||||
self.lastAnnouncedSecond[sid] = nil
|
||||
self:Refresh()
|
||||
return true, GetSpellName(sid) or tostring(sid)
|
||||
end
|
||||
409
Modules/BuffEndingAnnouncer/BuffEndingAnnouncerOptions.lua
Normal file
409
Modules/BuffEndingAnnouncer/BuffEndingAnnouncerOptions.lua
Normal file
@@ -0,0 +1,409 @@
|
||||
-- Modules/BuffEndingAnnouncer/BuffEndingAnnouncerOptions.lua
|
||||
|
||||
local ADDON_NAME = "HailMaryGuildTools"
|
||||
local HMGT = LibStub("AceAddon-3.0"):GetAddon(ADDON_NAME)
|
||||
if not HMGT then return end
|
||||
local BEA = HMGT.BuffEndingAnnouncer
|
||||
if not BEA then return end
|
||||
if not HMGT_Config or not HMGT_Config.RegisterOptionsProvider then return end
|
||||
|
||||
local L = LibStub("AceLocale-3.0"):GetLocale(ADDON_NAME, true) or {}
|
||||
local AceConfigRegistry = LibStub("AceConfigRegistry-3.0")
|
||||
local beaOptionsGroup
|
||||
|
||||
local function GetSpellName(spellId)
|
||||
local sid = tonumber(spellId)
|
||||
if not sid or sid <= 0 then return nil end
|
||||
if C_Spell and type(C_Spell.GetSpellName) == "function" then
|
||||
local name = C_Spell.GetSpellName(sid)
|
||||
if type(name) == "string" and name ~= "" then
|
||||
return name
|
||||
end
|
||||
end
|
||||
if type(GetSpellInfo) == "function" then
|
||||
local name = GetSpellInfo(sid)
|
||||
if type(name) == "string" and name ~= "" then
|
||||
return name
|
||||
end
|
||||
end
|
||||
return nil
|
||||
end
|
||||
|
||||
local function GetSpellIcon(spellId)
|
||||
local sid = tonumber(spellId)
|
||||
if not sid or sid <= 0 then return nil end
|
||||
if HMGT_SpellData and type(HMGT_SpellData.GetSpellIcon) == "function" then
|
||||
return HMGT_SpellData.GetSpellIcon(sid)
|
||||
end
|
||||
if C_Spell and type(C_Spell.GetSpellTexture) == "function" then
|
||||
return C_Spell.GetSpellTexture(sid)
|
||||
end
|
||||
local _, _, icon = GetSpellInfo(sid)
|
||||
return icon
|
||||
end
|
||||
|
||||
local function RefreshTrackedGroup()
|
||||
if not beaOptionsGroup or type(beaOptionsGroup.args) ~= "table" then
|
||||
return
|
||||
end
|
||||
|
||||
local fresh = BEA:BuildOptionsGroup()
|
||||
local currentTracked = beaOptionsGroup.args.tracked
|
||||
local freshTracked = fresh and fresh.args and fresh.args.tracked
|
||||
if type(currentTracked) == "table" and type(freshTracked) == "table" then
|
||||
currentTracked.name = freshTracked.name
|
||||
currentTracked.order = freshTracked.order
|
||||
currentTracked.inline = freshTracked.inline
|
||||
currentTracked.childGroups = freshTracked.childGroups
|
||||
currentTracked.args = freshTracked.args
|
||||
end
|
||||
end
|
||||
|
||||
local function NotifyOptionsChanged(rebuild)
|
||||
if rebuild ~= false then
|
||||
RefreshTrackedGroup()
|
||||
end
|
||||
AceConfigRegistry:NotifyChange(ADDON_NAME)
|
||||
end
|
||||
|
||||
local function GetFilterState()
|
||||
HMGT._beaSpellFilter = HMGT._beaSpellFilter or {
|
||||
search = "",
|
||||
}
|
||||
return HMGT._beaSpellFilter
|
||||
end
|
||||
|
||||
local function NormalizeSearchText(value)
|
||||
local text = tostring(value or ""):lower()
|
||||
text = text:gsub("^%s+", "")
|
||||
text = text:gsub("%s+$", "")
|
||||
return text
|
||||
end
|
||||
|
||||
local function GetDraft()
|
||||
HMGT._beaDraft = HMGT._beaDraft or {}
|
||||
return HMGT._beaDraft
|
||||
end
|
||||
|
||||
local function GetTrackedBuffLabel(entry)
|
||||
local spellId = tonumber(entry and entry.spellId) or 0
|
||||
local spellName = tostring(entry and entry.name or GetSpellName(spellId) or ("Spell " .. spellId))
|
||||
local icon = GetSpellIcon(spellId)
|
||||
local threshold = tonumber(entry and entry.threshold) or tonumber(BEA:GetDefaultThreshold()) or 0
|
||||
if icon and icon ~= "" then
|
||||
return string.format("|T%s:16:16:0:0|t %s (%ss)", tostring(icon), spellName, threshold)
|
||||
end
|
||||
return string.format("%s (%ss)", spellName, threshold)
|
||||
end
|
||||
|
||||
local function BuildTrackedBuffLeaf(entry)
|
||||
local spellId = tonumber(entry and entry.spellId) or 0
|
||||
return {
|
||||
type = "group",
|
||||
name = GetTrackedBuffLabel(entry),
|
||||
args = {
|
||||
header = {
|
||||
type = "header",
|
||||
order = 1,
|
||||
name = GetTrackedBuffLabel(entry),
|
||||
},
|
||||
spellInfo = {
|
||||
type = "description",
|
||||
order = 2,
|
||||
width = "full",
|
||||
name = string.format(
|
||||
"|cffffd100Spell ID|r: %d\n|cffffd100%s|r: %s",
|
||||
spellId,
|
||||
L["OPT_BEA_COL_SPELL"] or "Spellname",
|
||||
tostring(entry and entry.name or GetSpellName(spellId) or ("Spell " .. spellId))
|
||||
),
|
||||
},
|
||||
threshold = {
|
||||
type = "range",
|
||||
order = 3,
|
||||
min = 1,
|
||||
max = 60,
|
||||
step = 1,
|
||||
width = "full",
|
||||
name = L["OPT_BEA_COL_THRESHOLD"] or "Threshold",
|
||||
get = function()
|
||||
local current = BEA:GetTrackedBuffEntries()
|
||||
for _, candidate in ipairs(current) do
|
||||
if tonumber(candidate.spellId) == spellId then
|
||||
return tonumber(candidate.threshold) or BEA:GetDefaultThreshold()
|
||||
end
|
||||
end
|
||||
return tonumber(BEA:GetDefaultThreshold()) or 5
|
||||
end,
|
||||
set = function(_, val)
|
||||
BEA:SetTrackedBuffThreshold(spellId, val)
|
||||
NotifyOptionsChanged()
|
||||
end,
|
||||
},
|
||||
remove = {
|
||||
type = "execute",
|
||||
order = 4,
|
||||
width = "full",
|
||||
name = REMOVE or (L["OPT_BEA_REMOVE"] or "Remove buff"),
|
||||
confirm = function()
|
||||
return string.format("%s?", tostring(entry and entry.name or GetSpellName(spellId) or ("Spell " .. spellId)))
|
||||
end,
|
||||
func = function()
|
||||
local ok, info = BEA:RemoveTrackedBuff(spellId)
|
||||
if ok then
|
||||
HMGT:Print(string.format(L["OPT_BEA_MSG_REMOVED"] or "HMGT: buff removed: %s", tostring(info or spellId)))
|
||||
else
|
||||
HMGT:Print(L["OPT_BEA_MSG_NOT_FOUND"] or "HMGT: buff not found")
|
||||
end
|
||||
NotifyOptionsChanged()
|
||||
end,
|
||||
},
|
||||
},
|
||||
}
|
||||
end
|
||||
|
||||
local function IsTrackedBuffVisible(entry)
|
||||
local spellId = tonumber(entry and entry.spellId) or 0
|
||||
if spellId <= 0 then
|
||||
return false
|
||||
end
|
||||
|
||||
local search = NormalizeSearchText(GetFilterState().search)
|
||||
if search == "" then
|
||||
return true
|
||||
end
|
||||
|
||||
local haystack = table.concat({
|
||||
tostring(entry and entry.name or GetSpellName(spellId) or ""),
|
||||
tostring(spellId),
|
||||
}, " "):lower()
|
||||
|
||||
return haystack:find(search, 1, true) ~= nil
|
||||
end
|
||||
|
||||
local function CountVisibleEntries(entries)
|
||||
local count = 0
|
||||
for _, entry in ipairs(entries or {}) do
|
||||
if IsTrackedBuffVisible(entry) then
|
||||
count = count + 1
|
||||
end
|
||||
end
|
||||
return count
|
||||
end
|
||||
|
||||
function BEA:BuildOptionsGroup()
|
||||
local draft = GetDraft()
|
||||
local entries = self:GetTrackedBuffEntries()
|
||||
|
||||
local group = {
|
||||
type = "group",
|
||||
name = L["BEA_NAME"] or "Buff Ending Announcer",
|
||||
order = 3,
|
||||
childGroups = "tree",
|
||||
args = {
|
||||
general = {
|
||||
type = "group",
|
||||
order = 1,
|
||||
name = L["OPT_BEA_SECTION_GENERAL"] or "General",
|
||||
args = {
|
||||
enabled = {
|
||||
type = "toggle",
|
||||
order = 1,
|
||||
width = "full",
|
||||
name = L["OPT_BEA_ENABLED"] or "Enable buff ending announcer",
|
||||
desc = L["OPT_BEA_ENABLED_DESC"] or "Announce tracked buff countdowns in /say",
|
||||
get = function()
|
||||
return self:GetSettings().enabled == true
|
||||
end,
|
||||
set = function(_, val)
|
||||
self:GetSettings().enabled = val and true or false
|
||||
if val then
|
||||
self:Enable()
|
||||
else
|
||||
self:Disable()
|
||||
end
|
||||
end,
|
||||
},
|
||||
defaultThreshold = {
|
||||
type = "range",
|
||||
order = 2,
|
||||
min = 1,
|
||||
max = 60,
|
||||
step = 1,
|
||||
width = "full",
|
||||
name = L["OPT_BEA_DEFAULT_THRESHOLD"] or "Default threshold (sec)",
|
||||
desc = L["OPT_BEA_DEFAULT_THRESHOLD_DESC"] or "Used when you add a new tracked buff",
|
||||
get = function()
|
||||
return tonumber(self:GetDefaultThreshold()) or 5
|
||||
end,
|
||||
set = function(_, val)
|
||||
self:GetSettings().announceAtSec = math.floor((tonumber(val) or 5) + 0.5)
|
||||
self:Refresh()
|
||||
end,
|
||||
},
|
||||
},
|
||||
},
|
||||
tracked = {
|
||||
type = "group",
|
||||
order = 2,
|
||||
name = string.format(
|
||||
"%s (%d)",
|
||||
L["OPT_BEA_SECTION_BUFFS"] or "Tracked buffs",
|
||||
#entries
|
||||
),
|
||||
args = {
|
||||
header = {
|
||||
type = "header",
|
||||
order = 1,
|
||||
name = L["OPT_SPELL_BROWSER"] or "Spell Browser",
|
||||
},
|
||||
summary = {
|
||||
type = "description",
|
||||
order = 2,
|
||||
width = "full",
|
||||
name = function()
|
||||
local currentEntries = BEA:GetTrackedBuffEntries()
|
||||
return string.format(
|
||||
"%s\n\n|cffffd100%s|r: %d\n|cffffd100%s|r: %d",
|
||||
L["OPT_SPELL_BROWSER_DESC"] or "Filter tracked spells by name or Spell ID and apply quick actions to the visible results.",
|
||||
L["OPT_SPELLS_VISIBLE"] or "Visible spells",
|
||||
CountVisibleEntries(currentEntries),
|
||||
L["OPT_BEA_SECTION_BUFFS"] or "Tracked buffs",
|
||||
#currentEntries
|
||||
)
|
||||
end,
|
||||
},
|
||||
search = {
|
||||
type = "input",
|
||||
order = 3,
|
||||
width = "full",
|
||||
name = L["OPT_FILTER_SEARCH"] or "Search",
|
||||
desc = L["OPT_FILTER_SEARCH_DESC"] or "Search by spell name or Spell ID",
|
||||
get = function()
|
||||
return GetFilterState().search or ""
|
||||
end,
|
||||
set = function(_, val)
|
||||
GetFilterState().search = val or ""
|
||||
NotifyOptionsChanged(false)
|
||||
end,
|
||||
},
|
||||
resetFilters = {
|
||||
type = "execute",
|
||||
order = 4,
|
||||
width = "full",
|
||||
name = L["OPT_FILTER_RESET"] or "Reset filters",
|
||||
func = function()
|
||||
GetFilterState().search = ""
|
||||
NotifyOptionsChanged(false)
|
||||
end,
|
||||
},
|
||||
addGroup = {
|
||||
type = "group",
|
||||
order = 5,
|
||||
inline = true,
|
||||
name = L["OPT_BEA_CURRENT"] or "Current tracked buffs",
|
||||
args = {
|
||||
addSpellId = {
|
||||
type = "input",
|
||||
order = 1,
|
||||
width = 0.9,
|
||||
name = L["OPT_BEA_ADD_ID"] or "Add Spell ID",
|
||||
get = function()
|
||||
return tostring(draft.addSpellId or "")
|
||||
end,
|
||||
set = function(_, val)
|
||||
draft.addSpellId = val
|
||||
end,
|
||||
},
|
||||
addThreshold = {
|
||||
type = "range",
|
||||
order = 2,
|
||||
min = 1,
|
||||
max = 60,
|
||||
step = 1,
|
||||
width = 1.1,
|
||||
name = L["OPT_BEA_ADD_THRESHOLD"] or "Threshold",
|
||||
desc = L["OPT_BEA_ADD_THRESHOLD_DESC"] or "Countdown start in seconds for this buff",
|
||||
get = function()
|
||||
return tonumber(draft.addThreshold) or tonumber(self:GetDefaultThreshold()) or 5
|
||||
end,
|
||||
set = function(_, val)
|
||||
draft.addThreshold = tonumber(val) or self:GetDefaultThreshold()
|
||||
end,
|
||||
},
|
||||
addSpell = {
|
||||
type = "execute",
|
||||
order = 3,
|
||||
width = "full",
|
||||
name = L["OPT_BEA_ADD"] or "Add buff",
|
||||
func = function()
|
||||
local sid = tonumber(draft.addSpellId)
|
||||
local threshold = tonumber(draft.addThreshold) or self:GetDefaultThreshold()
|
||||
local ok, info = self:AddTrackedBuff(sid, threshold)
|
||||
if ok then
|
||||
draft.addSpellId = ""
|
||||
draft.addThreshold = tonumber(self:GetDefaultThreshold()) or 5
|
||||
HMGT:Print(string.format(L["OPT_BEA_MSG_ADDED"] or "HMGT: buff added: %s", tostring(info or sid)))
|
||||
else
|
||||
HMGT:Print(L["OPT_BEA_MSG_INVALID"] or "HMGT: invalid buff spell ID")
|
||||
end
|
||||
NotifyOptionsChanged()
|
||||
end,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
if #entries == 0 then
|
||||
group.args.tracked.args.empty = {
|
||||
type = "description",
|
||||
order = 10,
|
||||
width = "full",
|
||||
name = L["OPT_BEA_EMPTY"] or "No buffs configured.",
|
||||
}
|
||||
else
|
||||
for index, entry in ipairs(entries) do
|
||||
local key = "buff_" .. tostring(tonumber(entry.spellId) or index)
|
||||
local leaf = BuildTrackedBuffLeaf(entry)
|
||||
leaf.order = 10 + index
|
||||
leaf.inline = true
|
||||
leaf.name = " "
|
||||
leaf.hidden = function()
|
||||
return not IsTrackedBuffVisible(entry)
|
||||
end
|
||||
group.args.tracked.args[key] = leaf
|
||||
end
|
||||
|
||||
group.args.tracked.args.noVisible = {
|
||||
type = "description",
|
||||
order = 1000,
|
||||
width = "full",
|
||||
hidden = function()
|
||||
return CountVisibleEntries(BEA:GetTrackedBuffEntries()) > 0
|
||||
end,
|
||||
name = L["OPT_BEA_EMPTY"] or "No buffs configured.",
|
||||
}
|
||||
end
|
||||
|
||||
return group
|
||||
end
|
||||
|
||||
function BEA:GetOptionsGroup()
|
||||
if not beaOptionsGroup then
|
||||
beaOptionsGroup = self:BuildOptionsGroup()
|
||||
else
|
||||
RefreshTrackedGroup()
|
||||
end
|
||||
return beaOptionsGroup
|
||||
end
|
||||
|
||||
HMGT_Config:RegisterOptionsProvider("announcer.buffEndingAnnouncer", function()
|
||||
return {
|
||||
path = "announcer",
|
||||
order = 3,
|
||||
group = BEA:GetOptionsGroup(),
|
||||
}
|
||||
end)
|
||||
1193
Modules/MapOverlay/MapOverlay.lua
Normal file
1193
Modules/MapOverlay/MapOverlay.lua
Normal file
File diff suppressed because it is too large
Load Diff
11
Modules/MapOverlay/MapOverlay.xml
Normal file
11
Modules/MapOverlay/MapOverlay.xml
Normal file
@@ -0,0 +1,11 @@
|
||||
<Ui xmlns="http://www.blizzard.com/wow/ui/">
|
||||
<Button name="HMGTMapOverlayPinTemplate" mixin="HMGTMapOverlayPinMixin" virtual="true">
|
||||
<Size x="16" y="16" />
|
||||
<Scripts>
|
||||
<OnLoad method="OnLoad" />
|
||||
<OnEnter method="OnMouseEnter" />
|
||||
<OnLeave method="OnMouseLeave" />
|
||||
<OnClick method="OnClick" />
|
||||
</Scripts>
|
||||
</Button>
|
||||
</Ui>
|
||||
37
Modules/MapOverlay/MapOverlayIconConfig.lua
Normal file
37
Modules/MapOverlay/MapOverlayIconConfig.lua
Normal file
@@ -0,0 +1,37 @@
|
||||
local ADDON_NAME = "HailMaryGuildTools"
|
||||
local HMGT = LibStub("AceAddon-3.0"):GetAddon(ADDON_NAME)
|
||||
if not HMGT then return end
|
||||
|
||||
local MEDIA_PATH = "Interface\\AddOns\\HailMaryGuildTools\\Modules\\MapOverlay\\Media\\"
|
||||
local BLIP_TEXTURE = "Interface\\Minimap\\ObjectIconsAtlas"
|
||||
|
||||
-- Curated list of allowed Map Overlay icons.
|
||||
-- Add, remove, or reorder entries here without touching the runtime module.
|
||||
-- Supported fields per icon:
|
||||
-- key, label, texture
|
||||
-- atlasIndex + atlasSize = "32x32" (optionally textureWidth, textureHeight, atlasColumns)
|
||||
-- or exact iconCoords = { left, right, top, bottom }
|
||||
-- or exact cell = { col, row } for fixed 32x32 sheets
|
||||
-- Note: 32x32 ObjectIconsAtlas minimap blips are rendered through Blizzard's
|
||||
-- legacy ObjectIcons coordinate path internally, so the visible crop stays exact.
|
||||
HMGT.MapOverlayIconConfig = {
|
||||
defaultKey = "default",
|
||||
icons = {
|
||||
{ key = "default", label = "Default", texture = MEDIA_PATH .. "DefaultIcon.png" },
|
||||
{ key = "auctionhouse", label = "Auction House", texture = BLIP_TEXTURE, atlasIndex = 16, atlasSize = "32x32", aliases = { "auction_house" } },
|
||||
{ key = "bank", label = "Bank", texture = BLIP_TEXTURE, atlasIndex = 17, atlasSize = "32x32" },
|
||||
{ key = "battlemaster", label = "Battlemaster", texture = BLIP_TEXTURE, atlasIndex = 18, atlasSize = "32x32" },
|
||||
{ key = "classtrainer", label = "Class Trainer", texture = BLIP_TEXTURE, atlasIndex = 19, atlasSize = "32x32", aliases = { "class_trainer" } },
|
||||
{ key = "fooddrink", label = "Food & Drink", texture = BLIP_TEXTURE, atlasIndex = 21, atlasSize = "32x32", aliases = { "food_drink" } },
|
||||
{ key = "innkeeper", label = "Innkeeper", texture = BLIP_TEXTURE, atlasIndex = 22, atlasSize = "32x32" },
|
||||
{ key = "poisons", label = "Poisons", texture = BLIP_TEXTURE, atlasIndex = 24, atlasSize = "32x32" },
|
||||
{ key = "professiontrainer", label = "Profession Trainer", texture = BLIP_TEXTURE, atlasIndex = 25, atlasSize = "32x32", aliases = { "profession_trainer" } },
|
||||
{ key = "reagents", label = "Reagents", texture = BLIP_TEXTURE, atlasIndex = 26, atlasSize = "32x32" },
|
||||
{ key = "repairs", label = "Repairs", texture = BLIP_TEXTURE, atlasIndex = 27, atlasSize = "32x32" },
|
||||
{ key = "blueblip", label = "Blue Blip", texture = BLIP_TEXTURE, atlasIndex = 0, atlasSize = "32x32", aliases = { "blip_blue" } },
|
||||
{ key = "lightblueblip", label = "Light Blue Blip", texture = BLIP_TEXTURE, atlasIndex = 1, atlasSize = "32x32", aliases = { "blip_lightblue" } },
|
||||
{ key = "redblip", label = "Red Blip", texture = BLIP_TEXTURE, atlasIndex = 2, atlasSize = "32x32", aliases = { "blip_red" } },
|
||||
{ key = "yellowblip", label = "Yellow Blip", texture = BLIP_TEXTURE, atlasIndex = 3, atlasSize = "32x32", aliases = { "blip_yellow" } },
|
||||
{ key = "greenblip", label = "Green Blip", texture = BLIP_TEXTURE, atlasIndex = 4, atlasSize = "32x32", aliases = { "blip_green" } },
|
||||
},
|
||||
}
|
||||
554
Modules/MapOverlay/MapOverlayOptions.lua
Normal file
554
Modules/MapOverlay/MapOverlayOptions.lua
Normal file
@@ -0,0 +1,554 @@
|
||||
local ADDON_NAME = "HailMaryGuildTools"
|
||||
local HMGT = LibStub("AceAddon-3.0"):GetAddon(ADDON_NAME)
|
||||
if not HMGT then return end
|
||||
|
||||
local MapOverlay = HMGT.MapOverlay
|
||||
if not MapOverlay then return end
|
||||
if not HMGT_Config or not HMGT_Config.RegisterOptionsProvider then return end
|
||||
|
||||
local L = LibStub("AceLocale-3.0"):GetLocale(ADDON_NAME, true) or {}
|
||||
local AceConfigRegistry = LibStub("AceConfigRegistry-3.0", true)
|
||||
|
||||
local MAX_POI_EDITOR_ROWS = 40
|
||||
|
||||
local function NotifyOptionsChanged()
|
||||
if AceConfigRegistry and type(AceConfigRegistry.NotifyChange) == "function" then
|
||||
AceConfigRegistry:NotifyChange(ADDON_NAME)
|
||||
end
|
||||
end
|
||||
|
||||
local function MapCategoryValues()
|
||||
if HMGT.GetMapPOICategoryValues then
|
||||
return HMGT:GetMapPOICategoryValues()
|
||||
end
|
||||
return { default = "Default" }
|
||||
end
|
||||
|
||||
local function GetMapPOIs()
|
||||
local profile = HMGT.db and HMGT.db.profile
|
||||
if not profile then
|
||||
return {}
|
||||
end
|
||||
profile.mapOverlay = profile.mapOverlay or {}
|
||||
profile.mapOverlay.pois = profile.mapOverlay.pois or {}
|
||||
return profile.mapOverlay.pois
|
||||
end
|
||||
|
||||
local function HasPOIAt(index)
|
||||
return GetMapPOIs()[index] ~= nil
|
||||
end
|
||||
|
||||
local function HasAnyPOIs()
|
||||
return GetMapPOIs()[1] ~= nil
|
||||
end
|
||||
|
||||
local function GetPoiCount()
|
||||
local count = 0
|
||||
for _ in ipairs(GetMapPOIs()) do
|
||||
count = count + 1
|
||||
end
|
||||
return count
|
||||
end
|
||||
|
||||
local function EnsurePoiDraft(index)
|
||||
local poi = GetMapPOIs()[index]
|
||||
if not poi then
|
||||
return nil
|
||||
end
|
||||
|
||||
HMGT._mapPoiDrafts = HMGT._mapPoiDrafts or {}
|
||||
local draft = HMGT._mapPoiDrafts[index]
|
||||
if not draft then
|
||||
draft = {
|
||||
mapID = tostring(tonumber(poi.mapID) or ""),
|
||||
x = string.format("%.2f", tonumber(poi.x) or 0),
|
||||
y = string.format("%.2f", tonumber(poi.y) or 0),
|
||||
label = tostring(poi.label or ""),
|
||||
category = tostring(poi.category or "default"),
|
||||
}
|
||||
HMGT._mapPoiDrafts[index] = draft
|
||||
end
|
||||
|
||||
return draft
|
||||
end
|
||||
|
||||
local function BuildPoiEditorGroupArgs()
|
||||
local function GetPoiTitle(index)
|
||||
local poi = GetMapPOIs()[index]
|
||||
if not poi then
|
||||
return string.format("POI #%d", index)
|
||||
end
|
||||
return tostring(poi.label or ("POI " .. index))
|
||||
end
|
||||
|
||||
local args = {
|
||||
}
|
||||
|
||||
for index = 1, MAX_POI_EDITOR_ROWS do
|
||||
local row = index
|
||||
args["poi_" .. index] = {
|
||||
type = "group",
|
||||
name = function()
|
||||
return GetPoiTitle(row)
|
||||
end,
|
||||
icon = function()
|
||||
local poi = GetMapPOIs()[row]
|
||||
if not poi or not MapOverlay.GetCategoryVisual then
|
||||
return poi and poi.icon or nil
|
||||
end
|
||||
local texture = MapOverlay:GetCategoryVisual(poi.category)
|
||||
return texture or poi.icon or nil
|
||||
end,
|
||||
iconCoords = function()
|
||||
local poi = GetMapPOIs()[row]
|
||||
if not poi or not MapOverlay.GetCategoryVisual then
|
||||
return poi and poi.iconCoords or nil
|
||||
end
|
||||
local _, iconCoords = MapOverlay:GetCategoryVisual(poi.category)
|
||||
return iconCoords or poi.iconCoords or nil
|
||||
end,
|
||||
order = 20 + index,
|
||||
hidden = function()
|
||||
return not HasPOIAt(row)
|
||||
end,
|
||||
args = {
|
||||
details = {
|
||||
type = "description",
|
||||
order = 0.5,
|
||||
width = "full",
|
||||
name = function()
|
||||
local poi = GetMapPOIs()[row]
|
||||
if not poi then
|
||||
return "POI not found."
|
||||
end
|
||||
|
||||
local categoryValues = MapCategoryValues()
|
||||
local category = tostring(poi.category or "default")
|
||||
local categoryLabel = categoryValues[category] or category
|
||||
return string.format(
|
||||
"Map-ID: %d\nX: %.2f\nY: %.2f\nIcon: %s",
|
||||
tonumber(poi.mapID) or 0,
|
||||
tonumber(poi.x) or 0,
|
||||
tonumber(poi.y) or 0,
|
||||
tostring(categoryLabel)
|
||||
)
|
||||
end,
|
||||
},
|
||||
mapID = {
|
||||
type = "input",
|
||||
order = 1,
|
||||
width = 0.7,
|
||||
name = L["OPT_MAP_POI_MAPID"] or "Map ID",
|
||||
get = function()
|
||||
local draft = EnsurePoiDraft(row)
|
||||
return (draft and draft.mapID) or ""
|
||||
end,
|
||||
set = function(_, value)
|
||||
local draft = EnsurePoiDraft(row)
|
||||
if draft then
|
||||
draft.mapID = value
|
||||
end
|
||||
end,
|
||||
},
|
||||
x = {
|
||||
type = "input",
|
||||
order = 2,
|
||||
width = 0.7,
|
||||
name = L["OPT_MAP_POI_X"] or "X (0-100)",
|
||||
get = function()
|
||||
local draft = EnsurePoiDraft(row)
|
||||
return (draft and draft.x) or ""
|
||||
end,
|
||||
set = function(_, value)
|
||||
local draft = EnsurePoiDraft(row)
|
||||
if draft then
|
||||
draft.x = value
|
||||
end
|
||||
end,
|
||||
},
|
||||
y = {
|
||||
type = "input",
|
||||
order = 3,
|
||||
width = 0.7,
|
||||
name = L["OPT_MAP_POI_Y"] or "Y (0-100)",
|
||||
get = function()
|
||||
local draft = EnsurePoiDraft(row)
|
||||
return (draft and draft.y) or ""
|
||||
end,
|
||||
set = function(_, value)
|
||||
local draft = EnsurePoiDraft(row)
|
||||
if draft then
|
||||
draft.y = value
|
||||
end
|
||||
end,
|
||||
},
|
||||
label = {
|
||||
type = "input",
|
||||
order = 4,
|
||||
width = "full",
|
||||
name = L["OPT_MAP_POI_LABEL"] or "Label",
|
||||
get = function()
|
||||
local draft = EnsurePoiDraft(row)
|
||||
return (draft and draft.label) or ""
|
||||
end,
|
||||
set = function(_, value)
|
||||
local draft = EnsurePoiDraft(row)
|
||||
if draft then
|
||||
draft.label = value
|
||||
end
|
||||
end,
|
||||
},
|
||||
category = {
|
||||
type = "select",
|
||||
order = 5,
|
||||
width = "full",
|
||||
name = L["OPT_MAP_POI_CATEGORY"] or "Category",
|
||||
values = function()
|
||||
return MapCategoryValues()
|
||||
end,
|
||||
get = function()
|
||||
local draft = EnsurePoiDraft(row)
|
||||
return (draft and draft.category) or "default"
|
||||
end,
|
||||
set = function(_, value)
|
||||
local draft = EnsurePoiDraft(row)
|
||||
if draft then
|
||||
draft.category = value
|
||||
end
|
||||
end,
|
||||
},
|
||||
waypoint = {
|
||||
type = "execute",
|
||||
order = 6,
|
||||
width = "half",
|
||||
name = "Waypoint",
|
||||
func = function()
|
||||
local poi = GetMapPOIs()[row]
|
||||
if poi and MapOverlay.ToggleWaypointForPOI then
|
||||
MapOverlay:ToggleWaypointForPOI(poi)
|
||||
end
|
||||
end,
|
||||
},
|
||||
update = {
|
||||
type = "execute",
|
||||
order = 7,
|
||||
width = "half",
|
||||
name = L["OPT_MAP_POI_UPDATE"] or "Update POI",
|
||||
func = function()
|
||||
local draft = EnsurePoiDraft(row)
|
||||
if not draft then
|
||||
return
|
||||
end
|
||||
local ok = HMGT.UpdateMapPOI and HMGT:UpdateMapPOI(row, draft.mapID, draft.x, draft.y, draft.label, nil, draft.category)
|
||||
if ok then
|
||||
if HMGT._mapPoiDrafts then
|
||||
HMGT._mapPoiDrafts[row] = nil
|
||||
end
|
||||
HMGT:Print(L["OPT_MAP_POI_UPDATED"] or "HMGT: POI updated")
|
||||
else
|
||||
HMGT:Print(L["OPT_MAP_POI_UPDATE_FAILED"] or "HMGT: could not update POI")
|
||||
end
|
||||
NotifyOptionsChanged()
|
||||
end,
|
||||
},
|
||||
delete = {
|
||||
type = "execute",
|
||||
order = 8,
|
||||
width = "half",
|
||||
name = L["OPT_MAP_POI_REMOVE"] or "Remove POI",
|
||||
func = function()
|
||||
local ok = HMGT.RemoveMapPOI and HMGT:RemoveMapPOI(row)
|
||||
if ok then
|
||||
HMGT._mapPoiDrafts = nil
|
||||
HMGT:Print(L["OPT_MAP_POI_REMOVED"] or "HMGT: POI removed")
|
||||
else
|
||||
HMGT:Print(L["OPT_MAP_POI_REMOVE_FAILED"] or "HMGT: could not remove POI")
|
||||
end
|
||||
NotifyOptionsChanged()
|
||||
end,
|
||||
},
|
||||
},
|
||||
}
|
||||
end
|
||||
|
||||
return args
|
||||
end
|
||||
|
||||
function MapOverlay:GetOptionsGroup()
|
||||
local group = {
|
||||
type = "group",
|
||||
name = L["OPT_MODULE_MAP_OVERLAY"] or "Map Overlay",
|
||||
order = 30,
|
||||
childGroups = "tree",
|
||||
args = {
|
||||
general = {
|
||||
type = "group",
|
||||
order = 1,
|
||||
name = GENERAL or "General",
|
||||
args = {
|
||||
enabled = {
|
||||
type = "toggle",
|
||||
order = 1,
|
||||
width = "full",
|
||||
name = L["OPT_MAP_ENABLED"] or "Enable map overlay",
|
||||
get = function()
|
||||
local profile = HMGT.db and HMGT.db.profile
|
||||
if not profile then
|
||||
return true
|
||||
end
|
||||
profile.mapOverlay = profile.mapOverlay or {}
|
||||
return profile.mapOverlay.enabled ~= false
|
||||
end,
|
||||
set = function(_, value)
|
||||
local profile = HMGT.db and HMGT.db.profile
|
||||
if not profile then
|
||||
return
|
||||
end
|
||||
profile.mapOverlay = profile.mapOverlay or {}
|
||||
profile.mapOverlay.enabled = value
|
||||
if MapOverlay.Refresh then
|
||||
MapOverlay:Refresh()
|
||||
end
|
||||
end,
|
||||
},
|
||||
iconSize = {
|
||||
type = "range",
|
||||
order = 2,
|
||||
min = 8,
|
||||
max = 48,
|
||||
step = 1,
|
||||
name = L["OPT_MAP_ICON_SIZE"] or "Icon size",
|
||||
get = function()
|
||||
local profile = HMGT.db and HMGT.db.profile
|
||||
if not profile then
|
||||
return 16
|
||||
end
|
||||
profile.mapOverlay = profile.mapOverlay or {}
|
||||
return profile.mapOverlay.iconSize or 16
|
||||
end,
|
||||
set = function(_, value)
|
||||
local profile = HMGT.db and HMGT.db.profile
|
||||
if not profile then
|
||||
return
|
||||
end
|
||||
profile.mapOverlay = profile.mapOverlay or {}
|
||||
profile.mapOverlay.iconSize = value
|
||||
if MapOverlay.Refresh then
|
||||
MapOverlay:Refresh()
|
||||
end
|
||||
end,
|
||||
},
|
||||
alpha = {
|
||||
type = "range",
|
||||
order = 3,
|
||||
min = 0.1,
|
||||
max = 1,
|
||||
step = 0.05,
|
||||
name = L["OPT_MAP_ALPHA"] or "Icon alpha",
|
||||
get = function()
|
||||
local profile = HMGT.db and HMGT.db.profile
|
||||
if not profile then
|
||||
return 1
|
||||
end
|
||||
profile.mapOverlay = profile.mapOverlay or {}
|
||||
return profile.mapOverlay.alpha or 1
|
||||
end,
|
||||
set = function(_, value)
|
||||
local profile = HMGT.db and HMGT.db.profile
|
||||
if not profile then
|
||||
return
|
||||
end
|
||||
profile.mapOverlay = profile.mapOverlay or {}
|
||||
profile.mapOverlay.alpha = value
|
||||
if MapOverlay.Refresh then
|
||||
MapOverlay:Refresh()
|
||||
end
|
||||
end,
|
||||
},
|
||||
showLabels = {
|
||||
type = "toggle",
|
||||
order = 4,
|
||||
width = "full",
|
||||
name = L["OPT_MAP_SHOW_LABELS"] or "Show labels",
|
||||
get = function()
|
||||
local profile = HMGT.db and HMGT.db.profile
|
||||
if not profile then
|
||||
return true
|
||||
end
|
||||
profile.mapOverlay = profile.mapOverlay or {}
|
||||
return profile.mapOverlay.showLabels ~= false
|
||||
end,
|
||||
set = function(_, value)
|
||||
local profile = HMGT.db and HMGT.db.profile
|
||||
if not profile then
|
||||
return
|
||||
end
|
||||
profile.mapOverlay = profile.mapOverlay or {}
|
||||
profile.mapOverlay.showLabels = value
|
||||
if MapOverlay.Refresh then
|
||||
MapOverlay:Refresh()
|
||||
end
|
||||
end,
|
||||
},
|
||||
poiSection = {
|
||||
type = "header",
|
||||
order = 10,
|
||||
name = L["OPT_MAP_POI_SECTION"] or "Custom POIs",
|
||||
},
|
||||
poiSummary = {
|
||||
type = "description",
|
||||
order = 10.1,
|
||||
width = "full",
|
||||
name = function()
|
||||
local count = GetPoiCount()
|
||||
if count <= 0 then
|
||||
return L["OPT_MAP_POI_EMPTY"] or "No POIs configured."
|
||||
end
|
||||
return string.format(
|
||||
"%s: %d\n%s",
|
||||
L["OPT_MAP_POI_LIST"] or "Current POIs",
|
||||
count,
|
||||
L["OPT_MAP_POI_SELECT_HINT"] or "Select a POI in the tree on the left to edit it."
|
||||
)
|
||||
end,
|
||||
},
|
||||
draftMapID = {
|
||||
type = "input",
|
||||
order = 11,
|
||||
width = 0.8,
|
||||
name = L["OPT_MAP_POI_MAPID"] or "Map ID",
|
||||
get = function()
|
||||
HMGT._mapDraft = HMGT._mapDraft or {}
|
||||
if not HMGT._mapDraft.mapID and MapOverlay.GetActiveMapID then
|
||||
local activeMap = MapOverlay:GetActiveMapID()
|
||||
if activeMap then
|
||||
HMGT._mapDraft.mapID = tostring(activeMap)
|
||||
end
|
||||
end
|
||||
return HMGT._mapDraft.mapID or ""
|
||||
end,
|
||||
set = function(_, value)
|
||||
HMGT._mapDraft = HMGT._mapDraft or {}
|
||||
HMGT._mapDraft.mapID = value
|
||||
end,
|
||||
},
|
||||
useCurrentPosition = {
|
||||
type = "execute",
|
||||
order = 11.1,
|
||||
width = "half",
|
||||
name = L["OPT_MAP_POI_USE_CURRENT"] or "Use current position",
|
||||
desc = L["OPT_MAP_POI_USE_CURRENT_DESC"] or "Fill map ID, X and Y from your current player position",
|
||||
func = function()
|
||||
HMGT._mapDraft = HMGT._mapDraft or {}
|
||||
local mapID, x, y = nil, nil, nil
|
||||
if HMGT.GetCurrentMapPOIData then
|
||||
mapID, x, y = HMGT:GetCurrentMapPOIData()
|
||||
end
|
||||
|
||||
if mapID and x and y then
|
||||
HMGT._mapDraft.mapID = tostring(mapID)
|
||||
HMGT._mapDraft.x = string.format("%.2f", x)
|
||||
HMGT._mapDraft.y = string.format("%.2f", y)
|
||||
HMGT:Print(L["OPT_MAP_POI_CURRENT_SET"] or "HMGT: current position copied")
|
||||
else
|
||||
HMGT:Print(L["OPT_MAP_POI_CURRENT_FAILED"] or "HMGT: could not determine current position")
|
||||
end
|
||||
NotifyOptionsChanged()
|
||||
end,
|
||||
},
|
||||
draftX = {
|
||||
type = "input",
|
||||
order = 12,
|
||||
width = 0.8,
|
||||
name = L["OPT_MAP_POI_X"] or "X (0-100)",
|
||||
get = function()
|
||||
HMGT._mapDraft = HMGT._mapDraft or {}
|
||||
return HMGT._mapDraft.x or ""
|
||||
end,
|
||||
set = function(_, value)
|
||||
HMGT._mapDraft = HMGT._mapDraft or {}
|
||||
HMGT._mapDraft.x = value
|
||||
end,
|
||||
},
|
||||
draftY = {
|
||||
type = "input",
|
||||
order = 13,
|
||||
width = 0.8,
|
||||
name = L["OPT_MAP_POI_Y"] or "Y (0-100)",
|
||||
get = function()
|
||||
HMGT._mapDraft = HMGT._mapDraft or {}
|
||||
return HMGT._mapDraft.y or ""
|
||||
end,
|
||||
set = function(_, value)
|
||||
HMGT._mapDraft = HMGT._mapDraft or {}
|
||||
HMGT._mapDraft.y = value
|
||||
end,
|
||||
},
|
||||
draftLabel = {
|
||||
type = "input",
|
||||
order = 14,
|
||||
width = "full",
|
||||
name = L["OPT_MAP_POI_LABEL"] or "Label",
|
||||
get = function()
|
||||
HMGT._mapDraft = HMGT._mapDraft or {}
|
||||
return HMGT._mapDraft.label or ""
|
||||
end,
|
||||
set = function(_, value)
|
||||
HMGT._mapDraft = HMGT._mapDraft or {}
|
||||
HMGT._mapDraft.label = value
|
||||
end,
|
||||
},
|
||||
draftCategory = {
|
||||
type = "select",
|
||||
order = 14.1,
|
||||
width = "full",
|
||||
name = L["OPT_MAP_POI_CATEGORY"] or "Category",
|
||||
values = function()
|
||||
return MapCategoryValues()
|
||||
end,
|
||||
get = function()
|
||||
HMGT._mapDraft = HMGT._mapDraft or {}
|
||||
return HMGT._mapDraft.category or "default"
|
||||
end,
|
||||
set = function(_, value)
|
||||
HMGT._mapDraft = HMGT._mapDraft or {}
|
||||
HMGT._mapDraft.category = value
|
||||
end,
|
||||
},
|
||||
addPoi = {
|
||||
type = "execute",
|
||||
order = 15,
|
||||
width = "half",
|
||||
name = L["OPT_MAP_POI_ADD"] or "Add POI",
|
||||
func = function()
|
||||
HMGT._mapDraft = HMGT._mapDraft or {}
|
||||
local draft = HMGT._mapDraft
|
||||
local ok = HMGT.AddMapPOI and HMGT:AddMapPOI(draft.mapID, draft.x, draft.y, draft.label, nil, draft.category or "default")
|
||||
if ok then
|
||||
HMGT._mapPoiDrafts = nil
|
||||
HMGT:Print(L["OPT_MAP_POI_ADDED"] or "HMGT: POI added")
|
||||
else
|
||||
HMGT:Print(L["OPT_MAP_POI_ADD_FAILED"] or "HMGT: could not add POI")
|
||||
end
|
||||
NotifyOptionsChanged()
|
||||
end,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
for key, value in pairs(BuildPoiEditorGroupArgs()) do
|
||||
group.args[key] = value
|
||||
end
|
||||
|
||||
return group
|
||||
end
|
||||
|
||||
HMGT_Config:RegisterOptionsProvider("map.overlay", function()
|
||||
return {
|
||||
path = "map.overlay",
|
||||
order = 30,
|
||||
group = MapOverlay:GetOptionsGroup(),
|
||||
}
|
||||
end)
|
||||
BIN
Modules/MapOverlay/Media/DefaultIcon.png
Normal file
BIN
Modules/MapOverlay/Media/DefaultIcon.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 2.6 KiB |
2814
Modules/RaidTimeline/RaidTimeline.lua
Normal file
2814
Modules/RaidTimeline/RaidTimeline.lua
Normal file
File diff suppressed because it is too large
Load Diff
202
Modules/RaidTimeline/RaidTimelineBigWigs.lua
Normal file
202
Modules/RaidTimeline/RaidTimelineBigWigs.lua
Normal file
@@ -0,0 +1,202 @@
|
||||
local ADDON_NAME = "HailMaryGuildTools"
|
||||
local HMGT = LibStub("AceAddon-3.0"):GetAddon(ADDON_NAME)
|
||||
if not HMGT then return end
|
||||
|
||||
local RT = HMGT.RaidTimeline
|
||||
if not RT then return end
|
||||
|
||||
local function TrimText(value)
|
||||
local text = tostring(value or "")
|
||||
text = string.gsub(text, "^%s+", "")
|
||||
text = string.gsub(text, "%s+$", "")
|
||||
return text
|
||||
end
|
||||
|
||||
local function StripBarDisplayText(value)
|
||||
local text = tostring(value or "")
|
||||
text = string.gsub(text, "|T.-|t", "")
|
||||
text = string.gsub(text, "|c%x%x%x%x%x%x%x%x", "")
|
||||
text = string.gsub(text, "|r", "")
|
||||
return TrimText(text)
|
||||
end
|
||||
|
||||
local function NormalizeBossAbilityBarName(value)
|
||||
local text = TrimText(value)
|
||||
if text == "" then
|
||||
return ""
|
||||
end
|
||||
text = string.gsub(text, "%s*%(%d+%)%s*$", "")
|
||||
text = string.gsub(text, "%s*%([Cc]ount%)%s*$", "")
|
||||
text = string.gsub(text, "^%s*%[([A-Za-z])%]%s*", "%1 ")
|
||||
text = string.gsub(text, "%s*%[([A-Za-z])%]%s*$", " (%1)")
|
||||
text = string.gsub(text, "%s*%(([A-Za-z])%)%s*$", " (%1)")
|
||||
text = string.lower(text)
|
||||
return TrimText(text)
|
||||
end
|
||||
|
||||
local function EnsureBigWigsBridge()
|
||||
if RT._bigWigsBridgeRegistered == true then
|
||||
return true
|
||||
end
|
||||
if type(BigWigsLoader) ~= "table" or type(BigWigsLoader.RegisterMessage) ~= "function" then
|
||||
return false
|
||||
end
|
||||
|
||||
RT._bigWigsObservedBars = RT._bigWigsObservedBars or setmetatable({}, { __mode = "k" })
|
||||
RT._bigWigsReceiver = RT._bigWigsReceiver or {}
|
||||
|
||||
function RT._bigWigsReceiver:OnBigWigsBarCreated(_, plugin, bar, module, key, text, time)
|
||||
RT._bigWigsObservedBars[bar] = {
|
||||
plugin = plugin,
|
||||
module = module,
|
||||
key = key,
|
||||
createdText = tostring(text or ""),
|
||||
duration = tonumber(time) or 0,
|
||||
}
|
||||
end
|
||||
|
||||
function RT._bigWigsReceiver:OnBigWigsStopBar(_, plugin, module, text)
|
||||
local targetText = StripBarDisplayText(text)
|
||||
for bar, info in pairs(RT._bigWigsObservedBars) do
|
||||
local currentText = ""
|
||||
if type(bar) == "table" and type(bar.GetText) == "function" then
|
||||
currentText = StripBarDisplayText(bar:GetText())
|
||||
end
|
||||
if (info and info.plugin == plugin or not info or not info.plugin)
|
||||
and (currentText == targetText or StripBarDisplayText(info and info.createdText) == targetText) then
|
||||
RT._bigWigsObservedBars[bar] = nil
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
BigWigsLoader.RegisterMessage(RT._bigWigsReceiver, "BigWigs_BarCreated", "OnBigWigsBarCreated")
|
||||
BigWigsLoader.RegisterMessage(RT._bigWigsReceiver, "BigWigs_StopBar", "OnBigWigsStopBar")
|
||||
RT._bigWigsBridgeRegistered = true
|
||||
return true
|
||||
end
|
||||
|
||||
function RT:GetObservedBigWigsBars()
|
||||
local observed = {}
|
||||
if not EnsureBigWigsBridge() then
|
||||
return observed
|
||||
end
|
||||
|
||||
for bar, info in pairs(self._bigWigsObservedBars or {}) do
|
||||
local rawText = nil
|
||||
if type(bar) == "table" and type(bar.GetText) == "function" then
|
||||
rawText = bar:GetText()
|
||||
end
|
||||
rawText = StripBarDisplayText(rawText or (info and info.createdText) or "")
|
||||
local remaining = math.max(0, tonumber(bar and bar.remaining) or 0)
|
||||
if rawText ~= "" and remaining > 0 then
|
||||
observed[#observed + 1] = {
|
||||
rawText = rawText,
|
||||
normalizedName = NormalizeBossAbilityBarName(rawText),
|
||||
count = tonumber(string.match(rawText, "%((%d+)%)%s*$")) or 1,
|
||||
seconds = remaining,
|
||||
key = info and info.key or nil,
|
||||
}
|
||||
else
|
||||
self._bigWigsObservedBars[bar] = nil
|
||||
end
|
||||
end
|
||||
|
||||
return observed
|
||||
end
|
||||
|
||||
function RT:ProbeBossAbilityEntry(entryIndex, entry)
|
||||
if not self:IsLocalEditor() or not self.activeEncounterId or self:GetTriggerType(entry) ~= "bossAbility" then
|
||||
return
|
||||
end
|
||||
|
||||
local desiredRawName = TrimText(entry and entry.bossAbilityBarName or "")
|
||||
local desiredName = NormalizeBossAbilityBarName(desiredRawName)
|
||||
local desiredSelector = self:NormalizeCastCountSelector(entry and entry.castCount)
|
||||
local desiredSelectorText = self:FormatCastCountSelector(desiredSelector)
|
||||
if desiredName == "" then
|
||||
self:LogBossAbilityProbe(
|
||||
entryIndex,
|
||||
entry,
|
||||
"missing-bar-name",
|
||||
"RaidTimeline BigWigs probe encounter=%s slot=%s skipped=missing-bar-name",
|
||||
tostring(self.activeEncounterId),
|
||||
tostring(entryIndex)
|
||||
)
|
||||
return
|
||||
end
|
||||
|
||||
local bars = self:GetObservedBigWigsBars()
|
||||
if #bars == 0 then
|
||||
self:LogBossAbilityProbe(
|
||||
entryIndex,
|
||||
entry,
|
||||
"no-bars",
|
||||
"RaidTimeline BigWigs probe encounter=%s slot=%s target=%s normalized=%s selector=%s foundBar=false bars=0",
|
||||
tostring(self.activeEncounterId),
|
||||
tostring(entryIndex),
|
||||
tostring(desiredRawName),
|
||||
tostring(desiredName),
|
||||
tostring(desiredSelectorText)
|
||||
)
|
||||
return
|
||||
end
|
||||
|
||||
local seenNames = {}
|
||||
local foundName = false
|
||||
for _, observed in ipairs(bars) do
|
||||
seenNames[#seenNames + 1] = string.format("%s{key=%s}[%d]=%.1fs", tostring(observed.rawText), tostring(observed.key), tonumber(observed.count) or 1, tonumber(observed.seconds) or 0)
|
||||
if observed.normalizedName == desiredName then
|
||||
foundName = true
|
||||
if self:DoesCastCountSelectorMatch(desiredSelector, observed.count) then
|
||||
self:LogBossAbilityProbe(
|
||||
entryIndex,
|
||||
entry,
|
||||
string.format("match:%s:%s:%d", desiredName, tostring(desiredSelector), tonumber(observed.count) or 1),
|
||||
"RaidTimeline BigWigs probe encounter=%s slot=%s target=%s normalized=%s selector=%s foundBar=true countMatch=true observed=%s key=%s observedCount=%d remaining=%.1f",
|
||||
tostring(self.activeEncounterId),
|
||||
tostring(entryIndex),
|
||||
tostring(desiredRawName),
|
||||
tostring(desiredName),
|
||||
tostring(desiredSelectorText),
|
||||
tostring(observed.rawText),
|
||||
tostring(observed.key),
|
||||
tonumber(observed.count) or 1,
|
||||
tonumber(observed.seconds) or 0
|
||||
)
|
||||
self:TryDispatchBossAbilityEntry(entryIndex, entry, observed.count, observed.seconds)
|
||||
return
|
||||
end
|
||||
|
||||
self:LogBossAbilityProbe(
|
||||
entryIndex,
|
||||
entry,
|
||||
string.format("count-mismatch:%s:%d", desiredName, observed.count),
|
||||
"RaidTimeline BigWigs probe encounter=%s slot=%s target=%s normalized=%s selector=%s foundBar=true countMatch=false observed=%s key=%s observedCount=%d remaining=%.1f",
|
||||
tostring(self.activeEncounterId),
|
||||
tostring(entryIndex),
|
||||
tostring(desiredRawName),
|
||||
tostring(desiredName),
|
||||
tostring(desiredSelectorText),
|
||||
tostring(observed.rawText),
|
||||
tostring(observed.key),
|
||||
tonumber(observed.count) or 1,
|
||||
tonumber(observed.seconds) or 0
|
||||
)
|
||||
end
|
||||
end
|
||||
|
||||
if not foundName then
|
||||
self:LogBossAbilityProbe(
|
||||
entryIndex,
|
||||
entry,
|
||||
string.format("name-miss:%s", desiredName),
|
||||
"RaidTimeline BigWigs probe encounter=%s slot=%s target=%s normalized=%s selector=%s foundBar=false bars=%s",
|
||||
tostring(self.activeEncounterId),
|
||||
tostring(entryIndex),
|
||||
tostring(desiredRawName),
|
||||
tostring(desiredName),
|
||||
tostring(desiredSelectorText),
|
||||
table.concat(seenNames, ", ")
|
||||
)
|
||||
end
|
||||
end
|
||||
52
Modules/RaidTimeline/RaidTimelineBossAbilityData.lua
Normal file
52
Modules/RaidTimeline/RaidTimelineBossAbilityData.lua
Normal file
@@ -0,0 +1,52 @@
|
||||
local ADDON_NAME = "HailMaryGuildTools"
|
||||
local HMGT = LibStub("AceAddon-3.0"):GetAddon(ADDON_NAME)
|
||||
if not HMGT then return end
|
||||
|
||||
HMGT.RaidTimelineBossAbilityData = HMGT.RaidTimelineBossAbilityData or {
|
||||
raids = {
|
||||
--[[
|
||||
{
|
||||
name = "Void Spire",
|
||||
journalInstanceId = 1307,
|
||||
bosses = {
|
||||
[3176] = {
|
||||
name = "Imperator Averzian",
|
||||
abilities = {
|
||||
{
|
||||
key = "gloom",
|
||||
name = "Gloom",
|
||||
spellId = 123456,
|
||||
icon = 1234567,
|
||||
difficulties = {
|
||||
lfr = false,
|
||||
normal = true,
|
||||
heroic = true,
|
||||
mythic = true,
|
||||
},
|
||||
triggers = {
|
||||
bigwigs = { 123456, "gloom" },
|
||||
dbm = { 123456 },
|
||||
},
|
||||
},
|
||||
{
|
||||
key = "mythic_gloom",
|
||||
name = "Mythic Gloom",
|
||||
spellId = 123457,
|
||||
difficulties = {
|
||||
lfr = false,
|
||||
normal = false,
|
||||
heroic = false,
|
||||
mythic = true,
|
||||
},
|
||||
triggers = {
|
||||
bigwigs = { 123457 },
|
||||
dbm = { 123457 },
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
]]
|
||||
},
|
||||
}
|
||||
8
Modules/RaidTimeline/RaidTimelineDBM.lua
Normal file
8
Modules/RaidTimeline/RaidTimelineDBM.lua
Normal file
@@ -0,0 +1,8 @@
|
||||
local ADDON_NAME = "HailMaryGuildTools"
|
||||
local HMGT = LibStub("AceAddon-3.0"):GetAddon(ADDON_NAME)
|
||||
if not HMGT then return end
|
||||
|
||||
local RT = HMGT.RaidTimeline
|
||||
if not RT then return end
|
||||
|
||||
-- Placeholder for later DBM-specific raid timeline integration.
|
||||
799
Modules/RaidTimeline/RaidTimelineOptions.lua
Normal file
799
Modules/RaidTimeline/RaidTimelineOptions.lua
Normal file
@@ -0,0 +1,799 @@
|
||||
local ADDON_NAME = "HailMaryGuildTools"
|
||||
local HMGT = LibStub("AceAddon-3.0"):GetAddon(ADDON_NAME)
|
||||
if not HMGT then return end
|
||||
|
||||
local RT = HMGT.RaidTimeline
|
||||
if not RT then return end
|
||||
if not HMGT_Config or not HMGT_Config.RegisterOptionsProvider then return end
|
||||
|
||||
local L = LibStub("AceLocale-3.0"):GetLocale(ADDON_NAME, true) or {}
|
||||
local AceConfigRegistry = LibStub("AceConfigRegistry-3.0")
|
||||
local LSM = LibStub("LibSharedMedia-3.0", true)
|
||||
|
||||
local FONT_OUTLINE_VALUES = {
|
||||
NONE = NONE or "None",
|
||||
OUTLINE = "Outline",
|
||||
THICKOUTLINE = "Thick Outline",
|
||||
MONOCHROME = "Monochrome",
|
||||
["OUTLINE,MONOCHROME"] = "Outline Monochrome",
|
||||
}
|
||||
|
||||
local DIFFICULTY_KEYS = { "lfr", "normal", "heroic", "mythic" }
|
||||
local MAX_ENTRY_ROWS = 24
|
||||
local raidTimelineOptionsGroup
|
||||
local raidCooldownSpellValuesCache
|
||||
local bossAbilityValuesCache = {}
|
||||
|
||||
local function ClearOptionCaches()
|
||||
raidCooldownSpellValuesCache = nil
|
||||
bossAbilityValuesCache = {}
|
||||
end
|
||||
|
||||
local function Notify(rebuild)
|
||||
if rebuild then
|
||||
ClearOptionCaches()
|
||||
end
|
||||
if rebuild and raidTimelineOptionsGroup then
|
||||
local fresh = RT:GetOptionsGroup()
|
||||
raidTimelineOptionsGroup.name = fresh.name
|
||||
raidTimelineOptionsGroup.order = fresh.order
|
||||
raidTimelineOptionsGroup.childGroups = fresh.childGroups
|
||||
raidTimelineOptionsGroup.args = fresh.args
|
||||
end
|
||||
AceConfigRegistry:NotifyChange(ADDON_NAME)
|
||||
end
|
||||
|
||||
local function GetDrafts()
|
||||
HMGT._raidTimelineDraft = HMGT._raidTimelineDraft or {
|
||||
addEncounterId = "",
|
||||
addEncounterName = "",
|
||||
entries = {},
|
||||
}
|
||||
HMGT._raidTimelineDraft.entries = HMGT._raidTimelineDraft.entries or {}
|
||||
return HMGT._raidTimelineDraft
|
||||
end
|
||||
|
||||
local function TrimText(value)
|
||||
return tostring(value or ""):gsub("^%s+", ""):gsub("%s+$", "")
|
||||
end
|
||||
|
||||
local function GetEncounterDraft(encounterId)
|
||||
local drafts = GetDrafts()
|
||||
local key = tostring(tonumber(encounterId) or encounterId or "")
|
||||
drafts.entries[key] = drafts.entries[key] or {
|
||||
time = "",
|
||||
spellId = 0,
|
||||
alertText = "",
|
||||
playerName = "",
|
||||
entryType = "spell",
|
||||
triggerType = "time",
|
||||
actionType = "raidCooldown",
|
||||
targetSpec = "",
|
||||
bossAbilityId = "",
|
||||
bossAbilityBarName = "",
|
||||
castCount = "1",
|
||||
}
|
||||
return drafts.entries[key]
|
||||
end
|
||||
|
||||
local function GetTriggerTypeValues()
|
||||
return (RT.GetTriggerTypeValues and RT:GetTriggerTypeValues()) or {
|
||||
time = L["OPT_RT_TRIGGER_TIME"] or "Time",
|
||||
bossAbility = L["OPT_RT_TRIGGER_BOSS_ABILITY"] or "Boss ability",
|
||||
}
|
||||
end
|
||||
|
||||
local function GetActionTypeValues()
|
||||
return (RT.GetActionTypeValues and RT:GetActionTypeValues()) or {
|
||||
text = L["OPT_RT_ACTION_TEXT"] or "Text",
|
||||
raidCooldown = L["OPT_RT_ACTION_RAID_COOLDOWN"] or "Raid Cooldown",
|
||||
}
|
||||
end
|
||||
|
||||
local function GetEncounterEntry(encounterId, row)
|
||||
local encounter = encounterId and RT:GetEncounter(encounterId)
|
||||
return encounter and encounter.entries and encounter.entries[row] or nil
|
||||
end
|
||||
|
||||
local function GetTargetFieldLabel(kind, isAddRow)
|
||||
return (isAddRow and (L["OPT_RT_ADD_PLAYER"] or "Target")) or (L["OPT_RT_ENTRY_PLAYER"] or "Target")
|
||||
end
|
||||
|
||||
local function GetTargetFieldDesc(kind)
|
||||
return L["OPT_RT_ADD_PLAYER_DESC"] or "Optional. Comma-separated player names or variables like Group1, Group8, GroupEven, GroupOdd."
|
||||
end
|
||||
|
||||
local function FormatEntryTime(value)
|
||||
return RT.FormatTimelineClock and RT:FormatTimelineClock(value) or tostring(value or "")
|
||||
end
|
||||
|
||||
local function GetBossAbilityValues(encounterId)
|
||||
local encounterKey = tostring(tonumber(encounterId) or encounterId or 0)
|
||||
if bossAbilityValuesCache[encounterKey] then
|
||||
return bossAbilityValuesCache[encounterKey]
|
||||
end
|
||||
local values = RT.GetBossAbilityValues and RT:GetBossAbilityValues(encounterId) or { [""] = L["OPT_RT_NO_BOSS_ABILITY"] or "No boss ability" }
|
||||
bossAbilityValuesCache[encounterKey] = values
|
||||
return values
|
||||
end
|
||||
|
||||
local function GetDraftTriggerType(encounterId)
|
||||
local draft = GetEncounterDraft(encounterId)
|
||||
draft.triggerType = tostring(draft.triggerType or "time")
|
||||
return draft.triggerType == "bossAbility" and "bossAbility" or "time"
|
||||
end
|
||||
|
||||
local function GetDraftActionType(encounterId)
|
||||
local draft = GetEncounterDraft(encounterId)
|
||||
draft.actionType = tostring(draft.actionType or "raidCooldown")
|
||||
return draft.actionType == "text" and "text" or "raidCooldown"
|
||||
end
|
||||
|
||||
local function GetSpellName(spellId)
|
||||
local sid = tonumber(spellId)
|
||||
if not sid or sid <= 0 then return nil end
|
||||
if C_Spell and type(C_Spell.GetSpellName) == "function" then
|
||||
local name = C_Spell.GetSpellName(sid)
|
||||
if type(name) == "string" and name ~= "" then
|
||||
return name
|
||||
end
|
||||
end
|
||||
if type(GetSpellInfo) == "function" then
|
||||
local name = GetSpellInfo(sid)
|
||||
if type(name) == "string" and name ~= "" then
|
||||
return name
|
||||
end
|
||||
end
|
||||
return nil
|
||||
end
|
||||
|
||||
local function GetSpellIcon(spellId)
|
||||
local sid = tonumber(spellId)
|
||||
if not sid or sid <= 0 then return nil end
|
||||
if HMGT_SpellData and type(HMGT_SpellData.GetSpellIcon) == "function" then
|
||||
local icon = HMGT_SpellData.GetSpellIcon(sid)
|
||||
if icon and icon ~= "" then
|
||||
return icon
|
||||
end
|
||||
end
|
||||
if C_Spell and type(C_Spell.GetSpellTexture) == "function" then
|
||||
local icon = C_Spell.GetSpellTexture(sid)
|
||||
if icon and icon ~= "" then
|
||||
return icon
|
||||
end
|
||||
end
|
||||
local _, _, icon = GetSpellInfo(sid)
|
||||
return icon
|
||||
end
|
||||
|
||||
local function GetRaidCooldownSpellValues()
|
||||
if raidCooldownSpellValuesCache then
|
||||
return raidCooldownSpellValuesCache
|
||||
end
|
||||
local values = { [0] = L["OPT_RT_NO_SPELL"] or "No spell" }
|
||||
local seen = { [0] = true }
|
||||
for _, entry in ipairs(HMGT_SpellData and HMGT_SpellData.RaidCooldowns or {}) do
|
||||
local spellId = tonumber(entry and entry.spellId)
|
||||
if spellId and spellId > 0 and not seen[spellId] then
|
||||
seen[spellId] = true
|
||||
local spellName = GetSpellName(spellId) or tostring(entry.name or ("Spell " .. spellId))
|
||||
local icon = GetSpellIcon(spellId)
|
||||
values[spellId] = icon and icon ~= ""
|
||||
and string.format("|T%s:16:16:0:0|t %s (%d)", tostring(icon), spellName, spellId)
|
||||
or string.format("%s (%d)", spellName, spellId)
|
||||
end
|
||||
end
|
||||
raidCooldownSpellValuesCache = values
|
||||
return values
|
||||
end
|
||||
|
||||
local function GetDifficultyLabel(key)
|
||||
if key == "lfr" then return L["OPT_RT_DIFF_LFR"] or "LFR" end
|
||||
if key == "heroic" then return L["OPT_RT_DIFF_HEROIC"] or "HC" end
|
||||
if key == "mythic" then return L["OPT_RT_DIFF_MYTHIC"] or "Mythic" end
|
||||
return L["OPT_RT_DIFF_NORMAL"] or "Normal"
|
||||
end
|
||||
|
||||
local function GetEncounterLabel(encounterId)
|
||||
local encounter = RT:GetEncounter(encounterId)
|
||||
local name = TrimText(encounter and encounter.name or "")
|
||||
if name == "" then
|
||||
name = L["OPT_RT_ENCOUNTER"] or "Encounter"
|
||||
end
|
||||
return string.format("%s (%d)", name, tonumber(encounterId) or 0)
|
||||
end
|
||||
|
||||
local function GetBossTreeLabel(encounterId)
|
||||
local encounter = RT:GetEncounter(encounterId)
|
||||
local name = TrimText(encounter and encounter.name or "")
|
||||
if name == "" then
|
||||
name = L["OPT_RT_ENCOUNTER"] or "Encounter"
|
||||
end
|
||||
return string.format("%s (%d)", name, #(encounter and encounter.entries or {}))
|
||||
end
|
||||
|
||||
local function GetRaidBuckets()
|
||||
local buckets, raidNames = {}, {}
|
||||
for _, encounterId in ipairs(RT:GetEncounterIds()) do
|
||||
local encounter = RT:GetEncounter(encounterId)
|
||||
if encounter then
|
||||
local journalInstanceId, instanceName = RT:GetEncounterInstanceInfo(encounterId)
|
||||
local raidName = tostring(encounter.instanceName or instanceName or (L["OPT_RT_RAID_DEFAULT"] or "Encounter"))
|
||||
if not buckets[raidName] then
|
||||
buckets[raidName] = {
|
||||
key = tostring(journalInstanceId or raidName),
|
||||
ids = {},
|
||||
difficulties = { lfr = {}, normal = {}, heroic = {}, mythic = {} },
|
||||
}
|
||||
table.insert(raidNames, raidName)
|
||||
end
|
||||
table.insert(buckets[raidName].ids, encounterId)
|
||||
for _, difficultyKey in ipairs(DIFFICULTY_KEYS) do
|
||||
if encounter.difficulties and encounter.difficulties[difficultyKey] == true then
|
||||
table.insert(buckets[raidName].difficulties[difficultyKey], encounterId)
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
table.sort(raidNames)
|
||||
for _, raidName in ipairs(raidNames) do
|
||||
table.sort(buckets[raidName].ids)
|
||||
for _, difficultyKey in ipairs(DIFFICULTY_KEYS) do
|
||||
table.sort(buckets[raidName].difficulties[difficultyKey])
|
||||
end
|
||||
end
|
||||
return raidNames, buckets
|
||||
end
|
||||
|
||||
local function BuildEntryEditorArgs(encounterId)
|
||||
local args = {
|
||||
entriesHeader = {
|
||||
type = "header",
|
||||
order = 20,
|
||||
name = L["OPT_RT_ENCOUNTERS_HEADER"] or "Encounter timelines",
|
||||
},
|
||||
addTime = {
|
||||
type = "input", order = 21, width = 0.7, name = L["OPT_RT_ADD_TIME"] or "Time (MM:SS)",
|
||||
disabled = function() return not RT:IsLocalEditor() end,
|
||||
get = function() return tostring(GetEncounterDraft(encounterId).time or "") end,
|
||||
set = function(_, val) GetEncounterDraft(encounterId).time = val end,
|
||||
hidden = function() return GetDraftTriggerType(encounterId) == "bossAbility" end,
|
||||
},
|
||||
addTriggerType = {
|
||||
type = "select", order = 22, width = 0.8, name = L["OPT_RT_TRIGGER"] or "Trigger",
|
||||
disabled = function() return not RT:IsLocalEditor() end,
|
||||
values = GetTriggerTypeValues,
|
||||
get = function() return GetDraftTriggerType(encounterId) end,
|
||||
set = function(_, val)
|
||||
local draft = GetEncounterDraft(encounterId)
|
||||
draft.triggerType = (tostring(val or "") == "bossAbility") and "bossAbility" or "time"
|
||||
if draft.triggerType == "time" then
|
||||
draft.bossAbilityId = ""
|
||||
draft.bossAbilityBarName = ""
|
||||
draft.castCount = "1"
|
||||
end
|
||||
Notify(false)
|
||||
end,
|
||||
},
|
||||
addActionType = {
|
||||
type = "select", order = 22.1, width = 1.0, name = L["OPT_RT_ACTION"] or "Action",
|
||||
disabled = function() return not RT:IsLocalEditor() end,
|
||||
values = GetActionTypeValues,
|
||||
get = function() return GetDraftActionType(encounterId) end,
|
||||
set = function(_, val)
|
||||
local draft = GetEncounterDraft(encounterId)
|
||||
draft.actionType = (tostring(val or "") == "text") and "text" or "raidCooldown"
|
||||
if draft.actionType == "text" then
|
||||
draft.spellId = 0
|
||||
end
|
||||
Notify(false)
|
||||
end,
|
||||
},
|
||||
addSpellId = {
|
||||
type = "select", order = 23, width = 1.2, name = L["OPT_RT_ADD_SPELL"] or "Spell",
|
||||
disabled = function() return not RT:IsLocalEditor() end,
|
||||
values = GetRaidCooldownSpellValues,
|
||||
get = function() return math.max(0, tonumber(GetEncounterDraft(encounterId).spellId) or 0) end,
|
||||
set = function(_, val) GetEncounterDraft(encounterId).spellId = math.max(0, tonumber(val) or 0) end,
|
||||
hidden = function() return GetDraftActionType(encounterId) ~= "raidCooldown" end,
|
||||
},
|
||||
addCastCount = {
|
||||
type = "input", order = 23.1, width = 0.7, name = L["OPT_RT_CAST_COUNT"] or "Cast count",
|
||||
disabled = function() return not RT:IsLocalEditor() end,
|
||||
desc = L["OPT_RT_CAST_COUNT_DESC"] or "Use a number, All, Odd, or Even.",
|
||||
get = function() return RT:FormatCastCountSelector(GetEncounterDraft(encounterId).castCount or "1") end,
|
||||
set = function(_, val) GetEncounterDraft(encounterId).castCount = RT:NormalizeCastCountSelector(val) end,
|
||||
hidden = function() return GetDraftTriggerType(encounterId) ~= "bossAbility" end,
|
||||
},
|
||||
addBossAbilityBarName = {
|
||||
type = "input", order = 23.2, width = 1.2, name = L["OPT_RT_BOSS_BAR_NAME"] or "Bossmod bar name",
|
||||
disabled = function() return not RT:IsLocalEditor() end,
|
||||
get = function() return tostring(GetEncounterDraft(encounterId).bossAbilityBarName or "") end,
|
||||
set = function(_, val) GetEncounterDraft(encounterId).bossAbilityBarName = tostring(val or "") end,
|
||||
hidden = function() return GetDraftTriggerType(encounterId) ~= "bossAbility" end,
|
||||
},
|
||||
addAlertText = {
|
||||
type = "input", order = 24, width = 1.2, name = L["OPT_RT_ADD_TEXT"] or "Custom text",
|
||||
disabled = function() return not RT:IsLocalEditor() end,
|
||||
get = function() return tostring(GetEncounterDraft(encounterId).alertText or "") end,
|
||||
set = function(_, val) GetEncounterDraft(encounterId).alertText = tostring(val or "") end,
|
||||
hidden = function()
|
||||
return GetDraftActionType(encounterId) ~= "text"
|
||||
end,
|
||||
},
|
||||
addPlayerName = {
|
||||
type = "input", order = 25, width = 1.2,
|
||||
name = function() return GetTargetFieldLabel(GetDraftActionType(encounterId), true) end,
|
||||
desc = function() return GetTargetFieldDesc(GetDraftActionType(encounterId)) end,
|
||||
disabled = function() return not RT:IsLocalEditor() end,
|
||||
get = function()
|
||||
local draft = GetEncounterDraft(encounterId)
|
||||
return tostring(draft.playerName or draft.targetSpec or "")
|
||||
end,
|
||||
set = function(_, val)
|
||||
local draft = GetEncounterDraft(encounterId)
|
||||
draft.playerName = tostring(val or "")
|
||||
draft.targetSpec = tostring(val or "")
|
||||
end,
|
||||
},
|
||||
addEntry = {
|
||||
type = "execute", order = 26, width = "full", name = L["OPT_RT_ADD_ENTRY"] or "Add entry",
|
||||
disabled = function() return not RT:IsLocalEditor() end,
|
||||
func = function()
|
||||
local draft = GetEncounterDraft(encounterId)
|
||||
local ok = RT:AddDetailedEntry(encounterId, draft)
|
||||
if ok then
|
||||
draft.time, draft.spellId, draft.alertText, draft.playerName, draft.entryType, draft.triggerType, draft.actionType, draft.targetSpec, draft.bossAbilityId, draft.bossAbilityBarName, draft.castCount = "", 0, "", "", "spell", "time", "raidCooldown", "", "", "", "1"
|
||||
Notify(true)
|
||||
else
|
||||
HMGT:Print(L["OPT_RT_ADD_ENTRY_INVALID"] or "HMGT: invalid raid timeline entry")
|
||||
end
|
||||
end,
|
||||
},
|
||||
addBreak = { type = "description", order = 27, width = "full", name = " " },
|
||||
}
|
||||
|
||||
for entryRow = 1, MAX_ENTRY_ROWS do
|
||||
local order = 40 + (entryRow * 10)
|
||||
args["entryTime_" .. entryRow] = {
|
||||
type = "input", order = order, width = 0.7,
|
||||
disabled = function() return not RT:IsLocalEditor() end,
|
||||
name = function() return entryRow == 1 and (L["OPT_RT_ENTRY_TIME"] or "Time") or "" end,
|
||||
get = function() local entry = GetEncounterEntry(encounterId, entryRow); return entry and FormatEntryTime(entry.time or 0) or "" end,
|
||||
set = function(_, val)
|
||||
if not RT:SetEntryField(encounterId, entryRow, "time", val) then
|
||||
HMGT:Print(L["OPT_RT_INVALID_TIME"] or "HMGT: invalid time")
|
||||
end
|
||||
Notify(false)
|
||||
end,
|
||||
hidden = function()
|
||||
local entry = GetEncounterEntry(encounterId, entryRow)
|
||||
return not entry or RT:GetTriggerType(entry) == "bossAbility"
|
||||
end,
|
||||
}
|
||||
args["entryTriggerType_" .. entryRow] = {
|
||||
type = "select", order = order + 1, width = 0.8,
|
||||
disabled = function() return not RT:IsLocalEditor() end,
|
||||
name = function() return entryRow == 1 and (L["OPT_RT_TRIGGER"] or "Trigger") or "" end,
|
||||
values = GetTriggerTypeValues,
|
||||
get = function() local entry = GetEncounterEntry(encounterId, entryRow); return entry and RT:GetTriggerType(entry) or "time" end,
|
||||
set = function(_, val) RT:SetEntryField(encounterId, entryRow, "triggerType", val); Notify(false) end,
|
||||
hidden = function() return GetEncounterEntry(encounterId, entryRow) == nil end,
|
||||
}
|
||||
args["entryActionType_" .. entryRow] = {
|
||||
type = "select", order = order + 1.1, width = 1.0,
|
||||
disabled = function() return not RT:IsLocalEditor() end,
|
||||
name = function() return entryRow == 1 and (L["OPT_RT_ACTION"] or "Action") or "" end,
|
||||
values = GetActionTypeValues,
|
||||
get = function() local entry = GetEncounterEntry(encounterId, entryRow); return entry and RT:GetActionType(entry) or "raidCooldown" end,
|
||||
set = function(_, val) RT:SetEntryField(encounterId, entryRow, "actionType", val); Notify(false) end,
|
||||
hidden = function() return GetEncounterEntry(encounterId, entryRow) == nil end,
|
||||
}
|
||||
args["entrySpell_" .. entryRow] = {
|
||||
type = "select", order = order + 2, width = 1.2,
|
||||
disabled = function() return not RT:IsLocalEditor() end,
|
||||
name = function() return entryRow == 1 and (L["OPT_RT_ENTRY_SPELL"] or "Spell") or "" end,
|
||||
values = GetRaidCooldownSpellValues,
|
||||
get = function() local entry = GetEncounterEntry(encounterId, entryRow); return entry and math.max(0, tonumber(entry.spellId) or 0) or 0 end,
|
||||
set = function(_, val)
|
||||
if not RT:SetEntryField(encounterId, entryRow, "spellId", math.max(0, tonumber(val) or 0)) then
|
||||
HMGT:Print(L["OPT_RT_INVALID_SPELL"] or "HMGT: invalid spell ID")
|
||||
end
|
||||
Notify(false)
|
||||
end,
|
||||
hidden = function() local entry = GetEncounterEntry(encounterId, entryRow); return not entry or RT:GetActionType(entry) ~= "raidCooldown" end,
|
||||
}
|
||||
args["entryCastCount_" .. entryRow] = {
|
||||
type = "input", order = order + 2.1, width = 0.7,
|
||||
disabled = function() return not RT:IsLocalEditor() end,
|
||||
name = function() return entryRow == 1 and (L["OPT_RT_CAST_COUNT"] or "Cast count") or "" end,
|
||||
desc = L["OPT_RT_CAST_COUNT_DESC"] or "Use a number, All, Odd, or Even.",
|
||||
get = function() local entry = GetEncounterEntry(encounterId, entryRow); return entry and RT:FormatCastCountSelector(entry.castCount) or "1" end,
|
||||
set = function(_, val) RT:SetEntryField(encounterId, entryRow, "castCount", val); Notify(false) end,
|
||||
hidden = function() local entry = GetEncounterEntry(encounterId, entryRow); return not entry or RT:GetTriggerType(entry) ~= "bossAbility" end,
|
||||
}
|
||||
args["entryBossAbilityBarName_" .. entryRow] = {
|
||||
type = "input", order = order + 2.2, width = 1.2,
|
||||
disabled = function() return not RT:IsLocalEditor() end,
|
||||
name = function() return entryRow == 1 and (L["OPT_RT_BOSS_BAR_NAME"] or "Bossmod bar name") or "" end,
|
||||
get = function() local entry = GetEncounterEntry(encounterId, entryRow); return entry and tostring(entry.bossAbilityBarName or "") or "" end,
|
||||
set = function(_, val) RT:SetEntryField(encounterId, entryRow, "bossAbilityBarName", val); Notify(false) end,
|
||||
hidden = function() local entry = GetEncounterEntry(encounterId, entryRow); return not entry or RT:GetTriggerType(entry) ~= "bossAbility" end,
|
||||
}
|
||||
args["entryText_" .. entryRow] = {
|
||||
type = "input", order = order + 3, width = 1.2,
|
||||
disabled = function() return not RT:IsLocalEditor() end,
|
||||
name = function() return entryRow == 1 and (L["OPT_RT_ENTRY_TEXT"] or "Custom text") or "" end,
|
||||
get = function() local entry = GetEncounterEntry(encounterId, entryRow); return entry and tostring(entry.alertText or "") or "" end,
|
||||
set = function(_, val) RT:SetEntryField(encounterId, entryRow, "alertText", val); Notify(false) end,
|
||||
hidden = function()
|
||||
local entry = GetEncounterEntry(encounterId, entryRow)
|
||||
if not entry then
|
||||
return true
|
||||
end
|
||||
return RT:GetActionType(entry) ~= "text"
|
||||
end,
|
||||
}
|
||||
args["entryPlayer_" .. entryRow] = {
|
||||
type = "input", order = order + 4, width = 1.1,
|
||||
disabled = function() return not RT:IsLocalEditor() end,
|
||||
name = function()
|
||||
local entry = GetEncounterEntry(encounterId, entryRow)
|
||||
return entryRow == 1 and GetTargetFieldLabel(entry and RT:GetActionType(entry) or "raidCooldown", false) or ""
|
||||
end,
|
||||
desc = function()
|
||||
local entry = GetEncounterEntry(encounterId, entryRow)
|
||||
return GetTargetFieldDesc(entry and RT:GetActionType(entry) or "raidCooldown")
|
||||
end,
|
||||
get = function()
|
||||
local entry = GetEncounterEntry(encounterId, entryRow)
|
||||
if not entry then return "" end
|
||||
return tostring(entry.playerName or entry.targetSpec or "")
|
||||
end,
|
||||
set = function(_, val)
|
||||
local entry = GetEncounterEntry(encounterId, entryRow)
|
||||
if entry then
|
||||
RT:SetEntryField(encounterId, entryRow, "playerName", val)
|
||||
RT:SetEntryField(encounterId, entryRow, "targetSpec", val)
|
||||
Notify(false)
|
||||
end
|
||||
end,
|
||||
hidden = function() return GetEncounterEntry(encounterId, entryRow) == nil end,
|
||||
}
|
||||
args["entryDelete_" .. entryRow] = {
|
||||
type = "execute", order = order + 5, width = 0.6, name = REMOVE or "Remove",
|
||||
disabled = function() return not RT:IsLocalEditor() end,
|
||||
func = function() RT:RemoveEntry(encounterId, entryRow); Notify(true) end,
|
||||
hidden = function() return GetEncounterEntry(encounterId, entryRow) == nil end,
|
||||
}
|
||||
args["entryBreak_" .. entryRow] = {
|
||||
type = "description", order = order + 6, width = "full", name = " ",
|
||||
hidden = function() return GetEncounterEntry(encounterId, entryRow) == nil end,
|
||||
}
|
||||
end
|
||||
return args
|
||||
end
|
||||
|
||||
local function BuildEncounterDetailGroup(encounterId)
|
||||
local encounter = RT:GetEncounter(encounterId)
|
||||
if not encounter then return nil end
|
||||
|
||||
local args = {
|
||||
header = { type = "header", order = 1, name = GetEncounterLabel(encounterId) },
|
||||
encounterId = {
|
||||
type = "description", order = 2, width = "full",
|
||||
name = string.format("|cffffd100%s|r: %d", L["OPT_RT_ADD_ENCOUNTER_ID"] or "Encounter ID", tonumber(encounterId) or 0),
|
||||
},
|
||||
raidName = {
|
||||
type = "description", order = 3, width = "full",
|
||||
name = function()
|
||||
local _, instanceName = RT:GetEncounterInstanceInfo(encounterId)
|
||||
local current = RT:GetEncounter(encounterId)
|
||||
return string.format("|cffffd100%s|r: %s", L["OPT_RT_RAID_NAME"] or "Raid", tostring((current and current.instanceName) or instanceName or (L["OPT_RT_RAID_DEFAULT"] or "Encounter")))
|
||||
end,
|
||||
},
|
||||
encounterName = {
|
||||
type = "input", order = 4, width = "full", name = L["OPT_RT_ENCOUNTER_NAME"] or "Name",
|
||||
disabled = function() return not RT:IsLocalEditor() end,
|
||||
get = function() local current = RT:GetEncounter(encounterId); return current and tostring(current.name or "") or "" end,
|
||||
set = function(_, val) local current = RT:GetEncounter(encounterId); if current then current.name = tostring(val or ""); Notify(true) end end,
|
||||
},
|
||||
difficultyHeader = { type = "header", order = 5, name = L["OPT_RT_DIFFICULTY_HEADER"] or "Difficulties" },
|
||||
}
|
||||
|
||||
local diffOrder = 6
|
||||
for _, difficultyKey in ipairs(DIFFICULTY_KEYS) do
|
||||
args["difficulty_" .. difficultyKey] = {
|
||||
type = "toggle", order = diffOrder, width = 0.75, name = GetDifficultyLabel(difficultyKey),
|
||||
disabled = function() return not RT:IsLocalEditor() end,
|
||||
get = function()
|
||||
local current = RT:GetEncounter(encounterId)
|
||||
local difficulties = current and current.difficulties or nil
|
||||
return difficulties and difficulties[difficultyKey] ~= false or false
|
||||
end,
|
||||
set = function(_, val)
|
||||
local current = RT:GetEncounter(encounterId)
|
||||
if current then
|
||||
current.difficulties = current.difficulties or {}
|
||||
current.difficulties[difficultyKey] = val and true or false
|
||||
Notify(true)
|
||||
end
|
||||
end,
|
||||
}
|
||||
diffOrder = diffOrder + 0.01
|
||||
end
|
||||
|
||||
args.openTimelineEditor = {
|
||||
type = "execute", order = 7, width = "full", name = L["OPT_RT_OPEN_EDITOR"] or "Open timeline",
|
||||
func = function() if RT.OpenTimelineEditor then RT:OpenTimelineEditor(encounterId) end end,
|
||||
}
|
||||
args.runTest = {
|
||||
type = "execute",
|
||||
order = 7.5,
|
||||
width = "full",
|
||||
name = function()
|
||||
if RT:IsTestRunning(encounterId) then
|
||||
return L["OPT_RT_STOP_TEST"] or "Stop test"
|
||||
end
|
||||
return L["OPT_RT_START_TEST"] or "Start timeline test"
|
||||
end,
|
||||
disabled = function() return not RT:IsLocalEditor() end,
|
||||
func = function()
|
||||
if RT:IsTestRunning(encounterId) then
|
||||
RT:StopEncounterTest()
|
||||
else
|
||||
RT:StartEncounterTest(encounterId)
|
||||
end
|
||||
Notify()
|
||||
end,
|
||||
}
|
||||
args.testHint = {
|
||||
type = "description",
|
||||
order = 7.6,
|
||||
width = "full",
|
||||
name = L["OPT_RT_TEST_HINT"] or "Runs the encounter timeline outside of combat so you can verify assignments, whispers and debug output.",
|
||||
}
|
||||
args.editorHint = {
|
||||
type = "description", order = 8, width = "full",
|
||||
name = L["OPT_RT_TIMELINE_HINT"] or "Click the bar to add a cooldown. Drag markers horizontally to change the time. Mousewheel scrolls, Ctrl+Mousewheel zooms.",
|
||||
}
|
||||
args.encounterDelete = {
|
||||
type = "execute", order = 9, width = "full", name = DELETE or "Delete",
|
||||
disabled = function() return not RT:IsLocalEditor() end,
|
||||
confirm = function() return string.format(L["OPT_RT_DELETE_ENCOUNTER_CONFIRM"] or "Delete raid timeline for encounter %d?", tonumber(encounterId) or 0) end,
|
||||
func = function() RT:RemoveEncounter(encounterId); Notify(true) end,
|
||||
}
|
||||
args.entryBreak = { type = "description", order = 10, width = "full", name = " " }
|
||||
|
||||
for key, value in pairs(BuildEntryEditorArgs(encounterId)) do
|
||||
args[key] = value
|
||||
end
|
||||
|
||||
return { type = "group", name = GetBossTreeLabel(encounterId), args = args }
|
||||
end
|
||||
|
||||
local function BuildRaidTreeArgs()
|
||||
local args = {}
|
||||
local raidNames, buckets = GetRaidBuckets()
|
||||
|
||||
for raidIndex, raidName in ipairs(raidNames) do
|
||||
local raidKey = tostring(buckets[raidName].key or raidIndex)
|
||||
args["raid_" .. raidKey] = {
|
||||
type = "group",
|
||||
order = 100 + raidIndex,
|
||||
name = raidName,
|
||||
childGroups = "tree",
|
||||
args = {
|
||||
description = {
|
||||
type = "description",
|
||||
order = 1,
|
||||
width = "full",
|
||||
name = string.format("|cffffd100%s|r: %s", L["OPT_RT_RAID_NAME"] or "Raid", raidName),
|
||||
},
|
||||
raidId = {
|
||||
type = "description",
|
||||
order = 2,
|
||||
width = "full",
|
||||
name = string.format("|cffffd100%s|r: %s", L["OPT_RT_RAID_ID"] or "Raid ID", tostring(buckets[raidName].key or raidIndex)),
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
local addedEncounterIds = {}
|
||||
local bossOrder = 10
|
||||
for _, difficultyKey in ipairs(DIFFICULTY_KEYS) do
|
||||
local encounterIds = buckets[raidName].difficulties[difficultyKey] or {}
|
||||
for _, encounterId in ipairs(encounterIds) do
|
||||
if not addedEncounterIds[encounterId] then
|
||||
local encounterGroup = BuildEncounterDetailGroup(encounterId)
|
||||
if encounterGroup then
|
||||
encounterGroup.order = bossOrder
|
||||
args["raid_" .. raidKey].args["boss_" .. tostring(encounterId)] = encounterGroup
|
||||
bossOrder = bossOrder + 1
|
||||
addedEncounterIds[encounterId] = true
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
return args, #raidNames
|
||||
end
|
||||
|
||||
function RT:GetOptionsGroup()
|
||||
local drafts = GetDrafts()
|
||||
local raidTreeArgs, raidCount = BuildRaidTreeArgs()
|
||||
|
||||
local group = {
|
||||
type = "group",
|
||||
name = L["OPT_RT_NAME"] or "Raid Timeline",
|
||||
order = 4,
|
||||
childGroups = "tree",
|
||||
args = {
|
||||
general = {
|
||||
type = "group",
|
||||
order = 1,
|
||||
name = L["OPT_RT_SECTION_GENERAL"] or "General",
|
||||
args = {
|
||||
enabled = {
|
||||
type = "toggle",
|
||||
order = 1,
|
||||
width = "full",
|
||||
name = L["OPT_RT_ENABLED"] or "Enable Raid Timeline",
|
||||
get = function() return RT:GetSettings().enabled == true end,
|
||||
set = function(_, val)
|
||||
RT:GetSettings().enabled = val and true or false
|
||||
if val then RT:Enable() else RT:Disable() end
|
||||
end,
|
||||
},
|
||||
leadTime = {
|
||||
type = "range",
|
||||
order = 2,
|
||||
min = 1,
|
||||
max = 15,
|
||||
step = 1,
|
||||
name = L["OPT_RT_LEAD_TIME"] or "Warning lead time",
|
||||
get = function() return tonumber(RT:GetSettings().leadTime) or 5 end,
|
||||
set = function(_, val) RT:GetSettings().leadTime = math.floor((tonumber(val) or 5) + 0.5) end,
|
||||
},
|
||||
assignmentLeadTime = {
|
||||
type = "range",
|
||||
order = 2.1,
|
||||
min = 0,
|
||||
max = 60,
|
||||
step = 1,
|
||||
name = L["OPT_RT_ASSIGNMENT_LEAD_TIME"] or "Assignment lead time",
|
||||
desc = L["OPT_RT_ASSIGNMENT_LEAD_TIME_DESC"] or "How many seconds before the planned use the assigned player should be selected.",
|
||||
get = function()
|
||||
local settings = RT:GetSettings()
|
||||
return tonumber(settings.assignmentLeadTime) or tonumber(settings.leadTime) or 5
|
||||
end,
|
||||
set = function(_, val)
|
||||
RT:GetSettings().assignmentLeadTime = math.floor((tonumber(val) or 5) + 0.5)
|
||||
end,
|
||||
},
|
||||
header = { type = "header", order = 3, name = L["OPT_RT_ALERT_HEADER"] or "Alert frame" },
|
||||
unlocked = {
|
||||
type = "toggle", order = 4, width = "double", name = L["OPT_RT_UNLOCK"] or "Unlock alert frame",
|
||||
get = function() return RT:GetSettings().unlocked == true end,
|
||||
set = function(_, val)
|
||||
RT:GetSettings().unlocked = val and true or false
|
||||
RT:ApplyAlertStyle()
|
||||
if val then RT:ShowPreview() end
|
||||
end,
|
||||
},
|
||||
preview = {
|
||||
type = "toggle", order = 5, width = "double", name = L["OPT_RT_PREVIEW"] or "Preview alert",
|
||||
get = function() return RT.previewAlertActive == true end,
|
||||
set = function(_, val) if val then RT:ShowPreview() else RT:HideAlert() end end,
|
||||
},
|
||||
alertFont = {
|
||||
type = "select", order = 6, width = 1.4, name = L["OPT_FONT"] or "Typeface",
|
||||
dialogControl = "LSM30_Font",
|
||||
values = AceGUIWidgetLSMlists and AceGUIWidgetLSMlists.font or (LSM and LSM:HashTable("font")) or {},
|
||||
get = function() return RT:GetSettings().alertFont or "Friz Quadrata TT" end,
|
||||
set = function(_, val) RT:GetSettings().alertFont = val; RT:ApplyAlertStyle() end,
|
||||
},
|
||||
alertFontSize = {
|
||||
type = "range", order = 7, min = 10, max = 72, step = 1, width = 1.0, name = L["OPT_FONT_SIZE"] or "Font size",
|
||||
get = function() return tonumber(RT:GetSettings().alertFontSize) or 30 end,
|
||||
set = function(_, val) RT:GetSettings().alertFontSize = math.floor((tonumber(val) or 30) + 0.5); RT:ApplyAlertStyle() end,
|
||||
},
|
||||
alertFontOutline = {
|
||||
type = "select", order = 8, width = 1.0, name = L["OPT_FONT_OUTLINE"] or "Font outline",
|
||||
values = FONT_OUTLINE_VALUES,
|
||||
get = function() return RT:GetSettings().alertFontOutline or "OUTLINE" end,
|
||||
set = function(_, val) RT:GetSettings().alertFontOutline = val; RT:ApplyAlertStyle() end,
|
||||
},
|
||||
alertColor = {
|
||||
type = "color", order = 9, width = 0.8, name = L["OPT_RT_ALERT_COLOR"] or "Text colour", hasAlpha = true,
|
||||
get = function()
|
||||
local color = RT:GetSettings().alertColor or {}
|
||||
return color.r or 1, color.g or 0.82, color.b or 0.15, color.a or 1
|
||||
end,
|
||||
set = function(_, r, g, b, a)
|
||||
local color = RT:GetSettings().alertColor
|
||||
color.r, color.g, color.b, color.a = r, g, b, a
|
||||
RT:ApplyAlertStyle()
|
||||
end,
|
||||
},
|
||||
desc = {
|
||||
type = "description", order = 10, width = "full",
|
||||
name = L["OPT_RT_DESC"] or "Create encounter timelines here and open the interactive Ace3 timeline editor for visual planning.",
|
||||
},
|
||||
},
|
||||
},
|
||||
manage = {
|
||||
type = "group",
|
||||
order = 2,
|
||||
name = L["OPT_RT_SECTION_MANAGE"] or "Manage encounters",
|
||||
args = {
|
||||
header = { type = "header", order = 1, name = L["OPT_RT_ENCOUNTERS_HEADER"] or "Encounter timelines" },
|
||||
addEncounterId = {
|
||||
type = "input", order = 2, width = 0.8, name = L["OPT_RT_ADD_ENCOUNTER_ID"] or "Encounter ID",
|
||||
disabled = function() return not RT:IsLocalEditor() end,
|
||||
get = function() return tostring(drafts.addEncounterId or "") end,
|
||||
set = function(_, val) drafts.addEncounterId = val end,
|
||||
},
|
||||
addEncounterName = {
|
||||
type = "input", order = 3, width = 1.2, name = L["OPT_RT_ADD_ENCOUNTER_NAME"] or "Encounter name",
|
||||
disabled = function() return not RT:IsLocalEditor() end,
|
||||
get = function() return tostring(drafts.addEncounterName or "") end,
|
||||
set = function(_, val) drafts.addEncounterName = val end,
|
||||
},
|
||||
addEncounter = {
|
||||
type = "execute", order = 5, width = "full", name = L["OPT_RT_ADD_ENCOUNTER"] or "Add encounter",
|
||||
disabled = function() return not RT:IsLocalEditor() end,
|
||||
func = function()
|
||||
if RT:AddEncounter(drafts.addEncounterId, drafts.addEncounterName) then
|
||||
drafts.addEncounterId, drafts.addEncounterName = "", ""
|
||||
Notify(true)
|
||||
else
|
||||
HMGT:Print(L["OPT_RT_INVALID_ENCOUNTER"] or "HMGT: invalid encounter ID")
|
||||
end
|
||||
end,
|
||||
},
|
||||
summary = {
|
||||
type = "description", order = 6, width = "full",
|
||||
name = function()
|
||||
if raidCount == 0 then return L["OPT_RT_EMPTY"] or "No encounter timelines configured yet." end
|
||||
return string.format("|cffffd100%s|r: %d", L["OPT_RT_ENCOUNTERS_HEADER"] or "Encounter timelines", #RT:GetEncounterIds())
|
||||
end,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
if raidCount == 0 then
|
||||
group.args.empty = {
|
||||
type = "group",
|
||||
order = 50,
|
||||
name = L["OPT_RT_EMPTY"] or "No encounter timelines configured yet.",
|
||||
args = {
|
||||
description = { type = "description", order = 1, width = "full", name = L["OPT_RT_EMPTY"] or "No encounter timelines configured yet." },
|
||||
},
|
||||
}
|
||||
else
|
||||
for key, value in pairs(raidTreeArgs) do
|
||||
group.args[key] = value
|
||||
end
|
||||
end
|
||||
|
||||
return group
|
||||
end
|
||||
|
||||
HMGT_Config:RegisterOptionsProvider("raidTimeline", function()
|
||||
raidTimelineOptionsGroup = raidTimelineOptionsGroup or RT:GetOptionsGroup()
|
||||
ClearOptionCaches()
|
||||
local fresh = RT:GetOptionsGroup()
|
||||
raidTimelineOptionsGroup.name = fresh.name
|
||||
raidTimelineOptionsGroup.order = fresh.order
|
||||
raidTimelineOptionsGroup.childGroups = fresh.childGroups
|
||||
raidTimelineOptionsGroup.args = fresh.args
|
||||
return {
|
||||
path = "raidTimeline",
|
||||
order = 4,
|
||||
group = raidTimelineOptionsGroup,
|
||||
}
|
||||
end)
|
||||
1231
Modules/Tracker/Frame.lua
Normal file
1231
Modules/Tracker/Frame.lua
Normal file
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,391 @@
|
||||
-- Core/GroupCooldownSpellDatabase.lua
|
||||
-- Group cooldown database.
|
||||
|
||||
HMGT_SpellData = HMGT_SpellData or {}
|
||||
local Spell = HMGT_SpellData.Spell
|
||||
local Relation = HMGT_SpellData.Relation
|
||||
if not Spell then return end
|
||||
|
||||
HMGT_SpellData.GroupCooldowns = {
|
||||
-- WARRIOR
|
||||
-- Arms Spec
|
||||
Spell(118038, "Die by the Sword", {
|
||||
classes = {"WARRIOR"},
|
||||
specs = {1},
|
||||
category = "defensive",
|
||||
state = { kind = "cooldown", cooldown = 120 },
|
||||
}),
|
||||
Spell(167105, "Colossus Smash", {
|
||||
classes = {"WARRIOR"},
|
||||
specs = {1},
|
||||
category = "offensive",
|
||||
state = { kind = "cooldown", cooldown = 45 },
|
||||
}),
|
||||
|
||||
-- Fury Spec
|
||||
Spell(184364, "Enraged Regeneration", {
|
||||
classes = {"WARRIOR"},
|
||||
specs = {2},
|
||||
category = "defensive",
|
||||
state = { kind = "cooldown", cooldown = 120 },
|
||||
}),
|
||||
Spell(1719, "Recklessness", {
|
||||
classes = {"WARRIOR"},
|
||||
specs = {2},
|
||||
category = "offensive",
|
||||
state = { kind = "cooldown", cooldown = 90 },
|
||||
}),
|
||||
|
||||
-- Protection Spec
|
||||
Spell(871, "Shield Wall", {
|
||||
classes = {"WARRIOR"},
|
||||
specs = {3},
|
||||
category = "tank",
|
||||
state = { kind = "cooldown", cooldown = 108, charges = 1 },
|
||||
mods = {
|
||||
{ talentSpellId = 391271, value = 10, op = "reduceByPercent", target = "cooldown" }, -- Honed Reflexes
|
||||
{ talentSpellId = 397103, value = 2, op = "set", target = "charges" },
|
||||
},
|
||||
}),
|
||||
Spell(385952, "Shield Charge", {
|
||||
classes = {"WARRIOR"},
|
||||
specs = {3},
|
||||
category = "cc",
|
||||
state = { kind = "cooldown", cooldown = 45 },
|
||||
}),
|
||||
Spell(46968, "Shockwave", {
|
||||
classes = {"WARRIOR"},
|
||||
specs = {3},
|
||||
category = "cc",
|
||||
state = { kind = "cooldown", cooldown = 40 },
|
||||
}),
|
||||
Spell(3411, "Intervene", {
|
||||
classes = {"WARRIOR"},
|
||||
specs = {3},
|
||||
category = "defensive",
|
||||
state = { kind = "cooldown", cooldown = 27 },
|
||||
}),
|
||||
|
||||
-- All Specs
|
||||
Spell(107574, "Avatar", {
|
||||
classes = {"WARRIOR"},
|
||||
category = "offensive",
|
||||
state = { kind = "cooldown", cooldown = 90 },
|
||||
}),
|
||||
Spell(107570, "Storm Bolt", {
|
||||
classes = {"WARRIOR"},
|
||||
category = "cc",
|
||||
state = { kind = "cooldown", cooldown = 36 },
|
||||
}),
|
||||
|
||||
-- PALADIN
|
||||
Spell(498, "Divine Protection", {
|
||||
classes = {"PALADIN"},
|
||||
specs = {1, 2},
|
||||
category = "defensive",
|
||||
state = { kind = "cooldown", cooldown = 60 },
|
||||
}),
|
||||
Spell(642, "Divine Shield", {
|
||||
classes = {"PALADIN"},
|
||||
category = "defensive",
|
||||
state = { kind = "cooldown", cooldown = 300 },
|
||||
}),
|
||||
Spell(31884, "Avenging Wrath", {
|
||||
classes = {"PALADIN"},
|
||||
specs = {1, 3},
|
||||
category = "offensive",
|
||||
state = { kind = "cooldown", cooldown = 120 },
|
||||
}),
|
||||
Spell(86659, "Guardian of Ancient Kings", {
|
||||
classes = {"PALADIN"},
|
||||
specs = {2},
|
||||
category = "tank",
|
||||
state = { kind = "cooldown", cooldown = 300 },
|
||||
}),
|
||||
|
||||
-- HUNTER
|
||||
Spell(264735, "Survival of the Fittest", {
|
||||
classes = {"HUNTER"},
|
||||
specs = {1},
|
||||
category = "defensive",
|
||||
state = { kind = "cooldown", cooldown = 180 },
|
||||
}),
|
||||
Spell(109304, "Exhilaration", {
|
||||
classes = {"HUNTER"},
|
||||
category = "defensive",
|
||||
state = { kind = "cooldown", cooldown = 120 },
|
||||
}),
|
||||
|
||||
-- ROGUE
|
||||
Spell(31224, "Cloak of Shadows", {
|
||||
classes = {"ROGUE"},
|
||||
category = "defensive",
|
||||
state = { kind = "cooldown", cooldown = 60 },
|
||||
}),
|
||||
Spell(5277, "Evasion", {
|
||||
classes = {"ROGUE"},
|
||||
category = "defensive",
|
||||
state = { kind = "cooldown", cooldown = 120 },
|
||||
}),
|
||||
Spell(2094, "Blind", {
|
||||
classes = {"ROGUE"},
|
||||
category = "cc",
|
||||
state = { kind = "cooldown", cooldown = 120 },
|
||||
}),
|
||||
Spell(1856, "Vanish", {
|
||||
classes = {"ROGUE"},
|
||||
category = "defensive",
|
||||
state = { kind = "cooldown", cooldown = 120 },
|
||||
}),
|
||||
|
||||
-- DEATH KNIGHT
|
||||
Spell(55233, "Vampiric Blood", {
|
||||
classes = {"DEATHKNIGHT"},
|
||||
specs = {1},
|
||||
category = "tank",
|
||||
state = { kind = "cooldown", cooldown = 180 },
|
||||
mods = {
|
||||
{ talentSpellId = 374200, value = 150, op = "set", target = "cooldown" },
|
||||
},
|
||||
}),
|
||||
Spell(49028, "Dancing Rune Weapon", {
|
||||
classes = {"DEATHKNIGHT"},
|
||||
specs = {1},
|
||||
category = "tank",
|
||||
state = { kind = "cooldown", cooldown = 120 },
|
||||
mods = {
|
||||
{ talentSpellId = 377584, value = 60, op = "set", target = "cooldown" },
|
||||
},
|
||||
}),
|
||||
Spell(48707, "Anti-Magic Shell", {
|
||||
classes = {"DEATHKNIGHT"},
|
||||
category = "defensive",
|
||||
state = { kind = "cooldown", cooldown = 60 },
|
||||
}),
|
||||
Spell(48792, "Icebound Fortitude", {
|
||||
classes = {"DEATHKNIGHT"},
|
||||
category = "defensive",
|
||||
state = { kind = "cooldown", cooldown = 180 },
|
||||
}),
|
||||
Spell(42650, "Army of the Dead", {
|
||||
classes = {"DEATHKNIGHT"},
|
||||
category = "offensive",
|
||||
state = { kind = "cooldown", cooldown = 480 },
|
||||
}),
|
||||
Spell(49206, "Summon Gargoyle", {
|
||||
classes = {"DEATHKNIGHT"},
|
||||
specs = {3},
|
||||
category = "offensive",
|
||||
state = { kind = "cooldown", cooldown = 180 },
|
||||
}),
|
||||
|
||||
-- SHAMAN
|
||||
Spell(204336, "Grounding Totem", {
|
||||
classes = {"SHAMAN"},
|
||||
category = "defensive",
|
||||
state = { kind = "cooldown", cooldown = 30 },
|
||||
}),
|
||||
Spell(51490, "Thunderstorm", {
|
||||
classes = {"SHAMAN"},
|
||||
specs = {1},
|
||||
category = "cc",
|
||||
state = { kind = "cooldown", cooldown = 45 },
|
||||
}),
|
||||
|
||||
-- MAGE
|
||||
Spell(45438, "Ice Block", {
|
||||
classes = {"MAGE"},
|
||||
category = "defensive",
|
||||
state = { kind = "cooldown", cooldown = 240 },
|
||||
}),
|
||||
Spell(110959, "Greater Invisibility", {
|
||||
classes = {"MAGE"},
|
||||
category = "defensive",
|
||||
state = { kind = "cooldown", cooldown = 120 },
|
||||
}),
|
||||
Spell(235450, "Prismatic Barrier", {
|
||||
classes = {"MAGE"},
|
||||
specs = {3},
|
||||
category = "defensive",
|
||||
state = { kind = "cooldown", cooldown = 25 },
|
||||
}),
|
||||
|
||||
-- WARLOCK
|
||||
Spell(104773, "Unending Resolve", {
|
||||
classes = {"WARLOCK"},
|
||||
category = "defensive",
|
||||
state = { kind = "cooldown", cooldown = 180 },
|
||||
}),
|
||||
Spell(6229, "Twilight Ward", {
|
||||
classes = {"WARLOCK"},
|
||||
category = "defensive",
|
||||
state = { kind = "cooldown", cooldown = 0 },
|
||||
}),
|
||||
Spell(212295, "Nether Ward", {
|
||||
classes = {"WARLOCK"},
|
||||
specs = {3},
|
||||
category = "defensive",
|
||||
state = { kind = "cooldown", cooldown = 45 },
|
||||
}),
|
||||
|
||||
-- MONK
|
||||
Spell(122783, "Diffuse Magic", {
|
||||
classes = {"MONK"},
|
||||
category = "defensive",
|
||||
state = { kind = "cooldown", cooldown = 90 },
|
||||
}),
|
||||
Spell(122278, "Dampen Harm", {
|
||||
classes = {"MONK"},
|
||||
category = "defensive",
|
||||
state = { kind = "cooldown", cooldown = 120 },
|
||||
}),
|
||||
Spell(120954, "Fortifying Brew", {
|
||||
classes = {"MONK"},
|
||||
specs = {1},
|
||||
category = "tank",
|
||||
state = { kind = "cooldown", cooldown = 420 },
|
||||
}),
|
||||
Spell(115176, "Zen Meditation", {
|
||||
classes = {"MONK"},
|
||||
specs = {2},
|
||||
category = "defensive",
|
||||
state = { kind = "cooldown", cooldown = 300 },
|
||||
}),
|
||||
Spell(322118, "Invoke Niuzao", {
|
||||
classes = {"MONK"},
|
||||
specs = {1},
|
||||
category = "tank",
|
||||
state = { kind = "cooldown", cooldown = 180 },
|
||||
}),
|
||||
|
||||
-- DRUID
|
||||
Spell(22812, "Barkskin", {
|
||||
classes = {"DRUID"},
|
||||
category = "defensive",
|
||||
state = { kind = "cooldown", cooldown = 60 },
|
||||
}),
|
||||
Spell(61336, "Survival Instincts", {
|
||||
classes = {"DRUID"},
|
||||
specs = {2, 3},
|
||||
category = "defensive",
|
||||
state = { kind = "cooldown", cooldown = 180 },
|
||||
}),
|
||||
|
||||
-- DEMON HUNTER
|
||||
Spell(187827, "Metamorphosis", {
|
||||
classes = {"DEMONHUNTER"},
|
||||
specs = {1},
|
||||
category = "offensive",
|
||||
state = { kind = "cooldown", cooldown = 180 },
|
||||
}),
|
||||
Spell(162264, "Metamorphosis", {
|
||||
classes = {"DEMONHUNTER"},
|
||||
specs = {2},
|
||||
category = "tank",
|
||||
state = { kind = "cooldown", cooldown = 180 },
|
||||
}),
|
||||
Spell(1217605, "Metamorphosis", {
|
||||
classes = {"DEMONHUNTER"},
|
||||
specs = {3},
|
||||
category = "offensive",
|
||||
state = {
|
||||
kind = "availability",
|
||||
required = 50,
|
||||
source = {
|
||||
type = "auraStacks",
|
||||
auraSpellId = 1225789,
|
||||
fallbackSpellCountId = 1217605,
|
||||
},
|
||||
},
|
||||
}),
|
||||
Spell(203819, "Demon Spikes", {
|
||||
classes = {"DEMONHUNTER"},
|
||||
specs = {2},
|
||||
category = "tank",
|
||||
state = { kind = "cooldown", cooldown = 20 },
|
||||
}),
|
||||
Spell(212800, "Blur", {
|
||||
classes = {"DEMONHUNTER"},
|
||||
specs = {1},
|
||||
category = "defensive",
|
||||
state = { kind = "cooldown", cooldown = 60 },
|
||||
}),
|
||||
Spell(196555, "Netherwalk", {
|
||||
classes = {"DEMONHUNTER"},
|
||||
specs = {1},
|
||||
category = "defensive",
|
||||
state = { kind = "cooldown", cooldown = 180 },
|
||||
}),
|
||||
|
||||
-- EVOKER
|
||||
Spell(357214, "Time Stop", {
|
||||
classes = {"EVOKER"},
|
||||
specs = {2},
|
||||
category = "cc",
|
||||
state = { kind = "cooldown", cooldown = 120 },
|
||||
}),
|
||||
Spell(375087, "Tempest", {
|
||||
classes = {"EVOKER"},
|
||||
specs = {3},
|
||||
category = "offensive",
|
||||
state = { kind = "cooldown", cooldown = 30 },
|
||||
}),
|
||||
}
|
||||
|
||||
HMGT_SpellData.Relations = HMGT_SpellData.Relations or {}
|
||||
local hasShieldSlamShieldWallRelation = false
|
||||
local hasAngerManagementRelation = false
|
||||
for _, relation in ipairs(HMGT_SpellData.Relations) do
|
||||
local firstEffect = relation.effects and relation.effects[1]
|
||||
if tonumber(relation.triggerSpellId) == 23922 and tonumber(firstEffect and firstEffect.targetSpellId) == 871 then
|
||||
hasShieldSlamShieldWallRelation = true
|
||||
end
|
||||
if tostring(relation.when) == "powerSpent"
|
||||
and tonumber(relation.talentRequired) == 152278
|
||||
and tostring(relation.powerType) == "RAGE"
|
||||
then
|
||||
hasAngerManagementRelation = true
|
||||
end
|
||||
end
|
||||
if not hasShieldSlamShieldWallRelation and Relation then
|
||||
HMGT_SpellData.Relations[#HMGT_SpellData.Relations + 1] = Relation({
|
||||
triggerSpellId = 23922, -- Shield Slam
|
||||
classes = {"WARRIOR"},
|
||||
specs = {3}, -- Protection
|
||||
when = "cast",
|
||||
talentRequired = 384072, -- Impenetrable Wall
|
||||
effects = {
|
||||
{
|
||||
type = "reduceCooldown",
|
||||
targetSpellId = 871, -- Shield Wall
|
||||
amount = 6,
|
||||
},
|
||||
},
|
||||
})
|
||||
end
|
||||
if not hasAngerManagementRelation and Relation then
|
||||
HMGT_SpellData.Relations[#HMGT_SpellData.Relations + 1] = Relation({
|
||||
classes = {"WARRIOR"},
|
||||
specs = {3}, -- Protection
|
||||
when = "powerSpent",
|
||||
powerType = "RAGE",
|
||||
amountPerTrigger = 20,
|
||||
talentRequired = 152278, -- Anger Management
|
||||
effects = {
|
||||
{
|
||||
type = "reduceCooldown",
|
||||
targetSpellId = 107574, -- Avatar
|
||||
amount = 1,
|
||||
},
|
||||
{
|
||||
type = "reduceCooldown",
|
||||
targetSpellId = 871, -- Shield Wall
|
||||
amount = 1,
|
||||
},
|
||||
},
|
||||
})
|
||||
end
|
||||
|
||||
if HMGT_SpellData.RebuildLookups then
|
||||
HMGT_SpellData.RebuildLookups()
|
||||
end
|
||||
692
Modules/Tracker/GroupCooldownTracker/GroupCooldownTracker.lua
Normal file
692
Modules/Tracker/GroupCooldownTracker/GroupCooldownTracker.lua
Normal file
@@ -0,0 +1,692 @@
|
||||
-- Modules/GroupCooldownTracker.lua
|
||||
-- Group-Cooldown-Tracker Modul (ein Frame pro Spieler in der Gruppe)
|
||||
|
||||
local ADDON_NAME = "HailMaryGuildTools"
|
||||
local HMGT = LibStub("AceAddon-3.0"):GetAddon(ADDON_NAME)
|
||||
if not HMGT then return end
|
||||
local L = LibStub("AceLocale-3.0"):GetLocale(ADDON_NAME)
|
||||
|
||||
local GCT = HMGT:NewModule("GroupCooldownTracker")
|
||||
HMGT.GroupCooldownTracker = GCT
|
||||
|
||||
GCT.frame = nil
|
||||
GCT.frames = {}
|
||||
|
||||
local function SanitizeFrameToken(name)
|
||||
if not name or name == "" then return "Unknown" end
|
||||
return name:gsub("[^%w_]", "_")
|
||||
end
|
||||
|
||||
local function ShortName(name)
|
||||
if not name then return "" end
|
||||
local short = name:match("^[^-]+")
|
||||
return short or name
|
||||
end
|
||||
|
||||
local function IsUsableAnchorFrame(frame)
|
||||
return frame
|
||||
and frame.IsObjectType
|
||||
and (frame:IsObjectType("Frame") or frame:IsObjectType("Button"))
|
||||
end
|
||||
|
||||
local function GetFrameUnit(frame)
|
||||
if not frame then return nil end
|
||||
local unit = frame.unit
|
||||
if not unit and frame.GetAttribute then
|
||||
unit = frame:GetAttribute("unit")
|
||||
end
|
||||
return unit
|
||||
end
|
||||
|
||||
local function FrameMatchesUnit(frame, unitId)
|
||||
if not IsUsableAnchorFrame(frame) then return false end
|
||||
if not unitId then return true end
|
||||
local unit = GetFrameUnit(frame)
|
||||
return unit == unitId
|
||||
end
|
||||
|
||||
local PLAYER_FRAME_CANDIDATES = {
|
||||
"PlayerFrame",
|
||||
"ElvUF_Player",
|
||||
"NephUI_PlayerFrame",
|
||||
"NephUIPlayerFrame",
|
||||
"oUF_NephUI_Player",
|
||||
"SUFUnitplayer",
|
||||
}
|
||||
|
||||
local PARTY_FRAME_PATTERNS = {
|
||||
"PartyMemberFrame%d", -- Blizzard alt
|
||||
"CompactPartyFrameMember%d", -- Blizzard modern
|
||||
"ElvUF_PartyGroup1UnitButton%d", -- ElvUI
|
||||
"ElvUF_PartyUnitButton%d", -- ElvUI variant
|
||||
"NephUI_PartyUnitButton%d", -- NephUI (common naming variants)
|
||||
"NephUI_PartyFrame%d",
|
||||
"NephUIPartyFrame%d",
|
||||
"oUF_NephUI_PartyUnitButton%d",
|
||||
"SUFUnitparty%d", -- Shadowed Unit Frames
|
||||
}
|
||||
|
||||
local unitFrameCache = {}
|
||||
|
||||
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
|
||||
|
||||
local function BuildAnchorLayoutSignature(settings, ordered, unitByPlayer)
|
||||
local parts = {
|
||||
settings.attachToPartyFrame == true and "attach" or "stack",
|
||||
tostring(settings.partyAttachSide or "RIGHT"),
|
||||
tostring(tonumber(settings.partyAttachOffsetX) or 8),
|
||||
tostring(tonumber(settings.partyAttachOffsetY) or 0),
|
||||
tostring(settings.showBar and "bar" or "icon"),
|
||||
tostring(settings.growDirection or "DOWN"),
|
||||
tostring(settings.width or 250),
|
||||
tostring(settings.barHeight or 20),
|
||||
tostring(settings.iconSize or 32),
|
||||
tostring(settings.iconCols or 6),
|
||||
tostring(settings.barSpacing or 2),
|
||||
tostring(settings.locked),
|
||||
tostring(settings.anchorTo or "UIParent"),
|
||||
tostring(settings.anchorPoint or "TOPLEFT"),
|
||||
tostring(settings.anchorRelPoint or "TOPLEFT"),
|
||||
tostring(settings.anchorX or settings.posX or 0),
|
||||
tostring(settings.anchorY or settings.posY or 0),
|
||||
}
|
||||
|
||||
for _, playerName in ipairs(ordered or {}) do
|
||||
parts[#parts + 1] = tostring(playerName)
|
||||
parts[#parts + 1] = tostring(unitByPlayer and unitByPlayer[playerName] or "")
|
||||
end
|
||||
|
||||
return table.concat(parts, "|")
|
||||
end
|
||||
|
||||
local function ResolveNamedUnitFrame(unitId)
|
||||
if unitId == "player" then
|
||||
for _, frameName in ipairs(PLAYER_FRAME_CANDIDATES) do
|
||||
local frame = _G[frameName]
|
||||
if FrameMatchesUnit(frame, unitId) or (frameName == "PlayerFrame" and IsUsableAnchorFrame(frame)) then
|
||||
return frame
|
||||
end
|
||||
end
|
||||
return nil
|
||||
end
|
||||
|
||||
local idx = type(unitId) == "string" and unitId:match("^party(%d+)$")
|
||||
if not idx then
|
||||
return nil
|
||||
end
|
||||
|
||||
idx = tonumber(idx)
|
||||
for _, pattern in ipairs(PARTY_FRAME_PATTERNS) do
|
||||
local frame = _G[pattern:format(idx)]
|
||||
if FrameMatchesUnit(frame, unitId) then
|
||||
return frame
|
||||
end
|
||||
end
|
||||
return nil
|
||||
end
|
||||
|
||||
local function ScanUnitFrame(unitId)
|
||||
local frame = EnumerateFrames()
|
||||
local scanned = 0
|
||||
while frame and scanned < 8000 do
|
||||
if IsUsableAnchorFrame(frame) then
|
||||
local unit = GetFrameUnit(frame)
|
||||
if unit == unitId then
|
||||
HMGT:DebugScoped("verbose", HMGT:GetTrackerDebugScope("Group Cooldowns"), "GroupAttach scan unit=%s scanned=%d found=true", tostring(unitId), scanned)
|
||||
return frame
|
||||
end
|
||||
end
|
||||
scanned = scanned + 1
|
||||
frame = EnumerateFrames(frame)
|
||||
end
|
||||
HMGT:DebugScoped("verbose", HMGT:GetTrackerDebugScope("Group Cooldowns"), "GroupAttach scan unit=%s scanned=%d found=false", tostring(unitId), scanned)
|
||||
return nil
|
||||
end
|
||||
|
||||
local function ResolveUnitAnchorFrame(unitId)
|
||||
if not unitId then return nil end
|
||||
|
||||
local now = GetTime()
|
||||
local cached = unitFrameCache[unitId]
|
||||
if cached and now < (cached.expires or 0) then
|
||||
if cached.frame and cached.frame:IsShown() then
|
||||
return cached.frame
|
||||
end
|
||||
return nil
|
||||
end
|
||||
|
||||
local frame = ResolveNamedUnitFrame(unitId)
|
||||
if not frame then
|
||||
frame = ScanUnitFrame(unitId)
|
||||
end
|
||||
|
||||
local expiresIn = 1.0
|
||||
if frame and frame:IsShown() then
|
||||
expiresIn = 10.0
|
||||
end
|
||||
unitFrameCache[unitId] = {
|
||||
frame = frame,
|
||||
expires = now + expiresIn,
|
||||
}
|
||||
|
||||
if frame and frame:IsShown() then
|
||||
return frame
|
||||
end
|
||||
return nil
|
||||
end
|
||||
|
||||
function GCT:GetFrameIdForPlayer(playerName)
|
||||
return "GroupCooldownTracker_" .. SanitizeFrameToken(playerName)
|
||||
end
|
||||
|
||||
function GCT:GetPlayerFrame(playerName)
|
||||
if not playerName then return nil end
|
||||
return self.frames[playerName]
|
||||
end
|
||||
|
||||
function GCT:GetAnchorableFrames()
|
||||
return self.frames
|
||||
end
|
||||
|
||||
function GCT:EnsurePlayerFrame(playerName)
|
||||
local frame = self.frames[playerName]
|
||||
local s = HMGT.db.profile.groupCooldownTracker
|
||||
if frame then
|
||||
return frame
|
||||
end
|
||||
|
||||
frame = HMGT.TrackerFrame:CreateTrackerFrame(self:GetFrameIdForPlayer(playerName), s)
|
||||
frame._hmgtPlayerName = playerName
|
||||
self.frames[playerName] = frame
|
||||
return frame
|
||||
end
|
||||
|
||||
function GCT:HideAllFrames()
|
||||
for _, frame in pairs(self.frames) do
|
||||
frame:Hide()
|
||||
end
|
||||
self.activeOrder = nil
|
||||
self.unitByPlayer = nil
|
||||
self.frame = nil
|
||||
self._lastAnchorLayoutSignature = nil
|
||||
self._nextAnchorRetryAt = nil
|
||||
end
|
||||
|
||||
function GCT:SetLockedAll(locked)
|
||||
for _, frame in pairs(self.frames) do
|
||||
HMGT.TrackerFrame:SetLocked(frame, locked)
|
||||
end
|
||||
end
|
||||
|
||||
function GCT:EnsureUpdateTicker()
|
||||
if self.updateTicker then
|
||||
return
|
||||
end
|
||||
self.updateTicker = C_Timer.NewTicker(0.1, function()
|
||||
self:UpdateDisplay()
|
||||
end)
|
||||
end
|
||||
|
||||
function GCT:StopUpdateTicker()
|
||||
if self.updateTicker then
|
||||
self.updateTicker:Cancel()
|
||||
self.updateTicker = nil
|
||||
end
|
||||
end
|
||||
|
||||
function GCT:SetUpdateTickerEnabled(enabled)
|
||||
if enabled then
|
||||
self:EnsureUpdateTicker()
|
||||
else
|
||||
self:StopUpdateTicker()
|
||||
end
|
||||
end
|
||||
|
||||
function GCT:InvalidateAnchorLayout()
|
||||
self._lastAnchorLayoutSignature = nil
|
||||
self._nextAnchorRetryAt = nil
|
||||
end
|
||||
|
||||
function GCT:RefreshAnchors(force)
|
||||
local s = HMGT.db.profile.groupCooldownTracker
|
||||
if not s then return end
|
||||
|
||||
local ordered = {}
|
||||
for _, playerName in ipairs(self.activeOrder or {}) do
|
||||
local frame = self.frames[playerName]
|
||||
if frame and frame:IsShown() then
|
||||
table.insert(ordered, playerName)
|
||||
end
|
||||
end
|
||||
|
||||
if #ordered == 0 then
|
||||
self.frame = nil
|
||||
self._lastAnchorLayoutSignature = nil
|
||||
self._nextAnchorRetryAt = nil
|
||||
return
|
||||
end
|
||||
|
||||
local now = GetTime()
|
||||
local signature = BuildAnchorLayoutSignature(s, ordered, self.unitByPlayer)
|
||||
if not force and self._lastAnchorLayoutSignature == signature then
|
||||
local retryAt = tonumber(self._nextAnchorRetryAt) or 0
|
||||
if retryAt <= 0 or now < retryAt then
|
||||
return
|
||||
end
|
||||
end
|
||||
|
||||
-- Do not force anchor updates while user is dragging a tracker frame.
|
||||
for _, playerName in ipairs(ordered) do
|
||||
local frame = self.frames[playerName]
|
||||
if frame and frame._hmgtDragging then
|
||||
return
|
||||
end
|
||||
end
|
||||
|
||||
local primaryName = ordered[1]
|
||||
local primary = self.frames[primaryName]
|
||||
self.frame = primary
|
||||
|
||||
if s.attachToPartyFrame == true then
|
||||
local side = s.partyAttachSide or "RIGHT"
|
||||
local extraX = tonumber(s.partyAttachOffsetX) or 8
|
||||
local extraY = tonumber(s.partyAttachOffsetY) or 0
|
||||
local growsUp = s.showBar == true and s.growDirection == "UP"
|
||||
local barHeight = tonumber(s.barHeight) or 20
|
||||
local growUpAttachOffset = barHeight + 20
|
||||
local prevPlaced = nil
|
||||
local missingTargets = 0
|
||||
|
||||
for i = 1, #ordered do
|
||||
local playerName = ordered[i]
|
||||
local frame = self.frames[playerName]
|
||||
local unitId = self.unitByPlayer and self.unitByPlayer[playerName]
|
||||
local target = ResolveUnitAnchorFrame(unitId)
|
||||
local contentTopInset = HMGT.TrackerFrame.GetContentTopInset and HMGT.TrackerFrame:GetContentTopInset(frame) or 0
|
||||
|
||||
frame:ClearAllPoints()
|
||||
if target then
|
||||
if side == "LEFT" then
|
||||
if growsUp then
|
||||
frame:SetPoint("BOTTOMRIGHT", target, "TOPLEFT", -extraX, extraY - growUpAttachOffset)
|
||||
else
|
||||
frame:SetPoint("TOPRIGHT", target, "TOPLEFT", -extraX, extraY + contentTopInset)
|
||||
end
|
||||
else
|
||||
if growsUp then
|
||||
frame:SetPoint("BOTTOMLEFT", target, "TOPRIGHT", extraX, extraY - growUpAttachOffset)
|
||||
else
|
||||
frame:SetPoint("TOPLEFT", target, "TOPRIGHT", extraX, extraY + contentTopInset)
|
||||
end
|
||||
end
|
||||
elseif prevPlaced then
|
||||
missingTargets = missingTargets + 1
|
||||
HMGT:DebugScoped("verbose", HMGT:GetTrackerDebugScope("Group Cooldowns"), "GroupAttach fallback-stack player=%s unit=%s", tostring(playerName), tostring(unitId))
|
||||
if growsUp then
|
||||
frame:SetPoint("BOTTOMLEFT", prevPlaced, "TOPLEFT", 0, (s.barSpacing or 2) + 10)
|
||||
else
|
||||
frame:SetPoint("TOPLEFT", prevPlaced, "BOTTOMLEFT", 0, -((s.barSpacing or 2) + 10))
|
||||
end
|
||||
else
|
||||
missingTargets = missingTargets + 1
|
||||
HMGT:DebugScoped("info", HMGT:GetTrackerDebugScope("Group Cooldowns"), "GroupAttach fallback-anchor player=%s unit=%s (no party frame found)", tostring(playerName), tostring(unitId))
|
||||
HMGT.TrackerFrame:ApplyAnchor(frame)
|
||||
end
|
||||
|
||||
frame:EnableMouse(false)
|
||||
prevPlaced = frame
|
||||
end
|
||||
if missingTargets > 0 then
|
||||
self._lastAnchorLayoutSignature = nil
|
||||
self._nextAnchorRetryAt = now + 1.0
|
||||
else
|
||||
self._lastAnchorLayoutSignature = signature
|
||||
self._nextAnchorRetryAt = nil
|
||||
end
|
||||
return
|
||||
end
|
||||
|
||||
HMGT.TrackerFrame:ApplyAnchor(primary)
|
||||
primary:EnableMouse(not s.locked)
|
||||
|
||||
local gap = (s.barSpacing or 2) + 10
|
||||
local growsUp = s.showBar == true and s.growDirection == "UP"
|
||||
for i = 2, #ordered do
|
||||
local prev = self.frames[ordered[i - 1]]
|
||||
local frame = self.frames[ordered[i]]
|
||||
frame:ClearAllPoints()
|
||||
if growsUp then
|
||||
frame:SetPoint("BOTTOMLEFT", prev, "TOPLEFT", 0, gap)
|
||||
else
|
||||
frame:SetPoint("TOPLEFT", prev, "BOTTOMLEFT", 0, -gap)
|
||||
end
|
||||
frame:EnableMouse(false)
|
||||
end
|
||||
self._lastAnchorLayoutSignature = signature
|
||||
self._nextAnchorRetryAt = nil
|
||||
end
|
||||
|
||||
-- ============================================================
|
||||
-- ENABLE / DISABLE
|
||||
-- ============================================================
|
||||
|
||||
function GCT:Enable()
|
||||
local s = HMGT.db.profile.groupCooldownTracker
|
||||
if not s.enabled and not s.demoMode and not s.testMode then return end
|
||||
|
||||
self:UpdateDisplay()
|
||||
end
|
||||
|
||||
function GCT:Disable()
|
||||
self:StopUpdateTicker()
|
||||
self:HideAllFrames()
|
||||
end
|
||||
|
||||
-- ============================================================
|
||||
-- DISPLAY UPDATE
|
||||
-- ============================================================
|
||||
|
||||
function GCT:UpdateDisplay()
|
||||
local s = HMGT.db.profile.groupCooldownTracker
|
||||
if not s then return end
|
||||
|
||||
if s.testMode then
|
||||
local entries, playerName = HMGT:GetOwnTestEntries(HMGT_SpellData.GroupCooldowns, s, {
|
||||
deferChargeCooldownUntilEmpty = false,
|
||||
})
|
||||
local byPlayer = { [playerName] = {} }
|
||||
for _, entry in ipairs(entries) do
|
||||
entry.playerName = playerName
|
||||
table.insert(byPlayer[playerName], entry)
|
||||
end
|
||||
|
||||
self.activeOrder = { playerName }
|
||||
self.unitByPlayer = { [playerName] = "player" }
|
||||
self.lastEntryCount = 0
|
||||
local active = {}
|
||||
local shownOrder = {}
|
||||
local shouldTick = false
|
||||
for _, pName in ipairs(self.activeOrder) do
|
||||
local frame = self:EnsurePlayerFrame(pName)
|
||||
HMGT.TrackerFrame:SetLocked(frame, s.locked)
|
||||
HMGT.TrackerFrame:SetTitle(frame, string.format("%s - %s", L["GCD_TITLE"], ShortName(pName)))
|
||||
local displayEntries = byPlayer[pName]
|
||||
if HMGT.FilterDisplayEntries then
|
||||
displayEntries = HMGT:FilterDisplayEntries(s, displayEntries) or displayEntries
|
||||
end
|
||||
if HMGT.SortDisplayEntries then
|
||||
HMGT:SortDisplayEntries(displayEntries, "groupCooldownTracker")
|
||||
end
|
||||
if #displayEntries > 0 then
|
||||
HMGT.TrackerFrame:UpdateFrame(frame, displayEntries, true)
|
||||
self.lastEntryCount = self.lastEntryCount + #displayEntries
|
||||
frame:Show()
|
||||
active[pName] = true
|
||||
shownOrder[#shownOrder + 1] = pName
|
||||
for _, entry in ipairs(displayEntries) do
|
||||
if EntryNeedsVisualTicker(entry) then
|
||||
shouldTick = true
|
||||
break
|
||||
end
|
||||
end
|
||||
else
|
||||
frame:Hide()
|
||||
end
|
||||
end
|
||||
self.activeOrder = shownOrder
|
||||
|
||||
for pn, frame in pairs(self.frames) do
|
||||
if not active[pn] then
|
||||
frame:Hide()
|
||||
end
|
||||
end
|
||||
|
||||
self:RefreshAnchors()
|
||||
self:SetUpdateTickerEnabled(shouldTick)
|
||||
return
|
||||
end
|
||||
|
||||
if s.demoMode then
|
||||
local entries = HMGT:GetDemoEntries("groupCooldownTracker", HMGT_SpellData.GroupCooldowns, s)
|
||||
local playerName = HMGT:NormalizePlayerName(UnitName("player")) or "DemoPlayer"
|
||||
local byPlayer = { [playerName] = {} }
|
||||
for _, entry in ipairs(entries) do
|
||||
entry.playerName = playerName
|
||||
table.insert(byPlayer[playerName], entry)
|
||||
end
|
||||
|
||||
self.activeOrder = { playerName }
|
||||
self.unitByPlayer = { [playerName] = "player" }
|
||||
self.lastEntryCount = 0
|
||||
local active = {}
|
||||
local shownOrder = {}
|
||||
local shouldTick = false
|
||||
for _, playerName in ipairs(self.activeOrder) do
|
||||
local frame = self:EnsurePlayerFrame(playerName)
|
||||
HMGT.TrackerFrame:SetLocked(frame, s.locked)
|
||||
HMGT.TrackerFrame:SetTitle(frame, string.format("%s - %s", L["GCD_TITLE"], ShortName(playerName)))
|
||||
local displayEntries = byPlayer[playerName]
|
||||
if HMGT.FilterDisplayEntries then
|
||||
displayEntries = HMGT:FilterDisplayEntries(s, displayEntries) or displayEntries
|
||||
end
|
||||
if HMGT.SortDisplayEntries then
|
||||
HMGT:SortDisplayEntries(displayEntries, "groupCooldownTracker")
|
||||
end
|
||||
if #displayEntries > 0 then
|
||||
HMGT.TrackerFrame:UpdateFrame(frame, displayEntries, true)
|
||||
self.lastEntryCount = self.lastEntryCount + #displayEntries
|
||||
frame:Show()
|
||||
active[playerName] = true
|
||||
shownOrder[#shownOrder + 1] = playerName
|
||||
shouldTick = true
|
||||
else
|
||||
frame:Hide()
|
||||
end
|
||||
end
|
||||
self.activeOrder = shownOrder
|
||||
|
||||
for pn, frame in pairs(self.frames) do
|
||||
if not active[pn] then
|
||||
frame:Hide()
|
||||
end
|
||||
end
|
||||
|
||||
self:RefreshAnchors()
|
||||
self:SetUpdateTickerEnabled(shouldTick)
|
||||
return
|
||||
end
|
||||
|
||||
if IsInRaid() or not IsInGroup() then
|
||||
self.lastEntryCount = 0
|
||||
self:StopUpdateTicker()
|
||||
self:HideAllFrames()
|
||||
return
|
||||
end
|
||||
if not s.enabled then
|
||||
self.lastEntryCount = 0
|
||||
self:StopUpdateTicker()
|
||||
self:HideAllFrames()
|
||||
return
|
||||
end
|
||||
if not HMGT:IsVisibleForCurrentGroup(s) then
|
||||
self.lastEntryCount = 0
|
||||
self:StopUpdateTicker()
|
||||
self:HideAllFrames()
|
||||
return
|
||||
end
|
||||
|
||||
local entriesByPlayer, order, unitByPlayer = self:CollectEntriesByPlayer()
|
||||
self.activeOrder = order
|
||||
self.unitByPlayer = unitByPlayer
|
||||
self.lastEntryCount = 0
|
||||
local active = {}
|
||||
local shownOrder = {}
|
||||
local shouldTick = false
|
||||
|
||||
for _, playerName in ipairs(order) do
|
||||
local frame = self:EnsurePlayerFrame(playerName)
|
||||
HMGT.TrackerFrame:SetLocked(frame, s.locked)
|
||||
HMGT.TrackerFrame:SetTitle(frame, string.format("%s - %s", L["GCD_TITLE"], ShortName(playerName)))
|
||||
|
||||
local entries = entriesByPlayer[playerName] or {}
|
||||
if HMGT.FilterDisplayEntries then
|
||||
entries = HMGT:FilterDisplayEntries(s, entries) or entries
|
||||
end
|
||||
if HMGT.SortDisplayEntries then
|
||||
HMGT:SortDisplayEntries(entries, "groupCooldownTracker")
|
||||
end
|
||||
if #entries > 0 then
|
||||
HMGT.TrackerFrame:UpdateFrame(frame, entries, true)
|
||||
self.lastEntryCount = self.lastEntryCount + #entries
|
||||
frame:Show()
|
||||
active[playerName] = true
|
||||
shownOrder[#shownOrder + 1] = playerName
|
||||
for _, entry in ipairs(entries) do
|
||||
if EntryNeedsVisualTicker(entry) then
|
||||
shouldTick = true
|
||||
break
|
||||
end
|
||||
end
|
||||
else
|
||||
frame:Hide()
|
||||
end
|
||||
end
|
||||
self.activeOrder = shownOrder
|
||||
|
||||
for pn, frame in pairs(self.frames) do
|
||||
if not active[pn] then
|
||||
frame:Hide()
|
||||
end
|
||||
end
|
||||
|
||||
self:RefreshAnchors()
|
||||
self:SetUpdateTickerEnabled(shouldTick)
|
||||
end
|
||||
|
||||
function GCT:CollectEntriesByPlayer()
|
||||
local s = HMGT.db.profile.groupCooldownTracker
|
||||
local byPlayer = {}
|
||||
local playerOrder = {}
|
||||
local unitByPlayer = {}
|
||||
local players = self:GetGroupPlayers()
|
||||
|
||||
for _, playerInfo in ipairs(players) do
|
||||
repeat
|
||||
local name = playerInfo.name
|
||||
if not name then break end
|
||||
|
||||
local pData = HMGT.playerData[name]
|
||||
local class = pData and pData.class or playerInfo.class
|
||||
local specIdx
|
||||
if playerInfo.isOwn then
|
||||
specIdx = GetSpecialization()
|
||||
if not specIdx or specIdx == 0 then break end
|
||||
else
|
||||
specIdx = pData and pData.specIndex or nil
|
||||
if not specIdx or tonumber(specIdx) <= 0 then break end
|
||||
end
|
||||
local talents = pData and pData.talents or {}
|
||||
if not class then break end
|
||||
|
||||
local knownCDs = HMGT_SpellData.GetSpellsForSpec(class, specIdx, HMGT_SpellData.GroupCooldowns)
|
||||
local entries = {}
|
||||
for _, spellEntry in ipairs(knownCDs) do
|
||||
if s.enabledSpells[spellEntry.spellId] ~= false then
|
||||
local remaining, total, curCharges, maxCharges = HMGT:GetCooldownInfo(name, spellEntry.spellId, {
|
||||
deferChargeCooldownUntilEmpty = false,
|
||||
})
|
||||
local isAvailabilitySpell = HMGT.IsAvailabilitySpell and HMGT:IsAvailabilitySpell(spellEntry)
|
||||
local effectiveCd = HMGT_SpellData.GetEffectiveCooldown(spellEntry, talents)
|
||||
local hasChargeSpell = (tonumber(maxCharges) or 0) > 1
|
||||
local hasPartialCharges = (tonumber(maxCharges) or 0) > 0 and (tonumber(curCharges) or tonumber(maxCharges) or 0) < (tonumber(maxCharges) or 0)
|
||||
local include = HMGT:ShouldDisplayEntry(s, remaining, curCharges, maxCharges, spellEntry)
|
||||
local spellKnown = HMGT:IsTrackedSpellKnownForPlayer(name, spellEntry.spellId)
|
||||
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 then
|
||||
if isAvailabilitySpell and not HMGT:HasAvailabilityState(name, spellEntry.spellId) then
|
||||
include = false
|
||||
end
|
||||
end
|
||||
if include then
|
||||
table.insert(entries, {
|
||||
playerName = name,
|
||||
class = class,
|
||||
spellEntry = spellEntry,
|
||||
remaining = remaining,
|
||||
total = total > 0 and total or effectiveCd,
|
||||
currentCharges = curCharges,
|
||||
maxCharges = maxCharges,
|
||||
})
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
if #entries > 0 then
|
||||
byPlayer[name] = entries
|
||||
table.insert(playerOrder, name)
|
||||
unitByPlayer[name] = playerInfo.unitId
|
||||
end
|
||||
until true
|
||||
end
|
||||
|
||||
table.sort(playerOrder, function(a, b)
|
||||
local own = HMGT:NormalizePlayerName(UnitName("player"))
|
||||
if a == own and b ~= own then return true end
|
||||
if b == own and a ~= own then return false end
|
||||
return a < b
|
||||
end)
|
||||
|
||||
return byPlayer, playerOrder, unitByPlayer
|
||||
end
|
||||
|
||||
function GCT:GetGroupPlayers()
|
||||
local players = {}
|
||||
local ownName = HMGT:NormalizePlayerName(UnitName("player"))
|
||||
local settings = HMGT.db and HMGT.db.profile and HMGT.db.profile.groupCooldownTracker
|
||||
|
||||
if settings and settings.includeSelfFrame == true and ownName then
|
||||
table.insert(players, {
|
||||
name = ownName,
|
||||
class = select(2, UnitClass("player")),
|
||||
unitId = "player",
|
||||
isOwn = true,
|
||||
})
|
||||
end
|
||||
|
||||
if IsInGroup() and not IsInRaid() then
|
||||
for i = 1, GetNumGroupMembers() - 1 do
|
||||
local unitId = "party" .. i
|
||||
local name = HMGT:NormalizePlayerName(UnitName(unitId))
|
||||
local class = select(2, UnitClass(unitId))
|
||||
if name and name ~= ownName then
|
||||
table.insert(players, {name = name, class = class, unitId = unitId})
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
return players
|
||||
end
|
||||
|
||||
247
Modules/Tracker/GroupTrackerFrames.lua
Normal file
247
Modules/Tracker/GroupTrackerFrames.lua
Normal file
@@ -0,0 +1,247 @@
|
||||
local ADDON_NAME = "HailMaryGuildTools"
|
||||
local HMGT = LibStub("AceAddon-3.0"):GetAddon(ADDON_NAME)
|
||||
if not HMGT or not HMGT.TrackerManager then return end
|
||||
|
||||
local Manager = HMGT.TrackerManager
|
||||
local S = Manager._shared or {}
|
||||
|
||||
function Manager:EnsurePlayerFrame(tracker, playerName)
|
||||
local frameKey = S.GetTrackerFrameKey(tracker.id)
|
||||
self.perPlayerFrames[frameKey] = self.perPlayerFrames[frameKey] or {}
|
||||
local frame = self.perPlayerFrames[frameKey][playerName]
|
||||
if not frame then
|
||||
frame = HMGT.TrackerFrame:CreateTrackerFrame(S.GetTrackerPlayerFrameName(tracker.id, playerName), tracker)
|
||||
frame._hmgtTrackerId = tonumber(tracker.id) or 0
|
||||
frame._hmgtPlayerName = playerName
|
||||
self.perPlayerFrames[frameKey][playerName] = frame
|
||||
end
|
||||
frame._settings = tracker
|
||||
HMGT.TrackerFrame:SetTitle(frame, string.format("%s - %s", S.GetTrackerLabel(tracker), S.ShortName(playerName)))
|
||||
HMGT.TrackerFrame:SetLocked(frame, tracker.locked)
|
||||
return frame
|
||||
end
|
||||
|
||||
function Manager:HidePlayerFrames(frameKey)
|
||||
local frames = self.perPlayerFrames[frameKey]
|
||||
if type(frames) ~= "table" then
|
||||
self.activeOrders[frameKey] = nil
|
||||
self.unitByPlayer[frameKey] = nil
|
||||
self.anchorLayoutSignatures[frameKey] = nil
|
||||
self.nextAnchorRetryAt[frameKey] = nil
|
||||
self._displaySignatures[frameKey] = "0"
|
||||
return
|
||||
end
|
||||
for _, frame in pairs(frames) do
|
||||
frame:Hide()
|
||||
end
|
||||
self.activeOrders[frameKey] = nil
|
||||
self.unitByPlayer[frameKey] = nil
|
||||
self.anchorLayoutSignatures[frameKey] = nil
|
||||
self.nextAnchorRetryAt[frameKey] = nil
|
||||
self._displaySignatures[frameKey] = "0"
|
||||
end
|
||||
|
||||
function Manager:BuildEntriesByPlayerForTracker(tracker)
|
||||
local frameKey = S.GetTrackerFrameKey(tracker.id)
|
||||
local ownName = HMGT:NormalizePlayerName(UnitName("player")) or "Player"
|
||||
if tracker.testMode then
|
||||
local entries = self:CollectTestEntries(tracker)
|
||||
if S.IsGroupTracker(tracker) and tracker.attachToPartyFrame == true then
|
||||
return S.BuildPartyPreviewEntries(entries)
|
||||
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 = HMGT:GetDemoEntries(frameKey, S.GetTrackerSpellPool(tracker.categories), tracker)
|
||||
if S.IsGroupTracker(tracker) and tracker.attachToPartyFrame == true then
|
||||
return S.BuildPartyPreviewEntries(entries)
|
||||
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 HMGT:IsVisibleForCurrentGroup(tracker) then
|
||||
return {}, {}, {}, false
|
||||
end
|
||||
if IsInRaid() or not IsInGroup() then
|
||||
return {}, {}, {}, false
|
||||
end
|
||||
local byPlayer, order, unitByPlayer = {}, {}, {}
|
||||
for _, playerInfo in ipairs(S.GetGroupPlayers(tracker)) do
|
||||
local entries = S.CollectEntriesForPlayer(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 Manager:RefreshPerGroupAnchors(tracker, force)
|
||||
local frameKey = S.GetTrackerFrameKey(tracker.id)
|
||||
local frames = self.perPlayerFrames[frameKey] or {}
|
||||
local ordered = {}
|
||||
for _, playerName in ipairs(self.activeOrders[frameKey] or {}) do
|
||||
local frame = frames[playerName]
|
||||
if frame and frame:IsShown() then
|
||||
ordered[#ordered + 1] = playerName
|
||||
end
|
||||
end
|
||||
if #ordered == 0 then
|
||||
self.anchorLayoutSignatures[frameKey] = nil
|
||||
self.nextAnchorRetryAt[frameKey] = nil
|
||||
return
|
||||
end
|
||||
local now = GetTime()
|
||||
local signature = S.BuildAnchorLayoutSignature(tracker, ordered, self.unitByPlayer[frameKey])
|
||||
if not force and self.anchorLayoutSignatures[frameKey] == signature then
|
||||
local retryAt = tonumber(self.nextAnchorRetryAt[frameKey]) or 0
|
||||
if retryAt <= 0 or now < retryAt then
|
||||
return
|
||||
end
|
||||
end
|
||||
for _, playerName in ipairs(ordered) do
|
||||
local frame = frames[playerName]
|
||||
if frame and frame._hmgtDragging then
|
||||
return
|
||||
end
|
||||
end
|
||||
if tracker.attachToPartyFrame == true then
|
||||
local side = tracker.partyAttachSide or "RIGHT"
|
||||
local extraX = tonumber(tracker.partyAttachOffsetX) or 8
|
||||
local extraY = tonumber(tracker.partyAttachOffsetY) or 0
|
||||
local growsUp = tracker.showBar == true and tracker.growDirection == "UP"
|
||||
local barHeight = tonumber(tracker.barHeight) or 20
|
||||
local growUpAttachOffset = barHeight + 20
|
||||
local prevPlaced = nil
|
||||
local missingTargets = 0
|
||||
for _, playerName in ipairs(ordered) do
|
||||
local frame = frames[playerName]
|
||||
local unitId = self.unitByPlayer[frameKey] and self.unitByPlayer[frameKey][playerName]
|
||||
local target = S.ResolveUnitAnchorFrame(unitId)
|
||||
local contentTopInset = HMGT.TrackerFrame.GetContentTopInset and HMGT.TrackerFrame:GetContentTopInset(frame) or 0
|
||||
frame:ClearAllPoints()
|
||||
if target then
|
||||
if side == "LEFT" then
|
||||
if growsUp then
|
||||
frame:SetPoint("BOTTOMRIGHT", target, "TOPLEFT", -extraX, extraY - growUpAttachOffset)
|
||||
else
|
||||
frame:SetPoint("TOPRIGHT", target, "TOPLEFT", -extraX, extraY + contentTopInset)
|
||||
end
|
||||
else
|
||||
if growsUp then
|
||||
frame:SetPoint("BOTTOMLEFT", target, "TOPRIGHT", extraX, extraY - growUpAttachOffset)
|
||||
else
|
||||
frame:SetPoint("TOPLEFT", target, "TOPRIGHT", extraX, extraY + contentTopInset)
|
||||
end
|
||||
end
|
||||
elseif prevPlaced then
|
||||
missingTargets = missingTargets + 1
|
||||
HMGT:DebugScoped("verbose", HMGT:GetTrackerDebugScope(tracker), "TrackerAttach fallback-stack tracker=%s player=%s unit=%s", tostring(tracker.id), tostring(playerName), tostring(unitId))
|
||||
if growsUp then
|
||||
frame:SetPoint("BOTTOMLEFT", prevPlaced, "TOPLEFT", 0, (tracker.barSpacing or 2) + 10)
|
||||
else
|
||||
frame:SetPoint("TOPLEFT", prevPlaced, "BOTTOMLEFT", 0, -((tracker.barSpacing or 2) + 10))
|
||||
end
|
||||
else
|
||||
missingTargets = missingTargets + 1
|
||||
HMGT:DebugScoped("info", HMGT:GetTrackerDebugScope(tracker), "TrackerAttach fallback-anchor tracker=%s player=%s unit=%s", tostring(tracker.id), tostring(playerName), tostring(unitId))
|
||||
HMGT.TrackerFrame:ApplyAnchor(frame)
|
||||
end
|
||||
frame:EnableMouse(false)
|
||||
prevPlaced = frame
|
||||
end
|
||||
if missingTargets > 0 then
|
||||
self.anchorLayoutSignatures[frameKey] = nil
|
||||
self.nextAnchorRetryAt[frameKey] = now + 1.0
|
||||
else
|
||||
self.anchorLayoutSignatures[frameKey] = signature
|
||||
self.nextAnchorRetryAt[frameKey] = nil
|
||||
end
|
||||
return
|
||||
end
|
||||
local primary = frames[ordered[1]]
|
||||
HMGT.TrackerFrame:ApplyAnchor(primary)
|
||||
primary:EnableMouse(not tracker.locked)
|
||||
local gap = (tracker.barSpacing or 2) + 10
|
||||
local growsUp = tracker.showBar == true and tracker.growDirection == "UP"
|
||||
for index = 2, #ordered do
|
||||
local prev = frames[ordered[index - 1]]
|
||||
local frame = frames[ordered[index]]
|
||||
frame:ClearAllPoints()
|
||||
if growsUp then
|
||||
frame:SetPoint("BOTTOMLEFT", prev, "TOPLEFT", 0, gap)
|
||||
else
|
||||
frame:SetPoint("TOPLEFT", prev, "BOTTOMLEFT", 0, -gap)
|
||||
end
|
||||
frame:EnableMouse(false)
|
||||
end
|
||||
self.anchorLayoutSignatures[frameKey] = signature
|
||||
self.nextAnchorRetryAt[frameKey] = nil
|
||||
end
|
||||
|
||||
function Manager:UpdatePerGroupMemberTracker(tracker)
|
||||
local frameKey = S.GetTrackerFrameKey(tracker.id)
|
||||
local byPlayer, order, unitByPlayer, shouldShow = self:BuildEntriesByPlayerForTracker(tracker)
|
||||
self.activeOrders[frameKey] = order
|
||||
self.unitByPlayer[frameKey] = unitByPlayer
|
||||
local active, shownOrder = {}, {}
|
||||
local shownByPlayer = {}
|
||||
local entryCount, shouldTick = 0, false
|
||||
for _, playerName in ipairs(order) do
|
||||
local frame = self:EnsurePlayerFrame(tracker, playerName)
|
||||
local entries = byPlayer[playerName] or {}
|
||||
if HMGT.FilterDisplayEntries then
|
||||
entries = HMGT:FilterDisplayEntries(tracker, entries) or entries
|
||||
end
|
||||
if HMGT.SortDisplayEntries then
|
||||
HMGT:SortDisplayEntries(entries)
|
||||
end
|
||||
if #entries > 0 then
|
||||
HMGT.TrackerFrame:UpdateFrame(frame, entries, true)
|
||||
frame:Show()
|
||||
active[playerName] = true
|
||||
shownOrder[#shownOrder + 1] = playerName
|
||||
shownByPlayer[playerName] = entries
|
||||
entryCount = entryCount + #entries
|
||||
for _, entry in ipairs(entries) do
|
||||
if S.EntryNeedsVisualTicker(entry) then
|
||||
shouldTick = true
|
||||
break
|
||||
end
|
||||
end
|
||||
else
|
||||
frame:Hide()
|
||||
end
|
||||
end
|
||||
for playerName, frame in pairs(self.perPlayerFrames[frameKey] or {}) do
|
||||
if not active[playerName] then
|
||||
frame:Hide()
|
||||
end
|
||||
end
|
||||
self.activeOrders[frameKey] = shownOrder
|
||||
self.unitByPlayer[frameKey] = unitByPlayer
|
||||
self._displaySignatures[frameKey] = S.BuildGroupDisplaySignature and S.BuildGroupDisplaySignature(shownOrder, shownByPlayer) or nil
|
||||
if shouldShow and #shownOrder > 0 then
|
||||
self:RefreshPerGroupAnchors(tracker, false)
|
||||
return true, entryCount, shouldTick
|
||||
end
|
||||
self:HidePlayerFrames(frameKey)
|
||||
self._displaySignatures[frameKey] = "0"
|
||||
return false, 0, false
|
||||
end
|
||||
154
Modules/Tracker/InterruptTracker/InterruptSpellDatabase.lua
Normal file
154
Modules/Tracker/InterruptTracker/InterruptSpellDatabase.lua
Normal file
@@ -0,0 +1,154 @@
|
||||
-- Core/InterruptSpellDatabase.lua
|
||||
|
||||
HMGT_SpellData = HMGT_SpellData or {}
|
||||
local Spell = HMGT_SpellData.Spell
|
||||
if not Spell then return end
|
||||
|
||||
HMGT_SpellData.Interrupts = {
|
||||
-- WARRIOR
|
||||
Spell(6552, "Pummel", {
|
||||
classes = {"WARRIOR"},
|
||||
category = "interrupt",
|
||||
state = { kind = "cooldown", cooldown = 15 },
|
||||
mods = {
|
||||
{ talentSpellId = 391271, value = 10, op = "reduceByPercent", target = "cooldown" },
|
||||
},
|
||||
}),
|
||||
Spell(23920, "Spell Reflection", {
|
||||
classes = {"WARRIOR"},
|
||||
category = "interrupt",
|
||||
state = { kind = "cooldown", cooldown = 20 },
|
||||
mods = {
|
||||
{ talentSpellId = 391271, value = 10, op = "reduceByPercent", target = "cooldown" },
|
||||
},
|
||||
}),
|
||||
|
||||
-- PALADIN
|
||||
Spell(96231, "Rebuke", {
|
||||
classes = {"PALADIN"},
|
||||
specs = {3},
|
||||
category = "interrupt",
|
||||
state = { kind = "cooldown", cooldown = 15 },
|
||||
}),
|
||||
|
||||
-- HUNTER
|
||||
Spell(147362, "Counter Shot", {
|
||||
classes = {"HUNTER"},
|
||||
specs = {1, 2},
|
||||
category = "interrupt",
|
||||
state = { kind = "cooldown", cooldown = 24 },
|
||||
}),
|
||||
Spell(187707, "Muzzle", {
|
||||
classes = {"HUNTER"},
|
||||
specs = {3},
|
||||
category = "interrupt",
|
||||
state = { kind = "cooldown", cooldown = 15 },
|
||||
}),
|
||||
|
||||
-- ROGUE
|
||||
Spell(1766, "Kick", {
|
||||
classes = {"ROGUE"},
|
||||
category = "interrupt",
|
||||
state = { kind = "cooldown", cooldown = 15 },
|
||||
}),
|
||||
|
||||
-- PRIEST
|
||||
Spell(15487, "Silence", {
|
||||
classes = {"PRIEST"},
|
||||
specs = {3},
|
||||
category = "interrupt",
|
||||
state = { kind = "cooldown", cooldown = 45 },
|
||||
}),
|
||||
|
||||
-- DEATH KNIGHT
|
||||
Spell(47528, "Mind Freeze", {
|
||||
classes = {"DEATHKNIGHT"},
|
||||
category = "interrupt",
|
||||
state = { kind = "cooldown", cooldown = 15 },
|
||||
relations = {
|
||||
{
|
||||
when = "interruptSuccess",
|
||||
effects = {
|
||||
{
|
||||
type = "reduceCooldown",
|
||||
targetSpellId = 47528,
|
||||
amount = 3,
|
||||
talentSpellId = 378848,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
}),
|
||||
|
||||
-- SHAMAN
|
||||
Spell(57994, "Wind Shear", {
|
||||
classes = {"SHAMAN"},
|
||||
category = "interrupt",
|
||||
state = { kind = "cooldown", cooldown = 12 },
|
||||
}),
|
||||
|
||||
-- MAGE
|
||||
Spell(2139, "Counterspell", {
|
||||
classes = {"MAGE"},
|
||||
category = "interrupt",
|
||||
state = { kind = "cooldown", cooldown = 24 },
|
||||
}),
|
||||
|
||||
-- WARLOCK
|
||||
Spell(19647, "Spell Lock", {
|
||||
classes = {"WARLOCK"},
|
||||
specs = {2},
|
||||
category = "interrupt",
|
||||
state = { kind = "cooldown", cooldown = 24 },
|
||||
}),
|
||||
Spell(132409, "Spell Lock (Grimoire)", {
|
||||
classes = {"WARLOCK"},
|
||||
specs = {1, 3},
|
||||
category = "interrupt",
|
||||
state = { kind = "cooldown", cooldown = 24 },
|
||||
}),
|
||||
|
||||
-- MONK
|
||||
Spell(116705, "Spear Hand Strike", {
|
||||
classes = {"MONK"},
|
||||
specs = {1, 3},
|
||||
category = "interrupt",
|
||||
state = { kind = "cooldown", cooldown = 15 },
|
||||
}),
|
||||
|
||||
-- DEMON HUNTER
|
||||
Spell(183752, "Disrupt", {
|
||||
classes = {"DEMONHUNTER"},
|
||||
specs = {nil},
|
||||
category = "interrupt",
|
||||
state = { kind = "cooldown", cooldown = 15 },
|
||||
}),
|
||||
|
||||
-- DRUID
|
||||
Spell(78675, "Solar Beam", {
|
||||
classes = {"DRUID"},
|
||||
specs = {1},
|
||||
category = "interrupt",
|
||||
state = { kind = "cooldown", cooldown = 60 },
|
||||
}),
|
||||
Spell(106839, "Skull Bash", {
|
||||
classes = {"DRUID"},
|
||||
specs = {2, 3},
|
||||
category = "interrupt",
|
||||
state = { kind = "cooldown", cooldown = 15 },
|
||||
}),
|
||||
|
||||
-- EVOKER
|
||||
Spell(351338, "Quell", {
|
||||
classes = {"EVOKER"},
|
||||
category = "interrupt",
|
||||
state = { kind = "cooldown", cooldown = 40 },
|
||||
mods = {
|
||||
{ talentSpellId = 396371, value = 20, op = "set", target = "cooldown" },
|
||||
},
|
||||
}),
|
||||
}
|
||||
|
||||
if HMGT_SpellData.RebuildLookups then
|
||||
HMGT_SpellData.RebuildLookups()
|
||||
end
|
||||
21
Modules/Tracker/InterruptTracker/InterruptTracker.lua
Normal file
21
Modules/Tracker/InterruptTracker/InterruptTracker.lua
Normal file
@@ -0,0 +1,21 @@
|
||||
-- Modules/InterruptTracker.lua
|
||||
-- Interrupt tracker based on the shared single-frame tracker base.
|
||||
|
||||
local ADDON_NAME = "HailMaryGuildTools"
|
||||
local HMGT = LibStub("AceAddon-3.0"):GetAddon(ADDON_NAME)
|
||||
local L = LibStub("AceLocale-3.0"):GetLocale(ADDON_NAME)
|
||||
|
||||
local Base = HMGT.SingleFrameTrackerBase
|
||||
if not Base then return end
|
||||
|
||||
Base:CreateModule("InterruptTracker", {
|
||||
profileKey = "interruptTracker",
|
||||
frameName = "InterruptTracker",
|
||||
title = function()
|
||||
return L["IT_TITLE"]
|
||||
end,
|
||||
demoKey = "interruptTracker",
|
||||
database = function()
|
||||
return HMGT_SpellData.Interrupts
|
||||
end,
|
||||
})
|
||||
68
Modules/Tracker/NormalTrackerFrames.lua
Normal file
68
Modules/Tracker/NormalTrackerFrames.lua
Normal file
@@ -0,0 +1,68 @@
|
||||
local ADDON_NAME = "HailMaryGuildTools"
|
||||
local HMGT = LibStub("AceAddon-3.0"):GetAddon(ADDON_NAME)
|
||||
if not HMGT or not HMGT.TrackerManager then return end
|
||||
|
||||
local Manager = HMGT.TrackerManager
|
||||
local S = Manager._shared or {}
|
||||
|
||||
function Manager:CollectEntries(tracker)
|
||||
local entries = {}
|
||||
local players = S.GetGroupPlayers(tracker)
|
||||
for _, playerInfo in ipairs(players) do
|
||||
local playerEntries = S.CollectEntriesForPlayer(tracker, playerInfo)
|
||||
for _, entry in ipairs(playerEntries) do
|
||||
entries[#entries + 1] = entry
|
||||
end
|
||||
end
|
||||
return entries
|
||||
end
|
||||
|
||||
function Manager:CollectTestEntries(tracker)
|
||||
local playerName = HMGT:NormalizePlayerName(UnitName("player")) or "Player"
|
||||
local classToken = select(2, UnitClass("player"))
|
||||
if not classToken then
|
||||
return {}
|
||||
end
|
||||
|
||||
local entries = {}
|
||||
local pData = HMGT.playerData[playerName]
|
||||
local talents = pData and pData.talents or {}
|
||||
local spells = S.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 = HMGT:GetCooldownInfo(playerName, spellEntry.spellId)
|
||||
local effectiveCd = HMGT_SpellData.GetEffectiveCooldown(spellEntry, talents)
|
||||
local isAvailabilitySpell = HMGT:IsAvailabilitySpell(spellEntry)
|
||||
local spellKnown = HMGT: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 HMGT: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 Manager:BuildEntriesForTracker(tracker)
|
||||
if tracker.testMode then
|
||||
return self:CollectTestEntries(tracker), true
|
||||
end
|
||||
if tracker.demoMode then
|
||||
return HMGT:GetDemoEntries(S.GetTrackerFrameKey(tracker.id), S.GetTrackerSpellPool(tracker.categories), tracker), true
|
||||
end
|
||||
if not tracker.enabled or not HMGT:IsVisibleForCurrentGroup(tracker) then
|
||||
return {}, false
|
||||
end
|
||||
return self:CollectEntries(tracker), true
|
||||
end
|
||||
@@ -0,0 +1,128 @@
|
||||
-- Core/RaidCooldownSpellDatabase.lua
|
||||
-- Raid cooldown database.
|
||||
|
||||
HMGT_SpellData = HMGT_SpellData or {}
|
||||
local Spell = HMGT_SpellData.Spell
|
||||
if not Spell then return end
|
||||
|
||||
HMGT_SpellData.RaidCooldowns = {
|
||||
-- WARRIOR
|
||||
Spell(97462, "Rallying Cry", {
|
||||
classes = {"WARRIOR"},
|
||||
category = "defensive",
|
||||
state = { kind = "cooldown", cooldown = 180 },
|
||||
}),
|
||||
|
||||
-- PALADIN
|
||||
Spell(31821, "Aura Mastery", {
|
||||
classes = {"PALADIN"},
|
||||
specs = {1},
|
||||
category = "defensive",
|
||||
state = { kind = "cooldown", cooldown = 180 },
|
||||
}),
|
||||
|
||||
-- PRIEST
|
||||
Spell(62618, "Power Word: Barrier", {
|
||||
classes = {"PRIEST"},
|
||||
specs = {1},
|
||||
category = "defensive",
|
||||
state = { kind = "cooldown", cooldown = 180 },
|
||||
}),
|
||||
Spell(64843, "Divine Hymn", {
|
||||
classes = {"PRIEST"},
|
||||
specs = {1, 2},
|
||||
category = "healing",
|
||||
state = { kind = "cooldown", cooldown = 180 },
|
||||
}),
|
||||
Spell(47536, "Rapture", {
|
||||
classes = {"PRIEST"},
|
||||
specs = {1},
|
||||
category = "healing",
|
||||
state = { kind = "cooldown", cooldown = 90 },
|
||||
}),
|
||||
|
||||
-- DEATH KNIGHT
|
||||
Spell(51052, "Anti-Magic Zone", {
|
||||
classes = {"DEATHKNIGHT"},
|
||||
specs = {1, 2, 3},
|
||||
category = "defensive",
|
||||
state = { kind = "cooldown", cooldown = 120 },
|
||||
mods = {
|
||||
{ talentSpellId = 145629, value = 90, op = "set", target = "cooldown" },
|
||||
},
|
||||
}),
|
||||
-- SHAMAN
|
||||
Spell(98008, "Spirit Link Totem", {
|
||||
classes = {"SHAMAN"},
|
||||
specs = {3},
|
||||
category = "defensive",
|
||||
state = { kind = "cooldown", cooldown = 180 },
|
||||
}),
|
||||
Spell(108280, "Healing Tide Totem", {
|
||||
classes = {"SHAMAN"},
|
||||
specs = {3},
|
||||
category = "healing",
|
||||
state = { kind = "cooldown", cooldown = 180 },
|
||||
}),
|
||||
Spell(192077, "Wind Rush Totem", {
|
||||
classes = {"SHAMAN"},
|
||||
category = "utility",
|
||||
state = { kind = "cooldown", cooldown = 120 },
|
||||
}),
|
||||
|
||||
-- MONK
|
||||
Spell(115310, "Revival", {
|
||||
classes = {"MONK"},
|
||||
specs = {2},
|
||||
category = "healing",
|
||||
state = { kind = "cooldown", cooldown = 180 },
|
||||
}),
|
||||
|
||||
-- DRUID
|
||||
Spell(740, "Tranquility", {
|
||||
classes = {"DRUID"},
|
||||
specs = {4},
|
||||
category = "healing",
|
||||
state = { kind = "cooldown", cooldown = 180 },
|
||||
}),
|
||||
Spell(106898, "Stampeding Roar", {
|
||||
classes = {"DRUID"},
|
||||
category = "utility",
|
||||
state = { kind = "cooldown", cooldown = 120 },
|
||||
}),
|
||||
Spell(33891, "Incarnation: Tree of Life", {
|
||||
classes = {"DRUID"},
|
||||
specs = {4},
|
||||
category = "healing",
|
||||
state = { kind = "cooldown", cooldown = 180 },
|
||||
}),
|
||||
|
||||
-- DEMON HUNTER
|
||||
Spell(196718, "Darkness", {
|
||||
classes = {"DEMONHUNTER"},
|
||||
specs = {1},
|
||||
category = "defensive",
|
||||
state = { kind = "cooldown", cooldown = 180 },
|
||||
}),
|
||||
|
||||
-- EVOKER
|
||||
Spell(363534, "Rewind", {
|
||||
classes = {"EVOKER"},
|
||||
specs = {2},
|
||||
category = "healing",
|
||||
state = { kind = "cooldown", cooldown = 240 },
|
||||
mods = {
|
||||
{ talentSpellId = 373862, value = 120, op = "set", target = "cooldown" },
|
||||
},
|
||||
}),
|
||||
Spell(406732, "Dream Projection", {
|
||||
classes = {"EVOKER"},
|
||||
specs = {2},
|
||||
category = "healing",
|
||||
state = { kind = "cooldown", cooldown = 45 },
|
||||
}),
|
||||
}
|
||||
|
||||
if HMGT_SpellData.RebuildLookups then
|
||||
HMGT_SpellData.RebuildLookups()
|
||||
end
|
||||
21
Modules/Tracker/RaidcooldownTracker/RaidcooldownTracker.lua
Normal file
21
Modules/Tracker/RaidcooldownTracker/RaidcooldownTracker.lua
Normal file
@@ -0,0 +1,21 @@
|
||||
-- Modules/RaidCooldownTracker.lua
|
||||
-- Raid cooldown tracker based on the shared single-frame tracker base.
|
||||
|
||||
local ADDON_NAME = "HailMaryGuildTools"
|
||||
local HMGT = LibStub("AceAddon-3.0"):GetAddon(ADDON_NAME)
|
||||
local L = LibStub("AceLocale-3.0"):GetLocale(ADDON_NAME)
|
||||
|
||||
local Base = HMGT.SingleFrameTrackerBase
|
||||
if not Base then return end
|
||||
|
||||
Base:CreateModule("RaidCooldownTracker", {
|
||||
profileKey = "raidCooldownTracker",
|
||||
frameName = "RaidCooldownTracker",
|
||||
title = function()
|
||||
return L["RCD_TITLE"]
|
||||
end,
|
||||
demoKey = "raidCooldownTracker",
|
||||
database = function()
|
||||
return HMGT_SpellData.RaidCooldowns
|
||||
end,
|
||||
})
|
||||
305
Modules/Tracker/SingleFrameTrackerBase.lua
Normal file
305
Modules/Tracker/SingleFrameTrackerBase.lua
Normal file
@@ -0,0 +1,305 @@
|
||||
-- Modules/Tracker/SingleFrameTrackerBase.lua
|
||||
-- Shared implementation for single-frame tracker modules.
|
||||
|
||||
local ADDON_NAME = "HailMaryGuildTools"
|
||||
local HMGT = LibStub("AceAddon-3.0"):GetAddon(ADDON_NAME)
|
||||
if not HMGT then return end
|
||||
|
||||
HMGT.SingleFrameTrackerBase = HMGT.SingleFrameTrackerBase or {}
|
||||
local Base = HMGT.SingleFrameTrackerBase
|
||||
|
||||
local function GetDefaultGroupPlayers()
|
||||
local players = {}
|
||||
|
||||
local ownName = HMGT:NormalizePlayerName(UnitName("player"))
|
||||
local ownClass = select(2, UnitClass("player"))
|
||||
table.insert(players, { name = ownName, class = ownClass, isOwn = true, unitId = "player" })
|
||||
|
||||
if IsInRaid() then
|
||||
for i = 1, GetNumGroupMembers() do
|
||||
local unitId = "raid" .. i
|
||||
local name = HMGT:NormalizePlayerName(UnitName(unitId))
|
||||
local class = select(2, UnitClass(unitId))
|
||||
if name and name ~= ownName then
|
||||
table.insert(players, { name = name, class = class, unitId = unitId })
|
||||
end
|
||||
end
|
||||
elseif IsInGroup() then
|
||||
for i = 1, GetNumGroupMembers() - 1 do
|
||||
local unitId = "party" .. i
|
||||
local name = HMGT:NormalizePlayerName(UnitName(unitId))
|
||||
local class = select(2, UnitClass(unitId))
|
||||
if name and name ~= ownName then
|
||||
table.insert(players, { name = name, class = class, unitId = unitId })
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
return players
|
||||
end
|
||||
|
||||
local function ResolveConfigValue(configValue, self)
|
||||
if type(configValue) == "function" then
|
||||
return configValue(self)
|
||||
end
|
||||
return configValue
|
||||
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 Base:Create(config)
|
||||
local tracker = {
|
||||
frame = nil,
|
||||
updateTicker = nil,
|
||||
lastEntryCount = 0,
|
||||
}
|
||||
|
||||
function tracker:GetSettings()
|
||||
return HMGT.db.profile[config.profileKey]
|
||||
end
|
||||
|
||||
function tracker:GetDatabase()
|
||||
return ResolveConfigValue(config.database, self) or {}
|
||||
end
|
||||
|
||||
function tracker:GetTitle()
|
||||
return ResolveConfigValue(config.title, self) or config.frameName
|
||||
end
|
||||
|
||||
function tracker:GetDemoKey()
|
||||
return ResolveConfigValue(config.demoKey, self) or config.profileKey
|
||||
end
|
||||
|
||||
function tracker:GetCooldownInfoOpts()
|
||||
return ResolveConfigValue(config.cooldownInfoOpts, self)
|
||||
end
|
||||
|
||||
function tracker:GetGroupPlayers()
|
||||
local custom = ResolveConfigValue(config.groupPlayersProvider, self)
|
||||
if type(custom) == "table" then
|
||||
return custom
|
||||
end
|
||||
return GetDefaultGroupPlayers()
|
||||
end
|
||||
|
||||
function tracker:EnsureUpdateTicker()
|
||||
if self.updateTicker then
|
||||
return
|
||||
end
|
||||
self.updateTicker = C_Timer.NewTicker(0.1, function()
|
||||
self:UpdateDisplay()
|
||||
end)
|
||||
end
|
||||
|
||||
function tracker:StopUpdateTicker()
|
||||
if self.updateTicker then
|
||||
self.updateTicker:Cancel()
|
||||
self.updateTicker = nil
|
||||
end
|
||||
end
|
||||
|
||||
function tracker:SetUpdateTickerEnabled(enabled)
|
||||
if enabled then
|
||||
self:EnsureUpdateTicker()
|
||||
else
|
||||
self:StopUpdateTicker()
|
||||
end
|
||||
end
|
||||
|
||||
function tracker:Enable()
|
||||
local s = self:GetSettings()
|
||||
if not s.enabled and not s.demoMode and not s.testMode then return end
|
||||
|
||||
if not self.frame then
|
||||
self.frame = HMGT.TrackerFrame:CreateTrackerFrame(config.frameName, s)
|
||||
HMGT.TrackerFrame:SetTitle(self.frame, self:GetTitle())
|
||||
end
|
||||
|
||||
if HMGT:IsVisibleForCurrentGroup(s) then
|
||||
self.frame:Show()
|
||||
else
|
||||
self.frame:Hide()
|
||||
end
|
||||
self:UpdateDisplay()
|
||||
end
|
||||
|
||||
function tracker:Disable()
|
||||
self:StopUpdateTicker()
|
||||
if self.frame then
|
||||
self.frame:Hide()
|
||||
end
|
||||
end
|
||||
|
||||
function tracker:UpdateDisplay()
|
||||
if not self.frame then
|
||||
self:StopUpdateTicker()
|
||||
return
|
||||
end
|
||||
|
||||
local s = self:GetSettings()
|
||||
local database = self:GetDatabase()
|
||||
local cooldownInfoOpts = self:GetCooldownInfoOpts()
|
||||
|
||||
if s.testMode then
|
||||
HMGT.TrackerFrame:SetLocked(self.frame, s.locked)
|
||||
local entries = HMGT:GetOwnTestEntries(database, s, cooldownInfoOpts)
|
||||
self.lastEntryCount = #entries
|
||||
HMGT.TrackerFrame:UpdateFrame(self.frame, entries)
|
||||
self.frame:Show()
|
||||
local shouldTick = false
|
||||
for _, entry in ipairs(entries) do
|
||||
if EntryNeedsVisualTicker(entry) then
|
||||
shouldTick = true
|
||||
break
|
||||
end
|
||||
end
|
||||
self:SetUpdateTickerEnabled(shouldTick)
|
||||
return
|
||||
end
|
||||
|
||||
if s.demoMode then
|
||||
HMGT.TrackerFrame:SetLocked(self.frame, s.locked)
|
||||
local entries = HMGT:GetDemoEntries(self:GetDemoKey(), database, s)
|
||||
self.lastEntryCount = #entries
|
||||
HMGT.TrackerFrame:UpdateFrame(self.frame, entries)
|
||||
self.frame:Show()
|
||||
self:SetUpdateTickerEnabled(#entries > 0)
|
||||
return
|
||||
end
|
||||
|
||||
if not s.enabled then
|
||||
self.lastEntryCount = 0
|
||||
self.frame:Hide()
|
||||
self:StopUpdateTicker()
|
||||
return
|
||||
end
|
||||
|
||||
if not HMGT:IsVisibleForCurrentGroup(s) then
|
||||
self.lastEntryCount = 0
|
||||
self.frame:Hide()
|
||||
self:StopUpdateTicker()
|
||||
return
|
||||
end
|
||||
|
||||
HMGT.TrackerFrame:SetLocked(self.frame, s.locked)
|
||||
|
||||
local entries = self:CollectEntries()
|
||||
self.lastEntryCount = #entries
|
||||
if HMGT.SortDisplayEntries then
|
||||
HMGT:SortDisplayEntries(entries, config.profileKey)
|
||||
end
|
||||
|
||||
HMGT.TrackerFrame:UpdateFrame(self.frame, entries)
|
||||
self.frame:Show()
|
||||
|
||||
local shouldTick = false
|
||||
for _, entry in ipairs(entries) do
|
||||
if EntryNeedsVisualTicker(entry) then
|
||||
shouldTick = true
|
||||
break
|
||||
end
|
||||
end
|
||||
self:SetUpdateTickerEnabled(shouldTick)
|
||||
end
|
||||
|
||||
function tracker:CollectEntries()
|
||||
local entries = {}
|
||||
local s = self:GetSettings()
|
||||
local database = self:GetDatabase()
|
||||
local cooldownInfoOpts = self:GetCooldownInfoOpts()
|
||||
local players = self:GetGroupPlayers()
|
||||
|
||||
for _, playerInfo in ipairs(players) do
|
||||
repeat
|
||||
local name = playerInfo.name
|
||||
local pData = HMGT.playerData[name]
|
||||
local class = pData and pData.class or playerInfo.class
|
||||
local specIdx
|
||||
if playerInfo.isOwn then
|
||||
specIdx = GetSpecialization()
|
||||
if not specIdx or specIdx == 0 then break end
|
||||
else
|
||||
specIdx = pData and pData.specIndex or nil
|
||||
if not specIdx or tonumber(specIdx) <= 0 then break end
|
||||
end
|
||||
local talents = pData and pData.talents or {}
|
||||
|
||||
if not class then break end
|
||||
|
||||
local knownSpells = HMGT_SpellData.GetSpellsForSpec(class, specIdx, database)
|
||||
|
||||
for _, spellEntry in ipairs(knownSpells) do
|
||||
if s.enabledSpells[spellEntry.spellId] ~= false then
|
||||
local remaining, total, curCharges, maxCharges = HMGT:GetCooldownInfo(name, spellEntry.spellId, cooldownInfoOpts)
|
||||
local isAvailabilitySpell = HMGT.IsAvailabilitySpell and HMGT:IsAvailabilitySpell(spellEntry)
|
||||
local effectiveCd = HMGT_SpellData.GetEffectiveCooldown(spellEntry, talents)
|
||||
local include = HMGT:ShouldDisplayEntry(s, remaining, curCharges, maxCharges, spellEntry)
|
||||
local spellKnown = HMGT:IsTrackedSpellKnownForPlayer(name, spellEntry.spellId)
|
||||
local hasPartialCharges = (tonumber(maxCharges) or 0) > 0
|
||||
and (tonumber(curCharges) 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 then
|
||||
if isAvailabilitySpell and not HMGT:HasAvailabilityState(name, spellEntry.spellId) then
|
||||
include = false
|
||||
end
|
||||
end
|
||||
|
||||
if include then
|
||||
entries[#entries + 1] = {
|
||||
playerName = name,
|
||||
class = class,
|
||||
spellEntry = spellEntry,
|
||||
remaining = remaining,
|
||||
total = total > 0 and total or effectiveCd,
|
||||
currentCharges = curCharges,
|
||||
maxCharges = maxCharges,
|
||||
}
|
||||
end
|
||||
end
|
||||
end
|
||||
until true
|
||||
end
|
||||
|
||||
return entries
|
||||
end
|
||||
|
||||
return tracker
|
||||
end
|
||||
|
||||
function Base:CreateModule(moduleName, config, ...)
|
||||
if type(moduleName) ~= "string" or moduleName == "" then
|
||||
return self:Create(config)
|
||||
end
|
||||
|
||||
local module = HMGT:NewModule(moduleName, ...)
|
||||
local tracker = self:Create(config)
|
||||
for key, value in pairs(tracker) do
|
||||
module[key] = value
|
||||
end
|
||||
HMGT[moduleName] = module
|
||||
return module
|
||||
end
|
||||
1104
Modules/Tracker/SpellDatabase.lua
Normal file
1104
Modules/Tracker/SpellDatabase.lua
Normal file
File diff suppressed because it is too large
Load Diff
803
Modules/Tracker/TrackerManager.lua
Normal file
803
Modules/Tracker/TrackerManager.lua
Normal file
@@ -0,0 +1,803 @@
|
||||
-- Modules/Tracker/TrackerManager.lua
|
||||
-- Generic tracker manager for category-driven tracker frames.
|
||||
|
||||
local ADDON_NAME = "HailMaryGuildTools"
|
||||
local HMGT = LibStub("AceAddon-3.0"):GetAddon(ADDON_NAME)
|
||||
if not HMGT then return end
|
||||
|
||||
local Manager = {}
|
||||
HMGT.TrackerManager = Manager
|
||||
|
||||
Manager.frames = Manager.frames or {}
|
||||
Manager.perPlayerFrames = Manager.perPlayerFrames or {}
|
||||
Manager.activeOrders = Manager.activeOrders or {}
|
||||
Manager.unitByPlayer = Manager.unitByPlayer or {}
|
||||
Manager.anchorLayoutSignatures = Manager.anchorLayoutSignatures or {}
|
||||
Manager.nextAnchorRetryAt = Manager.nextAnchorRetryAt or {}
|
||||
Manager.enabled = false
|
||||
Manager.visualTicker = nil
|
||||
Manager.lastEntryCount = 0
|
||||
Manager._shared = Manager._shared or {}
|
||||
Manager._trackerCache = Manager._trackerCache or nil
|
||||
Manager._trackerCacheSignature = Manager._trackerCacheSignature or nil
|
||||
Manager._displaySignatures = Manager._displaySignatures or {}
|
||||
Manager._layoutDirty = Manager._layoutDirty == true
|
||||
|
||||
local function GetTrackerFrameKey(trackerId)
|
||||
return "tracker:" .. tostring(tonumber(trackerId) or 0)
|
||||
end
|
||||
|
||||
local function GetTrackerFrameName(trackerId)
|
||||
return "GenericTracker_" .. tostring(tonumber(trackerId) or 0)
|
||||
end
|
||||
|
||||
local function GetTrackerPlayerFrameName(trackerId, playerName)
|
||||
local token = tostring(playerName or "Unknown"):gsub("[^%w_]", "_")
|
||||
return string.format("%s_%s", GetTrackerFrameName(trackerId), token)
|
||||
end
|
||||
|
||||
local function ShortName(name)
|
||||
if not name then
|
||||
return ""
|
||||
end
|
||||
local short = name:match("^[^-]+")
|
||||
return short or name
|
||||
end
|
||||
|
||||
local function IsUsableAnchorFrame(frame)
|
||||
return frame
|
||||
and frame.IsObjectType
|
||||
and (frame:IsObjectType("Frame") or frame:IsObjectType("Button"))
|
||||
end
|
||||
|
||||
local function GetFrameUnit(frame)
|
||||
if not frame then
|
||||
return nil
|
||||
end
|
||||
local unit = frame.unit
|
||||
if not unit and frame.GetAttribute then
|
||||
unit = frame:GetAttribute("unit")
|
||||
end
|
||||
return unit
|
||||
end
|
||||
|
||||
local function FrameMatchesUnit(frame, unitId)
|
||||
if not IsUsableAnchorFrame(frame) then
|
||||
return false
|
||||
end
|
||||
if not unitId then
|
||||
return true
|
||||
end
|
||||
return GetFrameUnit(frame) == unitId
|
||||
end
|
||||
|
||||
local PLAYER_FRAME_CANDIDATES = {
|
||||
"PlayerFrame",
|
||||
"ElvUF_Player",
|
||||
"NephUI_PlayerFrame",
|
||||
"NephUIPlayerFrame",
|
||||
"oUF_NephUI_Player",
|
||||
"SUFUnitplayer",
|
||||
}
|
||||
|
||||
local PARTY_FRAME_PATTERNS = {
|
||||
"PartyMemberFrame%d",
|
||||
"CompactPartyFrameMember%d",
|
||||
"ElvUF_PartyGroup1UnitButton%d",
|
||||
"ElvUF_PartyUnitButton%d",
|
||||
"NephUI_PartyUnitButton%d",
|
||||
"NephUI_PartyFrame%d",
|
||||
"NephUIPartyFrame%d",
|
||||
"oUF_NephUI_PartyUnitButton%d",
|
||||
"SUFUnitparty%d",
|
||||
}
|
||||
|
||||
local unitFrameCache = {}
|
||||
|
||||
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
|
||||
|
||||
local function BuildAnchorLayoutSignature(settings, ordered, unitByPlayer)
|
||||
local parts = {
|
||||
settings.attachToPartyFrame == true and "attach" or "stack",
|
||||
tostring(settings.partyAttachSide or "RIGHT"),
|
||||
tostring(tonumber(settings.partyAttachOffsetX) or 8),
|
||||
tostring(tonumber(settings.partyAttachOffsetY) or 0),
|
||||
tostring(settings.showBar and "bar" or "icon"),
|
||||
tostring(settings.growDirection or "DOWN"),
|
||||
tostring(settings.width or 250),
|
||||
tostring(settings.barHeight or 20),
|
||||
tostring(settings.iconSize or 32),
|
||||
tostring(settings.iconCols or 6),
|
||||
tostring(settings.barSpacing or 2),
|
||||
tostring(settings.locked),
|
||||
tostring(settings.anchorTo or "UIParent"),
|
||||
tostring(settings.anchorPoint or "TOPLEFT"),
|
||||
tostring(settings.anchorRelPoint or "TOPLEFT"),
|
||||
tostring(settings.anchorX or settings.posX or 0),
|
||||
tostring(settings.anchorY or settings.posY or 0),
|
||||
}
|
||||
|
||||
for _, playerName in ipairs(ordered or {}) do
|
||||
parts[#parts + 1] = tostring(playerName)
|
||||
parts[#parts + 1] = tostring(unitByPlayer and unitByPlayer[playerName] or "")
|
||||
end
|
||||
|
||||
return table.concat(parts, "|")
|
||||
end
|
||||
|
||||
local function IsGroupTracker(tracker)
|
||||
return type(tracker) == "table" and tracker.trackerType == "group"
|
||||
end
|
||||
|
||||
local function ResolveNamedUnitFrame(unitId)
|
||||
if unitId == "player" then
|
||||
for _, frameName in ipairs(PLAYER_FRAME_CANDIDATES) do
|
||||
local frame = _G[frameName]
|
||||
if FrameMatchesUnit(frame, unitId) or (frameName == "PlayerFrame" and IsUsableAnchorFrame(frame)) then
|
||||
return frame
|
||||
end
|
||||
end
|
||||
return nil
|
||||
end
|
||||
|
||||
local idx = type(unitId) == "string" and unitId:match("^party(%d+)$")
|
||||
if not idx then
|
||||
return nil
|
||||
end
|
||||
|
||||
idx = tonumber(idx)
|
||||
for _, pattern in ipairs(PARTY_FRAME_PATTERNS) do
|
||||
local frame = _G[pattern:format(idx)]
|
||||
if FrameMatchesUnit(frame, unitId) then
|
||||
return frame
|
||||
end
|
||||
end
|
||||
|
||||
return nil
|
||||
end
|
||||
|
||||
local function ScanUnitFrame(unitId)
|
||||
local frame = EnumerateFrames()
|
||||
local scanned = 0
|
||||
while frame and scanned < 8000 do
|
||||
if IsUsableAnchorFrame(frame) and GetFrameUnit(frame) == unitId then
|
||||
HMGT:Debug("verbose", "TrackerAttach scan unit=%s scanned=%d found=true", tostring(unitId), scanned)
|
||||
return frame
|
||||
end
|
||||
scanned = scanned + 1
|
||||
frame = EnumerateFrames(frame)
|
||||
end
|
||||
HMGT:Debug("verbose", "TrackerAttach scan unit=%s scanned=%d found=false", tostring(unitId), scanned)
|
||||
return nil
|
||||
end
|
||||
|
||||
local function ResolveUnitAnchorFrame(unitId)
|
||||
if not unitId then
|
||||
return nil
|
||||
end
|
||||
|
||||
local now = GetTime()
|
||||
local cached = unitFrameCache[unitId]
|
||||
if cached and now < (cached.expires or 0) then
|
||||
if cached.frame and cached.frame:IsShown() then
|
||||
return cached.frame
|
||||
end
|
||||
return nil
|
||||
end
|
||||
|
||||
local frame = ResolveNamedUnitFrame(unitId)
|
||||
if not frame then
|
||||
frame = ScanUnitFrame(unitId)
|
||||
end
|
||||
|
||||
local expiresIn = 1.0
|
||||
if frame and frame:IsShown() then
|
||||
expiresIn = 10.0
|
||||
end
|
||||
unitFrameCache[unitId] = {
|
||||
frame = frame,
|
||||
expires = now + expiresIn,
|
||||
}
|
||||
|
||||
if frame and frame:IsShown() then
|
||||
return frame
|
||||
end
|
||||
return nil
|
||||
end
|
||||
|
||||
local function GetGroupPlayers(tracker)
|
||||
local players = {}
|
||||
|
||||
local ownName = HMGT:NormalizePlayerName(UnitName("player"))
|
||||
local ownClass = select(2, UnitClass("player"))
|
||||
local includeOwnPlayer = true
|
||||
if IsGroupTracker(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 = HMGT: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 = HMGT: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
|
||||
|
||||
local function GetTrackerLabel(tracker)
|
||||
if type(tracker) ~= "table" then
|
||||
return "Tracker"
|
||||
end
|
||||
|
||||
local name = tostring(tracker.name or ""):gsub("^%s+", ""):gsub("%s+$", "")
|
||||
local trackerId = tonumber(tracker.id) or 0
|
||||
if name ~= "" then
|
||||
return name
|
||||
end
|
||||
if trackerId > 0 then
|
||||
return string.format("Tracker %d", trackerId)
|
||||
end
|
||||
return "Tracker"
|
||||
end
|
||||
|
||||
local function GetTrackerSpellPool(categories)
|
||||
if HMGT_SpellData and type(HMGT_SpellData.GetSpellPoolForCategories) == "function" then
|
||||
return HMGT_SpellData.GetSpellPoolForCategories(categories)
|
||||
end
|
||||
return {}
|
||||
end
|
||||
|
||||
local function GetTrackerSpellsForPlayer(classToken, specIndex, categories)
|
||||
if HMGT_SpellData and type(HMGT_SpellData.GetSpellsForCategories) == "function" then
|
||||
return HMGT_SpellData.GetSpellsForCategories(classToken, specIndex, categories)
|
||||
end
|
||||
return {}
|
||||
end
|
||||
|
||||
local function CollectEntriesForPlayer(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 = HMGT.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 = GetTrackerSpellsForPlayer(classToken, specIndex, tracker.categories)
|
||||
for _, spellEntry in ipairs(spells) do
|
||||
if tracker.enabledSpells[spellEntry.spellId] ~= false then
|
||||
local remaining, total, currentCharges, maxCharges = HMGT:GetCooldownInfo(playerName, spellEntry.spellId)
|
||||
local effectiveCd = HMGT_SpellData.GetEffectiveCooldown(spellEntry, talents)
|
||||
local isAvailabilitySpell = HMGT:IsAvailabilitySpell(spellEntry)
|
||||
local include = HMGT:ShouldDisplayEntry(tracker, remaining, currentCharges, maxCharges, spellEntry)
|
||||
local spellKnown = HMGT: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 HMGT: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
|
||||
|
||||
local function GetAvailablePartyPreviewUnits()
|
||||
local units = {}
|
||||
for index = 1, 4 do
|
||||
local unitId = "party" .. index
|
||||
if ResolveUnitAnchorFrame(unitId) then
|
||||
units[#units + 1] = {
|
||||
playerName = string.format("Party %d", index),
|
||||
unitId = unitId,
|
||||
}
|
||||
end
|
||||
end
|
||||
return units
|
||||
end
|
||||
|
||||
local function BuildPartyPreviewEntries(entries)
|
||||
local byPlayer = {}
|
||||
local order = {}
|
||||
local unitByPlayer = {}
|
||||
local previewUnits = GetAvailablePartyPreviewUnits()
|
||||
|
||||
for _, previewUnit in ipairs(previewUnits) do
|
||||
local playerName = previewUnit.playerName
|
||||
local playerEntries = CopyEntriesForPreview(entries, playerName)
|
||||
if #playerEntries > 0 then
|
||||
byPlayer[playerName] = playerEntries
|
||||
order[#order + 1] = playerName
|
||||
unitByPlayer[playerName] = previewUnit.unitId
|
||||
end
|
||||
end
|
||||
|
||||
return byPlayer, order, unitByPlayer, #order > 0
|
||||
end
|
||||
|
||||
local function SortTrackers(trackers)
|
||||
table.sort(trackers, function(a, b)
|
||||
local aId = tonumber(a and a.id) or 0
|
||||
local bId = tonumber(b and b.id) or 0
|
||||
if aId ~= bId then
|
||||
return aId < bId
|
||||
end
|
||||
return tostring(a and a.name or "") < tostring(b and b.name or "")
|
||||
end)
|
||||
return trackers
|
||||
end
|
||||
|
||||
local function BuildTrackerCacheSignature(trackers)
|
||||
local parts = { tostring(#(trackers or {})) }
|
||||
for index, tracker in ipairs(trackers or {}) do
|
||||
parts[#parts + 1] = tostring(index)
|
||||
parts[#parts + 1] = tostring(tonumber(tracker and tracker.id) or 0)
|
||||
parts[#parts + 1] = tostring(tracker and tracker.trackerType or "normal")
|
||||
parts[#parts + 1] = tostring(tracker and tracker.enabled)
|
||||
parts[#parts + 1] = tostring(tracker and tracker.name or "")
|
||||
end
|
||||
return table.concat(parts, "|")
|
||||
end
|
||||
|
||||
local function BuildNormalDisplaySignature(frameShown, entries)
|
||||
local parts = { frameShown and "1" or "0", tostring(#(entries or {})) }
|
||||
for index, entry in ipairs(entries or {}) do
|
||||
parts[#parts + 1] = tostring(index)
|
||||
parts[#parts + 1] = tostring(entry and entry.playerName or "")
|
||||
parts[#parts + 1] = tostring(entry and entry.spellEntry and entry.spellEntry.spellId or 0)
|
||||
end
|
||||
return table.concat(parts, "|")
|
||||
end
|
||||
|
||||
local function BuildGroupDisplaySignature(order, byPlayer)
|
||||
local parts = { tostring(#(order or {})) }
|
||||
for _, playerName in ipairs(order or {}) do
|
||||
parts[#parts + 1] = tostring(playerName)
|
||||
parts[#parts + 1] = tostring(#((byPlayer and byPlayer[playerName]) or {}))
|
||||
end
|
||||
return table.concat(parts, "|")
|
||||
end
|
||||
|
||||
Manager._shared.GetTrackerFrameKey = GetTrackerFrameKey
|
||||
Manager._shared.GetTrackerFrameName = GetTrackerFrameName
|
||||
Manager._shared.GetTrackerPlayerFrameName = GetTrackerPlayerFrameName
|
||||
Manager._shared.ShortName = ShortName
|
||||
Manager._shared.BuildAnchorLayoutSignature = BuildAnchorLayoutSignature
|
||||
Manager._shared.IsGroupTracker = IsGroupTracker
|
||||
Manager._shared.ResolveUnitAnchorFrame = ResolveUnitAnchorFrame
|
||||
Manager._shared.GetGroupPlayers = GetGroupPlayers
|
||||
Manager._shared.GetTrackerLabel = GetTrackerLabel
|
||||
Manager._shared.GetTrackerSpellPool = GetTrackerSpellPool
|
||||
Manager._shared.GetTrackerSpellsForPlayer = GetTrackerSpellsForPlayer
|
||||
Manager._shared.CollectEntriesForPlayer = CollectEntriesForPlayer
|
||||
Manager._shared.BuildPartyPreviewEntries = BuildPartyPreviewEntries
|
||||
Manager._shared.EntryNeedsVisualTicker = EntryNeedsVisualTicker
|
||||
Manager._shared.BuildGroupDisplaySignature = BuildGroupDisplaySignature
|
||||
|
||||
function Manager:GetTrackers()
|
||||
local profile = HMGT and HMGT.db and HMGT.db.profile
|
||||
local trackers = profile and profile.trackers or {}
|
||||
local signature = BuildTrackerCacheSignature(trackers)
|
||||
if self._trackerCache and self._trackerCacheSignature == signature then
|
||||
return self._trackerCache
|
||||
end
|
||||
local result = {}
|
||||
for _, tracker in ipairs(trackers) do
|
||||
result[#result + 1] = tracker
|
||||
end
|
||||
self._trackerCache = SortTrackers(result)
|
||||
self._trackerCacheSignature = signature
|
||||
return self._trackerCache
|
||||
end
|
||||
|
||||
function Manager:MarkTrackersDirty()
|
||||
self._trackerCache = nil
|
||||
self._trackerCacheSignature = nil
|
||||
self._layoutDirty = true
|
||||
end
|
||||
|
||||
function Manager:MarkLayoutDirty()
|
||||
self._layoutDirty = true
|
||||
end
|
||||
|
||||
function Manager:EnsureFrame(tracker)
|
||||
local frameKey = GetTrackerFrameKey(tracker.id)
|
||||
local frame = self.frames[frameKey]
|
||||
if not frame then
|
||||
frame = HMGT.TrackerFrame:CreateTrackerFrame(GetTrackerFrameName(tracker.id), tracker)
|
||||
frame._hmgtTrackerId = tonumber(tracker.id) or 0
|
||||
self.frames[frameKey] = frame
|
||||
end
|
||||
|
||||
frame._settings = tracker
|
||||
HMGT.TrackerFrame:SetTitle(frame, GetTrackerLabel(tracker))
|
||||
HMGT.TrackerFrame:SetLocked(frame, tracker.locked)
|
||||
return frame
|
||||
end
|
||||
|
||||
function Manager:GetAnchorFrame(tracker)
|
||||
if type(tracker) ~= "table" then
|
||||
return nil
|
||||
end
|
||||
|
||||
if IsGroupTracker(tracker) then
|
||||
local frameKey = GetTrackerFrameKey(tracker.id)
|
||||
local order = self.activeOrders[frameKey] or {}
|
||||
local frames = self.perPlayerFrames[frameKey] or {}
|
||||
if order[1] and frames[order[1]] and frames[order[1]]:IsShown() then
|
||||
return frames[order[1]]
|
||||
end
|
||||
end
|
||||
|
||||
return self:EnsureFrame(tracker)
|
||||
end
|
||||
|
||||
function Manager:StopVisualTicker()
|
||||
if self.visualTicker then
|
||||
self.visualTicker:Cancel()
|
||||
self.visualTicker = nil
|
||||
end
|
||||
end
|
||||
|
||||
function Manager:SetVisualTickerEnabled(enabled)
|
||||
if enabled then
|
||||
if not self.visualTicker then
|
||||
self.visualTicker = C_Timer.NewTicker(0.1, function()
|
||||
self:RefreshVisibleVisuals()
|
||||
end)
|
||||
end
|
||||
else
|
||||
self:StopVisualTicker()
|
||||
end
|
||||
end
|
||||
|
||||
function Manager:RefreshAnchors(force)
|
||||
for _, tracker in ipairs(self:GetTrackers()) do
|
||||
local frameKey = GetTrackerFrameKey(tracker.id)
|
||||
if IsGroupTracker(tracker) then
|
||||
local anchorFrame = self.frames[frameKey]
|
||||
if anchorFrame and not anchorFrame._hmgtDragging then
|
||||
HMGT.TrackerFrame:ApplyAnchor(anchorFrame)
|
||||
end
|
||||
self:RefreshPerGroupAnchors(tracker, force)
|
||||
else
|
||||
local frame = self.frames[frameKey]
|
||||
if frame and (force or frame:IsShown()) then
|
||||
if not frame._hmgtDragging then
|
||||
HMGT.TrackerFrame:ApplyAnchor(frame)
|
||||
end
|
||||
frame:EnableMouse(not tracker.locked)
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
function Manager:InvalidateAnchorLayout()
|
||||
self.anchorLayoutSignatures = {}
|
||||
self.nextAnchorRetryAt = {}
|
||||
self:MarkLayoutDirty()
|
||||
self:RefreshAnchors(true)
|
||||
end
|
||||
|
||||
function Manager:SetAllLocked(locked)
|
||||
for _, frame in pairs(self.frames) do
|
||||
HMGT.TrackerFrame:SetLocked(frame, locked)
|
||||
end
|
||||
for _, frameSet in pairs(self.perPlayerFrames) do
|
||||
for _, frame in pairs(frameSet) do
|
||||
HMGT.TrackerFrame:SetLocked(frame, locked)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
function Manager:GetAnchorableFrames()
|
||||
local frames = {}
|
||||
for _, tracker in ipairs(self:GetTrackers()) do
|
||||
local anchorKey = HMGT.GetTrackerAnchorKey and HMGT:GetTrackerAnchorKey(tracker.id) or nil
|
||||
local frame = self:GetAnchorFrame(tracker)
|
||||
if anchorKey and frame then
|
||||
frames[anchorKey] = frame
|
||||
end
|
||||
end
|
||||
return frames
|
||||
end
|
||||
|
||||
function Manager:Enable()
|
||||
self.enabled = true
|
||||
self:MarkTrackersDirty()
|
||||
self:UpdateDisplay()
|
||||
end
|
||||
|
||||
function Manager:Disable()
|
||||
self.enabled = false
|
||||
self:StopVisualTicker()
|
||||
self._layoutDirty = true
|
||||
for _, frame in pairs(self.frames) do
|
||||
frame:Hide()
|
||||
end
|
||||
for frameKey in pairs(self.perPlayerFrames) do
|
||||
self:HidePlayerFrames(frameKey)
|
||||
end
|
||||
end
|
||||
|
||||
function Manager:RefreshVisibleVisuals()
|
||||
if not self.enabled then
|
||||
self:StopVisualTicker()
|
||||
return
|
||||
end
|
||||
|
||||
local shouldTick = false
|
||||
local needsFullRefresh = false
|
||||
local totalEntries = 0
|
||||
|
||||
for _, tracker in ipairs(self:GetTrackers()) do
|
||||
local frameKey = GetTrackerFrameKey(tracker.id)
|
||||
if IsGroupTracker(tracker) then
|
||||
local currentOrder = self.activeOrders[frameKey] or {}
|
||||
if #currentOrder > 0 then
|
||||
local byPlayer, order, unitByPlayer, shouldShow = self:BuildEntriesByPlayerForTracker(tracker)
|
||||
if not shouldShow or #order ~= #currentOrder then
|
||||
needsFullRefresh = true
|
||||
else
|
||||
local byPlayerFiltered = {}
|
||||
for index, playerName in ipairs(order) do
|
||||
if playerName ~= currentOrder[index] then
|
||||
needsFullRefresh = true
|
||||
break
|
||||
end
|
||||
local entries = byPlayer[playerName] or {}
|
||||
if HMGT.FilterDisplayEntries then
|
||||
entries = HMGT:FilterDisplayEntries(tracker, entries) or entries
|
||||
end
|
||||
if HMGT.SortDisplayEntries then
|
||||
HMGT:SortDisplayEntries(entries)
|
||||
end
|
||||
if #entries == 0 then
|
||||
needsFullRefresh = true
|
||||
break
|
||||
end
|
||||
local frame = self.perPlayerFrames[frameKey] and self.perPlayerFrames[frameKey][playerName]
|
||||
if not frame or not frame:IsShown() then
|
||||
needsFullRefresh = true
|
||||
break
|
||||
end
|
||||
HMGT.TrackerFrame:UpdateFrame(frame, entries, true)
|
||||
totalEntries = totalEntries + #entries
|
||||
byPlayerFiltered[playerName] = entries
|
||||
for _, entry in ipairs(entries) do
|
||||
if EntryNeedsVisualTicker(entry) then
|
||||
shouldTick = true
|
||||
break
|
||||
end
|
||||
end
|
||||
end
|
||||
local newSignature = BuildGroupDisplaySignature(currentOrder, byPlayerFiltered)
|
||||
if self._displaySignatures[frameKey] ~= newSignature then
|
||||
needsFullRefresh = true
|
||||
end
|
||||
end
|
||||
end
|
||||
else
|
||||
local frame = self.frames[frameKey]
|
||||
if frame and frame:IsShown() then
|
||||
local entries, shouldShow = self:BuildEntriesForTracker(tracker)
|
||||
if not shouldShow then
|
||||
needsFullRefresh = true
|
||||
else
|
||||
if HMGT.FilterDisplayEntries then
|
||||
entries = HMGT:FilterDisplayEntries(tracker, entries) or entries
|
||||
end
|
||||
if HMGT.SortDisplayEntries then
|
||||
HMGT:SortDisplayEntries(entries)
|
||||
end
|
||||
if #entries == 0 then
|
||||
needsFullRefresh = true
|
||||
else
|
||||
HMGT.TrackerFrame:UpdateFrame(frame, entries, true)
|
||||
totalEntries = totalEntries + #entries
|
||||
local newSignature = BuildNormalDisplaySignature(true, entries)
|
||||
if self._displaySignatures[frameKey] ~= newSignature then
|
||||
needsFullRefresh = true
|
||||
end
|
||||
for _, entry in ipairs(entries) do
|
||||
if EntryNeedsVisualTicker(entry) then
|
||||
shouldTick = true
|
||||
break
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
self.lastEntryCount = totalEntries
|
||||
self:SetVisualTickerEnabled(shouldTick)
|
||||
if needsFullRefresh then
|
||||
HMGT:TriggerTrackerUpdate("layout")
|
||||
end
|
||||
end
|
||||
|
||||
function Manager:UpdateDisplay()
|
||||
if not self.enabled then
|
||||
self:StopVisualTicker()
|
||||
return
|
||||
end
|
||||
|
||||
local trackers = self:GetTrackers()
|
||||
local activeFrames = {}
|
||||
local shouldTick = false
|
||||
local totalEntries = 0
|
||||
local layoutDirty = self._layoutDirty == true
|
||||
|
||||
for _, tracker in ipairs(trackers) do
|
||||
local frameKey = GetTrackerFrameKey(tracker.id)
|
||||
local frame = self:EnsureFrame(tracker)
|
||||
|
||||
if IsGroupTracker(tracker) then
|
||||
frame:Hide()
|
||||
local shown, entryCount, trackerShouldTick = self:UpdatePerGroupMemberTracker(tracker)
|
||||
totalEntries = totalEntries + (entryCount or 0)
|
||||
if trackerShouldTick then
|
||||
shouldTick = true
|
||||
end
|
||||
if not shown then
|
||||
self:HidePlayerFrames(frameKey)
|
||||
end
|
||||
else
|
||||
self:HidePlayerFrames(frameKey)
|
||||
local entries, shouldShow = self:BuildEntriesForTracker(tracker)
|
||||
|
||||
if shouldShow then
|
||||
if HMGT.FilterDisplayEntries then
|
||||
entries = HMGT:FilterDisplayEntries(tracker, entries) or entries
|
||||
end
|
||||
if HMGT.SortDisplayEntries then
|
||||
HMGT:SortDisplayEntries(entries)
|
||||
end
|
||||
|
||||
HMGT.TrackerFrame:UpdateFrame(frame, entries, true)
|
||||
frame:Show()
|
||||
frame:EnableMouse(not tracker.locked)
|
||||
activeFrames[frameKey] = true
|
||||
totalEntries = totalEntries + #entries
|
||||
local newSignature = BuildNormalDisplaySignature(true, entries)
|
||||
if self._displaySignatures[frameKey] ~= newSignature then
|
||||
self._displaySignatures[frameKey] = newSignature
|
||||
layoutDirty = true
|
||||
end
|
||||
|
||||
for _, entry in ipairs(entries) do
|
||||
if EntryNeedsVisualTicker(entry) then
|
||||
shouldTick = true
|
||||
break
|
||||
end
|
||||
end
|
||||
else
|
||||
frame:Hide()
|
||||
if self._displaySignatures[frameKey] ~= "0" then
|
||||
self._displaySignatures[frameKey] = "0"
|
||||
layoutDirty = true
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
for frameKey, frame in pairs(self.frames) do
|
||||
if not activeFrames[frameKey] then
|
||||
if frame:IsShown() then
|
||||
layoutDirty = true
|
||||
end
|
||||
frame:Hide()
|
||||
end
|
||||
end
|
||||
|
||||
self.lastEntryCount = totalEntries
|
||||
self:SetVisualTickerEnabled(shouldTick)
|
||||
if layoutDirty then
|
||||
self._layoutDirty = false
|
||||
self:RefreshAnchors()
|
||||
end
|
||||
end
|
||||
2175
Modules/Tracker/TrackerOptions.lua
Normal file
2175
Modules/Tracker/TrackerOptions.lua
Normal file
File diff suppressed because it is too large
Load Diff
Reference in New Issue
Block a user