initial commit
This commit is contained in:
@@ -0,0 +1,391 @@
|
||||
-- Core/GroupCooldownSpellDatabase.lua
|
||||
-- Group cooldown database.
|
||||
|
||||
HMGT_SpellData = HMGT_SpellData or {}
|
||||
local Spell = HMGT_SpellData.Spell
|
||||
local Relation = HMGT_SpellData.Relation
|
||||
if not Spell then return end
|
||||
|
||||
HMGT_SpellData.GroupCooldowns = {
|
||||
-- WARRIOR
|
||||
-- Arms Spec
|
||||
Spell(118038, "Die by the Sword", {
|
||||
classes = {"WARRIOR"},
|
||||
specs = {1},
|
||||
category = "defensive",
|
||||
state = { kind = "cooldown", cooldown = 120 },
|
||||
}),
|
||||
Spell(167105, "Colossus Smash", {
|
||||
classes = {"WARRIOR"},
|
||||
specs = {1},
|
||||
category = "offensive",
|
||||
state = { kind = "cooldown", cooldown = 45 },
|
||||
}),
|
||||
|
||||
-- Fury Spec
|
||||
Spell(184364, "Enraged Regeneration", {
|
||||
classes = {"WARRIOR"},
|
||||
specs = {2},
|
||||
category = "defensive",
|
||||
state = { kind = "cooldown", cooldown = 120 },
|
||||
}),
|
||||
Spell(1719, "Recklessness", {
|
||||
classes = {"WARRIOR"},
|
||||
specs = {2},
|
||||
category = "offensive",
|
||||
state = { kind = "cooldown", cooldown = 90 },
|
||||
}),
|
||||
|
||||
-- Protection Spec
|
||||
Spell(871, "Shield Wall", {
|
||||
classes = {"WARRIOR"},
|
||||
specs = {3},
|
||||
category = "tank",
|
||||
state = { kind = "cooldown", cooldown = 108, charges = 1 },
|
||||
mods = {
|
||||
{ talentSpellId = 391271, value = 10, op = "reduceByPercent", target = "cooldown" }, -- Honed Reflexes
|
||||
{ talentSpellId = 397103, value = 2, op = "set", target = "charges" },
|
||||
},
|
||||
}),
|
||||
Spell(385952, "Shield Charge", {
|
||||
classes = {"WARRIOR"},
|
||||
specs = {3},
|
||||
category = "cc",
|
||||
state = { kind = "cooldown", cooldown = 45 },
|
||||
}),
|
||||
Spell(46968, "Shockwave", {
|
||||
classes = {"WARRIOR"},
|
||||
specs = {3},
|
||||
category = "cc",
|
||||
state = { kind = "cooldown", cooldown = 40 },
|
||||
}),
|
||||
Spell(3411, "Intervene", {
|
||||
classes = {"WARRIOR"},
|
||||
specs = {3},
|
||||
category = "defensive",
|
||||
state = { kind = "cooldown", cooldown = 27 },
|
||||
}),
|
||||
|
||||
-- All Specs
|
||||
Spell(107574, "Avatar", {
|
||||
classes = {"WARRIOR"},
|
||||
category = "offensive",
|
||||
state = { kind = "cooldown", cooldown = 90 },
|
||||
}),
|
||||
Spell(107570, "Storm Bolt", {
|
||||
classes = {"WARRIOR"},
|
||||
category = "cc",
|
||||
state = { kind = "cooldown", cooldown = 36 },
|
||||
}),
|
||||
|
||||
-- PALADIN
|
||||
Spell(498, "Divine Protection", {
|
||||
classes = {"PALADIN"},
|
||||
specs = {1, 2},
|
||||
category = "defensive",
|
||||
state = { kind = "cooldown", cooldown = 60 },
|
||||
}),
|
||||
Spell(642, "Divine Shield", {
|
||||
classes = {"PALADIN"},
|
||||
category = "defensive",
|
||||
state = { kind = "cooldown", cooldown = 300 },
|
||||
}),
|
||||
Spell(31884, "Avenging Wrath", {
|
||||
classes = {"PALADIN"},
|
||||
specs = {1, 3},
|
||||
category = "offensive",
|
||||
state = { kind = "cooldown", cooldown = 120 },
|
||||
}),
|
||||
Spell(86659, "Guardian of Ancient Kings", {
|
||||
classes = {"PALADIN"},
|
||||
specs = {2},
|
||||
category = "tank",
|
||||
state = { kind = "cooldown", cooldown = 300 },
|
||||
}),
|
||||
|
||||
-- HUNTER
|
||||
Spell(264735, "Survival of the Fittest", {
|
||||
classes = {"HUNTER"},
|
||||
specs = {1},
|
||||
category = "defensive",
|
||||
state = { kind = "cooldown", cooldown = 180 },
|
||||
}),
|
||||
Spell(109304, "Exhilaration", {
|
||||
classes = {"HUNTER"},
|
||||
category = "defensive",
|
||||
state = { kind = "cooldown", cooldown = 120 },
|
||||
}),
|
||||
|
||||
-- ROGUE
|
||||
Spell(31224, "Cloak of Shadows", {
|
||||
classes = {"ROGUE"},
|
||||
category = "defensive",
|
||||
state = { kind = "cooldown", cooldown = 60 },
|
||||
}),
|
||||
Spell(5277, "Evasion", {
|
||||
classes = {"ROGUE"},
|
||||
category = "defensive",
|
||||
state = { kind = "cooldown", cooldown = 120 },
|
||||
}),
|
||||
Spell(2094, "Blind", {
|
||||
classes = {"ROGUE"},
|
||||
category = "cc",
|
||||
state = { kind = "cooldown", cooldown = 120 },
|
||||
}),
|
||||
Spell(1856, "Vanish", {
|
||||
classes = {"ROGUE"},
|
||||
category = "defensive",
|
||||
state = { kind = "cooldown", cooldown = 120 },
|
||||
}),
|
||||
|
||||
-- DEATH KNIGHT
|
||||
Spell(55233, "Vampiric Blood", {
|
||||
classes = {"DEATHKNIGHT"},
|
||||
specs = {1},
|
||||
category = "tank",
|
||||
state = { kind = "cooldown", cooldown = 180 },
|
||||
mods = {
|
||||
{ talentSpellId = 374200, value = 150, op = "set", target = "cooldown" },
|
||||
},
|
||||
}),
|
||||
Spell(49028, "Dancing Rune Weapon", {
|
||||
classes = {"DEATHKNIGHT"},
|
||||
specs = {1},
|
||||
category = "tank",
|
||||
state = { kind = "cooldown", cooldown = 120 },
|
||||
mods = {
|
||||
{ talentSpellId = 377584, value = 60, op = "set", target = "cooldown" },
|
||||
},
|
||||
}),
|
||||
Spell(48707, "Anti-Magic Shell", {
|
||||
classes = {"DEATHKNIGHT"},
|
||||
category = "defensive",
|
||||
state = { kind = "cooldown", cooldown = 60 },
|
||||
}),
|
||||
Spell(48792, "Icebound Fortitude", {
|
||||
classes = {"DEATHKNIGHT"},
|
||||
category = "defensive",
|
||||
state = { kind = "cooldown", cooldown = 180 },
|
||||
}),
|
||||
Spell(42650, "Army of the Dead", {
|
||||
classes = {"DEATHKNIGHT"},
|
||||
category = "offensive",
|
||||
state = { kind = "cooldown", cooldown = 480 },
|
||||
}),
|
||||
Spell(49206, "Summon Gargoyle", {
|
||||
classes = {"DEATHKNIGHT"},
|
||||
specs = {3},
|
||||
category = "offensive",
|
||||
state = { kind = "cooldown", cooldown = 180 },
|
||||
}),
|
||||
|
||||
-- SHAMAN
|
||||
Spell(204336, "Grounding Totem", {
|
||||
classes = {"SHAMAN"},
|
||||
category = "defensive",
|
||||
state = { kind = "cooldown", cooldown = 30 },
|
||||
}),
|
||||
Spell(51490, "Thunderstorm", {
|
||||
classes = {"SHAMAN"},
|
||||
specs = {1},
|
||||
category = "cc",
|
||||
state = { kind = "cooldown", cooldown = 45 },
|
||||
}),
|
||||
|
||||
-- MAGE
|
||||
Spell(45438, "Ice Block", {
|
||||
classes = {"MAGE"},
|
||||
category = "defensive",
|
||||
state = { kind = "cooldown", cooldown = 240 },
|
||||
}),
|
||||
Spell(110959, "Greater Invisibility", {
|
||||
classes = {"MAGE"},
|
||||
category = "defensive",
|
||||
state = { kind = "cooldown", cooldown = 120 },
|
||||
}),
|
||||
Spell(235450, "Prismatic Barrier", {
|
||||
classes = {"MAGE"},
|
||||
specs = {3},
|
||||
category = "defensive",
|
||||
state = { kind = "cooldown", cooldown = 25 },
|
||||
}),
|
||||
|
||||
-- WARLOCK
|
||||
Spell(104773, "Unending Resolve", {
|
||||
classes = {"WARLOCK"},
|
||||
category = "defensive",
|
||||
state = { kind = "cooldown", cooldown = 180 },
|
||||
}),
|
||||
Spell(6229, "Twilight Ward", {
|
||||
classes = {"WARLOCK"},
|
||||
category = "defensive",
|
||||
state = { kind = "cooldown", cooldown = 0 },
|
||||
}),
|
||||
Spell(212295, "Nether Ward", {
|
||||
classes = {"WARLOCK"},
|
||||
specs = {3},
|
||||
category = "defensive",
|
||||
state = { kind = "cooldown", cooldown = 45 },
|
||||
}),
|
||||
|
||||
-- MONK
|
||||
Spell(122783, "Diffuse Magic", {
|
||||
classes = {"MONK"},
|
||||
category = "defensive",
|
||||
state = { kind = "cooldown", cooldown = 90 },
|
||||
}),
|
||||
Spell(122278, "Dampen Harm", {
|
||||
classes = {"MONK"},
|
||||
category = "defensive",
|
||||
state = { kind = "cooldown", cooldown = 120 },
|
||||
}),
|
||||
Spell(120954, "Fortifying Brew", {
|
||||
classes = {"MONK"},
|
||||
specs = {1},
|
||||
category = "tank",
|
||||
state = { kind = "cooldown", cooldown = 420 },
|
||||
}),
|
||||
Spell(115176, "Zen Meditation", {
|
||||
classes = {"MONK"},
|
||||
specs = {2},
|
||||
category = "defensive",
|
||||
state = { kind = "cooldown", cooldown = 300 },
|
||||
}),
|
||||
Spell(322118, "Invoke Niuzao", {
|
||||
classes = {"MONK"},
|
||||
specs = {1},
|
||||
category = "tank",
|
||||
state = { kind = "cooldown", cooldown = 180 },
|
||||
}),
|
||||
|
||||
-- DRUID
|
||||
Spell(22812, "Barkskin", {
|
||||
classes = {"DRUID"},
|
||||
category = "defensive",
|
||||
state = { kind = "cooldown", cooldown = 60 },
|
||||
}),
|
||||
Spell(61336, "Survival Instincts", {
|
||||
classes = {"DRUID"},
|
||||
specs = {2, 3},
|
||||
category = "defensive",
|
||||
state = { kind = "cooldown", cooldown = 180 },
|
||||
}),
|
||||
|
||||
-- DEMON HUNTER
|
||||
Spell(187827, "Metamorphosis", {
|
||||
classes = {"DEMONHUNTER"},
|
||||
specs = {1},
|
||||
category = "offensive",
|
||||
state = { kind = "cooldown", cooldown = 180 },
|
||||
}),
|
||||
Spell(162264, "Metamorphosis", {
|
||||
classes = {"DEMONHUNTER"},
|
||||
specs = {2},
|
||||
category = "tank",
|
||||
state = { kind = "cooldown", cooldown = 180 },
|
||||
}),
|
||||
Spell(1217605, "Metamorphosis", {
|
||||
classes = {"DEMONHUNTER"},
|
||||
specs = {3},
|
||||
category = "offensive",
|
||||
state = {
|
||||
kind = "availability",
|
||||
required = 50,
|
||||
source = {
|
||||
type = "auraStacks",
|
||||
auraSpellId = 1225789,
|
||||
fallbackSpellCountId = 1217605,
|
||||
},
|
||||
},
|
||||
}),
|
||||
Spell(203819, "Demon Spikes", {
|
||||
classes = {"DEMONHUNTER"},
|
||||
specs = {2},
|
||||
category = "tank",
|
||||
state = { kind = "cooldown", cooldown = 20 },
|
||||
}),
|
||||
Spell(212800, "Blur", {
|
||||
classes = {"DEMONHUNTER"},
|
||||
specs = {1},
|
||||
category = "defensive",
|
||||
state = { kind = "cooldown", cooldown = 60 },
|
||||
}),
|
||||
Spell(196555, "Netherwalk", {
|
||||
classes = {"DEMONHUNTER"},
|
||||
specs = {1},
|
||||
category = "defensive",
|
||||
state = { kind = "cooldown", cooldown = 180 },
|
||||
}),
|
||||
|
||||
-- EVOKER
|
||||
Spell(357214, "Time Stop", {
|
||||
classes = {"EVOKER"},
|
||||
specs = {2},
|
||||
category = "cc",
|
||||
state = { kind = "cooldown", cooldown = 120 },
|
||||
}),
|
||||
Spell(375087, "Tempest", {
|
||||
classes = {"EVOKER"},
|
||||
specs = {3},
|
||||
category = "offensive",
|
||||
state = { kind = "cooldown", cooldown = 30 },
|
||||
}),
|
||||
}
|
||||
|
||||
HMGT_SpellData.Relations = HMGT_SpellData.Relations or {}
|
||||
local hasShieldSlamShieldWallRelation = false
|
||||
local hasAngerManagementRelation = false
|
||||
for _, relation in ipairs(HMGT_SpellData.Relations) do
|
||||
local firstEffect = relation.effects and relation.effects[1]
|
||||
if tonumber(relation.triggerSpellId) == 23922 and tonumber(firstEffect and firstEffect.targetSpellId) == 871 then
|
||||
hasShieldSlamShieldWallRelation = true
|
||||
end
|
||||
if tostring(relation.when) == "powerSpent"
|
||||
and tonumber(relation.talentRequired) == 152278
|
||||
and tostring(relation.powerType) == "RAGE"
|
||||
then
|
||||
hasAngerManagementRelation = true
|
||||
end
|
||||
end
|
||||
if not hasShieldSlamShieldWallRelation and Relation then
|
||||
HMGT_SpellData.Relations[#HMGT_SpellData.Relations + 1] = Relation({
|
||||
triggerSpellId = 23922, -- Shield Slam
|
||||
classes = {"WARRIOR"},
|
||||
specs = {3}, -- Protection
|
||||
when = "cast",
|
||||
talentRequired = 384072, -- Impenetrable Wall
|
||||
effects = {
|
||||
{
|
||||
type = "reduceCooldown",
|
||||
targetSpellId = 871, -- Shield Wall
|
||||
amount = 6,
|
||||
},
|
||||
},
|
||||
})
|
||||
end
|
||||
if not hasAngerManagementRelation and Relation then
|
||||
HMGT_SpellData.Relations[#HMGT_SpellData.Relations + 1] = Relation({
|
||||
classes = {"WARRIOR"},
|
||||
specs = {3}, -- Protection
|
||||
when = "powerSpent",
|
||||
powerType = "RAGE",
|
||||
amountPerTrigger = 20,
|
||||
talentRequired = 152278, -- Anger Management
|
||||
effects = {
|
||||
{
|
||||
type = "reduceCooldown",
|
||||
targetSpellId = 107574, -- Avatar
|
||||
amount = 1,
|
||||
},
|
||||
{
|
||||
type = "reduceCooldown",
|
||||
targetSpellId = 871, -- Shield Wall
|
||||
amount = 1,
|
||||
},
|
||||
},
|
||||
})
|
||||
end
|
||||
|
||||
if HMGT_SpellData.RebuildLookups then
|
||||
HMGT_SpellData.RebuildLookups()
|
||||
end
|
||||
692
Modules/Tracker/GroupCooldownTracker/GroupCooldownTracker.lua
Normal file
692
Modules/Tracker/GroupCooldownTracker/GroupCooldownTracker.lua
Normal file
@@ -0,0 +1,692 @@
|
||||
-- Modules/GroupCooldownTracker.lua
|
||||
-- Group-Cooldown-Tracker Modul (ein Frame pro Spieler in der Gruppe)
|
||||
|
||||
local ADDON_NAME = "HailMaryGuildTools"
|
||||
local HMGT = LibStub("AceAddon-3.0"):GetAddon(ADDON_NAME)
|
||||
if not HMGT then return end
|
||||
local L = LibStub("AceLocale-3.0"):GetLocale(ADDON_NAME)
|
||||
|
||||
local GCT = HMGT:NewModule("GroupCooldownTracker")
|
||||
HMGT.GroupCooldownTracker = GCT
|
||||
|
||||
GCT.frame = nil
|
||||
GCT.frames = {}
|
||||
|
||||
local function SanitizeFrameToken(name)
|
||||
if not name or name == "" then return "Unknown" end
|
||||
return name:gsub("[^%w_]", "_")
|
||||
end
|
||||
|
||||
local function ShortName(name)
|
||||
if not name then return "" end
|
||||
local short = name:match("^[^-]+")
|
||||
return short or name
|
||||
end
|
||||
|
||||
local function IsUsableAnchorFrame(frame)
|
||||
return frame
|
||||
and frame.IsObjectType
|
||||
and (frame:IsObjectType("Frame") or frame:IsObjectType("Button"))
|
||||
end
|
||||
|
||||
local function GetFrameUnit(frame)
|
||||
if not frame then return nil end
|
||||
local unit = frame.unit
|
||||
if not unit and frame.GetAttribute then
|
||||
unit = frame:GetAttribute("unit")
|
||||
end
|
||||
return unit
|
||||
end
|
||||
|
||||
local function FrameMatchesUnit(frame, unitId)
|
||||
if not IsUsableAnchorFrame(frame) then return false end
|
||||
if not unitId then return true end
|
||||
local unit = GetFrameUnit(frame)
|
||||
return unit == unitId
|
||||
end
|
||||
|
||||
local PLAYER_FRAME_CANDIDATES = {
|
||||
"PlayerFrame",
|
||||
"ElvUF_Player",
|
||||
"NephUI_PlayerFrame",
|
||||
"NephUIPlayerFrame",
|
||||
"oUF_NephUI_Player",
|
||||
"SUFUnitplayer",
|
||||
}
|
||||
|
||||
local PARTY_FRAME_PATTERNS = {
|
||||
"PartyMemberFrame%d", -- Blizzard alt
|
||||
"CompactPartyFrameMember%d", -- Blizzard modern
|
||||
"ElvUF_PartyGroup1UnitButton%d", -- ElvUI
|
||||
"ElvUF_PartyUnitButton%d", -- ElvUI variant
|
||||
"NephUI_PartyUnitButton%d", -- NephUI (common naming variants)
|
||||
"NephUI_PartyFrame%d",
|
||||
"NephUIPartyFrame%d",
|
||||
"oUF_NephUI_PartyUnitButton%d",
|
||||
"SUFUnitparty%d", -- Shadowed Unit Frames
|
||||
}
|
||||
|
||||
local unitFrameCache = {}
|
||||
|
||||
local function EntryNeedsVisualTicker(entry)
|
||||
if type(entry) ~= "table" then
|
||||
return false
|
||||
end
|
||||
|
||||
local remaining = tonumber(entry.remaining) or 0
|
||||
if remaining > 0 then
|
||||
return true
|
||||
end
|
||||
|
||||
local maxCharges = tonumber(entry.maxCharges) or 0
|
||||
local currentCharges = tonumber(entry.currentCharges)
|
||||
if maxCharges > 0 and currentCharges ~= nil and currentCharges < maxCharges then
|
||||
return true
|
||||
end
|
||||
|
||||
return false
|
||||
end
|
||||
|
||||
local function BuildAnchorLayoutSignature(settings, ordered, unitByPlayer)
|
||||
local parts = {
|
||||
settings.attachToPartyFrame == true and "attach" or "stack",
|
||||
tostring(settings.partyAttachSide or "RIGHT"),
|
||||
tostring(tonumber(settings.partyAttachOffsetX) or 8),
|
||||
tostring(tonumber(settings.partyAttachOffsetY) or 0),
|
||||
tostring(settings.showBar and "bar" or "icon"),
|
||||
tostring(settings.growDirection or "DOWN"),
|
||||
tostring(settings.width or 250),
|
||||
tostring(settings.barHeight or 20),
|
||||
tostring(settings.iconSize or 32),
|
||||
tostring(settings.iconCols or 6),
|
||||
tostring(settings.barSpacing or 2),
|
||||
tostring(settings.locked),
|
||||
tostring(settings.anchorTo or "UIParent"),
|
||||
tostring(settings.anchorPoint or "TOPLEFT"),
|
||||
tostring(settings.anchorRelPoint or "TOPLEFT"),
|
||||
tostring(settings.anchorX or settings.posX or 0),
|
||||
tostring(settings.anchorY or settings.posY or 0),
|
||||
}
|
||||
|
||||
for _, playerName in ipairs(ordered or {}) do
|
||||
parts[#parts + 1] = tostring(playerName)
|
||||
parts[#parts + 1] = tostring(unitByPlayer and unitByPlayer[playerName] or "")
|
||||
end
|
||||
|
||||
return table.concat(parts, "|")
|
||||
end
|
||||
|
||||
local function ResolveNamedUnitFrame(unitId)
|
||||
if unitId == "player" then
|
||||
for _, frameName in ipairs(PLAYER_FRAME_CANDIDATES) do
|
||||
local frame = _G[frameName]
|
||||
if FrameMatchesUnit(frame, unitId) or (frameName == "PlayerFrame" and IsUsableAnchorFrame(frame)) then
|
||||
return frame
|
||||
end
|
||||
end
|
||||
return nil
|
||||
end
|
||||
|
||||
local idx = type(unitId) == "string" and unitId:match("^party(%d+)$")
|
||||
if not idx then
|
||||
return nil
|
||||
end
|
||||
|
||||
idx = tonumber(idx)
|
||||
for _, pattern in ipairs(PARTY_FRAME_PATTERNS) do
|
||||
local frame = _G[pattern:format(idx)]
|
||||
if FrameMatchesUnit(frame, unitId) then
|
||||
return frame
|
||||
end
|
||||
end
|
||||
return nil
|
||||
end
|
||||
|
||||
local function ScanUnitFrame(unitId)
|
||||
local frame = EnumerateFrames()
|
||||
local scanned = 0
|
||||
while frame and scanned < 8000 do
|
||||
if IsUsableAnchorFrame(frame) then
|
||||
local unit = GetFrameUnit(frame)
|
||||
if unit == unitId then
|
||||
HMGT:DebugScoped("verbose", HMGT:GetTrackerDebugScope("Group Cooldowns"), "GroupAttach scan unit=%s scanned=%d found=true", tostring(unitId), scanned)
|
||||
return frame
|
||||
end
|
||||
end
|
||||
scanned = scanned + 1
|
||||
frame = EnumerateFrames(frame)
|
||||
end
|
||||
HMGT:DebugScoped("verbose", HMGT:GetTrackerDebugScope("Group Cooldowns"), "GroupAttach scan unit=%s scanned=%d found=false", tostring(unitId), scanned)
|
||||
return nil
|
||||
end
|
||||
|
||||
local function ResolveUnitAnchorFrame(unitId)
|
||||
if not unitId then return nil end
|
||||
|
||||
local now = GetTime()
|
||||
local cached = unitFrameCache[unitId]
|
||||
if cached and now < (cached.expires or 0) then
|
||||
if cached.frame and cached.frame:IsShown() then
|
||||
return cached.frame
|
||||
end
|
||||
return nil
|
||||
end
|
||||
|
||||
local frame = ResolveNamedUnitFrame(unitId)
|
||||
if not frame then
|
||||
frame = ScanUnitFrame(unitId)
|
||||
end
|
||||
|
||||
local expiresIn = 1.0
|
||||
if frame and frame:IsShown() then
|
||||
expiresIn = 10.0
|
||||
end
|
||||
unitFrameCache[unitId] = {
|
||||
frame = frame,
|
||||
expires = now + expiresIn,
|
||||
}
|
||||
|
||||
if frame and frame:IsShown() then
|
||||
return frame
|
||||
end
|
||||
return nil
|
||||
end
|
||||
|
||||
function GCT:GetFrameIdForPlayer(playerName)
|
||||
return "GroupCooldownTracker_" .. SanitizeFrameToken(playerName)
|
||||
end
|
||||
|
||||
function GCT:GetPlayerFrame(playerName)
|
||||
if not playerName then return nil end
|
||||
return self.frames[playerName]
|
||||
end
|
||||
|
||||
function GCT:GetAnchorableFrames()
|
||||
return self.frames
|
||||
end
|
||||
|
||||
function GCT:EnsurePlayerFrame(playerName)
|
||||
local frame = self.frames[playerName]
|
||||
local s = HMGT.db.profile.groupCooldownTracker
|
||||
if frame then
|
||||
return frame
|
||||
end
|
||||
|
||||
frame = HMGT.TrackerFrame:CreateTrackerFrame(self:GetFrameIdForPlayer(playerName), s)
|
||||
frame._hmgtPlayerName = playerName
|
||||
self.frames[playerName] = frame
|
||||
return frame
|
||||
end
|
||||
|
||||
function GCT:HideAllFrames()
|
||||
for _, frame in pairs(self.frames) do
|
||||
frame:Hide()
|
||||
end
|
||||
self.activeOrder = nil
|
||||
self.unitByPlayer = nil
|
||||
self.frame = nil
|
||||
self._lastAnchorLayoutSignature = nil
|
||||
self._nextAnchorRetryAt = nil
|
||||
end
|
||||
|
||||
function GCT:SetLockedAll(locked)
|
||||
for _, frame in pairs(self.frames) do
|
||||
HMGT.TrackerFrame:SetLocked(frame, locked)
|
||||
end
|
||||
end
|
||||
|
||||
function GCT:EnsureUpdateTicker()
|
||||
if self.updateTicker then
|
||||
return
|
||||
end
|
||||
self.updateTicker = C_Timer.NewTicker(0.1, function()
|
||||
self:UpdateDisplay()
|
||||
end)
|
||||
end
|
||||
|
||||
function GCT:StopUpdateTicker()
|
||||
if self.updateTicker then
|
||||
self.updateTicker:Cancel()
|
||||
self.updateTicker = nil
|
||||
end
|
||||
end
|
||||
|
||||
function GCT:SetUpdateTickerEnabled(enabled)
|
||||
if enabled then
|
||||
self:EnsureUpdateTicker()
|
||||
else
|
||||
self:StopUpdateTicker()
|
||||
end
|
||||
end
|
||||
|
||||
function GCT:InvalidateAnchorLayout()
|
||||
self._lastAnchorLayoutSignature = nil
|
||||
self._nextAnchorRetryAt = nil
|
||||
end
|
||||
|
||||
function GCT:RefreshAnchors(force)
|
||||
local s = HMGT.db.profile.groupCooldownTracker
|
||||
if not s then return end
|
||||
|
||||
local ordered = {}
|
||||
for _, playerName in ipairs(self.activeOrder or {}) do
|
||||
local frame = self.frames[playerName]
|
||||
if frame and frame:IsShown() then
|
||||
table.insert(ordered, playerName)
|
||||
end
|
||||
end
|
||||
|
||||
if #ordered == 0 then
|
||||
self.frame = nil
|
||||
self._lastAnchorLayoutSignature = nil
|
||||
self._nextAnchorRetryAt = nil
|
||||
return
|
||||
end
|
||||
|
||||
local now = GetTime()
|
||||
local signature = BuildAnchorLayoutSignature(s, ordered, self.unitByPlayer)
|
||||
if not force and self._lastAnchorLayoutSignature == signature then
|
||||
local retryAt = tonumber(self._nextAnchorRetryAt) or 0
|
||||
if retryAt <= 0 or now < retryAt then
|
||||
return
|
||||
end
|
||||
end
|
||||
|
||||
-- Do not force anchor updates while user is dragging a tracker frame.
|
||||
for _, playerName in ipairs(ordered) do
|
||||
local frame = self.frames[playerName]
|
||||
if frame and frame._hmgtDragging then
|
||||
return
|
||||
end
|
||||
end
|
||||
|
||||
local primaryName = ordered[1]
|
||||
local primary = self.frames[primaryName]
|
||||
self.frame = primary
|
||||
|
||||
if s.attachToPartyFrame == true then
|
||||
local side = s.partyAttachSide or "RIGHT"
|
||||
local extraX = tonumber(s.partyAttachOffsetX) or 8
|
||||
local extraY = tonumber(s.partyAttachOffsetY) or 0
|
||||
local growsUp = s.showBar == true and s.growDirection == "UP"
|
||||
local barHeight = tonumber(s.barHeight) or 20
|
||||
local growUpAttachOffset = barHeight + 20
|
||||
local prevPlaced = nil
|
||||
local missingTargets = 0
|
||||
|
||||
for i = 1, #ordered do
|
||||
local playerName = ordered[i]
|
||||
local frame = self.frames[playerName]
|
||||
local unitId = self.unitByPlayer and self.unitByPlayer[playerName]
|
||||
local target = ResolveUnitAnchorFrame(unitId)
|
||||
local contentTopInset = HMGT.TrackerFrame.GetContentTopInset and HMGT.TrackerFrame:GetContentTopInset(frame) or 0
|
||||
|
||||
frame:ClearAllPoints()
|
||||
if target then
|
||||
if side == "LEFT" then
|
||||
if growsUp then
|
||||
frame:SetPoint("BOTTOMRIGHT", target, "TOPLEFT", -extraX, extraY - growUpAttachOffset)
|
||||
else
|
||||
frame:SetPoint("TOPRIGHT", target, "TOPLEFT", -extraX, extraY + contentTopInset)
|
||||
end
|
||||
else
|
||||
if growsUp then
|
||||
frame:SetPoint("BOTTOMLEFT", target, "TOPRIGHT", extraX, extraY - growUpAttachOffset)
|
||||
else
|
||||
frame:SetPoint("TOPLEFT", target, "TOPRIGHT", extraX, extraY + contentTopInset)
|
||||
end
|
||||
end
|
||||
elseif prevPlaced then
|
||||
missingTargets = missingTargets + 1
|
||||
HMGT:DebugScoped("verbose", HMGT:GetTrackerDebugScope("Group Cooldowns"), "GroupAttach fallback-stack player=%s unit=%s", tostring(playerName), tostring(unitId))
|
||||
if growsUp then
|
||||
frame:SetPoint("BOTTOMLEFT", prevPlaced, "TOPLEFT", 0, (s.barSpacing or 2) + 10)
|
||||
else
|
||||
frame:SetPoint("TOPLEFT", prevPlaced, "BOTTOMLEFT", 0, -((s.barSpacing or 2) + 10))
|
||||
end
|
||||
else
|
||||
missingTargets = missingTargets + 1
|
||||
HMGT:DebugScoped("info", HMGT:GetTrackerDebugScope("Group Cooldowns"), "GroupAttach fallback-anchor player=%s unit=%s (no party frame found)", tostring(playerName), tostring(unitId))
|
||||
HMGT.TrackerFrame:ApplyAnchor(frame)
|
||||
end
|
||||
|
||||
frame:EnableMouse(false)
|
||||
prevPlaced = frame
|
||||
end
|
||||
if missingTargets > 0 then
|
||||
self._lastAnchorLayoutSignature = nil
|
||||
self._nextAnchorRetryAt = now + 1.0
|
||||
else
|
||||
self._lastAnchorLayoutSignature = signature
|
||||
self._nextAnchorRetryAt = nil
|
||||
end
|
||||
return
|
||||
end
|
||||
|
||||
HMGT.TrackerFrame:ApplyAnchor(primary)
|
||||
primary:EnableMouse(not s.locked)
|
||||
|
||||
local gap = (s.barSpacing or 2) + 10
|
||||
local growsUp = s.showBar == true and s.growDirection == "UP"
|
||||
for i = 2, #ordered do
|
||||
local prev = self.frames[ordered[i - 1]]
|
||||
local frame = self.frames[ordered[i]]
|
||||
frame:ClearAllPoints()
|
||||
if growsUp then
|
||||
frame:SetPoint("BOTTOMLEFT", prev, "TOPLEFT", 0, gap)
|
||||
else
|
||||
frame:SetPoint("TOPLEFT", prev, "BOTTOMLEFT", 0, -gap)
|
||||
end
|
||||
frame:EnableMouse(false)
|
||||
end
|
||||
self._lastAnchorLayoutSignature = signature
|
||||
self._nextAnchorRetryAt = nil
|
||||
end
|
||||
|
||||
-- ============================================================
|
||||
-- ENABLE / DISABLE
|
||||
-- ============================================================
|
||||
|
||||
function GCT:Enable()
|
||||
local s = HMGT.db.profile.groupCooldownTracker
|
||||
if not s.enabled and not s.demoMode and not s.testMode then return end
|
||||
|
||||
self:UpdateDisplay()
|
||||
end
|
||||
|
||||
function GCT:Disable()
|
||||
self:StopUpdateTicker()
|
||||
self:HideAllFrames()
|
||||
end
|
||||
|
||||
-- ============================================================
|
||||
-- DISPLAY UPDATE
|
||||
-- ============================================================
|
||||
|
||||
function GCT:UpdateDisplay()
|
||||
local s = HMGT.db.profile.groupCooldownTracker
|
||||
if not s then return end
|
||||
|
||||
if s.testMode then
|
||||
local entries, playerName = HMGT:GetOwnTestEntries(HMGT_SpellData.GroupCooldowns, s, {
|
||||
deferChargeCooldownUntilEmpty = false,
|
||||
})
|
||||
local byPlayer = { [playerName] = {} }
|
||||
for _, entry in ipairs(entries) do
|
||||
entry.playerName = playerName
|
||||
table.insert(byPlayer[playerName], entry)
|
||||
end
|
||||
|
||||
self.activeOrder = { playerName }
|
||||
self.unitByPlayer = { [playerName] = "player" }
|
||||
self.lastEntryCount = 0
|
||||
local active = {}
|
||||
local shownOrder = {}
|
||||
local shouldTick = false
|
||||
for _, pName in ipairs(self.activeOrder) do
|
||||
local frame = self:EnsurePlayerFrame(pName)
|
||||
HMGT.TrackerFrame:SetLocked(frame, s.locked)
|
||||
HMGT.TrackerFrame:SetTitle(frame, string.format("%s - %s", L["GCD_TITLE"], ShortName(pName)))
|
||||
local displayEntries = byPlayer[pName]
|
||||
if HMGT.FilterDisplayEntries then
|
||||
displayEntries = HMGT:FilterDisplayEntries(s, displayEntries) or displayEntries
|
||||
end
|
||||
if HMGT.SortDisplayEntries then
|
||||
HMGT:SortDisplayEntries(displayEntries, "groupCooldownTracker")
|
||||
end
|
||||
if #displayEntries > 0 then
|
||||
HMGT.TrackerFrame:UpdateFrame(frame, displayEntries, true)
|
||||
self.lastEntryCount = self.lastEntryCount + #displayEntries
|
||||
frame:Show()
|
||||
active[pName] = true
|
||||
shownOrder[#shownOrder + 1] = pName
|
||||
for _, entry in ipairs(displayEntries) do
|
||||
if EntryNeedsVisualTicker(entry) then
|
||||
shouldTick = true
|
||||
break
|
||||
end
|
||||
end
|
||||
else
|
||||
frame:Hide()
|
||||
end
|
||||
end
|
||||
self.activeOrder = shownOrder
|
||||
|
||||
for pn, frame in pairs(self.frames) do
|
||||
if not active[pn] then
|
||||
frame:Hide()
|
||||
end
|
||||
end
|
||||
|
||||
self:RefreshAnchors()
|
||||
self:SetUpdateTickerEnabled(shouldTick)
|
||||
return
|
||||
end
|
||||
|
||||
if s.demoMode then
|
||||
local entries = HMGT:GetDemoEntries("groupCooldownTracker", HMGT_SpellData.GroupCooldowns, s)
|
||||
local playerName = HMGT:NormalizePlayerName(UnitName("player")) or "DemoPlayer"
|
||||
local byPlayer = { [playerName] = {} }
|
||||
for _, entry in ipairs(entries) do
|
||||
entry.playerName = playerName
|
||||
table.insert(byPlayer[playerName], entry)
|
||||
end
|
||||
|
||||
self.activeOrder = { playerName }
|
||||
self.unitByPlayer = { [playerName] = "player" }
|
||||
self.lastEntryCount = 0
|
||||
local active = {}
|
||||
local shownOrder = {}
|
||||
local shouldTick = false
|
||||
for _, playerName in ipairs(self.activeOrder) do
|
||||
local frame = self:EnsurePlayerFrame(playerName)
|
||||
HMGT.TrackerFrame:SetLocked(frame, s.locked)
|
||||
HMGT.TrackerFrame:SetTitle(frame, string.format("%s - %s", L["GCD_TITLE"], ShortName(playerName)))
|
||||
local displayEntries = byPlayer[playerName]
|
||||
if HMGT.FilterDisplayEntries then
|
||||
displayEntries = HMGT:FilterDisplayEntries(s, displayEntries) or displayEntries
|
||||
end
|
||||
if HMGT.SortDisplayEntries then
|
||||
HMGT:SortDisplayEntries(displayEntries, "groupCooldownTracker")
|
||||
end
|
||||
if #displayEntries > 0 then
|
||||
HMGT.TrackerFrame:UpdateFrame(frame, displayEntries, true)
|
||||
self.lastEntryCount = self.lastEntryCount + #displayEntries
|
||||
frame:Show()
|
||||
active[playerName] = true
|
||||
shownOrder[#shownOrder + 1] = playerName
|
||||
shouldTick = true
|
||||
else
|
||||
frame:Hide()
|
||||
end
|
||||
end
|
||||
self.activeOrder = shownOrder
|
||||
|
||||
for pn, frame in pairs(self.frames) do
|
||||
if not active[pn] then
|
||||
frame:Hide()
|
||||
end
|
||||
end
|
||||
|
||||
self:RefreshAnchors()
|
||||
self:SetUpdateTickerEnabled(shouldTick)
|
||||
return
|
||||
end
|
||||
|
||||
if IsInRaid() or not IsInGroup() then
|
||||
self.lastEntryCount = 0
|
||||
self:StopUpdateTicker()
|
||||
self:HideAllFrames()
|
||||
return
|
||||
end
|
||||
if not s.enabled then
|
||||
self.lastEntryCount = 0
|
||||
self:StopUpdateTicker()
|
||||
self:HideAllFrames()
|
||||
return
|
||||
end
|
||||
if not HMGT:IsVisibleForCurrentGroup(s) then
|
||||
self.lastEntryCount = 0
|
||||
self:StopUpdateTicker()
|
||||
self:HideAllFrames()
|
||||
return
|
||||
end
|
||||
|
||||
local entriesByPlayer, order, unitByPlayer = self:CollectEntriesByPlayer()
|
||||
self.activeOrder = order
|
||||
self.unitByPlayer = unitByPlayer
|
||||
self.lastEntryCount = 0
|
||||
local active = {}
|
||||
local shownOrder = {}
|
||||
local shouldTick = false
|
||||
|
||||
for _, playerName in ipairs(order) do
|
||||
local frame = self:EnsurePlayerFrame(playerName)
|
||||
HMGT.TrackerFrame:SetLocked(frame, s.locked)
|
||||
HMGT.TrackerFrame:SetTitle(frame, string.format("%s - %s", L["GCD_TITLE"], ShortName(playerName)))
|
||||
|
||||
local entries = entriesByPlayer[playerName] or {}
|
||||
if HMGT.FilterDisplayEntries then
|
||||
entries = HMGT:FilterDisplayEntries(s, entries) or entries
|
||||
end
|
||||
if HMGT.SortDisplayEntries then
|
||||
HMGT:SortDisplayEntries(entries, "groupCooldownTracker")
|
||||
end
|
||||
if #entries > 0 then
|
||||
HMGT.TrackerFrame:UpdateFrame(frame, entries, true)
|
||||
self.lastEntryCount = self.lastEntryCount + #entries
|
||||
frame:Show()
|
||||
active[playerName] = true
|
||||
shownOrder[#shownOrder + 1] = playerName
|
||||
for _, entry in ipairs(entries) do
|
||||
if EntryNeedsVisualTicker(entry) then
|
||||
shouldTick = true
|
||||
break
|
||||
end
|
||||
end
|
||||
else
|
||||
frame:Hide()
|
||||
end
|
||||
end
|
||||
self.activeOrder = shownOrder
|
||||
|
||||
for pn, frame in pairs(self.frames) do
|
||||
if not active[pn] then
|
||||
frame:Hide()
|
||||
end
|
||||
end
|
||||
|
||||
self:RefreshAnchors()
|
||||
self:SetUpdateTickerEnabled(shouldTick)
|
||||
end
|
||||
|
||||
function GCT:CollectEntriesByPlayer()
|
||||
local s = HMGT.db.profile.groupCooldownTracker
|
||||
local byPlayer = {}
|
||||
local playerOrder = {}
|
||||
local unitByPlayer = {}
|
||||
local players = self:GetGroupPlayers()
|
||||
|
||||
for _, playerInfo in ipairs(players) do
|
||||
repeat
|
||||
local name = playerInfo.name
|
||||
if not name then break end
|
||||
|
||||
local pData = HMGT.playerData[name]
|
||||
local class = pData and pData.class or playerInfo.class
|
||||
local specIdx
|
||||
if playerInfo.isOwn then
|
||||
specIdx = GetSpecialization()
|
||||
if not specIdx or specIdx == 0 then break end
|
||||
else
|
||||
specIdx = pData and pData.specIndex or nil
|
||||
if not specIdx or tonumber(specIdx) <= 0 then break end
|
||||
end
|
||||
local talents = pData and pData.talents or {}
|
||||
if not class then break end
|
||||
|
||||
local knownCDs = HMGT_SpellData.GetSpellsForSpec(class, specIdx, HMGT_SpellData.GroupCooldowns)
|
||||
local entries = {}
|
||||
for _, spellEntry in ipairs(knownCDs) do
|
||||
if s.enabledSpells[spellEntry.spellId] ~= false then
|
||||
local remaining, total, curCharges, maxCharges = HMGT:GetCooldownInfo(name, spellEntry.spellId, {
|
||||
deferChargeCooldownUntilEmpty = false,
|
||||
})
|
||||
local isAvailabilitySpell = HMGT.IsAvailabilitySpell and HMGT:IsAvailabilitySpell(spellEntry)
|
||||
local effectiveCd = HMGT_SpellData.GetEffectiveCooldown(spellEntry, talents)
|
||||
local hasChargeSpell = (tonumber(maxCharges) or 0) > 1
|
||||
local hasPartialCharges = (tonumber(maxCharges) or 0) > 0 and (tonumber(curCharges) or tonumber(maxCharges) or 0) < (tonumber(maxCharges) or 0)
|
||||
local include = HMGT:ShouldDisplayEntry(s, remaining, curCharges, maxCharges, spellEntry)
|
||||
local spellKnown = HMGT:IsTrackedSpellKnownForPlayer(name, spellEntry.spellId)
|
||||
local hasActiveCd = ((remaining or 0) > 0) or hasPartialCharges
|
||||
if not spellKnown and not hasActiveCd then
|
||||
include = false
|
||||
end
|
||||
if isAvailabilitySpell and not spellKnown then
|
||||
include = false
|
||||
end
|
||||
if not playerInfo.isOwn then
|
||||
if isAvailabilitySpell and not HMGT:HasAvailabilityState(name, spellEntry.spellId) then
|
||||
include = false
|
||||
end
|
||||
end
|
||||
if include then
|
||||
table.insert(entries, {
|
||||
playerName = name,
|
||||
class = class,
|
||||
spellEntry = spellEntry,
|
||||
remaining = remaining,
|
||||
total = total > 0 and total or effectiveCd,
|
||||
currentCharges = curCharges,
|
||||
maxCharges = maxCharges,
|
||||
})
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
if #entries > 0 then
|
||||
byPlayer[name] = entries
|
||||
table.insert(playerOrder, name)
|
||||
unitByPlayer[name] = playerInfo.unitId
|
||||
end
|
||||
until true
|
||||
end
|
||||
|
||||
table.sort(playerOrder, function(a, b)
|
||||
local own = HMGT:NormalizePlayerName(UnitName("player"))
|
||||
if a == own and b ~= own then return true end
|
||||
if b == own and a ~= own then return false end
|
||||
return a < b
|
||||
end)
|
||||
|
||||
return byPlayer, playerOrder, unitByPlayer
|
||||
end
|
||||
|
||||
function GCT:GetGroupPlayers()
|
||||
local players = {}
|
||||
local ownName = HMGT:NormalizePlayerName(UnitName("player"))
|
||||
local settings = HMGT.db and HMGT.db.profile and HMGT.db.profile.groupCooldownTracker
|
||||
|
||||
if settings and settings.includeSelfFrame == true and ownName then
|
||||
table.insert(players, {
|
||||
name = ownName,
|
||||
class = select(2, UnitClass("player")),
|
||||
unitId = "player",
|
||||
isOwn = true,
|
||||
})
|
||||
end
|
||||
|
||||
if IsInGroup() and not IsInRaid() then
|
||||
for i = 1, GetNumGroupMembers() - 1 do
|
||||
local unitId = "party" .. i
|
||||
local name = HMGT:NormalizePlayerName(UnitName(unitId))
|
||||
local class = select(2, UnitClass(unitId))
|
||||
if name and name ~= ownName then
|
||||
table.insert(players, {name = name, class = class, unitId = unitId})
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
return players
|
||||
end
|
||||
|
||||
Reference in New Issue
Block a user