initial commit
This commit is contained in:
1231
Modules/Tracker/Frame.lua
Normal file
1231
Modules/Tracker/Frame.lua
Normal file
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,391 @@
|
||||
-- Core/GroupCooldownSpellDatabase.lua
|
||||
-- Group cooldown database.
|
||||
|
||||
HMGT_SpellData = HMGT_SpellData or {}
|
||||
local Spell = HMGT_SpellData.Spell
|
||||
local Relation = HMGT_SpellData.Relation
|
||||
if not Spell then return end
|
||||
|
||||
HMGT_SpellData.GroupCooldowns = {
|
||||
-- WARRIOR
|
||||
-- Arms Spec
|
||||
Spell(118038, "Die by the Sword", {
|
||||
classes = {"WARRIOR"},
|
||||
specs = {1},
|
||||
category = "defensive",
|
||||
state = { kind = "cooldown", cooldown = 120 },
|
||||
}),
|
||||
Spell(167105, "Colossus Smash", {
|
||||
classes = {"WARRIOR"},
|
||||
specs = {1},
|
||||
category = "offensive",
|
||||
state = { kind = "cooldown", cooldown = 45 },
|
||||
}),
|
||||
|
||||
-- Fury Spec
|
||||
Spell(184364, "Enraged Regeneration", {
|
||||
classes = {"WARRIOR"},
|
||||
specs = {2},
|
||||
category = "defensive",
|
||||
state = { kind = "cooldown", cooldown = 120 },
|
||||
}),
|
||||
Spell(1719, "Recklessness", {
|
||||
classes = {"WARRIOR"},
|
||||
specs = {2},
|
||||
category = "offensive",
|
||||
state = { kind = "cooldown", cooldown = 90 },
|
||||
}),
|
||||
|
||||
-- Protection Spec
|
||||
Spell(871, "Shield Wall", {
|
||||
classes = {"WARRIOR"},
|
||||
specs = {3},
|
||||
category = "tank",
|
||||
state = { kind = "cooldown", cooldown = 108, charges = 1 },
|
||||
mods = {
|
||||
{ talentSpellId = 391271, value = 10, op = "reduceByPercent", target = "cooldown" }, -- Honed Reflexes
|
||||
{ talentSpellId = 397103, value = 2, op = "set", target = "charges" },
|
||||
},
|
||||
}),
|
||||
Spell(385952, "Shield Charge", {
|
||||
classes = {"WARRIOR"},
|
||||
specs = {3},
|
||||
category = "cc",
|
||||
state = { kind = "cooldown", cooldown = 45 },
|
||||
}),
|
||||
Spell(46968, "Shockwave", {
|
||||
classes = {"WARRIOR"},
|
||||
specs = {3},
|
||||
category = "cc",
|
||||
state = { kind = "cooldown", cooldown = 40 },
|
||||
}),
|
||||
Spell(3411, "Intervene", {
|
||||
classes = {"WARRIOR"},
|
||||
specs = {3},
|
||||
category = "defensive",
|
||||
state = { kind = "cooldown", cooldown = 27 },
|
||||
}),
|
||||
|
||||
-- All Specs
|
||||
Spell(107574, "Avatar", {
|
||||
classes = {"WARRIOR"},
|
||||
category = "offensive",
|
||||
state = { kind = "cooldown", cooldown = 90 },
|
||||
}),
|
||||
Spell(107570, "Storm Bolt", {
|
||||
classes = {"WARRIOR"},
|
||||
category = "cc",
|
||||
state = { kind = "cooldown", cooldown = 36 },
|
||||
}),
|
||||
|
||||
-- PALADIN
|
||||
Spell(498, "Divine Protection", {
|
||||
classes = {"PALADIN"},
|
||||
specs = {1, 2},
|
||||
category = "defensive",
|
||||
state = { kind = "cooldown", cooldown = 60 },
|
||||
}),
|
||||
Spell(642, "Divine Shield", {
|
||||
classes = {"PALADIN"},
|
||||
category = "defensive",
|
||||
state = { kind = "cooldown", cooldown = 300 },
|
||||
}),
|
||||
Spell(31884, "Avenging Wrath", {
|
||||
classes = {"PALADIN"},
|
||||
specs = {1, 3},
|
||||
category = "offensive",
|
||||
state = { kind = "cooldown", cooldown = 120 },
|
||||
}),
|
||||
Spell(86659, "Guardian of Ancient Kings", {
|
||||
classes = {"PALADIN"},
|
||||
specs = {2},
|
||||
category = "tank",
|
||||
state = { kind = "cooldown", cooldown = 300 },
|
||||
}),
|
||||
|
||||
-- HUNTER
|
||||
Spell(264735, "Survival of the Fittest", {
|
||||
classes = {"HUNTER"},
|
||||
specs = {1},
|
||||
category = "defensive",
|
||||
state = { kind = "cooldown", cooldown = 180 },
|
||||
}),
|
||||
Spell(109304, "Exhilaration", {
|
||||
classes = {"HUNTER"},
|
||||
category = "defensive",
|
||||
state = { kind = "cooldown", cooldown = 120 },
|
||||
}),
|
||||
|
||||
-- ROGUE
|
||||
Spell(31224, "Cloak of Shadows", {
|
||||
classes = {"ROGUE"},
|
||||
category = "defensive",
|
||||
state = { kind = "cooldown", cooldown = 60 },
|
||||
}),
|
||||
Spell(5277, "Evasion", {
|
||||
classes = {"ROGUE"},
|
||||
category = "defensive",
|
||||
state = { kind = "cooldown", cooldown = 120 },
|
||||
}),
|
||||
Spell(2094, "Blind", {
|
||||
classes = {"ROGUE"},
|
||||
category = "cc",
|
||||
state = { kind = "cooldown", cooldown = 120 },
|
||||
}),
|
||||
Spell(1856, "Vanish", {
|
||||
classes = {"ROGUE"},
|
||||
category = "defensive",
|
||||
state = { kind = "cooldown", cooldown = 120 },
|
||||
}),
|
||||
|
||||
-- DEATH KNIGHT
|
||||
Spell(55233, "Vampiric Blood", {
|
||||
classes = {"DEATHKNIGHT"},
|
||||
specs = {1},
|
||||
category = "tank",
|
||||
state = { kind = "cooldown", cooldown = 180 },
|
||||
mods = {
|
||||
{ talentSpellId = 374200, value = 150, op = "set", target = "cooldown" },
|
||||
},
|
||||
}),
|
||||
Spell(49028, "Dancing Rune Weapon", {
|
||||
classes = {"DEATHKNIGHT"},
|
||||
specs = {1},
|
||||
category = "tank",
|
||||
state = { kind = "cooldown", cooldown = 120 },
|
||||
mods = {
|
||||
{ talentSpellId = 377584, value = 60, op = "set", target = "cooldown" },
|
||||
},
|
||||
}),
|
||||
Spell(48707, "Anti-Magic Shell", {
|
||||
classes = {"DEATHKNIGHT"},
|
||||
category = "defensive",
|
||||
state = { kind = "cooldown", cooldown = 60 },
|
||||
}),
|
||||
Spell(48792, "Icebound Fortitude", {
|
||||
classes = {"DEATHKNIGHT"},
|
||||
category = "defensive",
|
||||
state = { kind = "cooldown", cooldown = 180 },
|
||||
}),
|
||||
Spell(42650, "Army of the Dead", {
|
||||
classes = {"DEATHKNIGHT"},
|
||||
category = "offensive",
|
||||
state = { kind = "cooldown", cooldown = 480 },
|
||||
}),
|
||||
Spell(49206, "Summon Gargoyle", {
|
||||
classes = {"DEATHKNIGHT"},
|
||||
specs = {3},
|
||||
category = "offensive",
|
||||
state = { kind = "cooldown", cooldown = 180 },
|
||||
}),
|
||||
|
||||
-- SHAMAN
|
||||
Spell(204336, "Grounding Totem", {
|
||||
classes = {"SHAMAN"},
|
||||
category = "defensive",
|
||||
state = { kind = "cooldown", cooldown = 30 },
|
||||
}),
|
||||
Spell(51490, "Thunderstorm", {
|
||||
classes = {"SHAMAN"},
|
||||
specs = {1},
|
||||
category = "cc",
|
||||
state = { kind = "cooldown", cooldown = 45 },
|
||||
}),
|
||||
|
||||
-- MAGE
|
||||
Spell(45438, "Ice Block", {
|
||||
classes = {"MAGE"},
|
||||
category = "defensive",
|
||||
state = { kind = "cooldown", cooldown = 240 },
|
||||
}),
|
||||
Spell(110959, "Greater Invisibility", {
|
||||
classes = {"MAGE"},
|
||||
category = "defensive",
|
||||
state = { kind = "cooldown", cooldown = 120 },
|
||||
}),
|
||||
Spell(235450, "Prismatic Barrier", {
|
||||
classes = {"MAGE"},
|
||||
specs = {3},
|
||||
category = "defensive",
|
||||
state = { kind = "cooldown", cooldown = 25 },
|
||||
}),
|
||||
|
||||
-- WARLOCK
|
||||
Spell(104773, "Unending Resolve", {
|
||||
classes = {"WARLOCK"},
|
||||
category = "defensive",
|
||||
state = { kind = "cooldown", cooldown = 180 },
|
||||
}),
|
||||
Spell(6229, "Twilight Ward", {
|
||||
classes = {"WARLOCK"},
|
||||
category = "defensive",
|
||||
state = { kind = "cooldown", cooldown = 0 },
|
||||
}),
|
||||
Spell(212295, "Nether Ward", {
|
||||
classes = {"WARLOCK"},
|
||||
specs = {3},
|
||||
category = "defensive",
|
||||
state = { kind = "cooldown", cooldown = 45 },
|
||||
}),
|
||||
|
||||
-- MONK
|
||||
Spell(122783, "Diffuse Magic", {
|
||||
classes = {"MONK"},
|
||||
category = "defensive",
|
||||
state = { kind = "cooldown", cooldown = 90 },
|
||||
}),
|
||||
Spell(122278, "Dampen Harm", {
|
||||
classes = {"MONK"},
|
||||
category = "defensive",
|
||||
state = { kind = "cooldown", cooldown = 120 },
|
||||
}),
|
||||
Spell(120954, "Fortifying Brew", {
|
||||
classes = {"MONK"},
|
||||
specs = {1},
|
||||
category = "tank",
|
||||
state = { kind = "cooldown", cooldown = 420 },
|
||||
}),
|
||||
Spell(115176, "Zen Meditation", {
|
||||
classes = {"MONK"},
|
||||
specs = {2},
|
||||
category = "defensive",
|
||||
state = { kind = "cooldown", cooldown = 300 },
|
||||
}),
|
||||
Spell(322118, "Invoke Niuzao", {
|
||||
classes = {"MONK"},
|
||||
specs = {1},
|
||||
category = "tank",
|
||||
state = { kind = "cooldown", cooldown = 180 },
|
||||
}),
|
||||
|
||||
-- DRUID
|
||||
Spell(22812, "Barkskin", {
|
||||
classes = {"DRUID"},
|
||||
category = "defensive",
|
||||
state = { kind = "cooldown", cooldown = 60 },
|
||||
}),
|
||||
Spell(61336, "Survival Instincts", {
|
||||
classes = {"DRUID"},
|
||||
specs = {2, 3},
|
||||
category = "defensive",
|
||||
state = { kind = "cooldown", cooldown = 180 },
|
||||
}),
|
||||
|
||||
-- DEMON HUNTER
|
||||
Spell(187827, "Metamorphosis", {
|
||||
classes = {"DEMONHUNTER"},
|
||||
specs = {1},
|
||||
category = "offensive",
|
||||
state = { kind = "cooldown", cooldown = 180 },
|
||||
}),
|
||||
Spell(162264, "Metamorphosis", {
|
||||
classes = {"DEMONHUNTER"},
|
||||
specs = {2},
|
||||
category = "tank",
|
||||
state = { kind = "cooldown", cooldown = 180 },
|
||||
}),
|
||||
Spell(1217605, "Metamorphosis", {
|
||||
classes = {"DEMONHUNTER"},
|
||||
specs = {3},
|
||||
category = "offensive",
|
||||
state = {
|
||||
kind = "availability",
|
||||
required = 50,
|
||||
source = {
|
||||
type = "auraStacks",
|
||||
auraSpellId = 1225789,
|
||||
fallbackSpellCountId = 1217605,
|
||||
},
|
||||
},
|
||||
}),
|
||||
Spell(203819, "Demon Spikes", {
|
||||
classes = {"DEMONHUNTER"},
|
||||
specs = {2},
|
||||
category = "tank",
|
||||
state = { kind = "cooldown", cooldown = 20 },
|
||||
}),
|
||||
Spell(212800, "Blur", {
|
||||
classes = {"DEMONHUNTER"},
|
||||
specs = {1},
|
||||
category = "defensive",
|
||||
state = { kind = "cooldown", cooldown = 60 },
|
||||
}),
|
||||
Spell(196555, "Netherwalk", {
|
||||
classes = {"DEMONHUNTER"},
|
||||
specs = {1},
|
||||
category = "defensive",
|
||||
state = { kind = "cooldown", cooldown = 180 },
|
||||
}),
|
||||
|
||||
-- EVOKER
|
||||
Spell(357214, "Time Stop", {
|
||||
classes = {"EVOKER"},
|
||||
specs = {2},
|
||||
category = "cc",
|
||||
state = { kind = "cooldown", cooldown = 120 },
|
||||
}),
|
||||
Spell(375087, "Tempest", {
|
||||
classes = {"EVOKER"},
|
||||
specs = {3},
|
||||
category = "offensive",
|
||||
state = { kind = "cooldown", cooldown = 30 },
|
||||
}),
|
||||
}
|
||||
|
||||
HMGT_SpellData.Relations = HMGT_SpellData.Relations or {}
|
||||
local hasShieldSlamShieldWallRelation = false
|
||||
local hasAngerManagementRelation = false
|
||||
for _, relation in ipairs(HMGT_SpellData.Relations) do
|
||||
local firstEffect = relation.effects and relation.effects[1]
|
||||
if tonumber(relation.triggerSpellId) == 23922 and tonumber(firstEffect and firstEffect.targetSpellId) == 871 then
|
||||
hasShieldSlamShieldWallRelation = true
|
||||
end
|
||||
if tostring(relation.when) == "powerSpent"
|
||||
and tonumber(relation.talentRequired) == 152278
|
||||
and tostring(relation.powerType) == "RAGE"
|
||||
then
|
||||
hasAngerManagementRelation = true
|
||||
end
|
||||
end
|
||||
if not hasShieldSlamShieldWallRelation and Relation then
|
||||
HMGT_SpellData.Relations[#HMGT_SpellData.Relations + 1] = Relation({
|
||||
triggerSpellId = 23922, -- Shield Slam
|
||||
classes = {"WARRIOR"},
|
||||
specs = {3}, -- Protection
|
||||
when = "cast",
|
||||
talentRequired = 384072, -- Impenetrable Wall
|
||||
effects = {
|
||||
{
|
||||
type = "reduceCooldown",
|
||||
targetSpellId = 871, -- Shield Wall
|
||||
amount = 6,
|
||||
},
|
||||
},
|
||||
})
|
||||
end
|
||||
if not hasAngerManagementRelation and Relation then
|
||||
HMGT_SpellData.Relations[#HMGT_SpellData.Relations + 1] = Relation({
|
||||
classes = {"WARRIOR"},
|
||||
specs = {3}, -- Protection
|
||||
when = "powerSpent",
|
||||
powerType = "RAGE",
|
||||
amountPerTrigger = 20,
|
||||
talentRequired = 152278, -- Anger Management
|
||||
effects = {
|
||||
{
|
||||
type = "reduceCooldown",
|
||||
targetSpellId = 107574, -- Avatar
|
||||
amount = 1,
|
||||
},
|
||||
{
|
||||
type = "reduceCooldown",
|
||||
targetSpellId = 871, -- Shield Wall
|
||||
amount = 1,
|
||||
},
|
||||
},
|
||||
})
|
||||
end
|
||||
|
||||
if HMGT_SpellData.RebuildLookups then
|
||||
HMGT_SpellData.RebuildLookups()
|
||||
end
|
||||
692
Modules/Tracker/GroupCooldownTracker/GroupCooldownTracker.lua
Normal file
692
Modules/Tracker/GroupCooldownTracker/GroupCooldownTracker.lua
Normal file
@@ -0,0 +1,692 @@
|
||||
-- Modules/GroupCooldownTracker.lua
|
||||
-- Group-Cooldown-Tracker Modul (ein Frame pro Spieler in der Gruppe)
|
||||
|
||||
local ADDON_NAME = "HailMaryGuildTools"
|
||||
local HMGT = LibStub("AceAddon-3.0"):GetAddon(ADDON_NAME)
|
||||
if not HMGT then return end
|
||||
local L = LibStub("AceLocale-3.0"):GetLocale(ADDON_NAME)
|
||||
|
||||
local GCT = HMGT:NewModule("GroupCooldownTracker")
|
||||
HMGT.GroupCooldownTracker = GCT
|
||||
|
||||
GCT.frame = nil
|
||||
GCT.frames = {}
|
||||
|
||||
local function SanitizeFrameToken(name)
|
||||
if not name or name == "" then return "Unknown" end
|
||||
return name:gsub("[^%w_]", "_")
|
||||
end
|
||||
|
||||
local function ShortName(name)
|
||||
if not name then return "" end
|
||||
local short = name:match("^[^-]+")
|
||||
return short or name
|
||||
end
|
||||
|
||||
local function IsUsableAnchorFrame(frame)
|
||||
return frame
|
||||
and frame.IsObjectType
|
||||
and (frame:IsObjectType("Frame") or frame:IsObjectType("Button"))
|
||||
end
|
||||
|
||||
local function GetFrameUnit(frame)
|
||||
if not frame then return nil end
|
||||
local unit = frame.unit
|
||||
if not unit and frame.GetAttribute then
|
||||
unit = frame:GetAttribute("unit")
|
||||
end
|
||||
return unit
|
||||
end
|
||||
|
||||
local function FrameMatchesUnit(frame, unitId)
|
||||
if not IsUsableAnchorFrame(frame) then return false end
|
||||
if not unitId then return true end
|
||||
local unit = GetFrameUnit(frame)
|
||||
return unit == unitId
|
||||
end
|
||||
|
||||
local PLAYER_FRAME_CANDIDATES = {
|
||||
"PlayerFrame",
|
||||
"ElvUF_Player",
|
||||
"NephUI_PlayerFrame",
|
||||
"NephUIPlayerFrame",
|
||||
"oUF_NephUI_Player",
|
||||
"SUFUnitplayer",
|
||||
}
|
||||
|
||||
local PARTY_FRAME_PATTERNS = {
|
||||
"PartyMemberFrame%d", -- Blizzard alt
|
||||
"CompactPartyFrameMember%d", -- Blizzard modern
|
||||
"ElvUF_PartyGroup1UnitButton%d", -- ElvUI
|
||||
"ElvUF_PartyUnitButton%d", -- ElvUI variant
|
||||
"NephUI_PartyUnitButton%d", -- NephUI (common naming variants)
|
||||
"NephUI_PartyFrame%d",
|
||||
"NephUIPartyFrame%d",
|
||||
"oUF_NephUI_PartyUnitButton%d",
|
||||
"SUFUnitparty%d", -- Shadowed Unit Frames
|
||||
}
|
||||
|
||||
local unitFrameCache = {}
|
||||
|
||||
local function EntryNeedsVisualTicker(entry)
|
||||
if type(entry) ~= "table" then
|
||||
return false
|
||||
end
|
||||
|
||||
local remaining = tonumber(entry.remaining) or 0
|
||||
if remaining > 0 then
|
||||
return true
|
||||
end
|
||||
|
||||
local maxCharges = tonumber(entry.maxCharges) or 0
|
||||
local currentCharges = tonumber(entry.currentCharges)
|
||||
if maxCharges > 0 and currentCharges ~= nil and currentCharges < maxCharges then
|
||||
return true
|
||||
end
|
||||
|
||||
return false
|
||||
end
|
||||
|
||||
local function BuildAnchorLayoutSignature(settings, ordered, unitByPlayer)
|
||||
local parts = {
|
||||
settings.attachToPartyFrame == true and "attach" or "stack",
|
||||
tostring(settings.partyAttachSide or "RIGHT"),
|
||||
tostring(tonumber(settings.partyAttachOffsetX) or 8),
|
||||
tostring(tonumber(settings.partyAttachOffsetY) or 0),
|
||||
tostring(settings.showBar and "bar" or "icon"),
|
||||
tostring(settings.growDirection or "DOWN"),
|
||||
tostring(settings.width or 250),
|
||||
tostring(settings.barHeight or 20),
|
||||
tostring(settings.iconSize or 32),
|
||||
tostring(settings.iconCols or 6),
|
||||
tostring(settings.barSpacing or 2),
|
||||
tostring(settings.locked),
|
||||
tostring(settings.anchorTo or "UIParent"),
|
||||
tostring(settings.anchorPoint or "TOPLEFT"),
|
||||
tostring(settings.anchorRelPoint or "TOPLEFT"),
|
||||
tostring(settings.anchorX or settings.posX or 0),
|
||||
tostring(settings.anchorY or settings.posY or 0),
|
||||
}
|
||||
|
||||
for _, playerName in ipairs(ordered or {}) do
|
||||
parts[#parts + 1] = tostring(playerName)
|
||||
parts[#parts + 1] = tostring(unitByPlayer and unitByPlayer[playerName] or "")
|
||||
end
|
||||
|
||||
return table.concat(parts, "|")
|
||||
end
|
||||
|
||||
local function ResolveNamedUnitFrame(unitId)
|
||||
if unitId == "player" then
|
||||
for _, frameName in ipairs(PLAYER_FRAME_CANDIDATES) do
|
||||
local frame = _G[frameName]
|
||||
if FrameMatchesUnit(frame, unitId) or (frameName == "PlayerFrame" and IsUsableAnchorFrame(frame)) then
|
||||
return frame
|
||||
end
|
||||
end
|
||||
return nil
|
||||
end
|
||||
|
||||
local idx = type(unitId) == "string" and unitId:match("^party(%d+)$")
|
||||
if not idx then
|
||||
return nil
|
||||
end
|
||||
|
||||
idx = tonumber(idx)
|
||||
for _, pattern in ipairs(PARTY_FRAME_PATTERNS) do
|
||||
local frame = _G[pattern:format(idx)]
|
||||
if FrameMatchesUnit(frame, unitId) then
|
||||
return frame
|
||||
end
|
||||
end
|
||||
return nil
|
||||
end
|
||||
|
||||
local function ScanUnitFrame(unitId)
|
||||
local frame = EnumerateFrames()
|
||||
local scanned = 0
|
||||
while frame and scanned < 8000 do
|
||||
if IsUsableAnchorFrame(frame) then
|
||||
local unit = GetFrameUnit(frame)
|
||||
if unit == unitId then
|
||||
HMGT:DebugScoped("verbose", HMGT:GetTrackerDebugScope("Group Cooldowns"), "GroupAttach scan unit=%s scanned=%d found=true", tostring(unitId), scanned)
|
||||
return frame
|
||||
end
|
||||
end
|
||||
scanned = scanned + 1
|
||||
frame = EnumerateFrames(frame)
|
||||
end
|
||||
HMGT:DebugScoped("verbose", HMGT:GetTrackerDebugScope("Group Cooldowns"), "GroupAttach scan unit=%s scanned=%d found=false", tostring(unitId), scanned)
|
||||
return nil
|
||||
end
|
||||
|
||||
local function ResolveUnitAnchorFrame(unitId)
|
||||
if not unitId then return nil end
|
||||
|
||||
local now = GetTime()
|
||||
local cached = unitFrameCache[unitId]
|
||||
if cached and now < (cached.expires or 0) then
|
||||
if cached.frame and cached.frame:IsShown() then
|
||||
return cached.frame
|
||||
end
|
||||
return nil
|
||||
end
|
||||
|
||||
local frame = ResolveNamedUnitFrame(unitId)
|
||||
if not frame then
|
||||
frame = ScanUnitFrame(unitId)
|
||||
end
|
||||
|
||||
local expiresIn = 1.0
|
||||
if frame and frame:IsShown() then
|
||||
expiresIn = 10.0
|
||||
end
|
||||
unitFrameCache[unitId] = {
|
||||
frame = frame,
|
||||
expires = now + expiresIn,
|
||||
}
|
||||
|
||||
if frame and frame:IsShown() then
|
||||
return frame
|
||||
end
|
||||
return nil
|
||||
end
|
||||
|
||||
function GCT:GetFrameIdForPlayer(playerName)
|
||||
return "GroupCooldownTracker_" .. SanitizeFrameToken(playerName)
|
||||
end
|
||||
|
||||
function GCT:GetPlayerFrame(playerName)
|
||||
if not playerName then return nil end
|
||||
return self.frames[playerName]
|
||||
end
|
||||
|
||||
function GCT:GetAnchorableFrames()
|
||||
return self.frames
|
||||
end
|
||||
|
||||
function GCT:EnsurePlayerFrame(playerName)
|
||||
local frame = self.frames[playerName]
|
||||
local s = HMGT.db.profile.groupCooldownTracker
|
||||
if frame then
|
||||
return frame
|
||||
end
|
||||
|
||||
frame = HMGT.TrackerFrame:CreateTrackerFrame(self:GetFrameIdForPlayer(playerName), s)
|
||||
frame._hmgtPlayerName = playerName
|
||||
self.frames[playerName] = frame
|
||||
return frame
|
||||
end
|
||||
|
||||
function GCT:HideAllFrames()
|
||||
for _, frame in pairs(self.frames) do
|
||||
frame:Hide()
|
||||
end
|
||||
self.activeOrder = nil
|
||||
self.unitByPlayer = nil
|
||||
self.frame = nil
|
||||
self._lastAnchorLayoutSignature = nil
|
||||
self._nextAnchorRetryAt = nil
|
||||
end
|
||||
|
||||
function GCT:SetLockedAll(locked)
|
||||
for _, frame in pairs(self.frames) do
|
||||
HMGT.TrackerFrame:SetLocked(frame, locked)
|
||||
end
|
||||
end
|
||||
|
||||
function GCT:EnsureUpdateTicker()
|
||||
if self.updateTicker then
|
||||
return
|
||||
end
|
||||
self.updateTicker = C_Timer.NewTicker(0.1, function()
|
||||
self:UpdateDisplay()
|
||||
end)
|
||||
end
|
||||
|
||||
function GCT:StopUpdateTicker()
|
||||
if self.updateTicker then
|
||||
self.updateTicker:Cancel()
|
||||
self.updateTicker = nil
|
||||
end
|
||||
end
|
||||
|
||||
function GCT:SetUpdateTickerEnabled(enabled)
|
||||
if enabled then
|
||||
self:EnsureUpdateTicker()
|
||||
else
|
||||
self:StopUpdateTicker()
|
||||
end
|
||||
end
|
||||
|
||||
function GCT:InvalidateAnchorLayout()
|
||||
self._lastAnchorLayoutSignature = nil
|
||||
self._nextAnchorRetryAt = nil
|
||||
end
|
||||
|
||||
function GCT:RefreshAnchors(force)
|
||||
local s = HMGT.db.profile.groupCooldownTracker
|
||||
if not s then return end
|
||||
|
||||
local ordered = {}
|
||||
for _, playerName in ipairs(self.activeOrder or {}) do
|
||||
local frame = self.frames[playerName]
|
||||
if frame and frame:IsShown() then
|
||||
table.insert(ordered, playerName)
|
||||
end
|
||||
end
|
||||
|
||||
if #ordered == 0 then
|
||||
self.frame = nil
|
||||
self._lastAnchorLayoutSignature = nil
|
||||
self._nextAnchorRetryAt = nil
|
||||
return
|
||||
end
|
||||
|
||||
local now = GetTime()
|
||||
local signature = BuildAnchorLayoutSignature(s, ordered, self.unitByPlayer)
|
||||
if not force and self._lastAnchorLayoutSignature == signature then
|
||||
local retryAt = tonumber(self._nextAnchorRetryAt) or 0
|
||||
if retryAt <= 0 or now < retryAt then
|
||||
return
|
||||
end
|
||||
end
|
||||
|
||||
-- Do not force anchor updates while user is dragging a tracker frame.
|
||||
for _, playerName in ipairs(ordered) do
|
||||
local frame = self.frames[playerName]
|
||||
if frame and frame._hmgtDragging then
|
||||
return
|
||||
end
|
||||
end
|
||||
|
||||
local primaryName = ordered[1]
|
||||
local primary = self.frames[primaryName]
|
||||
self.frame = primary
|
||||
|
||||
if s.attachToPartyFrame == true then
|
||||
local side = s.partyAttachSide or "RIGHT"
|
||||
local extraX = tonumber(s.partyAttachOffsetX) or 8
|
||||
local extraY = tonumber(s.partyAttachOffsetY) or 0
|
||||
local growsUp = s.showBar == true and s.growDirection == "UP"
|
||||
local barHeight = tonumber(s.barHeight) or 20
|
||||
local growUpAttachOffset = barHeight + 20
|
||||
local prevPlaced = nil
|
||||
local missingTargets = 0
|
||||
|
||||
for i = 1, #ordered do
|
||||
local playerName = ordered[i]
|
||||
local frame = self.frames[playerName]
|
||||
local unitId = self.unitByPlayer and self.unitByPlayer[playerName]
|
||||
local target = ResolveUnitAnchorFrame(unitId)
|
||||
local contentTopInset = HMGT.TrackerFrame.GetContentTopInset and HMGT.TrackerFrame:GetContentTopInset(frame) or 0
|
||||
|
||||
frame:ClearAllPoints()
|
||||
if target then
|
||||
if side == "LEFT" then
|
||||
if growsUp then
|
||||
frame:SetPoint("BOTTOMRIGHT", target, "TOPLEFT", -extraX, extraY - growUpAttachOffset)
|
||||
else
|
||||
frame:SetPoint("TOPRIGHT", target, "TOPLEFT", -extraX, extraY + contentTopInset)
|
||||
end
|
||||
else
|
||||
if growsUp then
|
||||
frame:SetPoint("BOTTOMLEFT", target, "TOPRIGHT", extraX, extraY - growUpAttachOffset)
|
||||
else
|
||||
frame:SetPoint("TOPLEFT", target, "TOPRIGHT", extraX, extraY + contentTopInset)
|
||||
end
|
||||
end
|
||||
elseif prevPlaced then
|
||||
missingTargets = missingTargets + 1
|
||||
HMGT:DebugScoped("verbose", HMGT:GetTrackerDebugScope("Group Cooldowns"), "GroupAttach fallback-stack player=%s unit=%s", tostring(playerName), tostring(unitId))
|
||||
if growsUp then
|
||||
frame:SetPoint("BOTTOMLEFT", prevPlaced, "TOPLEFT", 0, (s.barSpacing or 2) + 10)
|
||||
else
|
||||
frame:SetPoint("TOPLEFT", prevPlaced, "BOTTOMLEFT", 0, -((s.barSpacing or 2) + 10))
|
||||
end
|
||||
else
|
||||
missingTargets = missingTargets + 1
|
||||
HMGT:DebugScoped("info", HMGT:GetTrackerDebugScope("Group Cooldowns"), "GroupAttach fallback-anchor player=%s unit=%s (no party frame found)", tostring(playerName), tostring(unitId))
|
||||
HMGT.TrackerFrame:ApplyAnchor(frame)
|
||||
end
|
||||
|
||||
frame:EnableMouse(false)
|
||||
prevPlaced = frame
|
||||
end
|
||||
if missingTargets > 0 then
|
||||
self._lastAnchorLayoutSignature = nil
|
||||
self._nextAnchorRetryAt = now + 1.0
|
||||
else
|
||||
self._lastAnchorLayoutSignature = signature
|
||||
self._nextAnchorRetryAt = nil
|
||||
end
|
||||
return
|
||||
end
|
||||
|
||||
HMGT.TrackerFrame:ApplyAnchor(primary)
|
||||
primary:EnableMouse(not s.locked)
|
||||
|
||||
local gap = (s.barSpacing or 2) + 10
|
||||
local growsUp = s.showBar == true and s.growDirection == "UP"
|
||||
for i = 2, #ordered do
|
||||
local prev = self.frames[ordered[i - 1]]
|
||||
local frame = self.frames[ordered[i]]
|
||||
frame:ClearAllPoints()
|
||||
if growsUp then
|
||||
frame:SetPoint("BOTTOMLEFT", prev, "TOPLEFT", 0, gap)
|
||||
else
|
||||
frame:SetPoint("TOPLEFT", prev, "BOTTOMLEFT", 0, -gap)
|
||||
end
|
||||
frame:EnableMouse(false)
|
||||
end
|
||||
self._lastAnchorLayoutSignature = signature
|
||||
self._nextAnchorRetryAt = nil
|
||||
end
|
||||
|
||||
-- ============================================================
|
||||
-- ENABLE / DISABLE
|
||||
-- ============================================================
|
||||
|
||||
function GCT:Enable()
|
||||
local s = HMGT.db.profile.groupCooldownTracker
|
||||
if not s.enabled and not s.demoMode and not s.testMode then return end
|
||||
|
||||
self:UpdateDisplay()
|
||||
end
|
||||
|
||||
function GCT:Disable()
|
||||
self:StopUpdateTicker()
|
||||
self:HideAllFrames()
|
||||
end
|
||||
|
||||
-- ============================================================
|
||||
-- DISPLAY UPDATE
|
||||
-- ============================================================
|
||||
|
||||
function GCT:UpdateDisplay()
|
||||
local s = HMGT.db.profile.groupCooldownTracker
|
||||
if not s then return end
|
||||
|
||||
if s.testMode then
|
||||
local entries, playerName = HMGT:GetOwnTestEntries(HMGT_SpellData.GroupCooldowns, s, {
|
||||
deferChargeCooldownUntilEmpty = false,
|
||||
})
|
||||
local byPlayer = { [playerName] = {} }
|
||||
for _, entry in ipairs(entries) do
|
||||
entry.playerName = playerName
|
||||
table.insert(byPlayer[playerName], entry)
|
||||
end
|
||||
|
||||
self.activeOrder = { playerName }
|
||||
self.unitByPlayer = { [playerName] = "player" }
|
||||
self.lastEntryCount = 0
|
||||
local active = {}
|
||||
local shownOrder = {}
|
||||
local shouldTick = false
|
||||
for _, pName in ipairs(self.activeOrder) do
|
||||
local frame = self:EnsurePlayerFrame(pName)
|
||||
HMGT.TrackerFrame:SetLocked(frame, s.locked)
|
||||
HMGT.TrackerFrame:SetTitle(frame, string.format("%s - %s", L["GCD_TITLE"], ShortName(pName)))
|
||||
local displayEntries = byPlayer[pName]
|
||||
if HMGT.FilterDisplayEntries then
|
||||
displayEntries = HMGT:FilterDisplayEntries(s, displayEntries) or displayEntries
|
||||
end
|
||||
if HMGT.SortDisplayEntries then
|
||||
HMGT:SortDisplayEntries(displayEntries, "groupCooldownTracker")
|
||||
end
|
||||
if #displayEntries > 0 then
|
||||
HMGT.TrackerFrame:UpdateFrame(frame, displayEntries, true)
|
||||
self.lastEntryCount = self.lastEntryCount + #displayEntries
|
||||
frame:Show()
|
||||
active[pName] = true
|
||||
shownOrder[#shownOrder + 1] = pName
|
||||
for _, entry in ipairs(displayEntries) do
|
||||
if EntryNeedsVisualTicker(entry) then
|
||||
shouldTick = true
|
||||
break
|
||||
end
|
||||
end
|
||||
else
|
||||
frame:Hide()
|
||||
end
|
||||
end
|
||||
self.activeOrder = shownOrder
|
||||
|
||||
for pn, frame in pairs(self.frames) do
|
||||
if not active[pn] then
|
||||
frame:Hide()
|
||||
end
|
||||
end
|
||||
|
||||
self:RefreshAnchors()
|
||||
self:SetUpdateTickerEnabled(shouldTick)
|
||||
return
|
||||
end
|
||||
|
||||
if s.demoMode then
|
||||
local entries = HMGT:GetDemoEntries("groupCooldownTracker", HMGT_SpellData.GroupCooldowns, s)
|
||||
local playerName = HMGT:NormalizePlayerName(UnitName("player")) or "DemoPlayer"
|
||||
local byPlayer = { [playerName] = {} }
|
||||
for _, entry in ipairs(entries) do
|
||||
entry.playerName = playerName
|
||||
table.insert(byPlayer[playerName], entry)
|
||||
end
|
||||
|
||||
self.activeOrder = { playerName }
|
||||
self.unitByPlayer = { [playerName] = "player" }
|
||||
self.lastEntryCount = 0
|
||||
local active = {}
|
||||
local shownOrder = {}
|
||||
local shouldTick = false
|
||||
for _, playerName in ipairs(self.activeOrder) do
|
||||
local frame = self:EnsurePlayerFrame(playerName)
|
||||
HMGT.TrackerFrame:SetLocked(frame, s.locked)
|
||||
HMGT.TrackerFrame:SetTitle(frame, string.format("%s - %s", L["GCD_TITLE"], ShortName(playerName)))
|
||||
local displayEntries = byPlayer[playerName]
|
||||
if HMGT.FilterDisplayEntries then
|
||||
displayEntries = HMGT:FilterDisplayEntries(s, displayEntries) or displayEntries
|
||||
end
|
||||
if HMGT.SortDisplayEntries then
|
||||
HMGT:SortDisplayEntries(displayEntries, "groupCooldownTracker")
|
||||
end
|
||||
if #displayEntries > 0 then
|
||||
HMGT.TrackerFrame:UpdateFrame(frame, displayEntries, true)
|
||||
self.lastEntryCount = self.lastEntryCount + #displayEntries
|
||||
frame:Show()
|
||||
active[playerName] = true
|
||||
shownOrder[#shownOrder + 1] = playerName
|
||||
shouldTick = true
|
||||
else
|
||||
frame:Hide()
|
||||
end
|
||||
end
|
||||
self.activeOrder = shownOrder
|
||||
|
||||
for pn, frame in pairs(self.frames) do
|
||||
if not active[pn] then
|
||||
frame:Hide()
|
||||
end
|
||||
end
|
||||
|
||||
self:RefreshAnchors()
|
||||
self:SetUpdateTickerEnabled(shouldTick)
|
||||
return
|
||||
end
|
||||
|
||||
if IsInRaid() or not IsInGroup() then
|
||||
self.lastEntryCount = 0
|
||||
self:StopUpdateTicker()
|
||||
self:HideAllFrames()
|
||||
return
|
||||
end
|
||||
if not s.enabled then
|
||||
self.lastEntryCount = 0
|
||||
self:StopUpdateTicker()
|
||||
self:HideAllFrames()
|
||||
return
|
||||
end
|
||||
if not HMGT:IsVisibleForCurrentGroup(s) then
|
||||
self.lastEntryCount = 0
|
||||
self:StopUpdateTicker()
|
||||
self:HideAllFrames()
|
||||
return
|
||||
end
|
||||
|
||||
local entriesByPlayer, order, unitByPlayer = self:CollectEntriesByPlayer()
|
||||
self.activeOrder = order
|
||||
self.unitByPlayer = unitByPlayer
|
||||
self.lastEntryCount = 0
|
||||
local active = {}
|
||||
local shownOrder = {}
|
||||
local shouldTick = false
|
||||
|
||||
for _, playerName in ipairs(order) do
|
||||
local frame = self:EnsurePlayerFrame(playerName)
|
||||
HMGT.TrackerFrame:SetLocked(frame, s.locked)
|
||||
HMGT.TrackerFrame:SetTitle(frame, string.format("%s - %s", L["GCD_TITLE"], ShortName(playerName)))
|
||||
|
||||
local entries = entriesByPlayer[playerName] or {}
|
||||
if HMGT.FilterDisplayEntries then
|
||||
entries = HMGT:FilterDisplayEntries(s, entries) or entries
|
||||
end
|
||||
if HMGT.SortDisplayEntries then
|
||||
HMGT:SortDisplayEntries(entries, "groupCooldownTracker")
|
||||
end
|
||||
if #entries > 0 then
|
||||
HMGT.TrackerFrame:UpdateFrame(frame, entries, true)
|
||||
self.lastEntryCount = self.lastEntryCount + #entries
|
||||
frame:Show()
|
||||
active[playerName] = true
|
||||
shownOrder[#shownOrder + 1] = playerName
|
||||
for _, entry in ipairs(entries) do
|
||||
if EntryNeedsVisualTicker(entry) then
|
||||
shouldTick = true
|
||||
break
|
||||
end
|
||||
end
|
||||
else
|
||||
frame:Hide()
|
||||
end
|
||||
end
|
||||
self.activeOrder = shownOrder
|
||||
|
||||
for pn, frame in pairs(self.frames) do
|
||||
if not active[pn] then
|
||||
frame:Hide()
|
||||
end
|
||||
end
|
||||
|
||||
self:RefreshAnchors()
|
||||
self:SetUpdateTickerEnabled(shouldTick)
|
||||
end
|
||||
|
||||
function GCT:CollectEntriesByPlayer()
|
||||
local s = HMGT.db.profile.groupCooldownTracker
|
||||
local byPlayer = {}
|
||||
local playerOrder = {}
|
||||
local unitByPlayer = {}
|
||||
local players = self:GetGroupPlayers()
|
||||
|
||||
for _, playerInfo in ipairs(players) do
|
||||
repeat
|
||||
local name = playerInfo.name
|
||||
if not name then break end
|
||||
|
||||
local pData = HMGT.playerData[name]
|
||||
local class = pData and pData.class or playerInfo.class
|
||||
local specIdx
|
||||
if playerInfo.isOwn then
|
||||
specIdx = GetSpecialization()
|
||||
if not specIdx or specIdx == 0 then break end
|
||||
else
|
||||
specIdx = pData and pData.specIndex or nil
|
||||
if not specIdx or tonumber(specIdx) <= 0 then break end
|
||||
end
|
||||
local talents = pData and pData.talents or {}
|
||||
if not class then break end
|
||||
|
||||
local knownCDs = HMGT_SpellData.GetSpellsForSpec(class, specIdx, HMGT_SpellData.GroupCooldowns)
|
||||
local entries = {}
|
||||
for _, spellEntry in ipairs(knownCDs) do
|
||||
if s.enabledSpells[spellEntry.spellId] ~= false then
|
||||
local remaining, total, curCharges, maxCharges = HMGT:GetCooldownInfo(name, spellEntry.spellId, {
|
||||
deferChargeCooldownUntilEmpty = false,
|
||||
})
|
||||
local isAvailabilitySpell = HMGT.IsAvailabilitySpell and HMGT:IsAvailabilitySpell(spellEntry)
|
||||
local effectiveCd = HMGT_SpellData.GetEffectiveCooldown(spellEntry, talents)
|
||||
local hasChargeSpell = (tonumber(maxCharges) or 0) > 1
|
||||
local hasPartialCharges = (tonumber(maxCharges) or 0) > 0 and (tonumber(curCharges) or tonumber(maxCharges) or 0) < (tonumber(maxCharges) or 0)
|
||||
local include = HMGT:ShouldDisplayEntry(s, remaining, curCharges, maxCharges, spellEntry)
|
||||
local spellKnown = HMGT:IsTrackedSpellKnownForPlayer(name, spellEntry.spellId)
|
||||
local hasActiveCd = ((remaining or 0) > 0) or hasPartialCharges
|
||||
if not spellKnown and not hasActiveCd then
|
||||
include = false
|
||||
end
|
||||
if isAvailabilitySpell and not spellKnown then
|
||||
include = false
|
||||
end
|
||||
if not playerInfo.isOwn then
|
||||
if isAvailabilitySpell and not HMGT:HasAvailabilityState(name, spellEntry.spellId) then
|
||||
include = false
|
||||
end
|
||||
end
|
||||
if include then
|
||||
table.insert(entries, {
|
||||
playerName = name,
|
||||
class = class,
|
||||
spellEntry = spellEntry,
|
||||
remaining = remaining,
|
||||
total = total > 0 and total or effectiveCd,
|
||||
currentCharges = curCharges,
|
||||
maxCharges = maxCharges,
|
||||
})
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
if #entries > 0 then
|
||||
byPlayer[name] = entries
|
||||
table.insert(playerOrder, name)
|
||||
unitByPlayer[name] = playerInfo.unitId
|
||||
end
|
||||
until true
|
||||
end
|
||||
|
||||
table.sort(playerOrder, function(a, b)
|
||||
local own = HMGT:NormalizePlayerName(UnitName("player"))
|
||||
if a == own and b ~= own then return true end
|
||||
if b == own and a ~= own then return false end
|
||||
return a < b
|
||||
end)
|
||||
|
||||
return byPlayer, playerOrder, unitByPlayer
|
||||
end
|
||||
|
||||
function GCT:GetGroupPlayers()
|
||||
local players = {}
|
||||
local ownName = HMGT:NormalizePlayerName(UnitName("player"))
|
||||
local settings = HMGT.db and HMGT.db.profile and HMGT.db.profile.groupCooldownTracker
|
||||
|
||||
if settings and settings.includeSelfFrame == true and ownName then
|
||||
table.insert(players, {
|
||||
name = ownName,
|
||||
class = select(2, UnitClass("player")),
|
||||
unitId = "player",
|
||||
isOwn = true,
|
||||
})
|
||||
end
|
||||
|
||||
if IsInGroup() and not IsInRaid() then
|
||||
for i = 1, GetNumGroupMembers() - 1 do
|
||||
local unitId = "party" .. i
|
||||
local name = HMGT:NormalizePlayerName(UnitName(unitId))
|
||||
local class = select(2, UnitClass(unitId))
|
||||
if name and name ~= ownName then
|
||||
table.insert(players, {name = name, class = class, unitId = unitId})
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
return players
|
||||
end
|
||||
|
||||
247
Modules/Tracker/GroupTrackerFrames.lua
Normal file
247
Modules/Tracker/GroupTrackerFrames.lua
Normal file
@@ -0,0 +1,247 @@
|
||||
local ADDON_NAME = "HailMaryGuildTools"
|
||||
local HMGT = LibStub("AceAddon-3.0"):GetAddon(ADDON_NAME)
|
||||
if not HMGT or not HMGT.TrackerManager then return end
|
||||
|
||||
local Manager = HMGT.TrackerManager
|
||||
local S = Manager._shared or {}
|
||||
|
||||
function Manager:EnsurePlayerFrame(tracker, playerName)
|
||||
local frameKey = S.GetTrackerFrameKey(tracker.id)
|
||||
self.perPlayerFrames[frameKey] = self.perPlayerFrames[frameKey] or {}
|
||||
local frame = self.perPlayerFrames[frameKey][playerName]
|
||||
if not frame then
|
||||
frame = HMGT.TrackerFrame:CreateTrackerFrame(S.GetTrackerPlayerFrameName(tracker.id, playerName), tracker)
|
||||
frame._hmgtTrackerId = tonumber(tracker.id) or 0
|
||||
frame._hmgtPlayerName = playerName
|
||||
self.perPlayerFrames[frameKey][playerName] = frame
|
||||
end
|
||||
frame._settings = tracker
|
||||
HMGT.TrackerFrame:SetTitle(frame, string.format("%s - %s", S.GetTrackerLabel(tracker), S.ShortName(playerName)))
|
||||
HMGT.TrackerFrame:SetLocked(frame, tracker.locked)
|
||||
return frame
|
||||
end
|
||||
|
||||
function Manager:HidePlayerFrames(frameKey)
|
||||
local frames = self.perPlayerFrames[frameKey]
|
||||
if type(frames) ~= "table" then
|
||||
self.activeOrders[frameKey] = nil
|
||||
self.unitByPlayer[frameKey] = nil
|
||||
self.anchorLayoutSignatures[frameKey] = nil
|
||||
self.nextAnchorRetryAt[frameKey] = nil
|
||||
self._displaySignatures[frameKey] = "0"
|
||||
return
|
||||
end
|
||||
for _, frame in pairs(frames) do
|
||||
frame:Hide()
|
||||
end
|
||||
self.activeOrders[frameKey] = nil
|
||||
self.unitByPlayer[frameKey] = nil
|
||||
self.anchorLayoutSignatures[frameKey] = nil
|
||||
self.nextAnchorRetryAt[frameKey] = nil
|
||||
self._displaySignatures[frameKey] = "0"
|
||||
end
|
||||
|
||||
function Manager:BuildEntriesByPlayerForTracker(tracker)
|
||||
local frameKey = S.GetTrackerFrameKey(tracker.id)
|
||||
local ownName = HMGT:NormalizePlayerName(UnitName("player")) or "Player"
|
||||
if tracker.testMode then
|
||||
local entries = self:CollectTestEntries(tracker)
|
||||
if S.IsGroupTracker(tracker) and tracker.attachToPartyFrame == true then
|
||||
return S.BuildPartyPreviewEntries(entries)
|
||||
end
|
||||
local byPlayer, order, unitByPlayer = {}, {}, {}
|
||||
if #entries > 0 then
|
||||
byPlayer[ownName] = entries
|
||||
order[1] = ownName
|
||||
unitByPlayer[ownName] = "player"
|
||||
end
|
||||
return byPlayer, order, unitByPlayer, true
|
||||
end
|
||||
if tracker.demoMode then
|
||||
local entries = HMGT:GetDemoEntries(frameKey, S.GetTrackerSpellPool(tracker.categories), tracker)
|
||||
if S.IsGroupTracker(tracker) and tracker.attachToPartyFrame == true then
|
||||
return S.BuildPartyPreviewEntries(entries)
|
||||
end
|
||||
for _, entry in ipairs(entries) do
|
||||
entry.playerName = ownName
|
||||
end
|
||||
local byPlayer, order, unitByPlayer = {}, {}, {}
|
||||
if #entries > 0 then
|
||||
byPlayer[ownName] = entries
|
||||
order[1] = ownName
|
||||
unitByPlayer[ownName] = "player"
|
||||
end
|
||||
return byPlayer, order, unitByPlayer, true
|
||||
end
|
||||
if not tracker.enabled or not HMGT:IsVisibleForCurrentGroup(tracker) then
|
||||
return {}, {}, {}, false
|
||||
end
|
||||
if IsInRaid() or not IsInGroup() then
|
||||
return {}, {}, {}, false
|
||||
end
|
||||
local byPlayer, order, unitByPlayer = {}, {}, {}
|
||||
for _, playerInfo in ipairs(S.GetGroupPlayers(tracker)) do
|
||||
local entries = S.CollectEntriesForPlayer(tracker, playerInfo)
|
||||
if #entries > 0 then
|
||||
local playerName = playerInfo.name
|
||||
byPlayer[playerName] = entries
|
||||
order[#order + 1] = playerName
|
||||
unitByPlayer[playerName] = playerInfo.unitId
|
||||
end
|
||||
end
|
||||
return byPlayer, order, unitByPlayer, true
|
||||
end
|
||||
|
||||
function Manager:RefreshPerGroupAnchors(tracker, force)
|
||||
local frameKey = S.GetTrackerFrameKey(tracker.id)
|
||||
local frames = self.perPlayerFrames[frameKey] or {}
|
||||
local ordered = {}
|
||||
for _, playerName in ipairs(self.activeOrders[frameKey] or {}) do
|
||||
local frame = frames[playerName]
|
||||
if frame and frame:IsShown() then
|
||||
ordered[#ordered + 1] = playerName
|
||||
end
|
||||
end
|
||||
if #ordered == 0 then
|
||||
self.anchorLayoutSignatures[frameKey] = nil
|
||||
self.nextAnchorRetryAt[frameKey] = nil
|
||||
return
|
||||
end
|
||||
local now = GetTime()
|
||||
local signature = S.BuildAnchorLayoutSignature(tracker, ordered, self.unitByPlayer[frameKey])
|
||||
if not force and self.anchorLayoutSignatures[frameKey] == signature then
|
||||
local retryAt = tonumber(self.nextAnchorRetryAt[frameKey]) or 0
|
||||
if retryAt <= 0 or now < retryAt then
|
||||
return
|
||||
end
|
||||
end
|
||||
for _, playerName in ipairs(ordered) do
|
||||
local frame = frames[playerName]
|
||||
if frame and frame._hmgtDragging then
|
||||
return
|
||||
end
|
||||
end
|
||||
if tracker.attachToPartyFrame == true then
|
||||
local side = tracker.partyAttachSide or "RIGHT"
|
||||
local extraX = tonumber(tracker.partyAttachOffsetX) or 8
|
||||
local extraY = tonumber(tracker.partyAttachOffsetY) or 0
|
||||
local growsUp = tracker.showBar == true and tracker.growDirection == "UP"
|
||||
local barHeight = tonumber(tracker.barHeight) or 20
|
||||
local growUpAttachOffset = barHeight + 20
|
||||
local prevPlaced = nil
|
||||
local missingTargets = 0
|
||||
for _, playerName in ipairs(ordered) do
|
||||
local frame = frames[playerName]
|
||||
local unitId = self.unitByPlayer[frameKey] and self.unitByPlayer[frameKey][playerName]
|
||||
local target = S.ResolveUnitAnchorFrame(unitId)
|
||||
local contentTopInset = HMGT.TrackerFrame.GetContentTopInset and HMGT.TrackerFrame:GetContentTopInset(frame) or 0
|
||||
frame:ClearAllPoints()
|
||||
if target then
|
||||
if side == "LEFT" then
|
||||
if growsUp then
|
||||
frame:SetPoint("BOTTOMRIGHT", target, "TOPLEFT", -extraX, extraY - growUpAttachOffset)
|
||||
else
|
||||
frame:SetPoint("TOPRIGHT", target, "TOPLEFT", -extraX, extraY + contentTopInset)
|
||||
end
|
||||
else
|
||||
if growsUp then
|
||||
frame:SetPoint("BOTTOMLEFT", target, "TOPRIGHT", extraX, extraY - growUpAttachOffset)
|
||||
else
|
||||
frame:SetPoint("TOPLEFT", target, "TOPRIGHT", extraX, extraY + contentTopInset)
|
||||
end
|
||||
end
|
||||
elseif prevPlaced then
|
||||
missingTargets = missingTargets + 1
|
||||
HMGT:DebugScoped("verbose", HMGT:GetTrackerDebugScope(tracker), "TrackerAttach fallback-stack tracker=%s player=%s unit=%s", tostring(tracker.id), tostring(playerName), tostring(unitId))
|
||||
if growsUp then
|
||||
frame:SetPoint("BOTTOMLEFT", prevPlaced, "TOPLEFT", 0, (tracker.barSpacing or 2) + 10)
|
||||
else
|
||||
frame:SetPoint("TOPLEFT", prevPlaced, "BOTTOMLEFT", 0, -((tracker.barSpacing or 2) + 10))
|
||||
end
|
||||
else
|
||||
missingTargets = missingTargets + 1
|
||||
HMGT:DebugScoped("info", HMGT:GetTrackerDebugScope(tracker), "TrackerAttach fallback-anchor tracker=%s player=%s unit=%s", tostring(tracker.id), tostring(playerName), tostring(unitId))
|
||||
HMGT.TrackerFrame:ApplyAnchor(frame)
|
||||
end
|
||||
frame:EnableMouse(false)
|
||||
prevPlaced = frame
|
||||
end
|
||||
if missingTargets > 0 then
|
||||
self.anchorLayoutSignatures[frameKey] = nil
|
||||
self.nextAnchorRetryAt[frameKey] = now + 1.0
|
||||
else
|
||||
self.anchorLayoutSignatures[frameKey] = signature
|
||||
self.nextAnchorRetryAt[frameKey] = nil
|
||||
end
|
||||
return
|
||||
end
|
||||
local primary = frames[ordered[1]]
|
||||
HMGT.TrackerFrame:ApplyAnchor(primary)
|
||||
primary:EnableMouse(not tracker.locked)
|
||||
local gap = (tracker.barSpacing or 2) + 10
|
||||
local growsUp = tracker.showBar == true and tracker.growDirection == "UP"
|
||||
for index = 2, #ordered do
|
||||
local prev = frames[ordered[index - 1]]
|
||||
local frame = frames[ordered[index]]
|
||||
frame:ClearAllPoints()
|
||||
if growsUp then
|
||||
frame:SetPoint("BOTTOMLEFT", prev, "TOPLEFT", 0, gap)
|
||||
else
|
||||
frame:SetPoint("TOPLEFT", prev, "BOTTOMLEFT", 0, -gap)
|
||||
end
|
||||
frame:EnableMouse(false)
|
||||
end
|
||||
self.anchorLayoutSignatures[frameKey] = signature
|
||||
self.nextAnchorRetryAt[frameKey] = nil
|
||||
end
|
||||
|
||||
function Manager:UpdatePerGroupMemberTracker(tracker)
|
||||
local frameKey = S.GetTrackerFrameKey(tracker.id)
|
||||
local byPlayer, order, unitByPlayer, shouldShow = self:BuildEntriesByPlayerForTracker(tracker)
|
||||
self.activeOrders[frameKey] = order
|
||||
self.unitByPlayer[frameKey] = unitByPlayer
|
||||
local active, shownOrder = {}, {}
|
||||
local shownByPlayer = {}
|
||||
local entryCount, shouldTick = 0, false
|
||||
for _, playerName in ipairs(order) do
|
||||
local frame = self:EnsurePlayerFrame(tracker, playerName)
|
||||
local entries = byPlayer[playerName] or {}
|
||||
if HMGT.FilterDisplayEntries then
|
||||
entries = HMGT:FilterDisplayEntries(tracker, entries) or entries
|
||||
end
|
||||
if HMGT.SortDisplayEntries then
|
||||
HMGT:SortDisplayEntries(entries)
|
||||
end
|
||||
if #entries > 0 then
|
||||
HMGT.TrackerFrame:UpdateFrame(frame, entries, true)
|
||||
frame:Show()
|
||||
active[playerName] = true
|
||||
shownOrder[#shownOrder + 1] = playerName
|
||||
shownByPlayer[playerName] = entries
|
||||
entryCount = entryCount + #entries
|
||||
for _, entry in ipairs(entries) do
|
||||
if S.EntryNeedsVisualTicker(entry) then
|
||||
shouldTick = true
|
||||
break
|
||||
end
|
||||
end
|
||||
else
|
||||
frame:Hide()
|
||||
end
|
||||
end
|
||||
for playerName, frame in pairs(self.perPlayerFrames[frameKey] or {}) do
|
||||
if not active[playerName] then
|
||||
frame:Hide()
|
||||
end
|
||||
end
|
||||
self.activeOrders[frameKey] = shownOrder
|
||||
self.unitByPlayer[frameKey] = unitByPlayer
|
||||
self._displaySignatures[frameKey] = S.BuildGroupDisplaySignature and S.BuildGroupDisplaySignature(shownOrder, shownByPlayer) or nil
|
||||
if shouldShow and #shownOrder > 0 then
|
||||
self:RefreshPerGroupAnchors(tracker, false)
|
||||
return true, entryCount, shouldTick
|
||||
end
|
||||
self:HidePlayerFrames(frameKey)
|
||||
self._displaySignatures[frameKey] = "0"
|
||||
return false, 0, false
|
||||
end
|
||||
154
Modules/Tracker/InterruptTracker/InterruptSpellDatabase.lua
Normal file
154
Modules/Tracker/InterruptTracker/InterruptSpellDatabase.lua
Normal file
@@ -0,0 +1,154 @@
|
||||
-- Core/InterruptSpellDatabase.lua
|
||||
|
||||
HMGT_SpellData = HMGT_SpellData or {}
|
||||
local Spell = HMGT_SpellData.Spell
|
||||
if not Spell then return end
|
||||
|
||||
HMGT_SpellData.Interrupts = {
|
||||
-- WARRIOR
|
||||
Spell(6552, "Pummel", {
|
||||
classes = {"WARRIOR"},
|
||||
category = "interrupt",
|
||||
state = { kind = "cooldown", cooldown = 15 },
|
||||
mods = {
|
||||
{ talentSpellId = 391271, value = 10, op = "reduceByPercent", target = "cooldown" },
|
||||
},
|
||||
}),
|
||||
Spell(23920, "Spell Reflection", {
|
||||
classes = {"WARRIOR"},
|
||||
category = "interrupt",
|
||||
state = { kind = "cooldown", cooldown = 20 },
|
||||
mods = {
|
||||
{ talentSpellId = 391271, value = 10, op = "reduceByPercent", target = "cooldown" },
|
||||
},
|
||||
}),
|
||||
|
||||
-- PALADIN
|
||||
Spell(96231, "Rebuke", {
|
||||
classes = {"PALADIN"},
|
||||
specs = {3},
|
||||
category = "interrupt",
|
||||
state = { kind = "cooldown", cooldown = 15 },
|
||||
}),
|
||||
|
||||
-- HUNTER
|
||||
Spell(147362, "Counter Shot", {
|
||||
classes = {"HUNTER"},
|
||||
specs = {1, 2},
|
||||
category = "interrupt",
|
||||
state = { kind = "cooldown", cooldown = 24 },
|
||||
}),
|
||||
Spell(187707, "Muzzle", {
|
||||
classes = {"HUNTER"},
|
||||
specs = {3},
|
||||
category = "interrupt",
|
||||
state = { kind = "cooldown", cooldown = 15 },
|
||||
}),
|
||||
|
||||
-- ROGUE
|
||||
Spell(1766, "Kick", {
|
||||
classes = {"ROGUE"},
|
||||
category = "interrupt",
|
||||
state = { kind = "cooldown", cooldown = 15 },
|
||||
}),
|
||||
|
||||
-- PRIEST
|
||||
Spell(15487, "Silence", {
|
||||
classes = {"PRIEST"},
|
||||
specs = {3},
|
||||
category = "interrupt",
|
||||
state = { kind = "cooldown", cooldown = 45 },
|
||||
}),
|
||||
|
||||
-- DEATH KNIGHT
|
||||
Spell(47528, "Mind Freeze", {
|
||||
classes = {"DEATHKNIGHT"},
|
||||
category = "interrupt",
|
||||
state = { kind = "cooldown", cooldown = 15 },
|
||||
relations = {
|
||||
{
|
||||
when = "interruptSuccess",
|
||||
effects = {
|
||||
{
|
||||
type = "reduceCooldown",
|
||||
targetSpellId = 47528,
|
||||
amount = 3,
|
||||
talentSpellId = 378848,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
}),
|
||||
|
||||
-- SHAMAN
|
||||
Spell(57994, "Wind Shear", {
|
||||
classes = {"SHAMAN"},
|
||||
category = "interrupt",
|
||||
state = { kind = "cooldown", cooldown = 12 },
|
||||
}),
|
||||
|
||||
-- MAGE
|
||||
Spell(2139, "Counterspell", {
|
||||
classes = {"MAGE"},
|
||||
category = "interrupt",
|
||||
state = { kind = "cooldown", cooldown = 24 },
|
||||
}),
|
||||
|
||||
-- WARLOCK
|
||||
Spell(19647, "Spell Lock", {
|
||||
classes = {"WARLOCK"},
|
||||
specs = {2},
|
||||
category = "interrupt",
|
||||
state = { kind = "cooldown", cooldown = 24 },
|
||||
}),
|
||||
Spell(132409, "Spell Lock (Grimoire)", {
|
||||
classes = {"WARLOCK"},
|
||||
specs = {1, 3},
|
||||
category = "interrupt",
|
||||
state = { kind = "cooldown", cooldown = 24 },
|
||||
}),
|
||||
|
||||
-- MONK
|
||||
Spell(116705, "Spear Hand Strike", {
|
||||
classes = {"MONK"},
|
||||
specs = {1, 3},
|
||||
category = "interrupt",
|
||||
state = { kind = "cooldown", cooldown = 15 },
|
||||
}),
|
||||
|
||||
-- DEMON HUNTER
|
||||
Spell(183752, "Disrupt", {
|
||||
classes = {"DEMONHUNTER"},
|
||||
specs = {nil},
|
||||
category = "interrupt",
|
||||
state = { kind = "cooldown", cooldown = 15 },
|
||||
}),
|
||||
|
||||
-- DRUID
|
||||
Spell(78675, "Solar Beam", {
|
||||
classes = {"DRUID"},
|
||||
specs = {1},
|
||||
category = "interrupt",
|
||||
state = { kind = "cooldown", cooldown = 60 },
|
||||
}),
|
||||
Spell(106839, "Skull Bash", {
|
||||
classes = {"DRUID"},
|
||||
specs = {2, 3},
|
||||
category = "interrupt",
|
||||
state = { kind = "cooldown", cooldown = 15 },
|
||||
}),
|
||||
|
||||
-- EVOKER
|
||||
Spell(351338, "Quell", {
|
||||
classes = {"EVOKER"},
|
||||
category = "interrupt",
|
||||
state = { kind = "cooldown", cooldown = 40 },
|
||||
mods = {
|
||||
{ talentSpellId = 396371, value = 20, op = "set", target = "cooldown" },
|
||||
},
|
||||
}),
|
||||
}
|
||||
|
||||
if HMGT_SpellData.RebuildLookups then
|
||||
HMGT_SpellData.RebuildLookups()
|
||||
end
|
||||
21
Modules/Tracker/InterruptTracker/InterruptTracker.lua
Normal file
21
Modules/Tracker/InterruptTracker/InterruptTracker.lua
Normal file
@@ -0,0 +1,21 @@
|
||||
-- Modules/InterruptTracker.lua
|
||||
-- Interrupt tracker based on the shared single-frame tracker base.
|
||||
|
||||
local ADDON_NAME = "HailMaryGuildTools"
|
||||
local HMGT = LibStub("AceAddon-3.0"):GetAddon(ADDON_NAME)
|
||||
local L = LibStub("AceLocale-3.0"):GetLocale(ADDON_NAME)
|
||||
|
||||
local Base = HMGT.SingleFrameTrackerBase
|
||||
if not Base then return end
|
||||
|
||||
Base:CreateModule("InterruptTracker", {
|
||||
profileKey = "interruptTracker",
|
||||
frameName = "InterruptTracker",
|
||||
title = function()
|
||||
return L["IT_TITLE"]
|
||||
end,
|
||||
demoKey = "interruptTracker",
|
||||
database = function()
|
||||
return HMGT_SpellData.Interrupts
|
||||
end,
|
||||
})
|
||||
68
Modules/Tracker/NormalTrackerFrames.lua
Normal file
68
Modules/Tracker/NormalTrackerFrames.lua
Normal file
@@ -0,0 +1,68 @@
|
||||
local ADDON_NAME = "HailMaryGuildTools"
|
||||
local HMGT = LibStub("AceAddon-3.0"):GetAddon(ADDON_NAME)
|
||||
if not HMGT or not HMGT.TrackerManager then return end
|
||||
|
||||
local Manager = HMGT.TrackerManager
|
||||
local S = Manager._shared or {}
|
||||
|
||||
function Manager:CollectEntries(tracker)
|
||||
local entries = {}
|
||||
local players = S.GetGroupPlayers(tracker)
|
||||
for _, playerInfo in ipairs(players) do
|
||||
local playerEntries = S.CollectEntriesForPlayer(tracker, playerInfo)
|
||||
for _, entry in ipairs(playerEntries) do
|
||||
entries[#entries + 1] = entry
|
||||
end
|
||||
end
|
||||
return entries
|
||||
end
|
||||
|
||||
function Manager:CollectTestEntries(tracker)
|
||||
local playerName = HMGT:NormalizePlayerName(UnitName("player")) or "Player"
|
||||
local classToken = select(2, UnitClass("player"))
|
||||
if not classToken then
|
||||
return {}
|
||||
end
|
||||
|
||||
local entries = {}
|
||||
local pData = HMGT.playerData[playerName]
|
||||
local talents = pData and pData.talents or {}
|
||||
local spells = S.GetTrackerSpellsForPlayer(classToken, GetSpecialization() or 0, tracker.categories)
|
||||
for _, spellEntry in ipairs(spells) do
|
||||
if tracker.enabledSpells[spellEntry.spellId] ~= false then
|
||||
local remaining, total, currentCharges, maxCharges = HMGT:GetCooldownInfo(playerName, spellEntry.spellId)
|
||||
local effectiveCd = HMGT_SpellData.GetEffectiveCooldown(spellEntry, talents)
|
||||
local isAvailabilitySpell = HMGT:IsAvailabilitySpell(spellEntry)
|
||||
local spellKnown = HMGT:IsTrackedSpellKnownForPlayer(playerName, spellEntry.spellId)
|
||||
local hasPartialCharges = (tonumber(maxCharges) or 0) > 0
|
||||
and (tonumber(currentCharges) or tonumber(maxCharges) or 0) < (tonumber(maxCharges) or 0)
|
||||
local hasActiveCd = ((remaining or 0) > 0) or hasPartialCharges
|
||||
local hasAvailabilityState = isAvailabilitySpell and HMGT:HasAvailabilityState(playerName, spellEntry.spellId)
|
||||
if spellKnown or hasActiveCd or hasAvailabilityState then
|
||||
entries[#entries + 1] = {
|
||||
playerName = playerName,
|
||||
class = classToken,
|
||||
spellEntry = spellEntry,
|
||||
remaining = remaining,
|
||||
total = total > 0 and total or effectiveCd,
|
||||
currentCharges = currentCharges,
|
||||
maxCharges = maxCharges,
|
||||
}
|
||||
end
|
||||
end
|
||||
end
|
||||
return entries
|
||||
end
|
||||
|
||||
function Manager:BuildEntriesForTracker(tracker)
|
||||
if tracker.testMode then
|
||||
return self:CollectTestEntries(tracker), true
|
||||
end
|
||||
if tracker.demoMode then
|
||||
return HMGT:GetDemoEntries(S.GetTrackerFrameKey(tracker.id), S.GetTrackerSpellPool(tracker.categories), tracker), true
|
||||
end
|
||||
if not tracker.enabled or not HMGT:IsVisibleForCurrentGroup(tracker) then
|
||||
return {}, false
|
||||
end
|
||||
return self:CollectEntries(tracker), true
|
||||
end
|
||||
@@ -0,0 +1,128 @@
|
||||
-- Core/RaidCooldownSpellDatabase.lua
|
||||
-- Raid cooldown database.
|
||||
|
||||
HMGT_SpellData = HMGT_SpellData or {}
|
||||
local Spell = HMGT_SpellData.Spell
|
||||
if not Spell then return end
|
||||
|
||||
HMGT_SpellData.RaidCooldowns = {
|
||||
-- WARRIOR
|
||||
Spell(97462, "Rallying Cry", {
|
||||
classes = {"WARRIOR"},
|
||||
category = "defensive",
|
||||
state = { kind = "cooldown", cooldown = 180 },
|
||||
}),
|
||||
|
||||
-- PALADIN
|
||||
Spell(31821, "Aura Mastery", {
|
||||
classes = {"PALADIN"},
|
||||
specs = {1},
|
||||
category = "defensive",
|
||||
state = { kind = "cooldown", cooldown = 180 },
|
||||
}),
|
||||
|
||||
-- PRIEST
|
||||
Spell(62618, "Power Word: Barrier", {
|
||||
classes = {"PRIEST"},
|
||||
specs = {1},
|
||||
category = "defensive",
|
||||
state = { kind = "cooldown", cooldown = 180 },
|
||||
}),
|
||||
Spell(64843, "Divine Hymn", {
|
||||
classes = {"PRIEST"},
|
||||
specs = {1, 2},
|
||||
category = "healing",
|
||||
state = { kind = "cooldown", cooldown = 180 },
|
||||
}),
|
||||
Spell(47536, "Rapture", {
|
||||
classes = {"PRIEST"},
|
||||
specs = {1},
|
||||
category = "healing",
|
||||
state = { kind = "cooldown", cooldown = 90 },
|
||||
}),
|
||||
|
||||
-- DEATH KNIGHT
|
||||
Spell(51052, "Anti-Magic Zone", {
|
||||
classes = {"DEATHKNIGHT"},
|
||||
specs = {1, 2, 3},
|
||||
category = "defensive",
|
||||
state = { kind = "cooldown", cooldown = 120 },
|
||||
mods = {
|
||||
{ talentSpellId = 145629, value = 90, op = "set", target = "cooldown" },
|
||||
},
|
||||
}),
|
||||
-- SHAMAN
|
||||
Spell(98008, "Spirit Link Totem", {
|
||||
classes = {"SHAMAN"},
|
||||
specs = {3},
|
||||
category = "defensive",
|
||||
state = { kind = "cooldown", cooldown = 180 },
|
||||
}),
|
||||
Spell(108280, "Healing Tide Totem", {
|
||||
classes = {"SHAMAN"},
|
||||
specs = {3},
|
||||
category = "healing",
|
||||
state = { kind = "cooldown", cooldown = 180 },
|
||||
}),
|
||||
Spell(192077, "Wind Rush Totem", {
|
||||
classes = {"SHAMAN"},
|
||||
category = "utility",
|
||||
state = { kind = "cooldown", cooldown = 120 },
|
||||
}),
|
||||
|
||||
-- MONK
|
||||
Spell(115310, "Revival", {
|
||||
classes = {"MONK"},
|
||||
specs = {2},
|
||||
category = "healing",
|
||||
state = { kind = "cooldown", cooldown = 180 },
|
||||
}),
|
||||
|
||||
-- DRUID
|
||||
Spell(740, "Tranquility", {
|
||||
classes = {"DRUID"},
|
||||
specs = {4},
|
||||
category = "healing",
|
||||
state = { kind = "cooldown", cooldown = 180 },
|
||||
}),
|
||||
Spell(106898, "Stampeding Roar", {
|
||||
classes = {"DRUID"},
|
||||
category = "utility",
|
||||
state = { kind = "cooldown", cooldown = 120 },
|
||||
}),
|
||||
Spell(33891, "Incarnation: Tree of Life", {
|
||||
classes = {"DRUID"},
|
||||
specs = {4},
|
||||
category = "healing",
|
||||
state = { kind = "cooldown", cooldown = 180 },
|
||||
}),
|
||||
|
||||
-- DEMON HUNTER
|
||||
Spell(196718, "Darkness", {
|
||||
classes = {"DEMONHUNTER"},
|
||||
specs = {1},
|
||||
category = "defensive",
|
||||
state = { kind = "cooldown", cooldown = 180 },
|
||||
}),
|
||||
|
||||
-- EVOKER
|
||||
Spell(363534, "Rewind", {
|
||||
classes = {"EVOKER"},
|
||||
specs = {2},
|
||||
category = "healing",
|
||||
state = { kind = "cooldown", cooldown = 240 },
|
||||
mods = {
|
||||
{ talentSpellId = 373862, value = 120, op = "set", target = "cooldown" },
|
||||
},
|
||||
}),
|
||||
Spell(406732, "Dream Projection", {
|
||||
classes = {"EVOKER"},
|
||||
specs = {2},
|
||||
category = "healing",
|
||||
state = { kind = "cooldown", cooldown = 45 },
|
||||
}),
|
||||
}
|
||||
|
||||
if HMGT_SpellData.RebuildLookups then
|
||||
HMGT_SpellData.RebuildLookups()
|
||||
end
|
||||
21
Modules/Tracker/RaidcooldownTracker/RaidcooldownTracker.lua
Normal file
21
Modules/Tracker/RaidcooldownTracker/RaidcooldownTracker.lua
Normal file
@@ -0,0 +1,21 @@
|
||||
-- Modules/RaidCooldownTracker.lua
|
||||
-- Raid cooldown tracker based on the shared single-frame tracker base.
|
||||
|
||||
local ADDON_NAME = "HailMaryGuildTools"
|
||||
local HMGT = LibStub("AceAddon-3.0"):GetAddon(ADDON_NAME)
|
||||
local L = LibStub("AceLocale-3.0"):GetLocale(ADDON_NAME)
|
||||
|
||||
local Base = HMGT.SingleFrameTrackerBase
|
||||
if not Base then return end
|
||||
|
||||
Base:CreateModule("RaidCooldownTracker", {
|
||||
profileKey = "raidCooldownTracker",
|
||||
frameName = "RaidCooldownTracker",
|
||||
title = function()
|
||||
return L["RCD_TITLE"]
|
||||
end,
|
||||
demoKey = "raidCooldownTracker",
|
||||
database = function()
|
||||
return HMGT_SpellData.RaidCooldowns
|
||||
end,
|
||||
})
|
||||
305
Modules/Tracker/SingleFrameTrackerBase.lua
Normal file
305
Modules/Tracker/SingleFrameTrackerBase.lua
Normal file
@@ -0,0 +1,305 @@
|
||||
-- Modules/Tracker/SingleFrameTrackerBase.lua
|
||||
-- Shared implementation for single-frame tracker modules.
|
||||
|
||||
local ADDON_NAME = "HailMaryGuildTools"
|
||||
local HMGT = LibStub("AceAddon-3.0"):GetAddon(ADDON_NAME)
|
||||
if not HMGT then return end
|
||||
|
||||
HMGT.SingleFrameTrackerBase = HMGT.SingleFrameTrackerBase or {}
|
||||
local Base = HMGT.SingleFrameTrackerBase
|
||||
|
||||
local function GetDefaultGroupPlayers()
|
||||
local players = {}
|
||||
|
||||
local ownName = HMGT:NormalizePlayerName(UnitName("player"))
|
||||
local ownClass = select(2, UnitClass("player"))
|
||||
table.insert(players, { name = ownName, class = ownClass, isOwn = true, unitId = "player" })
|
||||
|
||||
if IsInRaid() then
|
||||
for i = 1, GetNumGroupMembers() do
|
||||
local unitId = "raid" .. i
|
||||
local name = HMGT:NormalizePlayerName(UnitName(unitId))
|
||||
local class = select(2, UnitClass(unitId))
|
||||
if name and name ~= ownName then
|
||||
table.insert(players, { name = name, class = class, unitId = unitId })
|
||||
end
|
||||
end
|
||||
elseif IsInGroup() then
|
||||
for i = 1, GetNumGroupMembers() - 1 do
|
||||
local unitId = "party" .. i
|
||||
local name = HMGT:NormalizePlayerName(UnitName(unitId))
|
||||
local class = select(2, UnitClass(unitId))
|
||||
if name and name ~= ownName then
|
||||
table.insert(players, { name = name, class = class, unitId = unitId })
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
return players
|
||||
end
|
||||
|
||||
local function ResolveConfigValue(configValue, self)
|
||||
if type(configValue) == "function" then
|
||||
return configValue(self)
|
||||
end
|
||||
return configValue
|
||||
end
|
||||
|
||||
local function EntryNeedsVisualTicker(entry)
|
||||
if type(entry) ~= "table" then
|
||||
return false
|
||||
end
|
||||
|
||||
local remaining = tonumber(entry.remaining) or 0
|
||||
if remaining > 0 then
|
||||
return true
|
||||
end
|
||||
|
||||
local maxCharges = tonumber(entry.maxCharges) or 0
|
||||
local currentCharges = tonumber(entry.currentCharges)
|
||||
if maxCharges > 0 and currentCharges ~= nil and currentCharges < maxCharges then
|
||||
return true
|
||||
end
|
||||
|
||||
return false
|
||||
end
|
||||
|
||||
function Base:Create(config)
|
||||
local tracker = {
|
||||
frame = nil,
|
||||
updateTicker = nil,
|
||||
lastEntryCount = 0,
|
||||
}
|
||||
|
||||
function tracker:GetSettings()
|
||||
return HMGT.db.profile[config.profileKey]
|
||||
end
|
||||
|
||||
function tracker:GetDatabase()
|
||||
return ResolveConfigValue(config.database, self) or {}
|
||||
end
|
||||
|
||||
function tracker:GetTitle()
|
||||
return ResolveConfigValue(config.title, self) or config.frameName
|
||||
end
|
||||
|
||||
function tracker:GetDemoKey()
|
||||
return ResolveConfigValue(config.demoKey, self) or config.profileKey
|
||||
end
|
||||
|
||||
function tracker:GetCooldownInfoOpts()
|
||||
return ResolveConfigValue(config.cooldownInfoOpts, self)
|
||||
end
|
||||
|
||||
function tracker:GetGroupPlayers()
|
||||
local custom = ResolveConfigValue(config.groupPlayersProvider, self)
|
||||
if type(custom) == "table" then
|
||||
return custom
|
||||
end
|
||||
return GetDefaultGroupPlayers()
|
||||
end
|
||||
|
||||
function tracker:EnsureUpdateTicker()
|
||||
if self.updateTicker then
|
||||
return
|
||||
end
|
||||
self.updateTicker = C_Timer.NewTicker(0.1, function()
|
||||
self:UpdateDisplay()
|
||||
end)
|
||||
end
|
||||
|
||||
function tracker:StopUpdateTicker()
|
||||
if self.updateTicker then
|
||||
self.updateTicker:Cancel()
|
||||
self.updateTicker = nil
|
||||
end
|
||||
end
|
||||
|
||||
function tracker:SetUpdateTickerEnabled(enabled)
|
||||
if enabled then
|
||||
self:EnsureUpdateTicker()
|
||||
else
|
||||
self:StopUpdateTicker()
|
||||
end
|
||||
end
|
||||
|
||||
function tracker:Enable()
|
||||
local s = self:GetSettings()
|
||||
if not s.enabled and not s.demoMode and not s.testMode then return end
|
||||
|
||||
if not self.frame then
|
||||
self.frame = HMGT.TrackerFrame:CreateTrackerFrame(config.frameName, s)
|
||||
HMGT.TrackerFrame:SetTitle(self.frame, self:GetTitle())
|
||||
end
|
||||
|
||||
if HMGT:IsVisibleForCurrentGroup(s) then
|
||||
self.frame:Show()
|
||||
else
|
||||
self.frame:Hide()
|
||||
end
|
||||
self:UpdateDisplay()
|
||||
end
|
||||
|
||||
function tracker:Disable()
|
||||
self:StopUpdateTicker()
|
||||
if self.frame then
|
||||
self.frame:Hide()
|
||||
end
|
||||
end
|
||||
|
||||
function tracker:UpdateDisplay()
|
||||
if not self.frame then
|
||||
self:StopUpdateTicker()
|
||||
return
|
||||
end
|
||||
|
||||
local s = self:GetSettings()
|
||||
local database = self:GetDatabase()
|
||||
local cooldownInfoOpts = self:GetCooldownInfoOpts()
|
||||
|
||||
if s.testMode then
|
||||
HMGT.TrackerFrame:SetLocked(self.frame, s.locked)
|
||||
local entries = HMGT:GetOwnTestEntries(database, s, cooldownInfoOpts)
|
||||
self.lastEntryCount = #entries
|
||||
HMGT.TrackerFrame:UpdateFrame(self.frame, entries)
|
||||
self.frame:Show()
|
||||
local shouldTick = false
|
||||
for _, entry in ipairs(entries) do
|
||||
if EntryNeedsVisualTicker(entry) then
|
||||
shouldTick = true
|
||||
break
|
||||
end
|
||||
end
|
||||
self:SetUpdateTickerEnabled(shouldTick)
|
||||
return
|
||||
end
|
||||
|
||||
if s.demoMode then
|
||||
HMGT.TrackerFrame:SetLocked(self.frame, s.locked)
|
||||
local entries = HMGT:GetDemoEntries(self:GetDemoKey(), database, s)
|
||||
self.lastEntryCount = #entries
|
||||
HMGT.TrackerFrame:UpdateFrame(self.frame, entries)
|
||||
self.frame:Show()
|
||||
self:SetUpdateTickerEnabled(#entries > 0)
|
||||
return
|
||||
end
|
||||
|
||||
if not s.enabled then
|
||||
self.lastEntryCount = 0
|
||||
self.frame:Hide()
|
||||
self:StopUpdateTicker()
|
||||
return
|
||||
end
|
||||
|
||||
if not HMGT:IsVisibleForCurrentGroup(s) then
|
||||
self.lastEntryCount = 0
|
||||
self.frame:Hide()
|
||||
self:StopUpdateTicker()
|
||||
return
|
||||
end
|
||||
|
||||
HMGT.TrackerFrame:SetLocked(self.frame, s.locked)
|
||||
|
||||
local entries = self:CollectEntries()
|
||||
self.lastEntryCount = #entries
|
||||
if HMGT.SortDisplayEntries then
|
||||
HMGT:SortDisplayEntries(entries, config.profileKey)
|
||||
end
|
||||
|
||||
HMGT.TrackerFrame:UpdateFrame(self.frame, entries)
|
||||
self.frame:Show()
|
||||
|
||||
local shouldTick = false
|
||||
for _, entry in ipairs(entries) do
|
||||
if EntryNeedsVisualTicker(entry) then
|
||||
shouldTick = true
|
||||
break
|
||||
end
|
||||
end
|
||||
self:SetUpdateTickerEnabled(shouldTick)
|
||||
end
|
||||
|
||||
function tracker:CollectEntries()
|
||||
local entries = {}
|
||||
local s = self:GetSettings()
|
||||
local database = self:GetDatabase()
|
||||
local cooldownInfoOpts = self:GetCooldownInfoOpts()
|
||||
local players = self:GetGroupPlayers()
|
||||
|
||||
for _, playerInfo in ipairs(players) do
|
||||
repeat
|
||||
local name = playerInfo.name
|
||||
local pData = HMGT.playerData[name]
|
||||
local class = pData and pData.class or playerInfo.class
|
||||
local specIdx
|
||||
if playerInfo.isOwn then
|
||||
specIdx = GetSpecialization()
|
||||
if not specIdx or specIdx == 0 then break end
|
||||
else
|
||||
specIdx = pData and pData.specIndex or nil
|
||||
if not specIdx or tonumber(specIdx) <= 0 then break end
|
||||
end
|
||||
local talents = pData and pData.talents or {}
|
||||
|
||||
if not class then break end
|
||||
|
||||
local knownSpells = HMGT_SpellData.GetSpellsForSpec(class, specIdx, database)
|
||||
|
||||
for _, spellEntry in ipairs(knownSpells) do
|
||||
if s.enabledSpells[spellEntry.spellId] ~= false then
|
||||
local remaining, total, curCharges, maxCharges = HMGT:GetCooldownInfo(name, spellEntry.spellId, cooldownInfoOpts)
|
||||
local isAvailabilitySpell = HMGT.IsAvailabilitySpell and HMGT:IsAvailabilitySpell(spellEntry)
|
||||
local effectiveCd = HMGT_SpellData.GetEffectiveCooldown(spellEntry, talents)
|
||||
local include = HMGT:ShouldDisplayEntry(s, remaining, curCharges, maxCharges, spellEntry)
|
||||
local spellKnown = HMGT:IsTrackedSpellKnownForPlayer(name, spellEntry.spellId)
|
||||
local hasPartialCharges = (tonumber(maxCharges) or 0) > 0
|
||||
and (tonumber(curCharges) or tonumber(maxCharges) or 0) < (tonumber(maxCharges) or 0)
|
||||
local hasActiveCd = ((remaining or 0) > 0) or hasPartialCharges
|
||||
if not spellKnown and not hasActiveCd then
|
||||
include = false
|
||||
end
|
||||
if isAvailabilitySpell and not spellKnown then
|
||||
include = false
|
||||
end
|
||||
|
||||
if not playerInfo.isOwn then
|
||||
if isAvailabilitySpell and not HMGT:HasAvailabilityState(name, spellEntry.spellId) then
|
||||
include = false
|
||||
end
|
||||
end
|
||||
|
||||
if include then
|
||||
entries[#entries + 1] = {
|
||||
playerName = name,
|
||||
class = class,
|
||||
spellEntry = spellEntry,
|
||||
remaining = remaining,
|
||||
total = total > 0 and total or effectiveCd,
|
||||
currentCharges = curCharges,
|
||||
maxCharges = maxCharges,
|
||||
}
|
||||
end
|
||||
end
|
||||
end
|
||||
until true
|
||||
end
|
||||
|
||||
return entries
|
||||
end
|
||||
|
||||
return tracker
|
||||
end
|
||||
|
||||
function Base:CreateModule(moduleName, config, ...)
|
||||
if type(moduleName) ~= "string" or moduleName == "" then
|
||||
return self:Create(config)
|
||||
end
|
||||
|
||||
local module = HMGT:NewModule(moduleName, ...)
|
||||
local tracker = self:Create(config)
|
||||
for key, value in pairs(tracker) do
|
||||
module[key] = value
|
||||
end
|
||||
HMGT[moduleName] = module
|
||||
return module
|
||||
end
|
||||
1104
Modules/Tracker/SpellDatabase.lua
Normal file
1104
Modules/Tracker/SpellDatabase.lua
Normal file
File diff suppressed because it is too large
Load Diff
803
Modules/Tracker/TrackerManager.lua
Normal file
803
Modules/Tracker/TrackerManager.lua
Normal file
@@ -0,0 +1,803 @@
|
||||
-- Modules/Tracker/TrackerManager.lua
|
||||
-- Generic tracker manager for category-driven tracker frames.
|
||||
|
||||
local ADDON_NAME = "HailMaryGuildTools"
|
||||
local HMGT = LibStub("AceAddon-3.0"):GetAddon(ADDON_NAME)
|
||||
if not HMGT then return end
|
||||
|
||||
local Manager = {}
|
||||
HMGT.TrackerManager = Manager
|
||||
|
||||
Manager.frames = Manager.frames or {}
|
||||
Manager.perPlayerFrames = Manager.perPlayerFrames or {}
|
||||
Manager.activeOrders = Manager.activeOrders or {}
|
||||
Manager.unitByPlayer = Manager.unitByPlayer or {}
|
||||
Manager.anchorLayoutSignatures = Manager.anchorLayoutSignatures or {}
|
||||
Manager.nextAnchorRetryAt = Manager.nextAnchorRetryAt or {}
|
||||
Manager.enabled = false
|
||||
Manager.visualTicker = nil
|
||||
Manager.lastEntryCount = 0
|
||||
Manager._shared = Manager._shared or {}
|
||||
Manager._trackerCache = Manager._trackerCache or nil
|
||||
Manager._trackerCacheSignature = Manager._trackerCacheSignature or nil
|
||||
Manager._displaySignatures = Manager._displaySignatures or {}
|
||||
Manager._layoutDirty = Manager._layoutDirty == true
|
||||
|
||||
local function GetTrackerFrameKey(trackerId)
|
||||
return "tracker:" .. tostring(tonumber(trackerId) or 0)
|
||||
end
|
||||
|
||||
local function GetTrackerFrameName(trackerId)
|
||||
return "GenericTracker_" .. tostring(tonumber(trackerId) or 0)
|
||||
end
|
||||
|
||||
local function GetTrackerPlayerFrameName(trackerId, playerName)
|
||||
local token = tostring(playerName or "Unknown"):gsub("[^%w_]", "_")
|
||||
return string.format("%s_%s", GetTrackerFrameName(trackerId), token)
|
||||
end
|
||||
|
||||
local function ShortName(name)
|
||||
if not name then
|
||||
return ""
|
||||
end
|
||||
local short = name:match("^[^-]+")
|
||||
return short or name
|
||||
end
|
||||
|
||||
local function IsUsableAnchorFrame(frame)
|
||||
return frame
|
||||
and frame.IsObjectType
|
||||
and (frame:IsObjectType("Frame") or frame:IsObjectType("Button"))
|
||||
end
|
||||
|
||||
local function GetFrameUnit(frame)
|
||||
if not frame then
|
||||
return nil
|
||||
end
|
||||
local unit = frame.unit
|
||||
if not unit and frame.GetAttribute then
|
||||
unit = frame:GetAttribute("unit")
|
||||
end
|
||||
return unit
|
||||
end
|
||||
|
||||
local function FrameMatchesUnit(frame, unitId)
|
||||
if not IsUsableAnchorFrame(frame) then
|
||||
return false
|
||||
end
|
||||
if not unitId then
|
||||
return true
|
||||
end
|
||||
return GetFrameUnit(frame) == unitId
|
||||
end
|
||||
|
||||
local PLAYER_FRAME_CANDIDATES = {
|
||||
"PlayerFrame",
|
||||
"ElvUF_Player",
|
||||
"NephUI_PlayerFrame",
|
||||
"NephUIPlayerFrame",
|
||||
"oUF_NephUI_Player",
|
||||
"SUFUnitplayer",
|
||||
}
|
||||
|
||||
local PARTY_FRAME_PATTERNS = {
|
||||
"PartyMemberFrame%d",
|
||||
"CompactPartyFrameMember%d",
|
||||
"ElvUF_PartyGroup1UnitButton%d",
|
||||
"ElvUF_PartyUnitButton%d",
|
||||
"NephUI_PartyUnitButton%d",
|
||||
"NephUI_PartyFrame%d",
|
||||
"NephUIPartyFrame%d",
|
||||
"oUF_NephUI_PartyUnitButton%d",
|
||||
"SUFUnitparty%d",
|
||||
}
|
||||
|
||||
local unitFrameCache = {}
|
||||
|
||||
local function EntryNeedsVisualTicker(entry)
|
||||
if type(entry) ~= "table" then
|
||||
return false
|
||||
end
|
||||
|
||||
local remaining = tonumber(entry.remaining) or 0
|
||||
if remaining > 0 then
|
||||
return true
|
||||
end
|
||||
|
||||
local maxCharges = tonumber(entry.maxCharges) or 0
|
||||
local currentCharges = tonumber(entry.currentCharges)
|
||||
if maxCharges > 0 and currentCharges ~= nil and currentCharges < maxCharges then
|
||||
return true
|
||||
end
|
||||
|
||||
return false
|
||||
end
|
||||
|
||||
local function BuildAnchorLayoutSignature(settings, ordered, unitByPlayer)
|
||||
local parts = {
|
||||
settings.attachToPartyFrame == true and "attach" or "stack",
|
||||
tostring(settings.partyAttachSide or "RIGHT"),
|
||||
tostring(tonumber(settings.partyAttachOffsetX) or 8),
|
||||
tostring(tonumber(settings.partyAttachOffsetY) or 0),
|
||||
tostring(settings.showBar and "bar" or "icon"),
|
||||
tostring(settings.growDirection or "DOWN"),
|
||||
tostring(settings.width or 250),
|
||||
tostring(settings.barHeight or 20),
|
||||
tostring(settings.iconSize or 32),
|
||||
tostring(settings.iconCols or 6),
|
||||
tostring(settings.barSpacing or 2),
|
||||
tostring(settings.locked),
|
||||
tostring(settings.anchorTo or "UIParent"),
|
||||
tostring(settings.anchorPoint or "TOPLEFT"),
|
||||
tostring(settings.anchorRelPoint or "TOPLEFT"),
|
||||
tostring(settings.anchorX or settings.posX or 0),
|
||||
tostring(settings.anchorY or settings.posY or 0),
|
||||
}
|
||||
|
||||
for _, playerName in ipairs(ordered or {}) do
|
||||
parts[#parts + 1] = tostring(playerName)
|
||||
parts[#parts + 1] = tostring(unitByPlayer and unitByPlayer[playerName] or "")
|
||||
end
|
||||
|
||||
return table.concat(parts, "|")
|
||||
end
|
||||
|
||||
local function IsGroupTracker(tracker)
|
||||
return type(tracker) == "table" and tracker.trackerType == "group"
|
||||
end
|
||||
|
||||
local function ResolveNamedUnitFrame(unitId)
|
||||
if unitId == "player" then
|
||||
for _, frameName in ipairs(PLAYER_FRAME_CANDIDATES) do
|
||||
local frame = _G[frameName]
|
||||
if FrameMatchesUnit(frame, unitId) or (frameName == "PlayerFrame" and IsUsableAnchorFrame(frame)) then
|
||||
return frame
|
||||
end
|
||||
end
|
||||
return nil
|
||||
end
|
||||
|
||||
local idx = type(unitId) == "string" and unitId:match("^party(%d+)$")
|
||||
if not idx then
|
||||
return nil
|
||||
end
|
||||
|
||||
idx = tonumber(idx)
|
||||
for _, pattern in ipairs(PARTY_FRAME_PATTERNS) do
|
||||
local frame = _G[pattern:format(idx)]
|
||||
if FrameMatchesUnit(frame, unitId) then
|
||||
return frame
|
||||
end
|
||||
end
|
||||
|
||||
return nil
|
||||
end
|
||||
|
||||
local function ScanUnitFrame(unitId)
|
||||
local frame = EnumerateFrames()
|
||||
local scanned = 0
|
||||
while frame and scanned < 8000 do
|
||||
if IsUsableAnchorFrame(frame) and GetFrameUnit(frame) == unitId then
|
||||
HMGT:Debug("verbose", "TrackerAttach scan unit=%s scanned=%d found=true", tostring(unitId), scanned)
|
||||
return frame
|
||||
end
|
||||
scanned = scanned + 1
|
||||
frame = EnumerateFrames(frame)
|
||||
end
|
||||
HMGT:Debug("verbose", "TrackerAttach scan unit=%s scanned=%d found=false", tostring(unitId), scanned)
|
||||
return nil
|
||||
end
|
||||
|
||||
local function ResolveUnitAnchorFrame(unitId)
|
||||
if not unitId then
|
||||
return nil
|
||||
end
|
||||
|
||||
local now = GetTime()
|
||||
local cached = unitFrameCache[unitId]
|
||||
if cached and now < (cached.expires or 0) then
|
||||
if cached.frame and cached.frame:IsShown() then
|
||||
return cached.frame
|
||||
end
|
||||
return nil
|
||||
end
|
||||
|
||||
local frame = ResolveNamedUnitFrame(unitId)
|
||||
if not frame then
|
||||
frame = ScanUnitFrame(unitId)
|
||||
end
|
||||
|
||||
local expiresIn = 1.0
|
||||
if frame and frame:IsShown() then
|
||||
expiresIn = 10.0
|
||||
end
|
||||
unitFrameCache[unitId] = {
|
||||
frame = frame,
|
||||
expires = now + expiresIn,
|
||||
}
|
||||
|
||||
if frame and frame:IsShown() then
|
||||
return frame
|
||||
end
|
||||
return nil
|
||||
end
|
||||
|
||||
local function GetGroupPlayers(tracker)
|
||||
local players = {}
|
||||
|
||||
local ownName = HMGT:NormalizePlayerName(UnitName("player"))
|
||||
local ownClass = select(2, UnitClass("player"))
|
||||
local includeOwnPlayer = true
|
||||
if IsGroupTracker(tracker) then
|
||||
includeOwnPlayer = tracker.includeSelfFrame == true
|
||||
end
|
||||
if includeOwnPlayer then
|
||||
players[#players + 1] = {
|
||||
name = ownName,
|
||||
class = ownClass,
|
||||
isOwn = true,
|
||||
unitId = "player",
|
||||
}
|
||||
end
|
||||
|
||||
if IsInRaid() then
|
||||
for i = 1, GetNumGroupMembers() do
|
||||
local unitId = "raid" .. i
|
||||
local name = HMGT:NormalizePlayerName(UnitName(unitId))
|
||||
local class = select(2, UnitClass(unitId))
|
||||
if name and name ~= ownName then
|
||||
players[#players + 1] = {
|
||||
name = name,
|
||||
class = class,
|
||||
unitId = unitId,
|
||||
}
|
||||
end
|
||||
end
|
||||
elseif IsInGroup() then
|
||||
for i = 1, GetNumGroupMembers() - 1 do
|
||||
local unitId = "party" .. i
|
||||
local name = HMGT:NormalizePlayerName(UnitName(unitId))
|
||||
local class = select(2, UnitClass(unitId))
|
||||
if name and name ~= ownName then
|
||||
players[#players + 1] = {
|
||||
name = name,
|
||||
class = class,
|
||||
unitId = unitId,
|
||||
}
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
return players
|
||||
end
|
||||
|
||||
local function GetTrackerLabel(tracker)
|
||||
if type(tracker) ~= "table" then
|
||||
return "Tracker"
|
||||
end
|
||||
|
||||
local name = tostring(tracker.name or ""):gsub("^%s+", ""):gsub("%s+$", "")
|
||||
local trackerId = tonumber(tracker.id) or 0
|
||||
if name ~= "" then
|
||||
return name
|
||||
end
|
||||
if trackerId > 0 then
|
||||
return string.format("Tracker %d", trackerId)
|
||||
end
|
||||
return "Tracker"
|
||||
end
|
||||
|
||||
local function GetTrackerSpellPool(categories)
|
||||
if HMGT_SpellData and type(HMGT_SpellData.GetSpellPoolForCategories) == "function" then
|
||||
return HMGT_SpellData.GetSpellPoolForCategories(categories)
|
||||
end
|
||||
return {}
|
||||
end
|
||||
|
||||
local function GetTrackerSpellsForPlayer(classToken, specIndex, categories)
|
||||
if HMGT_SpellData and type(HMGT_SpellData.GetSpellsForCategories) == "function" then
|
||||
return HMGT_SpellData.GetSpellsForCategories(classToken, specIndex, categories)
|
||||
end
|
||||
return {}
|
||||
end
|
||||
|
||||
local function CollectEntriesForPlayer(tracker, playerInfo)
|
||||
local entries = {}
|
||||
if type(tracker) ~= "table" or type(playerInfo) ~= "table" then
|
||||
return entries
|
||||
end
|
||||
|
||||
local playerName = playerInfo.name
|
||||
if not playerName then
|
||||
return entries
|
||||
end
|
||||
|
||||
local pData = HMGT.playerData[playerName]
|
||||
local classToken = pData and pData.class or playerInfo.class
|
||||
if not classToken then
|
||||
return entries
|
||||
end
|
||||
|
||||
local specIndex
|
||||
if playerInfo.isOwn then
|
||||
specIndex = GetSpecialization()
|
||||
if not specIndex or specIndex == 0 then
|
||||
return entries
|
||||
end
|
||||
else
|
||||
specIndex = pData and pData.specIndex or nil
|
||||
if not specIndex or tonumber(specIndex) <= 0 then
|
||||
return entries
|
||||
end
|
||||
end
|
||||
|
||||
local talents = pData and pData.talents or {}
|
||||
local spells = GetTrackerSpellsForPlayer(classToken, specIndex, tracker.categories)
|
||||
for _, spellEntry in ipairs(spells) do
|
||||
if tracker.enabledSpells[spellEntry.spellId] ~= false then
|
||||
local remaining, total, currentCharges, maxCharges = HMGT:GetCooldownInfo(playerName, spellEntry.spellId)
|
||||
local effectiveCd = HMGT_SpellData.GetEffectiveCooldown(spellEntry, talents)
|
||||
local isAvailabilitySpell = HMGT:IsAvailabilitySpell(spellEntry)
|
||||
local include = HMGT:ShouldDisplayEntry(tracker, remaining, currentCharges, maxCharges, spellEntry)
|
||||
local spellKnown = HMGT:IsTrackedSpellKnownForPlayer(playerName, spellEntry.spellId)
|
||||
local hasPartialCharges = (tonumber(maxCharges) or 0) > 0
|
||||
and (tonumber(currentCharges) or tonumber(maxCharges) or 0) < (tonumber(maxCharges) or 0)
|
||||
local hasActiveCd = ((remaining or 0) > 0) or hasPartialCharges
|
||||
|
||||
if not spellKnown and not hasActiveCd then
|
||||
include = false
|
||||
end
|
||||
if isAvailabilitySpell and not spellKnown then
|
||||
include = false
|
||||
end
|
||||
if not playerInfo.isOwn and isAvailabilitySpell and not HMGT:HasAvailabilityState(playerName, spellEntry.spellId) then
|
||||
include = false
|
||||
end
|
||||
|
||||
if include then
|
||||
entries[#entries + 1] = {
|
||||
playerName = playerName,
|
||||
class = classToken,
|
||||
spellEntry = spellEntry,
|
||||
remaining = remaining,
|
||||
total = total > 0 and total or effectiveCd,
|
||||
currentCharges = currentCharges,
|
||||
maxCharges = maxCharges,
|
||||
}
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
return entries
|
||||
end
|
||||
|
||||
local function CopyEntriesForPreview(entries, playerName)
|
||||
local copies = {}
|
||||
for _, entry in ipairs(entries or {}) do
|
||||
local nextEntry = {}
|
||||
for key, value in pairs(entry) do
|
||||
nextEntry[key] = value
|
||||
end
|
||||
nextEntry.playerName = playerName
|
||||
copies[#copies + 1] = nextEntry
|
||||
end
|
||||
return copies
|
||||
end
|
||||
|
||||
local function GetAvailablePartyPreviewUnits()
|
||||
local units = {}
|
||||
for index = 1, 4 do
|
||||
local unitId = "party" .. index
|
||||
if ResolveUnitAnchorFrame(unitId) then
|
||||
units[#units + 1] = {
|
||||
playerName = string.format("Party %d", index),
|
||||
unitId = unitId,
|
||||
}
|
||||
end
|
||||
end
|
||||
return units
|
||||
end
|
||||
|
||||
local function BuildPartyPreviewEntries(entries)
|
||||
local byPlayer = {}
|
||||
local order = {}
|
||||
local unitByPlayer = {}
|
||||
local previewUnits = GetAvailablePartyPreviewUnits()
|
||||
|
||||
for _, previewUnit in ipairs(previewUnits) do
|
||||
local playerName = previewUnit.playerName
|
||||
local playerEntries = CopyEntriesForPreview(entries, playerName)
|
||||
if #playerEntries > 0 then
|
||||
byPlayer[playerName] = playerEntries
|
||||
order[#order + 1] = playerName
|
||||
unitByPlayer[playerName] = previewUnit.unitId
|
||||
end
|
||||
end
|
||||
|
||||
return byPlayer, order, unitByPlayer, #order > 0
|
||||
end
|
||||
|
||||
local function SortTrackers(trackers)
|
||||
table.sort(trackers, function(a, b)
|
||||
local aId = tonumber(a and a.id) or 0
|
||||
local bId = tonumber(b and b.id) or 0
|
||||
if aId ~= bId then
|
||||
return aId < bId
|
||||
end
|
||||
return tostring(a and a.name or "") < tostring(b and b.name or "")
|
||||
end)
|
||||
return trackers
|
||||
end
|
||||
|
||||
local function BuildTrackerCacheSignature(trackers)
|
||||
local parts = { tostring(#(trackers or {})) }
|
||||
for index, tracker in ipairs(trackers or {}) do
|
||||
parts[#parts + 1] = tostring(index)
|
||||
parts[#parts + 1] = tostring(tonumber(tracker and tracker.id) or 0)
|
||||
parts[#parts + 1] = tostring(tracker and tracker.trackerType or "normal")
|
||||
parts[#parts + 1] = tostring(tracker and tracker.enabled)
|
||||
parts[#parts + 1] = tostring(tracker and tracker.name or "")
|
||||
end
|
||||
return table.concat(parts, "|")
|
||||
end
|
||||
|
||||
local function BuildNormalDisplaySignature(frameShown, entries)
|
||||
local parts = { frameShown and "1" or "0", tostring(#(entries or {})) }
|
||||
for index, entry in ipairs(entries or {}) do
|
||||
parts[#parts + 1] = tostring(index)
|
||||
parts[#parts + 1] = tostring(entry and entry.playerName or "")
|
||||
parts[#parts + 1] = tostring(entry and entry.spellEntry and entry.spellEntry.spellId or 0)
|
||||
end
|
||||
return table.concat(parts, "|")
|
||||
end
|
||||
|
||||
local function BuildGroupDisplaySignature(order, byPlayer)
|
||||
local parts = { tostring(#(order or {})) }
|
||||
for _, playerName in ipairs(order or {}) do
|
||||
parts[#parts + 1] = tostring(playerName)
|
||||
parts[#parts + 1] = tostring(#((byPlayer and byPlayer[playerName]) or {}))
|
||||
end
|
||||
return table.concat(parts, "|")
|
||||
end
|
||||
|
||||
Manager._shared.GetTrackerFrameKey = GetTrackerFrameKey
|
||||
Manager._shared.GetTrackerFrameName = GetTrackerFrameName
|
||||
Manager._shared.GetTrackerPlayerFrameName = GetTrackerPlayerFrameName
|
||||
Manager._shared.ShortName = ShortName
|
||||
Manager._shared.BuildAnchorLayoutSignature = BuildAnchorLayoutSignature
|
||||
Manager._shared.IsGroupTracker = IsGroupTracker
|
||||
Manager._shared.ResolveUnitAnchorFrame = ResolveUnitAnchorFrame
|
||||
Manager._shared.GetGroupPlayers = GetGroupPlayers
|
||||
Manager._shared.GetTrackerLabel = GetTrackerLabel
|
||||
Manager._shared.GetTrackerSpellPool = GetTrackerSpellPool
|
||||
Manager._shared.GetTrackerSpellsForPlayer = GetTrackerSpellsForPlayer
|
||||
Manager._shared.CollectEntriesForPlayer = CollectEntriesForPlayer
|
||||
Manager._shared.BuildPartyPreviewEntries = BuildPartyPreviewEntries
|
||||
Manager._shared.EntryNeedsVisualTicker = EntryNeedsVisualTicker
|
||||
Manager._shared.BuildGroupDisplaySignature = BuildGroupDisplaySignature
|
||||
|
||||
function Manager:GetTrackers()
|
||||
local profile = HMGT and HMGT.db and HMGT.db.profile
|
||||
local trackers = profile and profile.trackers or {}
|
||||
local signature = BuildTrackerCacheSignature(trackers)
|
||||
if self._trackerCache and self._trackerCacheSignature == signature then
|
||||
return self._trackerCache
|
||||
end
|
||||
local result = {}
|
||||
for _, tracker in ipairs(trackers) do
|
||||
result[#result + 1] = tracker
|
||||
end
|
||||
self._trackerCache = SortTrackers(result)
|
||||
self._trackerCacheSignature = signature
|
||||
return self._trackerCache
|
||||
end
|
||||
|
||||
function Manager:MarkTrackersDirty()
|
||||
self._trackerCache = nil
|
||||
self._trackerCacheSignature = nil
|
||||
self._layoutDirty = true
|
||||
end
|
||||
|
||||
function Manager:MarkLayoutDirty()
|
||||
self._layoutDirty = true
|
||||
end
|
||||
|
||||
function Manager:EnsureFrame(tracker)
|
||||
local frameKey = GetTrackerFrameKey(tracker.id)
|
||||
local frame = self.frames[frameKey]
|
||||
if not frame then
|
||||
frame = HMGT.TrackerFrame:CreateTrackerFrame(GetTrackerFrameName(tracker.id), tracker)
|
||||
frame._hmgtTrackerId = tonumber(tracker.id) or 0
|
||||
self.frames[frameKey] = frame
|
||||
end
|
||||
|
||||
frame._settings = tracker
|
||||
HMGT.TrackerFrame:SetTitle(frame, GetTrackerLabel(tracker))
|
||||
HMGT.TrackerFrame:SetLocked(frame, tracker.locked)
|
||||
return frame
|
||||
end
|
||||
|
||||
function Manager:GetAnchorFrame(tracker)
|
||||
if type(tracker) ~= "table" then
|
||||
return nil
|
||||
end
|
||||
|
||||
if IsGroupTracker(tracker) then
|
||||
local frameKey = GetTrackerFrameKey(tracker.id)
|
||||
local order = self.activeOrders[frameKey] or {}
|
||||
local frames = self.perPlayerFrames[frameKey] or {}
|
||||
if order[1] and frames[order[1]] and frames[order[1]]:IsShown() then
|
||||
return frames[order[1]]
|
||||
end
|
||||
end
|
||||
|
||||
return self:EnsureFrame(tracker)
|
||||
end
|
||||
|
||||
function Manager:StopVisualTicker()
|
||||
if self.visualTicker then
|
||||
self.visualTicker:Cancel()
|
||||
self.visualTicker = nil
|
||||
end
|
||||
end
|
||||
|
||||
function Manager:SetVisualTickerEnabled(enabled)
|
||||
if enabled then
|
||||
if not self.visualTicker then
|
||||
self.visualTicker = C_Timer.NewTicker(0.1, function()
|
||||
self:RefreshVisibleVisuals()
|
||||
end)
|
||||
end
|
||||
else
|
||||
self:StopVisualTicker()
|
||||
end
|
||||
end
|
||||
|
||||
function Manager:RefreshAnchors(force)
|
||||
for _, tracker in ipairs(self:GetTrackers()) do
|
||||
local frameKey = GetTrackerFrameKey(tracker.id)
|
||||
if IsGroupTracker(tracker) then
|
||||
local anchorFrame = self.frames[frameKey]
|
||||
if anchorFrame and not anchorFrame._hmgtDragging then
|
||||
HMGT.TrackerFrame:ApplyAnchor(anchorFrame)
|
||||
end
|
||||
self:RefreshPerGroupAnchors(tracker, force)
|
||||
else
|
||||
local frame = self.frames[frameKey]
|
||||
if frame and (force or frame:IsShown()) then
|
||||
if not frame._hmgtDragging then
|
||||
HMGT.TrackerFrame:ApplyAnchor(frame)
|
||||
end
|
||||
frame:EnableMouse(not tracker.locked)
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
function Manager:InvalidateAnchorLayout()
|
||||
self.anchorLayoutSignatures = {}
|
||||
self.nextAnchorRetryAt = {}
|
||||
self:MarkLayoutDirty()
|
||||
self:RefreshAnchors(true)
|
||||
end
|
||||
|
||||
function Manager:SetAllLocked(locked)
|
||||
for _, frame in pairs(self.frames) do
|
||||
HMGT.TrackerFrame:SetLocked(frame, locked)
|
||||
end
|
||||
for _, frameSet in pairs(self.perPlayerFrames) do
|
||||
for _, frame in pairs(frameSet) do
|
||||
HMGT.TrackerFrame:SetLocked(frame, locked)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
function Manager:GetAnchorableFrames()
|
||||
local frames = {}
|
||||
for _, tracker in ipairs(self:GetTrackers()) do
|
||||
local anchorKey = HMGT.GetTrackerAnchorKey and HMGT:GetTrackerAnchorKey(tracker.id) or nil
|
||||
local frame = self:GetAnchorFrame(tracker)
|
||||
if anchorKey and frame then
|
||||
frames[anchorKey] = frame
|
||||
end
|
||||
end
|
||||
return frames
|
||||
end
|
||||
|
||||
function Manager:Enable()
|
||||
self.enabled = true
|
||||
self:MarkTrackersDirty()
|
||||
self:UpdateDisplay()
|
||||
end
|
||||
|
||||
function Manager:Disable()
|
||||
self.enabled = false
|
||||
self:StopVisualTicker()
|
||||
self._layoutDirty = true
|
||||
for _, frame in pairs(self.frames) do
|
||||
frame:Hide()
|
||||
end
|
||||
for frameKey in pairs(self.perPlayerFrames) do
|
||||
self:HidePlayerFrames(frameKey)
|
||||
end
|
||||
end
|
||||
|
||||
function Manager:RefreshVisibleVisuals()
|
||||
if not self.enabled then
|
||||
self:StopVisualTicker()
|
||||
return
|
||||
end
|
||||
|
||||
local shouldTick = false
|
||||
local needsFullRefresh = false
|
||||
local totalEntries = 0
|
||||
|
||||
for _, tracker in ipairs(self:GetTrackers()) do
|
||||
local frameKey = GetTrackerFrameKey(tracker.id)
|
||||
if IsGroupTracker(tracker) then
|
||||
local currentOrder = self.activeOrders[frameKey] or {}
|
||||
if #currentOrder > 0 then
|
||||
local byPlayer, order, unitByPlayer, shouldShow = self:BuildEntriesByPlayerForTracker(tracker)
|
||||
if not shouldShow or #order ~= #currentOrder then
|
||||
needsFullRefresh = true
|
||||
else
|
||||
local byPlayerFiltered = {}
|
||||
for index, playerName in ipairs(order) do
|
||||
if playerName ~= currentOrder[index] then
|
||||
needsFullRefresh = true
|
||||
break
|
||||
end
|
||||
local entries = byPlayer[playerName] or {}
|
||||
if HMGT.FilterDisplayEntries then
|
||||
entries = HMGT:FilterDisplayEntries(tracker, entries) or entries
|
||||
end
|
||||
if HMGT.SortDisplayEntries then
|
||||
HMGT:SortDisplayEntries(entries)
|
||||
end
|
||||
if #entries == 0 then
|
||||
needsFullRefresh = true
|
||||
break
|
||||
end
|
||||
local frame = self.perPlayerFrames[frameKey] and self.perPlayerFrames[frameKey][playerName]
|
||||
if not frame or not frame:IsShown() then
|
||||
needsFullRefresh = true
|
||||
break
|
||||
end
|
||||
HMGT.TrackerFrame:UpdateFrame(frame, entries, true)
|
||||
totalEntries = totalEntries + #entries
|
||||
byPlayerFiltered[playerName] = entries
|
||||
for _, entry in ipairs(entries) do
|
||||
if EntryNeedsVisualTicker(entry) then
|
||||
shouldTick = true
|
||||
break
|
||||
end
|
||||
end
|
||||
end
|
||||
local newSignature = BuildGroupDisplaySignature(currentOrder, byPlayerFiltered)
|
||||
if self._displaySignatures[frameKey] ~= newSignature then
|
||||
needsFullRefresh = true
|
||||
end
|
||||
end
|
||||
end
|
||||
else
|
||||
local frame = self.frames[frameKey]
|
||||
if frame and frame:IsShown() then
|
||||
local entries, shouldShow = self:BuildEntriesForTracker(tracker)
|
||||
if not shouldShow then
|
||||
needsFullRefresh = true
|
||||
else
|
||||
if HMGT.FilterDisplayEntries then
|
||||
entries = HMGT:FilterDisplayEntries(tracker, entries) or entries
|
||||
end
|
||||
if HMGT.SortDisplayEntries then
|
||||
HMGT:SortDisplayEntries(entries)
|
||||
end
|
||||
if #entries == 0 then
|
||||
needsFullRefresh = true
|
||||
else
|
||||
HMGT.TrackerFrame:UpdateFrame(frame, entries, true)
|
||||
totalEntries = totalEntries + #entries
|
||||
local newSignature = BuildNormalDisplaySignature(true, entries)
|
||||
if self._displaySignatures[frameKey] ~= newSignature then
|
||||
needsFullRefresh = true
|
||||
end
|
||||
for _, entry in ipairs(entries) do
|
||||
if EntryNeedsVisualTicker(entry) then
|
||||
shouldTick = true
|
||||
break
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
self.lastEntryCount = totalEntries
|
||||
self:SetVisualTickerEnabled(shouldTick)
|
||||
if needsFullRefresh then
|
||||
HMGT:TriggerTrackerUpdate("layout")
|
||||
end
|
||||
end
|
||||
|
||||
function Manager:UpdateDisplay()
|
||||
if not self.enabled then
|
||||
self:StopVisualTicker()
|
||||
return
|
||||
end
|
||||
|
||||
local trackers = self:GetTrackers()
|
||||
local activeFrames = {}
|
||||
local shouldTick = false
|
||||
local totalEntries = 0
|
||||
local layoutDirty = self._layoutDirty == true
|
||||
|
||||
for _, tracker in ipairs(trackers) do
|
||||
local frameKey = GetTrackerFrameKey(tracker.id)
|
||||
local frame = self:EnsureFrame(tracker)
|
||||
|
||||
if IsGroupTracker(tracker) then
|
||||
frame:Hide()
|
||||
local shown, entryCount, trackerShouldTick = self:UpdatePerGroupMemberTracker(tracker)
|
||||
totalEntries = totalEntries + (entryCount or 0)
|
||||
if trackerShouldTick then
|
||||
shouldTick = true
|
||||
end
|
||||
if not shown then
|
||||
self:HidePlayerFrames(frameKey)
|
||||
end
|
||||
else
|
||||
self:HidePlayerFrames(frameKey)
|
||||
local entries, shouldShow = self:BuildEntriesForTracker(tracker)
|
||||
|
||||
if shouldShow then
|
||||
if HMGT.FilterDisplayEntries then
|
||||
entries = HMGT:FilterDisplayEntries(tracker, entries) or entries
|
||||
end
|
||||
if HMGT.SortDisplayEntries then
|
||||
HMGT:SortDisplayEntries(entries)
|
||||
end
|
||||
|
||||
HMGT.TrackerFrame:UpdateFrame(frame, entries, true)
|
||||
frame:Show()
|
||||
frame:EnableMouse(not tracker.locked)
|
||||
activeFrames[frameKey] = true
|
||||
totalEntries = totalEntries + #entries
|
||||
local newSignature = BuildNormalDisplaySignature(true, entries)
|
||||
if self._displaySignatures[frameKey] ~= newSignature then
|
||||
self._displaySignatures[frameKey] = newSignature
|
||||
layoutDirty = true
|
||||
end
|
||||
|
||||
for _, entry in ipairs(entries) do
|
||||
if EntryNeedsVisualTicker(entry) then
|
||||
shouldTick = true
|
||||
break
|
||||
end
|
||||
end
|
||||
else
|
||||
frame:Hide()
|
||||
if self._displaySignatures[frameKey] ~= "0" then
|
||||
self._displaySignatures[frameKey] = "0"
|
||||
layoutDirty = true
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
for frameKey, frame in pairs(self.frames) do
|
||||
if not activeFrames[frameKey] then
|
||||
if frame:IsShown() then
|
||||
layoutDirty = true
|
||||
end
|
||||
frame:Hide()
|
||||
end
|
||||
end
|
||||
|
||||
self.lastEntryCount = totalEntries
|
||||
self:SetVisualTickerEnabled(shouldTick)
|
||||
if layoutDirty then
|
||||
self._layoutDirty = false
|
||||
self:RefreshAnchors()
|
||||
end
|
||||
end
|
||||
2175
Modules/Tracker/TrackerOptions.lua
Normal file
2175
Modules/Tracker/TrackerOptions.lua
Normal file
File diff suppressed because it is too large
Load Diff
Reference in New Issue
Block a user