initial commit

This commit is contained in:
Torsten Brendgen
2026-04-10 21:30:31 +02:00
commit fc5a8aa361
108 changed files with 40568 additions and 0 deletions

1231
Modules/Tracker/Frame.lua Normal file

File diff suppressed because it is too large Load Diff

View File

@@ -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

View 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

View 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

View 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

View 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,
})

View 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

View File

@@ -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

View 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,
})

View 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

File diff suppressed because it is too large Load Diff

View 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

File diff suppressed because it is too large Load Diff