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

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