dev-v.2.1.0 #10
File diff suppressed because it is too large
Load Diff
@@ -31,7 +31,17 @@ HailMaryGuildToolsOptions.lua
|
|||||||
# ────── Tracker ──────────────────────────────────────────────────────
|
# ────── Tracker ──────────────────────────────────────────────────────
|
||||||
Modules\Tracker\Frame.lua
|
Modules\Tracker\Frame.lua
|
||||||
Modules\Tracker\SpellDatabase.lua
|
Modules\Tracker\SpellDatabase.lua
|
||||||
Modules\Tracker\SingleFrameTrackerBase.lua
|
Modules\Tracker\TrackerCore.lua
|
||||||
|
Modules\Tracker\TrackerState.lua
|
||||||
|
Modules\Tracker\TrackerPlayerState.lua
|
||||||
|
Modules\Tracker\TrackerBridge.lua
|
||||||
|
Modules\Tracker\TrackerDataProvider.lua
|
||||||
|
Modules\Tracker\TrackerSync.lua
|
||||||
|
Modules\Tracker\TrackerAvailability.lua
|
||||||
|
Modules\Tracker\TrackerDetection.lua
|
||||||
|
Modules\Tracker\InterruptTracker\InterruptTracker.lua
|
||||||
|
Modules\Tracker\RaidCooldownTracker\RaidCooldownTracker.lua
|
||||||
|
Modules\Tracker\GroupCooldownTracker\GroupCooldownTracker.lua
|
||||||
|
|
||||||
Modules\Tracker\InterruptTracker\InterruptSpellDatabase.lua
|
Modules\Tracker\InterruptTracker\InterruptSpellDatabase.lua
|
||||||
Modules\Tracker\RaidcooldownTracker\RaidCooldownSpellDatabase.lua
|
Modules\Tracker\RaidcooldownTracker\RaidCooldownSpellDatabase.lua
|
||||||
|
|||||||
@@ -1,692 +1,65 @@
|
|||||||
-- Modules/GroupCooldownTracker.lua
|
|
||||||
-- Group-Cooldown-Tracker Modul (ein Frame pro Spieler in der Gruppe)
|
|
||||||
|
|
||||||
local ADDON_NAME = "HailMaryGuildTools"
|
local ADDON_NAME = "HailMaryGuildTools"
|
||||||
local HMGT = LibStub("AceAddon-3.0"):GetAddon(ADDON_NAME)
|
local HMGT = LibStub("AceAddon-3.0"):GetAddon(ADDON_NAME)
|
||||||
if not HMGT then return end
|
if not HMGT then return end
|
||||||
|
|
||||||
local L = LibStub("AceLocale-3.0"):GetLocale(ADDON_NAME)
|
local L = LibStub("AceLocale-3.0"):GetLocale(ADDON_NAME)
|
||||||
|
|
||||||
local GCT = HMGT:NewModule("GroupCooldownTracker")
|
local module = HMGT:NewModule("GroupCooldownTracker")
|
||||||
HMGT.GroupCooldownTracker = GCT
|
HMGT.GroupCooldownTracker = module
|
||||||
|
|
||||||
GCT.frame = nil
|
module.definition = {
|
||||||
GCT.frames = {}
|
moduleName = "GroupCooldownTracker",
|
||||||
|
dbKey = "groupCooldownTracker",
|
||||||
local function SanitizeFrameToken(name)
|
trackerType = "group",
|
||||||
if not name or name == "" then return "Unknown" end
|
trackerKey = "groupCooldownTracker",
|
||||||
return name:gsub("[^%w_]", "_")
|
title = function()
|
||||||
end
|
return L["GCD_TITLE"]
|
||||||
|
end,
|
||||||
local function ShortName(name)
|
categories = { "tank", "defensive", "healing", "cc", "utility", "offensive", "lust", "interrupt" },
|
||||||
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 = {
|
function module:GetDefinition()
|
||||||
"PartyMemberFrame%d", -- Blizzard alt
|
return self.definition
|
||||||
"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
|
end
|
||||||
|
|
||||||
local function BuildAnchorLayoutSignature(settings, ordered, unitByPlayer)
|
function module:GetSettings()
|
||||||
local parts = {
|
local profile = HMGT.db and HMGT.db.profile
|
||||||
settings.attachToPartyFrame == true and "attach" or "stack",
|
return profile and profile[self.definition.dbKey] or nil
|
||||||
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
|
end
|
||||||
|
|
||||||
local function ResolveNamedUnitFrame(unitId)
|
function module:Enable()
|
||||||
if unitId == "player" then
|
if HMGT.TrackerManager and HMGT.TrackerManager.Enable then
|
||||||
for _, frameName in ipairs(PLAYER_FRAME_CANDIDATES) do
|
HMGT.TrackerManager:Enable()
|
||||||
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
|
||||||
end
|
end
|
||||||
|
|
||||||
function GCT:EnsureUpdateTicker()
|
function module:Disable()
|
||||||
if self.updateTicker then
|
if HMGT.TrackerManager and HMGT.TrackerManager.UpdateDisplay then
|
||||||
return
|
HMGT.TrackerManager:UpdateDisplay()
|
||||||
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
|
||||||
end
|
end
|
||||||
|
|
||||||
function GCT:SetUpdateTickerEnabled(enabled)
|
function module:SetLockedAll(locked)
|
||||||
if enabled then
|
if HMGT.TrackerManager and HMGT.TrackerManager.SetAllLocked then
|
||||||
self:EnsureUpdateTicker()
|
HMGT.TrackerManager:SetAllLocked(locked)
|
||||||
else
|
|
||||||
self:StopUpdateTicker()
|
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
function GCT:InvalidateAnchorLayout()
|
function module:RefreshAnchors(force)
|
||||||
self._lastAnchorLayoutSignature = nil
|
if HMGT.TrackerManager and HMGT.TrackerManager.RefreshAnchors then
|
||||||
self._nextAnchorRetryAt = nil
|
HMGT.TrackerManager:RefreshAnchors(force)
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
function GCT:RefreshAnchors(force)
|
function module:InvalidateAnchorLayout()
|
||||||
local s = HMGT.db.profile.groupCooldownTracker
|
if HMGT.TrackerManager and HMGT.TrackerManager.InvalidateAnchorLayout then
|
||||||
if not s then return end
|
HMGT.TrackerManager:InvalidateAnchorLayout()
|
||||||
|
|
||||||
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
|
||||||
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
|
end
|
||||||
|
|
||||||
-- ============================================================
|
function module:GetAnchorableFrames()
|
||||||
-- ENABLE / DISABLE
|
if HMGT.TrackerManager and HMGT.TrackerManager.GetAnchorableFrames then
|
||||||
-- ============================================================
|
return HMGT.TrackerManager:GetAnchorableFrames()
|
||||||
|
end
|
||||||
function GCT:Enable()
|
return {}
|
||||||
local s = HMGT.db.profile.groupCooldownTracker
|
|
||||||
if not s.enabled and not s.demoMode and not s.testMode then return end
|
|
||||||
|
|
||||||
self:UpdateDisplay()
|
|
||||||
end
|
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
|
|
||||||
|
|
||||||
|
|||||||
@@ -42,54 +42,13 @@ function Manager:HidePlayerFrames(frameKey)
|
|||||||
end
|
end
|
||||||
|
|
||||||
function Manager:BuildEntriesByPlayerForTracker(tracker)
|
function Manager:BuildEntriesByPlayerForTracker(tracker)
|
||||||
local frameKey = S.GetTrackerFrameKey(tracker.id)
|
return HMGT:BuildEntriesByPlayerForTracker(
|
||||||
local ownName = HMGT:NormalizePlayerName(UnitName("player")) or "Player"
|
tracker,
|
||||||
if tracker.testMode then
|
self:GetTrackerFrameKey(tracker),
|
||||||
local entries = self:CollectTestEntries(tracker)
|
function(unitId)
|
||||||
if S.IsGroupTracker(tracker) and tracker.attachToPartyFrame == true then
|
return S.ResolveUnitAnchorFrame(unitId)
|
||||||
return S.BuildPartyPreviewEntries(entries)
|
|
||||||
end
|
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
|
end
|
||||||
|
|
||||||
function Manager:RefreshPerGroupAnchors(tracker, force)
|
function Manager:RefreshPerGroupAnchors(tracker, force)
|
||||||
@@ -206,11 +165,10 @@ function Manager:UpdatePerGroupMemberTracker(tracker)
|
|||||||
for _, playerName in ipairs(order) do
|
for _, playerName in ipairs(order) do
|
||||||
local frame = self:EnsurePlayerFrame(tracker, playerName)
|
local frame = self:EnsurePlayerFrame(tracker, playerName)
|
||||||
local entries = byPlayer[playerName] or {}
|
local entries = byPlayer[playerName] or {}
|
||||||
if HMGT.FilterDisplayEntries then
|
local tickThis = false
|
||||||
entries = HMGT:FilterDisplayEntries(tracker, entries) or entries
|
entries, tickThis = HMGT:FinalizeTrackerEntries(tracker, entries, tracker.trackerKey or nil)
|
||||||
end
|
if tickThis then
|
||||||
if HMGT.SortDisplayEntries then
|
shouldTick = true
|
||||||
HMGT:SortDisplayEntries(entries)
|
|
||||||
end
|
end
|
||||||
if #entries > 0 then
|
if #entries > 0 then
|
||||||
HMGT.TrackerFrame:UpdateFrame(frame, entries, true)
|
HMGT.TrackerFrame:UpdateFrame(frame, entries, true)
|
||||||
@@ -219,12 +177,6 @@ function Manager:UpdatePerGroupMemberTracker(tracker)
|
|||||||
shownOrder[#shownOrder + 1] = playerName
|
shownOrder[#shownOrder + 1] = playerName
|
||||||
shownByPlayer[playerName] = entries
|
shownByPlayer[playerName] = entries
|
||||||
entryCount = entryCount + #entries
|
entryCount = entryCount + #entries
|
||||||
for _, entry in ipairs(entries) do
|
|
||||||
if S.EntryNeedsVisualTicker(entry) then
|
|
||||||
shouldTick = true
|
|
||||||
break
|
|
||||||
end
|
|
||||||
end
|
|
||||||
else
|
else
|
||||||
frame:Hide()
|
frame:Hide()
|
||||||
end
|
end
|
||||||
|
|||||||
@@ -1,21 +1,40 @@
|
|||||||
-- Modules/InterruptTracker.lua
|
|
||||||
-- Interrupt tracker based on the shared single-frame tracker base.
|
|
||||||
|
|
||||||
local ADDON_NAME = "HailMaryGuildTools"
|
local ADDON_NAME = "HailMaryGuildTools"
|
||||||
local HMGT = LibStub("AceAddon-3.0"):GetAddon(ADDON_NAME)
|
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 L = LibStub("AceLocale-3.0"):GetLocale(ADDON_NAME)
|
||||||
|
|
||||||
local Base = HMGT.SingleFrameTrackerBase
|
local module = HMGT:NewModule("InterruptTracker")
|
||||||
if not Base then return end
|
HMGT.InterruptTracker = module
|
||||||
|
|
||||||
Base:CreateModule("InterruptTracker", {
|
module.definition = {
|
||||||
profileKey = "interruptTracker",
|
moduleName = "InterruptTracker",
|
||||||
frameName = "InterruptTracker",
|
dbKey = "interruptTracker",
|
||||||
|
trackerType = "normal",
|
||||||
|
trackerKey = "interruptTracker",
|
||||||
title = function()
|
title = function()
|
||||||
return L["IT_TITLE"]
|
return L["IT_TITLE"]
|
||||||
end,
|
end,
|
||||||
demoKey = "interruptTracker",
|
categories = { "interrupt" },
|
||||||
database = function()
|
}
|
||||||
return HMGT_SpellData.Interrupts
|
|
||||||
end,
|
function module:GetDefinition()
|
||||||
})
|
return self.definition
|
||||||
|
end
|
||||||
|
|
||||||
|
function module:GetSettings()
|
||||||
|
local profile = HMGT.db and HMGT.db.profile
|
||||||
|
return profile and profile[self.definition.dbKey] or nil
|
||||||
|
end
|
||||||
|
|
||||||
|
function module:Enable()
|
||||||
|
if HMGT.TrackerManager and HMGT.TrackerManager.Enable then
|
||||||
|
HMGT.TrackerManager:Enable()
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
function module:Disable()
|
||||||
|
if HMGT.TrackerManager and HMGT.TrackerManager.UpdateDisplay then
|
||||||
|
HMGT.TrackerManager:UpdateDisplay()
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|||||||
@@ -3,66 +3,15 @@ local HMGT = LibStub("AceAddon-3.0"):GetAddon(ADDON_NAME)
|
|||||||
if not HMGT or not HMGT.TrackerManager then return end
|
if not HMGT or not HMGT.TrackerManager then return end
|
||||||
|
|
||||||
local Manager = HMGT.TrackerManager
|
local Manager = HMGT.TrackerManager
|
||||||
local S = Manager._shared or {}
|
|
||||||
|
|
||||||
function Manager:CollectEntries(tracker)
|
function Manager:CollectEntries(tracker)
|
||||||
local entries = {}
|
return HMGT:CollectTrackerEntries(tracker)
|
||||||
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
|
end
|
||||||
|
|
||||||
function Manager:CollectTestEntries(tracker)
|
function Manager:CollectTestEntries(tracker)
|
||||||
local playerName = HMGT:NormalizePlayerName(UnitName("player")) or "Player"
|
return HMGT:CollectTrackerTestEntries(tracker)
|
||||||
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
|
end
|
||||||
|
|
||||||
function Manager:BuildEntriesForTracker(tracker)
|
function Manager:BuildEntriesForTracker(tracker)
|
||||||
if tracker.testMode then
|
return HMGT:BuildEntriesForTracker(tracker, self:GetTrackerFrameKey(tracker))
|
||||||
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
|
end
|
||||||
|
|||||||
@@ -1,21 +1,40 @@
|
|||||||
-- Modules/RaidCooldownTracker.lua
|
|
||||||
-- Raid cooldown tracker based on the shared single-frame tracker base.
|
|
||||||
|
|
||||||
local ADDON_NAME = "HailMaryGuildTools"
|
local ADDON_NAME = "HailMaryGuildTools"
|
||||||
local HMGT = LibStub("AceAddon-3.0"):GetAddon(ADDON_NAME)
|
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 L = LibStub("AceLocale-3.0"):GetLocale(ADDON_NAME)
|
||||||
|
|
||||||
local Base = HMGT.SingleFrameTrackerBase
|
local module = HMGT:NewModule("RaidCooldownTracker")
|
||||||
if not Base then return end
|
HMGT.RaidCooldownTracker = module
|
||||||
|
|
||||||
Base:CreateModule("RaidCooldownTracker", {
|
module.definition = {
|
||||||
profileKey = "raidCooldownTracker",
|
moduleName = "RaidCooldownTracker",
|
||||||
frameName = "RaidCooldownTracker",
|
dbKey = "raidCooldownTracker",
|
||||||
|
trackerType = "normal",
|
||||||
|
trackerKey = "raidCooldownTracker",
|
||||||
title = function()
|
title = function()
|
||||||
return L["RCD_TITLE"]
|
return L["RCD_TITLE"]
|
||||||
end,
|
end,
|
||||||
demoKey = "raidCooldownTracker",
|
categories = { "lust", "defensive", "healing", "tank", "utility", "offensive", "cc", "interrupt" },
|
||||||
database = function()
|
}
|
||||||
return HMGT_SpellData.RaidCooldowns
|
|
||||||
end,
|
function module:GetDefinition()
|
||||||
})
|
return self.definition
|
||||||
|
end
|
||||||
|
|
||||||
|
function module:GetSettings()
|
||||||
|
local profile = HMGT.db and HMGT.db.profile
|
||||||
|
return profile and profile[self.definition.dbKey] or nil
|
||||||
|
end
|
||||||
|
|
||||||
|
function module:Enable()
|
||||||
|
if HMGT.TrackerManager and HMGT.TrackerManager.Enable then
|
||||||
|
HMGT.TrackerManager:Enable()
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
function module:Disable()
|
||||||
|
if HMGT.TrackerManager and HMGT.TrackerManager.UpdateDisplay then
|
||||||
|
HMGT.TrackerManager:UpdateDisplay()
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|||||||
@@ -1,305 +0,0 @@
|
|||||||
-- 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
|
|
||||||
169
Modules/Tracker/TrackerAvailability.lua
Normal file
169
Modules/Tracker/TrackerAvailability.lua
Normal file
@@ -0,0 +1,169 @@
|
|||||||
|
local ADDON_NAME = "HailMaryGuildTools"
|
||||||
|
local HMGT = LibStub("AceAddon-3.0"):GetAddon(ADDON_NAME)
|
||||||
|
if not HMGT then return end
|
||||||
|
|
||||||
|
HMGT.TrackerAvailability = HMGT.TrackerAvailability or {}
|
||||||
|
|
||||||
|
local internals = HMGT.TrackerInternals or {}
|
||||||
|
local GetPlayerAuraApplications = internals.GetPlayerAuraApplications
|
||||||
|
local GetSpellCastCountInfo = internals.GetSpellCastCountInfo
|
||||||
|
|
||||||
|
function HMGT:GetOwnAvailabilityProgress(spellEntry)
|
||||||
|
local availability = self:GetAvailabilityConfig(spellEntry)
|
||||||
|
if not availability then
|
||||||
|
return nil, nil
|
||||||
|
end
|
||||||
|
|
||||||
|
local required = self:GetAvailabilityRequiredCount(spellEntry)
|
||||||
|
if required <= 0 then
|
||||||
|
return nil, nil
|
||||||
|
end
|
||||||
|
|
||||||
|
local current = 0
|
||||||
|
if availability.type == "auraStacks" then
|
||||||
|
current = GetPlayerAuraApplications and GetPlayerAuraApplications(availability.auraSpellId) or 0
|
||||||
|
if current <= 0 then
|
||||||
|
local fallbackSpellId = tonumber(availability.fallbackSpellCountId)
|
||||||
|
or tonumber(availability.progressSpellId)
|
||||||
|
or tonumber(spellEntry and spellEntry.spellId)
|
||||||
|
if fallbackSpellId and fallbackSpellId > 0 and GetSpellCastCountInfo then
|
||||||
|
current = GetSpellCastCountInfo(fallbackSpellId)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
else
|
||||||
|
return nil, nil
|
||||||
|
end
|
||||||
|
|
||||||
|
current = math.max(0, math.min(required, tonumber(current) or 0))
|
||||||
|
return current, required
|
||||||
|
end
|
||||||
|
|
||||||
|
function HMGT:GetAvailabilityState(playerName, spellId)
|
||||||
|
local state = self:GetAvailabilityStateEntry(playerName, spellId)
|
||||||
|
if not state then
|
||||||
|
return nil, nil
|
||||||
|
end
|
||||||
|
return tonumber(state.current) or 0, tonumber(state.max) or 0
|
||||||
|
end
|
||||||
|
|
||||||
|
function HMGT:HasAvailabilityState(playerName, spellId)
|
||||||
|
local _, max = self:GetAvailabilityState(playerName, spellId)
|
||||||
|
return (tonumber(max) or 0) > 0
|
||||||
|
end
|
||||||
|
|
||||||
|
function HMGT:StoreAvailabilityState(playerName, spellId, current, max, spellEntry)
|
||||||
|
local normalizedName = self:NormalizePlayerName(playerName)
|
||||||
|
local sid = tonumber(spellId)
|
||||||
|
if not normalizedName or not sid or sid <= 0 then
|
||||||
|
return false
|
||||||
|
end
|
||||||
|
|
||||||
|
local maxCount = math.max(0, math.floor((tonumber(max) or 0) + 0.5))
|
||||||
|
if maxCount <= 0 then
|
||||||
|
return self:ClearAvailabilityState(normalizedName, sid)
|
||||||
|
end
|
||||||
|
|
||||||
|
local currentCount = math.max(0, math.min(maxCount, math.floor((tonumber(current) or 0) + 0.5)))
|
||||||
|
local previous = self:GetAvailabilityStateEntry(normalizedName, sid)
|
||||||
|
local changed = (not previous)
|
||||||
|
or (tonumber(previous.current) or -1) ~= currentCount
|
||||||
|
or (tonumber(previous.max) or -1) ~= maxCount
|
||||||
|
|
||||||
|
self:SetAvailabilityStateEntry(normalizedName, sid, {
|
||||||
|
current = currentCount,
|
||||||
|
max = maxCount,
|
||||||
|
spellEntry = spellEntry,
|
||||||
|
updatedAt = GetTime(),
|
||||||
|
})
|
||||||
|
|
||||||
|
return changed
|
||||||
|
end
|
||||||
|
|
||||||
|
function HMGT:RefreshOwnAvailabilitySpell(spellEntry)
|
||||||
|
if not self:IsAvailabilitySpell(spellEntry) then
|
||||||
|
return false
|
||||||
|
end
|
||||||
|
|
||||||
|
local playerName = self:NormalizePlayerName(UnitName("player"))
|
||||||
|
if not playerName then
|
||||||
|
return false
|
||||||
|
end
|
||||||
|
|
||||||
|
local current, max = self:GetOwnAvailabilityProgress(spellEntry)
|
||||||
|
if (tonumber(max) or 0) > 0 then
|
||||||
|
local pData = self.playerData[playerName]
|
||||||
|
if pData and type(pData.knownSpells) == "table" then
|
||||||
|
pData.knownSpells[tonumber(spellEntry.spellId)] = true
|
||||||
|
end
|
||||||
|
end
|
||||||
|
return self:StoreAvailabilityState(playerName, spellEntry.spellId, current, max, spellEntry)
|
||||||
|
end
|
||||||
|
|
||||||
|
function HMGT:RefreshOwnAvailabilityStates()
|
||||||
|
local playerName = self:NormalizePlayerName(UnitName("player"))
|
||||||
|
local pData = playerName and self.playerData[playerName]
|
||||||
|
if not pData or not pData.class or not pData.specIndex then
|
||||||
|
return false
|
||||||
|
end
|
||||||
|
|
||||||
|
local changed = false
|
||||||
|
local groupCooldowns = HMGT_SpellData.GetSpellsForSpec(pData.class, pData.specIndex, HMGT_SpellData.GroupCooldowns)
|
||||||
|
for _, spellEntry in ipairs(groupCooldowns or {}) do
|
||||||
|
if self:IsAvailabilitySpell(spellEntry) and self:RefreshOwnAvailabilitySpell(spellEntry) then
|
||||||
|
changed = true
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
if self:PruneAvailabilityStates(playerName, pData.knownSpells or {}) then
|
||||||
|
changed = true
|
||||||
|
end
|
||||||
|
|
||||||
|
return changed
|
||||||
|
end
|
||||||
|
|
||||||
|
function HMGT:RefreshAndPublishOwnAvailabilityStates()
|
||||||
|
local playerName = self:NormalizePlayerName(UnitName("player"))
|
||||||
|
local pData = playerName and self.playerData[playerName]
|
||||||
|
if not pData or not pData.class or not pData.specIndex then
|
||||||
|
return false
|
||||||
|
end
|
||||||
|
|
||||||
|
local changed = false
|
||||||
|
local groupCooldowns = HMGT_SpellData.GetSpellsForSpec(pData.class, pData.specIndex, HMGT_SpellData.GroupCooldowns)
|
||||||
|
for _, spellEntry in ipairs(groupCooldowns or {}) do
|
||||||
|
if self:IsAvailabilitySpell(spellEntry) and self:RefreshOwnAvailabilitySpell(spellEntry) then
|
||||||
|
self:PublishOwnSpellState(spellEntry.spellId, { sendLegacy = true })
|
||||||
|
changed = true
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
if self:PruneAvailabilityStates(playerName, pData.knownSpells or {}) then
|
||||||
|
changed = true
|
||||||
|
end
|
||||||
|
|
||||||
|
return changed
|
||||||
|
end
|
||||||
|
|
||||||
|
function HMGT:SendOwnAvailabilityStates(target)
|
||||||
|
local playerName = self:NormalizePlayerName(UnitName("player"))
|
||||||
|
local pData = playerName and self.playerData[playerName]
|
||||||
|
if not pData or not pData.class or not pData.specIndex then
|
||||||
|
return 0
|
||||||
|
end
|
||||||
|
|
||||||
|
self:RefreshOwnAvailabilityStates()
|
||||||
|
|
||||||
|
local sent = 0
|
||||||
|
local groupCooldowns = HMGT_SpellData.GetSpellsForSpec(pData.class, pData.specIndex, HMGT_SpellData.GroupCooldowns)
|
||||||
|
for _, spellEntry in ipairs(groupCooldowns or {}) do
|
||||||
|
if self:IsAvailabilitySpell(spellEntry) and self:IsTrackedSpellKnownForPlayer(playerName, spellEntry.spellId) then
|
||||||
|
local current, max = self:GetAvailabilityState(playerName, spellEntry.spellId)
|
||||||
|
if (tonumber(max) or 0) > 0 then
|
||||||
|
self:BroadcastAvailabilityState(spellEntry.spellId, current, max, target)
|
||||||
|
sent = sent + 1
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
return sent
|
||||||
|
end
|
||||||
186
Modules/Tracker/TrackerBridge.lua
Normal file
186
Modules/Tracker/TrackerBridge.lua
Normal file
@@ -0,0 +1,186 @@
|
|||||||
|
local ADDON_NAME = "HailMaryGuildTools"
|
||||||
|
local HMGT = LibStub("AceAddon-3.0"):GetAddon(ADDON_NAME)
|
||||||
|
if not HMGT then return end
|
||||||
|
|
||||||
|
HMGT.TrackerBridge = HMGT.TrackerBridge or {}
|
||||||
|
|
||||||
|
function HMGT:RegisterExternalAddonSource(sourceName)
|
||||||
|
local source = tostring(sourceName or "")
|
||||||
|
if source == "" then
|
||||||
|
return false
|
||||||
|
end
|
||||||
|
self.externalAddonSources = self.externalAddonSources or {}
|
||||||
|
self.externalAddonSources[source] = true
|
||||||
|
return true
|
||||||
|
end
|
||||||
|
|
||||||
|
function HMGT:GetCanonicalExternalSpellEntry(spellId)
|
||||||
|
local sid = tonumber(spellId)
|
||||||
|
if not sid or sid <= 0 or not HMGT_SpellData then
|
||||||
|
return nil, sid
|
||||||
|
end
|
||||||
|
|
||||||
|
local spellEntry = HMGT_SpellData.InterruptLookup and HMGT_SpellData.InterruptLookup[sid]
|
||||||
|
or HMGT_SpellData.CooldownLookup and HMGT_SpellData.CooldownLookup[sid]
|
||||||
|
if not spellEntry then
|
||||||
|
return nil, sid
|
||||||
|
end
|
||||||
|
|
||||||
|
return spellEntry, tonumber(spellEntry.spellId) or sid
|
||||||
|
end
|
||||||
|
|
||||||
|
function HMGT:InferClassFromSpellEntry(spellEntry)
|
||||||
|
if type(spellEntry) ~= "table" or type(spellEntry.classes) ~= "table" then
|
||||||
|
return nil
|
||||||
|
end
|
||||||
|
|
||||||
|
local foundClass
|
||||||
|
for key, value in pairs(spellEntry.classes) do
|
||||||
|
local classToken = type(value) == "string" and value or key
|
||||||
|
if foundClass and foundClass ~= classToken then
|
||||||
|
return nil
|
||||||
|
end
|
||||||
|
foundClass = classToken
|
||||||
|
end
|
||||||
|
return foundClass
|
||||||
|
end
|
||||||
|
|
||||||
|
function HMGT:ApplyExternalKnownSpell(sourceName, playerName, spellId, class, cooldown)
|
||||||
|
local source = tostring(sourceName or "External")
|
||||||
|
local normalizedName = self:NormalizePlayerName(playerName)
|
||||||
|
local sid = tonumber(spellId)
|
||||||
|
if not normalizedName or normalizedName == "" or not sid or sid <= 0 then
|
||||||
|
return false, "invalid_args"
|
||||||
|
end
|
||||||
|
if not self:IsPlayerInCurrentGroup(normalizedName) then
|
||||||
|
return false, "not_in_group"
|
||||||
|
end
|
||||||
|
|
||||||
|
local spellEntry, canonicalSid = self:GetCanonicalExternalSpellEntry(sid)
|
||||||
|
if not spellEntry or not canonicalSid or canonicalSid <= 0 then
|
||||||
|
return false, "unknown_spell"
|
||||||
|
end
|
||||||
|
sid = canonicalSid
|
||||||
|
|
||||||
|
self:RegisterExternalAddonSource(source)
|
||||||
|
local previous = self.playerData[normalizedName] or {}
|
||||||
|
local knownSpells = previous.knownSpells
|
||||||
|
if type(knownSpells) ~= "table" then
|
||||||
|
knownSpells = {}
|
||||||
|
end
|
||||||
|
knownSpells[sid] = true
|
||||||
|
|
||||||
|
local classToken = class or previous.class or self:InferClassFromSpellEntry(spellEntry)
|
||||||
|
|
||||||
|
self.playerData[normalizedName] = {
|
||||||
|
class = classToken,
|
||||||
|
specIndex = previous.specIndex,
|
||||||
|
talentHash = previous.talentHash,
|
||||||
|
talents = previous.talents or {},
|
||||||
|
knownSpells = knownSpells,
|
||||||
|
externalSource = source,
|
||||||
|
}
|
||||||
|
|
||||||
|
if tonumber(cooldown) and tonumber(cooldown) > 0 then
|
||||||
|
spellEntry._hmgtExternalBaseCd = tonumber(cooldown)
|
||||||
|
end
|
||||||
|
|
||||||
|
self:TriggerTrackerUpdate("trackers")
|
||||||
|
return true
|
||||||
|
end
|
||||||
|
|
||||||
|
function HMGT:ApplyExternalSpecInfo(sourceName, playerName, class, specId, talentHash)
|
||||||
|
local source = tostring(sourceName or "External")
|
||||||
|
local normalizedName = self:NormalizePlayerName(playerName)
|
||||||
|
local spec = tonumber(specId)
|
||||||
|
local classToken = class and tostring(class) or self:GetClassTokenForSpecId(spec)
|
||||||
|
if not normalizedName or normalizedName == "" or not classToken or classToken == "" or not spec or spec <= 0 then
|
||||||
|
return false, "invalid_args"
|
||||||
|
end
|
||||||
|
if not self:IsPlayerInCurrentGroup(normalizedName) then
|
||||||
|
return false, "not_in_group"
|
||||||
|
end
|
||||||
|
|
||||||
|
self:RegisterExternalAddonSource(source)
|
||||||
|
local previous = self.playerData[normalizedName] or {}
|
||||||
|
local knownSpells = previous.knownSpells
|
||||||
|
if type(knownSpells) ~= "table" then
|
||||||
|
knownSpells = {}
|
||||||
|
end
|
||||||
|
|
||||||
|
if HMGT_SpellData and type(HMGT_SpellData.GetSpellsForSpec) == "function" then
|
||||||
|
for _, datasetName in ipairs({ "Interrupts", "RaidCooldowns", "GroupCooldowns" }) do
|
||||||
|
local dataset = HMGT_SpellData[datasetName]
|
||||||
|
for _, spellEntry in ipairs(HMGT_SpellData.GetSpellsForSpec(classToken, spec, dataset)) do
|
||||||
|
local sid = tonumber(spellEntry and spellEntry.spellId)
|
||||||
|
if sid and sid > 0 then
|
||||||
|
knownSpells[sid] = true
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
self.playerData[normalizedName] = {
|
||||||
|
class = classToken,
|
||||||
|
specIndex = spec,
|
||||||
|
talentHash = talentHash or previous.talentHash,
|
||||||
|
talents = self:ParseTalentHash(talentHash or previous.talentHash),
|
||||||
|
knownSpells = knownSpells,
|
||||||
|
externalSource = source,
|
||||||
|
}
|
||||||
|
|
||||||
|
self:PruneAvailabilityStates(normalizedName, knownSpells)
|
||||||
|
self:TriggerTrackerUpdate("trackers")
|
||||||
|
return true
|
||||||
|
end
|
||||||
|
|
||||||
|
function HMGT:ApplyExternalCooldown(sourceName, playerName, spellId, cooldown)
|
||||||
|
local source = tostring(sourceName or "External")
|
||||||
|
local normalizedName = self:NormalizePlayerName(playerName)
|
||||||
|
local sid = tonumber(spellId)
|
||||||
|
local cd = tonumber(cooldown)
|
||||||
|
if not normalizedName or normalizedName == "" or not sid or sid <= 0 or not cd or cd <= 0 then
|
||||||
|
return false, "invalid_args"
|
||||||
|
end
|
||||||
|
if not self:IsPlayerInCurrentGroup(normalizedName) then
|
||||||
|
return false, "not_in_group"
|
||||||
|
end
|
||||||
|
|
||||||
|
local spellEntry, canonicalSid = self:GetCanonicalExternalSpellEntry(sid)
|
||||||
|
if not spellEntry or not canonicalSid or canonicalSid <= 0 then
|
||||||
|
return false, "unknown_spell"
|
||||||
|
end
|
||||||
|
sid = canonicalSid
|
||||||
|
|
||||||
|
self:RegisterExternalAddonSource(source)
|
||||||
|
self:ApplyExternalKnownSpell(source, normalizedName, sid, nil, cd)
|
||||||
|
self:HandleRemoteSpellCast(normalizedName, sid, GetServerTime(), nil, nil, nil, cd)
|
||||||
|
return true
|
||||||
|
end
|
||||||
|
|
||||||
|
function HMGT:IsPlayerInCurrentGroup(playerName)
|
||||||
|
local target = self:NormalizePlayerName(playerName)
|
||||||
|
if not target then return false end
|
||||||
|
local own = self:NormalizePlayerName(UnitName("player"))
|
||||||
|
if target == own then return true end
|
||||||
|
|
||||||
|
if IsInRaid() then
|
||||||
|
for i = 1, GetNumGroupMembers() do
|
||||||
|
local n = self:NormalizePlayerName(UnitName("raid" .. i))
|
||||||
|
if n == target then
|
||||||
|
return true
|
||||||
|
end
|
||||||
|
end
|
||||||
|
return false
|
||||||
|
end
|
||||||
|
|
||||||
|
if IsInGroup() then
|
||||||
|
for i = 1, GetNumSubgroupMembers() do
|
||||||
|
local n = self:NormalizePlayerName(UnitName("party" .. i))
|
||||||
|
if n == target then
|
||||||
|
return true
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
return false
|
||||||
|
end
|
||||||
404
Modules/Tracker/TrackerCore.lua
Normal file
404
Modules/Tracker/TrackerCore.lua
Normal file
@@ -0,0 +1,404 @@
|
|||||||
|
local ADDON_NAME = "HailMaryGuildTools"
|
||||||
|
local HMGT = LibStub("AceAddon-3.0"):GetAddon(ADDON_NAME)
|
||||||
|
if not HMGT then return end
|
||||||
|
|
||||||
|
HMGT.TrackerCore = HMGT.TrackerCore or {}
|
||||||
|
|
||||||
|
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 HMGT:IsGroupTrackerConfig(tracker)
|
||||||
|
return type(tracker) == "table" and tracker.trackerType == "group"
|
||||||
|
end
|
||||||
|
|
||||||
|
function HMGT:GetTrackerSpellPool(categories)
|
||||||
|
if HMGT_SpellData and type(HMGT_SpellData.GetSpellPoolForCategories) == "function" then
|
||||||
|
return HMGT_SpellData.GetSpellPoolForCategories(categories)
|
||||||
|
end
|
||||||
|
return {}
|
||||||
|
end
|
||||||
|
|
||||||
|
function HMGT:GetTrackerSpellsForPlayer(classToken, specIndex, categories)
|
||||||
|
if HMGT_SpellData and type(HMGT_SpellData.GetSpellsForCategories) == "function" then
|
||||||
|
return HMGT_SpellData.GetSpellsForCategories(classToken, specIndex, categories)
|
||||||
|
end
|
||||||
|
return {}
|
||||||
|
end
|
||||||
|
|
||||||
|
function HMGT:GetTrackerPlayers(tracker)
|
||||||
|
local players = {}
|
||||||
|
|
||||||
|
local ownName = self:NormalizePlayerName(UnitName("player"))
|
||||||
|
local ownClass = select(2, UnitClass("player"))
|
||||||
|
local includeOwnPlayer = true
|
||||||
|
if self:IsGroupTrackerConfig(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 = self: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 = self: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
|
||||||
|
|
||||||
|
function HMGT:CollectTrackerEntriesForPlayer(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 = self.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 = self:GetTrackerSpellsForPlayer(classToken, specIndex, tracker.categories)
|
||||||
|
for _, spellEntry in ipairs(spells) do
|
||||||
|
if tracker.enabledSpells[spellEntry.spellId] ~= false then
|
||||||
|
local remaining, total, currentCharges, maxCharges = self:GetCooldownInfo(playerName, spellEntry.spellId)
|
||||||
|
local effectiveCd = HMGT_SpellData.GetEffectiveCooldown(spellEntry, talents)
|
||||||
|
local isAvailabilitySpell = self:IsAvailabilitySpell(spellEntry)
|
||||||
|
local include = self:ShouldDisplayEntry(tracker, remaining, currentCharges, maxCharges, spellEntry)
|
||||||
|
local spellKnown = self: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 self: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
|
||||||
|
|
||||||
|
function HMGT:BuildPartyPreviewEntries(entries, resolveUnitAnchorFrame)
|
||||||
|
local byPlayer = {}
|
||||||
|
local order = {}
|
||||||
|
local unitByPlayer = {}
|
||||||
|
for index = 1, 4 do
|
||||||
|
local unitId = "party" .. index
|
||||||
|
if not resolveUnitAnchorFrame or resolveUnitAnchorFrame(unitId) then
|
||||||
|
local playerName = string.format("Party %d", index)
|
||||||
|
local playerEntries = CopyEntriesForPreview(entries, playerName)
|
||||||
|
if #playerEntries > 0 then
|
||||||
|
byPlayer[playerName] = playerEntries
|
||||||
|
order[#order + 1] = playerName
|
||||||
|
unitByPlayer[playerName] = unitId
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
return byPlayer, order, unitByPlayer, #order > 0
|
||||||
|
end
|
||||||
|
|
||||||
|
function HMGT:CollectTrackerEntries(tracker)
|
||||||
|
local entries = {}
|
||||||
|
local players = self:GetTrackerPlayers(tracker)
|
||||||
|
for _, playerInfo in ipairs(players) do
|
||||||
|
local playerEntries = self:CollectTrackerEntriesForPlayer(tracker, playerInfo)
|
||||||
|
for _, entry in ipairs(playerEntries) do
|
||||||
|
entries[#entries + 1] = entry
|
||||||
|
end
|
||||||
|
end
|
||||||
|
return entries
|
||||||
|
end
|
||||||
|
|
||||||
|
function HMGT:CollectTrackerTestEntries(tracker)
|
||||||
|
local playerName = self:NormalizePlayerName(UnitName("player")) or "Player"
|
||||||
|
local classToken = select(2, UnitClass("player"))
|
||||||
|
if not classToken then
|
||||||
|
return {}
|
||||||
|
end
|
||||||
|
|
||||||
|
local entries = {}
|
||||||
|
local pData = self.playerData[playerName]
|
||||||
|
local talents = pData and pData.talents or {}
|
||||||
|
local spells = self: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 = self:GetCooldownInfo(playerName, spellEntry.spellId)
|
||||||
|
local effectiveCd = HMGT_SpellData.GetEffectiveCooldown(spellEntry, talents)
|
||||||
|
local isAvailabilitySpell = self:IsAvailabilitySpell(spellEntry)
|
||||||
|
local spellKnown = self: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 self: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 HMGT:BuildEntriesForTracker(tracker, trackerKey)
|
||||||
|
local key = trackerKey or tostring(tonumber(tracker and tracker.id) or 0)
|
||||||
|
if tracker and tracker.testMode then
|
||||||
|
return self:CollectTrackerTestEntries(tracker), true
|
||||||
|
end
|
||||||
|
if tracker and tracker.demoMode then
|
||||||
|
return self:GetDemoEntries(key, self:GetTrackerSpellPool(tracker.categories), tracker), true
|
||||||
|
end
|
||||||
|
if not tracker or not tracker.enabled or not self:IsVisibleForCurrentGroup(tracker) then
|
||||||
|
return {}, false
|
||||||
|
end
|
||||||
|
return self:CollectTrackerEntries(tracker), true
|
||||||
|
end
|
||||||
|
|
||||||
|
function HMGT:BuildEntriesByPlayerForTracker(tracker, trackerKey, resolveUnitAnchorFrame)
|
||||||
|
local key = trackerKey or tostring(tonumber(tracker and tracker.id) or 0)
|
||||||
|
local ownName = self:NormalizePlayerName(UnitName("player")) or "Player"
|
||||||
|
if tracker.testMode then
|
||||||
|
local entries = self:CollectTrackerTestEntries(tracker)
|
||||||
|
if self:IsGroupTrackerConfig(tracker) and tracker.attachToPartyFrame == true then
|
||||||
|
return self:BuildPartyPreviewEntries(entries, resolveUnitAnchorFrame)
|
||||||
|
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 = self:GetDemoEntries(key, self:GetTrackerSpellPool(tracker.categories), tracker)
|
||||||
|
if self:IsGroupTrackerConfig(tracker) and tracker.attachToPartyFrame == true then
|
||||||
|
return self:BuildPartyPreviewEntries(entries, resolveUnitAnchorFrame)
|
||||||
|
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 self:IsVisibleForCurrentGroup(tracker) then
|
||||||
|
return {}, {}, {}, false
|
||||||
|
end
|
||||||
|
if IsInRaid() or not IsInGroup() then
|
||||||
|
return {}, {}, {}, false
|
||||||
|
end
|
||||||
|
local byPlayer, order, unitByPlayer = {}, {}, {}
|
||||||
|
for _, playerInfo in ipairs(self:GetTrackerPlayers(tracker)) do
|
||||||
|
local entries = self:CollectTrackerEntriesForPlayer(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 HMGT:FinalizeTrackerEntries(tracker, entries, trackerKey)
|
||||||
|
local result = entries or {}
|
||||||
|
if self.FilterDisplayEntries then
|
||||||
|
result = self:FilterDisplayEntries(tracker, result) or result
|
||||||
|
end
|
||||||
|
if self.SortDisplayEntries then
|
||||||
|
self:SortDisplayEntries(result, trackerKey)
|
||||||
|
end
|
||||||
|
|
||||||
|
local shouldTick = false
|
||||||
|
for _, entry in ipairs(result) do
|
||||||
|
if EntryNeedsVisualTicker(entry) then
|
||||||
|
shouldTick = true
|
||||||
|
break
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
return result, shouldTick
|
||||||
|
end
|
||||||
|
|
||||||
|
function HMGT:TriggerTrackerUpdate(reason)
|
||||||
|
local function normalizeReason(value)
|
||||||
|
if value == true then
|
||||||
|
return "trackers"
|
||||||
|
elseif value == "trackers" or value == "layout" or value == "visual" then
|
||||||
|
return value
|
||||||
|
end
|
||||||
|
return "full"
|
||||||
|
end
|
||||||
|
|
||||||
|
local function mergeReasons(current, incoming)
|
||||||
|
local priority = {
|
||||||
|
visual = 1,
|
||||||
|
layout = 2,
|
||||||
|
trackers = 3,
|
||||||
|
full = 4,
|
||||||
|
}
|
||||||
|
current = normalizeReason(current)
|
||||||
|
incoming = normalizeReason(incoming)
|
||||||
|
if (priority[incoming] or 4) >= (priority[current] or 4) then
|
||||||
|
return incoming
|
||||||
|
end
|
||||||
|
return current
|
||||||
|
end
|
||||||
|
|
||||||
|
self._trackerUpdateMinDelay = self._trackerUpdateMinDelay or 0.08
|
||||||
|
self._trackerUpdatePending = true
|
||||||
|
self._trackerUpdateReason = mergeReasons(self._trackerUpdateReason, reason)
|
||||||
|
if HMGT.TrackerManager then
|
||||||
|
local normalizedReason = normalizeReason(reason)
|
||||||
|
if normalizedReason == "trackers" then
|
||||||
|
HMGT.TrackerManager:MarkTrackersDirty()
|
||||||
|
elseif normalizedReason == "layout" then
|
||||||
|
HMGT.TrackerManager:MarkLayoutDirty()
|
||||||
|
end
|
||||||
|
end
|
||||||
|
if self._updateScheduled then return end
|
||||||
|
|
||||||
|
local now = GetTime()
|
||||||
|
local last = self._lastTrackerUpdateAt or 0
|
||||||
|
local delay = math.max(0, self._trackerUpdateMinDelay - (now - last))
|
||||||
|
self._updateScheduled = true
|
||||||
|
|
||||||
|
self:ScheduleTimer(function()
|
||||||
|
self._updateScheduled = nil
|
||||||
|
if not self._trackerUpdatePending then return end
|
||||||
|
self._trackerUpdatePending = nil
|
||||||
|
self._lastTrackerUpdateAt = GetTime()
|
||||||
|
local pendingReason = self._trackerUpdateReason
|
||||||
|
self._trackerUpdateReason = nil
|
||||||
|
|
||||||
|
local function profileModule(name, fn)
|
||||||
|
if not fn then return end
|
||||||
|
local t0 = debugprofilestop and debugprofilestop() or nil
|
||||||
|
fn()
|
||||||
|
local t1 = debugprofilestop and debugprofilestop() or nil
|
||||||
|
if t0 and t1 then
|
||||||
|
local mod = HMGT[name]
|
||||||
|
local count = mod and mod.lastEntryCount or 0
|
||||||
|
self:Debug("verbose", "UIUpdate %s took %.2fms entries=%s", tostring(name), t1 - t0, tostring(count))
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
profileModule("TrackerManager", HMGT.TrackerManager and function()
|
||||||
|
if pendingReason == "visual" and HMGT.TrackerManager.RefreshVisibleVisuals then
|
||||||
|
HMGT.TrackerManager:RefreshVisibleVisuals()
|
||||||
|
else
|
||||||
|
HMGT.TrackerManager:UpdateDisplay()
|
||||||
|
end
|
||||||
|
end or nil)
|
||||||
|
|
||||||
|
if self._trackerUpdatePending then
|
||||||
|
self:TriggerTrackerUpdate()
|
||||||
|
end
|
||||||
|
end, delay)
|
||||||
|
end
|
||||||
268
Modules/Tracker/TrackerDataProvider.lua
Normal file
268
Modules/Tracker/TrackerDataProvider.lua
Normal file
@@ -0,0 +1,268 @@
|
|||||||
|
local ADDON_NAME = "HailMaryGuildTools"
|
||||||
|
local HMGT = LibStub("AceAddon-3.0"):GetAddon(ADDON_NAME)
|
||||||
|
if not HMGT then return end
|
||||||
|
|
||||||
|
HMGT.TrackerDataProvider = HMGT.TrackerDataProvider or {}
|
||||||
|
|
||||||
|
local internals = HMGT.TrackerInternals or {}
|
||||||
|
local SafeApiNumber = internals.SafeApiNumber
|
||||||
|
local GetSpellChargesInfo = internals.GetSpellChargesInfo
|
||||||
|
local GetSpellCooldownInfo = internals.GetSpellCooldownInfo
|
||||||
|
|
||||||
|
function HMGT:GetCooldownInfo(playerName, spellId, opts)
|
||||||
|
opts = opts or {}
|
||||||
|
local deferUntilEmpty = opts.deferChargeCooldownUntilEmpty and true or false
|
||||||
|
local spellEntry = HMGT_SpellData.InterruptLookup[spellId]
|
||||||
|
or HMGT_SpellData.CooldownLookup[spellId]
|
||||||
|
local ownName = self:NormalizePlayerName(UnitName("player"))
|
||||||
|
local isOwnPlayer = playerName == ownName
|
||||||
|
local pData = isOwnPlayer and self.playerData[ownName] or nil
|
||||||
|
local talents = pData and pData.talents or {}
|
||||||
|
local effectiveCd = spellEntry and HMGT_SpellData.GetEffectiveCooldown(spellEntry, talents) or 0
|
||||||
|
local knownMaxCharges, knownChargeDuration = 0, 0
|
||||||
|
if spellEntry and isOwnPlayer then
|
||||||
|
knownMaxCharges, knownChargeDuration = self:GetKnownChargeInfo(spellEntry, talents, spellId, effectiveCd)
|
||||||
|
end
|
||||||
|
|
||||||
|
if self:IsAvailabilitySpell(spellEntry) then
|
||||||
|
local normalizedName = self:NormalizePlayerName(playerName)
|
||||||
|
if normalizedName == ownName then
|
||||||
|
local current, max = self:GetOwnAvailabilityProgress(spellEntry)
|
||||||
|
if (tonumber(max) or 0) > 0 then
|
||||||
|
self:StoreAvailabilityState(ownName, spellId, current, max, spellEntry)
|
||||||
|
return 0, 0, current, max
|
||||||
|
end
|
||||||
|
else
|
||||||
|
local current, max = self:GetAvailabilityState(normalizedName, spellId)
|
||||||
|
if (tonumber(max) or 0) > 0 then
|
||||||
|
return 0, 0, current, max
|
||||||
|
end
|
||||||
|
end
|
||||||
|
return 0, 0, nil, nil
|
||||||
|
end
|
||||||
|
|
||||||
|
local cdData = self:GetActiveCooldown(playerName, spellId)
|
||||||
|
|
||||||
|
if isOwnPlayer and not (InCombatLockdown and InCombatLockdown()) then
|
||||||
|
local charges, maxCharges, chargeStart, chargeDuration = nil, nil, nil, nil
|
||||||
|
if GetSpellChargesInfo then
|
||||||
|
charges, maxCharges, chargeStart, chargeDuration = GetSpellChargesInfo(spellId)
|
||||||
|
end
|
||||||
|
charges = SafeApiNumber and SafeApiNumber(charges, 0) or tonumber(charges) or 0
|
||||||
|
maxCharges = SafeApiNumber and SafeApiNumber(maxCharges, 0) or tonumber(maxCharges) or 0
|
||||||
|
chargeStart = SafeApiNumber and SafeApiNumber(chargeStart) or tonumber(chargeStart)
|
||||||
|
chargeDuration = SafeApiNumber and SafeApiNumber(chargeDuration, 0) or tonumber(chargeDuration) or 0
|
||||||
|
|
||||||
|
if maxCharges > 0 then
|
||||||
|
local tempChargeState = {
|
||||||
|
currentCharges = charges,
|
||||||
|
maxCharges = maxCharges,
|
||||||
|
chargeStart = chargeStart,
|
||||||
|
chargeDuration = chargeDuration,
|
||||||
|
duration = chargeDuration,
|
||||||
|
}
|
||||||
|
local remaining, total, curCharges, maxChargeCount = self:ResolveChargeState(tempChargeState)
|
||||||
|
self:StoreKnownChargeInfo(spellId, maxChargeCount, total > 0 and total or chargeDuration)
|
||||||
|
if (curCharges or 0) < maxChargeCount and remaining <= 0 and GetSpellCooldownInfo then
|
||||||
|
local cdStart, cdDuration = GetSpellCooldownInfo(spellId)
|
||||||
|
if cdDuration > 0 then
|
||||||
|
remaining = math.max(0, cdDuration - (GetTime() - cdStart))
|
||||||
|
total = math.max(total or 0, cdDuration)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
if deferUntilEmpty and (curCharges or 0) > 0 then
|
||||||
|
remaining = 0
|
||||||
|
end
|
||||||
|
return remaining, total, curCharges, maxChargeCount
|
||||||
|
end
|
||||||
|
|
||||||
|
if GetSpellCooldownInfo then
|
||||||
|
local cdStart, cdDuration = GetSpellCooldownInfo(spellId)
|
||||||
|
cdStart = tonumber(cdStart) or 0
|
||||||
|
cdDuration = tonumber(cdDuration) or 0
|
||||||
|
if cdDuration > 0 then
|
||||||
|
local remaining = math.max(0, cdDuration - (GetTime() - cdStart))
|
||||||
|
remaining = math.max(0, math.min(cdDuration, remaining))
|
||||||
|
if cdData and (tonumber(cdData.maxCharges) or 0) <= 0 then
|
||||||
|
local cachedRemaining = (tonumber(cdData.duration) or 0) - (GetTime() - (tonumber(cdData.startTime) or GetTime()))
|
||||||
|
cachedRemaining = math.max(0, math.min(tonumber(cdData.duration) or cachedRemaining, cachedRemaining))
|
||||||
|
local cachedDuration = math.max(0, tonumber(cdData.duration) or 0)
|
||||||
|
if cachedDuration > 2.0 and cachedRemaining > 2.0 and cdDuration < math.max(2.0, cachedDuration * 0.35) then
|
||||||
|
return cachedRemaining, cachedDuration, nil, nil
|
||||||
|
end
|
||||||
|
end
|
||||||
|
return remaining, cdDuration, nil, nil
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
if not cdData then
|
||||||
|
if isOwnPlayer and knownMaxCharges > 1 then
|
||||||
|
return 0, math.max(0, knownChargeDuration or effectiveCd or 0), knownMaxCharges, knownMaxCharges
|
||||||
|
end
|
||||||
|
return 0, 0, nil, nil
|
||||||
|
end
|
||||||
|
if (tonumber(cdData.maxCharges) or 0) > 0 then
|
||||||
|
local remaining, chargeDur, charges, maxCharges = self:ResolveChargeState(cdData)
|
||||||
|
self:StoreKnownChargeInfo(spellId, maxCharges, chargeDur)
|
||||||
|
if deferUntilEmpty and charges > 0 then
|
||||||
|
remaining = 0
|
||||||
|
end
|
||||||
|
return remaining, chargeDur, charges, maxCharges
|
||||||
|
end
|
||||||
|
if isOwnPlayer and knownMaxCharges > 1 then
|
||||||
|
local remaining = (tonumber(cdData.duration) or 0) - (GetTime() - (tonumber(cdData.startTime) or GetTime()))
|
||||||
|
remaining = math.max(0, math.min(tonumber(cdData.duration) or remaining, remaining))
|
||||||
|
local currentCharges = knownMaxCharges
|
||||||
|
if remaining > 0 then
|
||||||
|
currentCharges = math.max(0, knownMaxCharges - 1)
|
||||||
|
end
|
||||||
|
if deferUntilEmpty and currentCharges > 0 then
|
||||||
|
remaining = 0
|
||||||
|
end
|
||||||
|
return remaining, math.max(0, knownChargeDuration or effectiveCd or 0), currentCharges, knownMaxCharges
|
||||||
|
end
|
||||||
|
local remaining = cdData.duration - (GetTime() - cdData.startTime)
|
||||||
|
remaining = math.max(0, math.min(cdData.duration, remaining))
|
||||||
|
return remaining, cdData.duration, nil, nil
|
||||||
|
end
|
||||||
|
|
||||||
|
function HMGT:ShouldDisplayEntry(settings, remaining, currentCharges, maxCharges, spellEntry)
|
||||||
|
local rem = tonumber(remaining) or 0
|
||||||
|
local cur = tonumber(currentCharges) or 0
|
||||||
|
local max = tonumber(maxCharges) or 0
|
||||||
|
local soon = tonumber(settings.readySoonSec) or 0
|
||||||
|
local isAvailabilitySpell = spellEntry and self:IsAvailabilitySpell(spellEntry) or false
|
||||||
|
local isReady
|
||||||
|
|
||||||
|
if isAvailabilitySpell then
|
||||||
|
isReady = max > 0 and cur >= max
|
||||||
|
else
|
||||||
|
isReady = rem <= 0 or (max > 0 and cur > 0)
|
||||||
|
end
|
||||||
|
|
||||||
|
if settings.showOnlyReady then
|
||||||
|
return isReady
|
||||||
|
end
|
||||||
|
if soon > 0 then
|
||||||
|
if isAvailabilitySpell then
|
||||||
|
return isReady
|
||||||
|
end
|
||||||
|
return isReady or rem <= soon
|
||||||
|
end
|
||||||
|
return true
|
||||||
|
end
|
||||||
|
|
||||||
|
local DEFAULT_CATEGORY_PRIORITY = {
|
||||||
|
interrupt = 1,
|
||||||
|
lust = 2,
|
||||||
|
defensive = 3,
|
||||||
|
tank = 4,
|
||||||
|
healing = 5,
|
||||||
|
offensive = 6,
|
||||||
|
utility = 7,
|
||||||
|
cc = 8,
|
||||||
|
}
|
||||||
|
|
||||||
|
local TRACKER_CATEGORY_PRIORITY = {
|
||||||
|
interruptTracker = {
|
||||||
|
interrupt = 1,
|
||||||
|
defensive = 2,
|
||||||
|
utility = 3,
|
||||||
|
cc = 4,
|
||||||
|
healing = 5,
|
||||||
|
tank = 6,
|
||||||
|
offensive = 7,
|
||||||
|
lust = 8,
|
||||||
|
},
|
||||||
|
raidCooldownTracker = {
|
||||||
|
lust = 1,
|
||||||
|
defensive = 2,
|
||||||
|
healing = 3,
|
||||||
|
tank = 4,
|
||||||
|
utility = 5,
|
||||||
|
offensive = 6,
|
||||||
|
cc = 7,
|
||||||
|
interrupt = 8,
|
||||||
|
},
|
||||||
|
groupCooldownTracker = {
|
||||||
|
tank = 1,
|
||||||
|
defensive = 2,
|
||||||
|
healing = 3,
|
||||||
|
cc = 4,
|
||||||
|
utility = 5,
|
||||||
|
offensive = 6,
|
||||||
|
lust = 7,
|
||||||
|
interrupt = 8,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
local function GetCategoryPriority(category, trackerKey)
|
||||||
|
local cat = tostring(category or "utility")
|
||||||
|
local trackerOrder = trackerKey and TRACKER_CATEGORY_PRIORITY[trackerKey]
|
||||||
|
if trackerOrder and trackerOrder[cat] then
|
||||||
|
return trackerOrder[cat]
|
||||||
|
end
|
||||||
|
local order = HMGT_SpellData and HMGT_SpellData.CategoryOrder
|
||||||
|
if type(order) == "table" then
|
||||||
|
for idx, key in ipairs(order) do
|
||||||
|
if key == cat then
|
||||||
|
return idx
|
||||||
|
end
|
||||||
|
end
|
||||||
|
return #order + 10
|
||||||
|
end
|
||||||
|
return DEFAULT_CATEGORY_PRIORITY[cat] or 99
|
||||||
|
end
|
||||||
|
|
||||||
|
function HMGT:SortDisplayEntries(entries, trackerKey)
|
||||||
|
if type(entries) ~= "table" then return end
|
||||||
|
table.sort(entries, function(a, b)
|
||||||
|
local aRemaining = tonumber(a and a.remaining) or 0
|
||||||
|
local bRemaining = tonumber(b and b.remaining) or 0
|
||||||
|
local aActive = aRemaining > 0
|
||||||
|
local bActive = bRemaining > 0
|
||||||
|
if aActive ~= bActive then
|
||||||
|
return aActive
|
||||||
|
end
|
||||||
|
|
||||||
|
local aEntry = a and a.spellEntry
|
||||||
|
local bEntry = b and b.spellEntry
|
||||||
|
|
||||||
|
local aPriority = tonumber(aEntry and aEntry.priority) or GetCategoryPriority(aEntry and aEntry.category, trackerKey)
|
||||||
|
local bPriority = tonumber(bEntry and bEntry.priority) or GetCategoryPriority(bEntry and bEntry.category, trackerKey)
|
||||||
|
if aPriority ~= bPriority then
|
||||||
|
return aPriority < bPriority
|
||||||
|
end
|
||||||
|
|
||||||
|
if aActive and aRemaining ~= bRemaining then
|
||||||
|
return aRemaining < bRemaining
|
||||||
|
end
|
||||||
|
|
||||||
|
local aTotal = tonumber(a and a.total)
|
||||||
|
or tonumber(aEntry and HMGT_SpellData.GetBaseCooldown and HMGT_SpellData.GetBaseCooldown(aEntry))
|
||||||
|
or tonumber(aEntry and aEntry.cooldown)
|
||||||
|
or 0
|
||||||
|
local bTotal = tonumber(b and b.total)
|
||||||
|
or tonumber(bEntry and HMGT_SpellData.GetBaseCooldown and HMGT_SpellData.GetBaseCooldown(bEntry))
|
||||||
|
or tonumber(bEntry and bEntry.cooldown)
|
||||||
|
or 0
|
||||||
|
if (not aActive) and aTotal ~= bTotal then
|
||||||
|
return aTotal > bTotal
|
||||||
|
end
|
||||||
|
|
||||||
|
if aRemaining ~= bRemaining then
|
||||||
|
return aRemaining < bRemaining
|
||||||
|
end
|
||||||
|
|
||||||
|
local aName = tostring(a and a.playerName or "")
|
||||||
|
local bName = tostring(b and b.playerName or "")
|
||||||
|
if aName ~= bName then
|
||||||
|
return aName < bName
|
||||||
|
end
|
||||||
|
|
||||||
|
local aSpell = tonumber(aEntry and aEntry.spellId) or 0
|
||||||
|
local bSpell = tonumber(bEntry and bEntry.spellId) or 0
|
||||||
|
return aSpell < bSpell
|
||||||
|
end)
|
||||||
|
end
|
||||||
524
Modules/Tracker/TrackerDetection.lua
Normal file
524
Modules/Tracker/TrackerDetection.lua
Normal file
@@ -0,0 +1,524 @@
|
|||||||
|
local ADDON_NAME = "HailMaryGuildTools"
|
||||||
|
local HMGT = LibStub("AceAddon-3.0"):GetAddon(ADDON_NAME)
|
||||||
|
if not HMGT then return end
|
||||||
|
|
||||||
|
HMGT.TrackerDetection = HMGT.TrackerDetection or {}
|
||||||
|
|
||||||
|
local internals = HMGT.TrackerInternals or {}
|
||||||
|
local GetSpellChargesInfo = internals.GetSpellChargesInfo
|
||||||
|
local GetSpellCooldownInfo = internals.GetSpellCooldownInfo
|
||||||
|
local GetGlobalCooldownInfo = internals.GetGlobalCooldownInfo
|
||||||
|
local GetSpellDebugLabel = internals.GetSpellDebugLabel
|
||||||
|
local BuildCooldownStateFingerprint = internals.BuildCooldownStateFingerprint
|
||||||
|
local ApplyOwnCooldownReducers = internals.ApplyOwnCooldownReducers
|
||||||
|
local ApplyObservedCooldownReducers = internals.ApplyObservedCooldownReducers
|
||||||
|
|
||||||
|
function HMGT:HandleOwnSpellCast(spellId)
|
||||||
|
local isInterrupt = HMGT_SpellData.InterruptLookup[spellId] ~= nil
|
||||||
|
local isCooldown = HMGT_SpellData.CooldownLookup[spellId] ~= nil
|
||||||
|
if not isInterrupt and not isCooldown then return end
|
||||||
|
|
||||||
|
local spellEntry = HMGT_SpellData.InterruptLookup[spellId]
|
||||||
|
or HMGT_SpellData.CooldownLookup[spellId]
|
||||||
|
spellId = tonumber(spellEntry and spellEntry.spellId) or spellId
|
||||||
|
local name = self:NormalizePlayerName(UnitName("player"))
|
||||||
|
local pData = self.playerData[name]
|
||||||
|
local talents = pData and pData.talents or {}
|
||||||
|
if self:IsAvailabilitySpell(spellEntry) then
|
||||||
|
self:LogTrackedSpellCast(name, spellEntry, {
|
||||||
|
stateKind = "availability",
|
||||||
|
required = HMGT_SpellData.GetEffectiveAvailabilityRequired(spellEntry, talents),
|
||||||
|
})
|
||||||
|
if self:RefreshOwnAvailabilitySpell(spellEntry) then
|
||||||
|
self:PublishOwnSpellState(spellId, { sendLegacy = true })
|
||||||
|
end
|
||||||
|
self:TriggerTrackerUpdate()
|
||||||
|
return
|
||||||
|
end
|
||||||
|
|
||||||
|
local effectiveCd = HMGT_SpellData.GetEffectiveCooldown(spellEntry, talents)
|
||||||
|
local now = GetTime()
|
||||||
|
local inCombat = InCombatLockdown and InCombatLockdown()
|
||||||
|
local cur, max, chargeStart, chargeDuration = nil, nil, nil, nil
|
||||||
|
if not inCombat and GetSpellChargesInfo then
|
||||||
|
cur, max, chargeStart, chargeDuration = GetSpellChargesInfo(spellId)
|
||||||
|
end
|
||||||
|
local cachedMaxCharges, cachedChargeDuration = self:GetKnownChargeInfo(
|
||||||
|
spellEntry,
|
||||||
|
talents,
|
||||||
|
spellId,
|
||||||
|
(not inCombat and tonumber(chargeDuration) and tonumber(chargeDuration) > 0) and tonumber(chargeDuration) or effectiveCd
|
||||||
|
)
|
||||||
|
local inferredMaxCharges, inferredChargeDuration = HMGT_SpellData.GetEffectiveChargeInfo(
|
||||||
|
spellEntry,
|
||||||
|
talents,
|
||||||
|
(not inCombat and tonumber(max) and tonumber(max) > 0) and tonumber(max) or ((cachedMaxCharges > 0) and cachedMaxCharges or nil),
|
||||||
|
(not inCombat and tonumber(chargeDuration) and tonumber(chargeDuration) > 0) and tonumber(chargeDuration)
|
||||||
|
or ((cachedChargeDuration > 0) and cachedChargeDuration or effectiveCd)
|
||||||
|
)
|
||||||
|
|
||||||
|
local hasCharges = ((tonumber(max) or 0) > 1) or (tonumber(inferredMaxCharges) or 0) > 1
|
||||||
|
local currentCharges = 0
|
||||||
|
local maxCharges = 0
|
||||||
|
local chargeDur = 0
|
||||||
|
local chargeStartTime = nil
|
||||||
|
|
||||||
|
local startTime = now
|
||||||
|
local duration = effectiveCd
|
||||||
|
local expiresIn = effectiveCd
|
||||||
|
|
||||||
|
local existingCd = self:GetActiveCooldown(name, spellId)
|
||||||
|
if existingCd and (tonumber(existingCd.maxCharges) or 0) > 0 then
|
||||||
|
self:ResolveChargeState(existingCd, now)
|
||||||
|
end
|
||||||
|
|
||||||
|
if hasCharges then
|
||||||
|
maxCharges = math.max(1, tonumber(max) or cachedMaxCharges or tonumber(inferredMaxCharges) or 1)
|
||||||
|
currentCharges = tonumber(cur)
|
||||||
|
if currentCharges == nil then
|
||||||
|
local prevCharges = existingCd and tonumber(existingCd.currentCharges)
|
||||||
|
local prevMax = existingCd and tonumber(existingCd.maxCharges)
|
||||||
|
if prevCharges and prevMax and prevMax == maxCharges then
|
||||||
|
currentCharges = math.max(0, prevCharges - 1)
|
||||||
|
else
|
||||||
|
currentCharges = math.max(0, maxCharges - 1)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
currentCharges = math.max(0, math.min(maxCharges, currentCharges))
|
||||||
|
|
||||||
|
chargeDur = tonumber(chargeDuration)
|
||||||
|
or cachedChargeDuration
|
||||||
|
or tonumber(inferredChargeDuration)
|
||||||
|
or tonumber(effectiveCd)
|
||||||
|
or 0
|
||||||
|
chargeDur = math.max(0, chargeDur)
|
||||||
|
self:StoreKnownChargeInfo(spellId, maxCharges, chargeDur)
|
||||||
|
|
||||||
|
if currentCharges < maxCharges and chargeDur > 0 then
|
||||||
|
chargeStartTime = tonumber(chargeStart) or now
|
||||||
|
local missing = maxCharges - currentCharges
|
||||||
|
startTime = chargeStartTime
|
||||||
|
duration = missing * chargeDur
|
||||||
|
expiresIn = math.max(0, duration - (now - startTime))
|
||||||
|
else
|
||||||
|
startTime = now
|
||||||
|
duration = 0
|
||||||
|
expiresIn = 0
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
self:Debug(
|
||||||
|
"verbose",
|
||||||
|
"HandleOwnSpellCast name=%s spellId=%s cd=%.2f charges=%s/%s",
|
||||||
|
tostring(name),
|
||||||
|
tostring(spellId),
|
||||||
|
tonumber(effectiveCd) or 0,
|
||||||
|
hasCharges and tostring(currentCharges) or "-",
|
||||||
|
hasCharges and tostring(maxCharges) or "-"
|
||||||
|
)
|
||||||
|
|
||||||
|
self._cdNonce = (self._cdNonce or 0) + 1
|
||||||
|
local nonce = self._cdNonce
|
||||||
|
|
||||||
|
self:SetActiveCooldown(name, spellId, {
|
||||||
|
startTime = startTime,
|
||||||
|
duration = duration,
|
||||||
|
spellEntry = spellEntry,
|
||||||
|
currentCharges = hasCharges and currentCharges or nil,
|
||||||
|
maxCharges = hasCharges and maxCharges or nil,
|
||||||
|
chargeStart = hasCharges and chargeStartTime or nil,
|
||||||
|
chargeDuration = hasCharges and chargeDur or nil,
|
||||||
|
_nonce = nonce,
|
||||||
|
})
|
||||||
|
|
||||||
|
self:LogTrackedSpellCast(name, spellEntry, {
|
||||||
|
cooldown = effectiveCd,
|
||||||
|
currentCharges = hasCharges and currentCharges or nil,
|
||||||
|
maxCharges = hasCharges and maxCharges or nil,
|
||||||
|
chargeCooldown = hasCharges and chargeDur or nil,
|
||||||
|
})
|
||||||
|
|
||||||
|
if expiresIn > 0 then
|
||||||
|
self:ScheduleTimer(function()
|
||||||
|
local current = self:GetActiveCooldown(name, spellId)
|
||||||
|
if current and current._nonce == nonce then
|
||||||
|
self:ClearActiveCooldown(name, spellId)
|
||||||
|
self:PublishOwnSpellState(spellId)
|
||||||
|
self:TriggerTrackerUpdate()
|
||||||
|
end
|
||||||
|
end, expiresIn)
|
||||||
|
end
|
||||||
|
|
||||||
|
self:PublishOwnSpellState(spellId, { sendLegacy = true })
|
||||||
|
self:TriggerTrackerUpdate()
|
||||||
|
end
|
||||||
|
|
||||||
|
function HMGT:RefreshOwnCooldownStateFromGame(spellId)
|
||||||
|
local sid = tonumber(spellId)
|
||||||
|
if not sid then return false end
|
||||||
|
if InCombatLockdown and InCombatLockdown() then
|
||||||
|
return false
|
||||||
|
end
|
||||||
|
|
||||||
|
local ownName = self:NormalizePlayerName(UnitName("player"))
|
||||||
|
if not ownName then return false end
|
||||||
|
|
||||||
|
local spellEntry = HMGT_SpellData.InterruptLookup[sid]
|
||||||
|
or HMGT_SpellData.CooldownLookup[sid]
|
||||||
|
if not spellEntry or self:IsAvailabilitySpell(spellEntry) then
|
||||||
|
return false
|
||||||
|
end
|
||||||
|
sid = tonumber(spellEntry.spellId) or sid
|
||||||
|
|
||||||
|
local existing = self:GetActiveCooldown(ownName, sid)
|
||||||
|
local before = BuildCooldownStateFingerprint and BuildCooldownStateFingerprint(existing) or "nil"
|
||||||
|
local now = GetTime()
|
||||||
|
local pData = self.playerData[ownName]
|
||||||
|
local talents = pData and pData.talents or {}
|
||||||
|
local effectiveCd = HMGT_SpellData.GetEffectiveCooldown(spellEntry, talents)
|
||||||
|
local cur, max, chargeStart, chargeDuration = nil, nil, nil, nil
|
||||||
|
if GetSpellChargesInfo then
|
||||||
|
cur, max, chargeStart, chargeDuration = GetSpellChargesInfo(sid)
|
||||||
|
end
|
||||||
|
local inferredMaxCharges, inferredChargeDuration = HMGT_SpellData.GetEffectiveChargeInfo(
|
||||||
|
spellEntry,
|
||||||
|
talents,
|
||||||
|
(tonumber(max) and tonumber(max) > 0) and tonumber(max) or nil,
|
||||||
|
(tonumber(chargeDuration) and tonumber(chargeDuration) > 0) and tonumber(chargeDuration) or effectiveCd
|
||||||
|
)
|
||||||
|
|
||||||
|
local hasCharges = ((tonumber(max) or 0) > 1) or (tonumber(inferredMaxCharges) or 0) > 1
|
||||||
|
|
||||||
|
if hasCharges then
|
||||||
|
local maxCharges = math.max(1, tonumber(max) or tonumber(inferredMaxCharges) or 1)
|
||||||
|
local currentCharges = tonumber(cur)
|
||||||
|
if currentCharges == nil then
|
||||||
|
currentCharges = maxCharges
|
||||||
|
end
|
||||||
|
currentCharges = math.max(0, math.min(maxCharges, currentCharges))
|
||||||
|
|
||||||
|
local chargeDur = tonumber(chargeDuration) or tonumber(inferredChargeDuration) or tonumber(effectiveCd) or 0
|
||||||
|
chargeDur = math.max(0, chargeDur)
|
||||||
|
|
||||||
|
if currentCharges < maxCharges and chargeDur > 0 then
|
||||||
|
local chargeStartTime = tonumber(chargeStart) or now
|
||||||
|
local missing = maxCharges - currentCharges
|
||||||
|
local updatedEntry = self:SetActiveCooldown(ownName, sid, {
|
||||||
|
startTime = chargeStartTime,
|
||||||
|
duration = missing * chargeDur,
|
||||||
|
spellEntry = spellEntry,
|
||||||
|
currentCharges = currentCharges,
|
||||||
|
maxCharges = maxCharges,
|
||||||
|
chargeStart = chargeStartTime,
|
||||||
|
chargeDuration = chargeDur,
|
||||||
|
})
|
||||||
|
self:RefreshCooldownExpiryTimer(ownName, sid, updatedEntry)
|
||||||
|
else
|
||||||
|
self:ClearActiveCooldown(ownName, sid)
|
||||||
|
end
|
||||||
|
else
|
||||||
|
local cooldownStart, cooldownDuration = 0, 0
|
||||||
|
if GetSpellCooldownInfo then
|
||||||
|
cooldownStart, cooldownDuration = GetSpellCooldownInfo(sid)
|
||||||
|
end
|
||||||
|
cooldownStart = tonumber(cooldownStart) or 0
|
||||||
|
cooldownDuration = tonumber(cooldownDuration) or 0
|
||||||
|
local gcdStart, gcdDuration = 0, 0
|
||||||
|
if GetGlobalCooldownInfo then
|
||||||
|
gcdStart, gcdDuration = GetGlobalCooldownInfo()
|
||||||
|
end
|
||||||
|
gcdStart = tonumber(gcdStart) or 0
|
||||||
|
gcdDuration = tonumber(gcdDuration) or 0
|
||||||
|
local existingDuration = tonumber(existing and existing.duration) or 0
|
||||||
|
local existingStart = tonumber(existing and existing.startTime) or now
|
||||||
|
local existingRemaining = math.max(0, existingDuration - (now - existingStart))
|
||||||
|
|
||||||
|
local isLikelyGlobalCooldown = cooldownDuration > 0
|
||||||
|
and gcdDuration > 0
|
||||||
|
and math.abs(cooldownDuration - gcdDuration) <= 0.15
|
||||||
|
and (tonumber(effectiveCd) or 0) > (gcdDuration + 1.0)
|
||||||
|
|
||||||
|
local isSuspiciousShortRefresh = cooldownDuration > 0
|
||||||
|
and existingRemaining > 2.0
|
||||||
|
and existingDuration > 2.0
|
||||||
|
and cooldownDuration < math.max(2.0, existingDuration * 0.35)
|
||||||
|
and cooldownDuration < math.max(2.0, (tonumber(effectiveCd) or 0) * 0.35)
|
||||||
|
|
||||||
|
if isLikelyGlobalCooldown or isSuspiciousShortRefresh then
|
||||||
|
self:DebugScoped(
|
||||||
|
"verbose",
|
||||||
|
"TrackedSpells",
|
||||||
|
"Ignore suspicious refresh for %s: spellCD=%.3f gcd=%.3f existing=%.3f remaining=%.3f effective=%.3f",
|
||||||
|
GetSpellDebugLabel and GetSpellDebugLabel(sid) or tostring(sid),
|
||||||
|
cooldownDuration,
|
||||||
|
gcdDuration,
|
||||||
|
existingDuration,
|
||||||
|
existingRemaining,
|
||||||
|
tonumber(effectiveCd) or 0
|
||||||
|
)
|
||||||
|
return false
|
||||||
|
end
|
||||||
|
|
||||||
|
if cooldownDuration > 0 then
|
||||||
|
local updatedEntry = self:SetActiveCooldown(ownName, sid, {
|
||||||
|
startTime = cooldownStart,
|
||||||
|
duration = cooldownDuration,
|
||||||
|
spellEntry = spellEntry,
|
||||||
|
})
|
||||||
|
self:RefreshCooldownExpiryTimer(ownName, sid, updatedEntry)
|
||||||
|
else
|
||||||
|
self:ClearActiveCooldown(ownName, sid)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
local after = BuildCooldownStateFingerprint and BuildCooldownStateFingerprint(self:GetActiveCooldown(ownName, sid)) or "nil"
|
||||||
|
return before ~= after
|
||||||
|
end
|
||||||
|
|
||||||
|
function HMGT:DidOwnInterruptSucceed(triggerSpellId, talents)
|
||||||
|
local sid = tonumber(triggerSpellId)
|
||||||
|
if not sid then return false end
|
||||||
|
|
||||||
|
local spellEntry = HMGT_SpellData and HMGT_SpellData.InterruptLookup and HMGT_SpellData.InterruptLookup[sid]
|
||||||
|
if not spellEntry then return false end
|
||||||
|
sid = tonumber(spellEntry.spellId) or sid
|
||||||
|
|
||||||
|
local observedDuration = 0
|
||||||
|
if GetSpellCooldownInfo then
|
||||||
|
local _, duration = GetSpellCooldownInfo(sid)
|
||||||
|
observedDuration = duration
|
||||||
|
end
|
||||||
|
observedDuration = tonumber(observedDuration) or 0
|
||||||
|
if observedDuration <= 0 then return false end
|
||||||
|
|
||||||
|
local expectedDuration = HMGT_SpellData.GetEffectiveCooldown(spellEntry, talents)
|
||||||
|
expectedDuration = tonumber(expectedDuration) or 0
|
||||||
|
if expectedDuration <= 0 then return false end
|
||||||
|
|
||||||
|
return observedDuration < (expectedDuration - 0.05)
|
||||||
|
end
|
||||||
|
|
||||||
|
function HMGT:HandleOwnCooldownReductionTrigger(triggerSpellId)
|
||||||
|
local ownName = self:NormalizePlayerName(UnitName("player"))
|
||||||
|
if not ownName then return end
|
||||||
|
|
||||||
|
local pData = self.playerData[ownName]
|
||||||
|
local classToken = pData and pData.class or select(2, UnitClass("player"))
|
||||||
|
local specIndex = pData and pData.specIndex or GetSpecialization()
|
||||||
|
local talents = pData and pData.talents or {}
|
||||||
|
if not classToken or not specIndex then return end
|
||||||
|
|
||||||
|
local reducers = HMGT_SpellData.GetCooldownReducersForCast(classToken, specIndex, triggerSpellId, talents)
|
||||||
|
if not reducers or #reducers == 0 then return end
|
||||||
|
|
||||||
|
local instantReducers = {}
|
||||||
|
local observedInstantReducers = {}
|
||||||
|
local successReducers = {}
|
||||||
|
local observedSuccessReducers = {}
|
||||||
|
for _, reducer in ipairs(reducers) do
|
||||||
|
local observed = type(reducer.observe) == "table"
|
||||||
|
if reducer.requireInterruptSuccess then
|
||||||
|
if observed then
|
||||||
|
observedSuccessReducers[#observedSuccessReducers + 1] = reducer
|
||||||
|
else
|
||||||
|
successReducers[#successReducers + 1] = reducer
|
||||||
|
end
|
||||||
|
else
|
||||||
|
if observed then
|
||||||
|
observedInstantReducers[#observedInstantReducers + 1] = reducer
|
||||||
|
else
|
||||||
|
instantReducers[#instantReducers + 1] = reducer
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
local castTs = GetServerTime()
|
||||||
|
if #instantReducers > 0 and ApplyOwnCooldownReducers then
|
||||||
|
ApplyOwnCooldownReducers(self, ownName, triggerSpellId, instantReducers, castTs)
|
||||||
|
end
|
||||||
|
if #observedInstantReducers > 0 and ApplyObservedCooldownReducers then
|
||||||
|
ApplyObservedCooldownReducers(self, ownName, observedInstantReducers)
|
||||||
|
end
|
||||||
|
|
||||||
|
if #successReducers > 0 or #observedSuccessReducers > 0 then
|
||||||
|
local function ApplySuccessReducers()
|
||||||
|
if not self:DidOwnInterruptSucceed(triggerSpellId, talents) then
|
||||||
|
return false
|
||||||
|
end
|
||||||
|
if #successReducers > 0 and ApplyOwnCooldownReducers then
|
||||||
|
ApplyOwnCooldownReducers(self, ownName, triggerSpellId, successReducers, castTs)
|
||||||
|
end
|
||||||
|
if #observedSuccessReducers > 0 and ApplyObservedCooldownReducers then
|
||||||
|
ApplyObservedCooldownReducers(self, ownName, observedSuccessReducers)
|
||||||
|
end
|
||||||
|
return true
|
||||||
|
end
|
||||||
|
|
||||||
|
if not ApplySuccessReducers() then
|
||||||
|
C_Timer.After(0.12, function()
|
||||||
|
if not self or not self.playerData or not self.playerData[ownName] then
|
||||||
|
return
|
||||||
|
end
|
||||||
|
ApplySuccessReducers()
|
||||||
|
end)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
function HMGT:HandleRemoteSpellCast(playerName, spellId, castTimestamp, curCharges, maxCharges, chargeRemaining, chargeDuration)
|
||||||
|
local spellEntry = HMGT_SpellData.InterruptLookup[spellId]
|
||||||
|
or HMGT_SpellData.CooldownLookup[spellId]
|
||||||
|
if not spellEntry then return end
|
||||||
|
spellId = tonumber(spellEntry.spellId) or spellId
|
||||||
|
if self:IsAvailabilitySpell(spellEntry) then return end
|
||||||
|
|
||||||
|
local pData = self.playerData[playerName]
|
||||||
|
local talents = pData and pData.talents or {}
|
||||||
|
local effectiveCd = HMGT_SpellData.GetEffectiveCooldown(spellEntry, talents)
|
||||||
|
|
||||||
|
castTimestamp = tonumber(castTimestamp) or GetServerTime()
|
||||||
|
local existingEntry = self:GetActiveCooldown(playerName, spellId)
|
||||||
|
if (tonumber(maxCharges) or 0) <= 0 and existingEntry and existingEntry.lastCastTimestamp then
|
||||||
|
local prevTs = tonumber(existingEntry.lastCastTimestamp) or 0
|
||||||
|
if math.abs(prevTs - castTimestamp) <= 1 then
|
||||||
|
return
|
||||||
|
end
|
||||||
|
end
|
||||||
|
local now = GetTime()
|
||||||
|
local elapsed = math.max(0, GetServerTime() - castTimestamp)
|
||||||
|
|
||||||
|
local incomingCur = tonumber(curCharges) or 0
|
||||||
|
local incomingMax = tonumber(maxCharges) or 0
|
||||||
|
local incomingChargeRemaining = tonumber(chargeRemaining) or 0
|
||||||
|
local incomingChargeDuration = tonumber(chargeDuration) or 0
|
||||||
|
|
||||||
|
local inferredMaxCharges, inferredChargeDuration = HMGT_SpellData.GetEffectiveChargeInfo(
|
||||||
|
spellEntry,
|
||||||
|
talents,
|
||||||
|
(incomingMax > 0) and incomingMax or nil,
|
||||||
|
(incomingChargeDuration > 0) and incomingChargeDuration or effectiveCd
|
||||||
|
)
|
||||||
|
local hasCharges = (incomingMax > 1) or (tonumber(inferredMaxCharges) or 0) > 1
|
||||||
|
|
||||||
|
local currentCharges = 0
|
||||||
|
local maxChargeCount = 0
|
||||||
|
local chargeDur = 0
|
||||||
|
local nextChargeRemaining = 0
|
||||||
|
local chargeStartTime = nil
|
||||||
|
local startTime, duration, expiresIn
|
||||||
|
|
||||||
|
if hasCharges then
|
||||||
|
maxChargeCount = math.max(1, (incomingMax > 0 and incomingMax) or tonumber(inferredMaxCharges) or 1)
|
||||||
|
chargeDur = tonumber(incomingChargeDuration) or tonumber(inferredChargeDuration) or tonumber(effectiveCd) or 0
|
||||||
|
chargeDur = math.max(0, chargeDur)
|
||||||
|
if chargeDur <= 0 then
|
||||||
|
chargeDur = math.max(0, tonumber(effectiveCd) or 0)
|
||||||
|
end
|
||||||
|
|
||||||
|
if incomingMax > 0 then
|
||||||
|
currentCharges = math.max(0, math.min(maxChargeCount, incomingCur))
|
||||||
|
nextChargeRemaining = math.max(0, math.min(chargeDur, incomingChargeRemaining - elapsed))
|
||||||
|
if currentCharges < maxChargeCount and chargeDur > 0 then
|
||||||
|
chargeStartTime = now - math.max(0, chargeDur - nextChargeRemaining)
|
||||||
|
end
|
||||||
|
else
|
||||||
|
local existing = self:GetActiveCooldown(playerName, spellId)
|
||||||
|
if existing and (tonumber(existing.maxCharges) or 0) == maxChargeCount then
|
||||||
|
self:ResolveChargeState(existing, now)
|
||||||
|
local prevCharges = tonumber(existing.currentCharges) or maxChargeCount
|
||||||
|
local prevStart = tonumber(existing.chargeStart)
|
||||||
|
local prevDur = tonumber(existing.chargeDuration) or chargeDur
|
||||||
|
if prevDur > 0 then
|
||||||
|
chargeDur = prevDur
|
||||||
|
end
|
||||||
|
|
||||||
|
currentCharges = math.max(0, prevCharges - 1)
|
||||||
|
if currentCharges < maxChargeCount and chargeDur > 0 then
|
||||||
|
if prevCharges >= maxChargeCount then
|
||||||
|
chargeStartTime = now
|
||||||
|
else
|
||||||
|
chargeStartTime = prevStart or now
|
||||||
|
end
|
||||||
|
nextChargeRemaining = math.max(0, chargeDur - (now - chargeStartTime))
|
||||||
|
end
|
||||||
|
else
|
||||||
|
currentCharges = math.max(0, maxChargeCount - 1)
|
||||||
|
if currentCharges < maxChargeCount and chargeDur > 0 then
|
||||||
|
chargeStartTime = now
|
||||||
|
nextChargeRemaining = chargeDur
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
if currentCharges >= maxChargeCount and maxChargeCount > 0 then
|
||||||
|
currentCharges = math.max(0, maxChargeCount - 1)
|
||||||
|
if chargeDur > 0 then
|
||||||
|
chargeStartTime = now
|
||||||
|
nextChargeRemaining = chargeDur
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
if currentCharges < maxChargeCount and chargeDur > 0 then
|
||||||
|
chargeStartTime = chargeStartTime or now
|
||||||
|
local missing = maxChargeCount - currentCharges
|
||||||
|
startTime = chargeStartTime
|
||||||
|
duration = missing * chargeDur
|
||||||
|
expiresIn = math.max(0, duration - (now - startTime))
|
||||||
|
else
|
||||||
|
startTime = now
|
||||||
|
duration = 0
|
||||||
|
expiresIn = 0
|
||||||
|
end
|
||||||
|
else
|
||||||
|
local remaining = effectiveCd - elapsed
|
||||||
|
if remaining <= 0 then return end
|
||||||
|
startTime = now - elapsed
|
||||||
|
duration = effectiveCd
|
||||||
|
expiresIn = remaining
|
||||||
|
end
|
||||||
|
|
||||||
|
self:Debug(
|
||||||
|
"verbose",
|
||||||
|
"HandleRemoteSpellCast name=%s spellId=%s elapsed=%.2f expiresIn=%.2f charges=%s/%s",
|
||||||
|
tostring(playerName),
|
||||||
|
tostring(spellId),
|
||||||
|
tonumber(elapsed) or 0,
|
||||||
|
tonumber(expiresIn) or 0,
|
||||||
|
hasCharges and tostring(currentCharges) or "-",
|
||||||
|
hasCharges and tostring(maxChargeCount) or "-"
|
||||||
|
)
|
||||||
|
|
||||||
|
self._cdNonce = (self._cdNonce or 0) + 1
|
||||||
|
local nonce = self._cdNonce
|
||||||
|
|
||||||
|
self:SetActiveCooldown(playerName, spellId, {
|
||||||
|
startTime = startTime,
|
||||||
|
duration = duration,
|
||||||
|
spellEntry = spellEntry,
|
||||||
|
currentCharges = hasCharges and currentCharges or nil,
|
||||||
|
maxCharges = hasCharges and maxChargeCount or nil,
|
||||||
|
chargeStart = hasCharges and chargeStartTime or nil,
|
||||||
|
chargeDuration = hasCharges and chargeDur or nil,
|
||||||
|
lastCastTimestamp = castTimestamp,
|
||||||
|
_nonce = nonce,
|
||||||
|
})
|
||||||
|
|
||||||
|
self:LogTrackedSpellCast(playerName, spellEntry, {
|
||||||
|
cooldown = effectiveCd,
|
||||||
|
currentCharges = hasCharges and currentCharges or nil,
|
||||||
|
maxCharges = hasCharges and maxChargeCount or nil,
|
||||||
|
chargeCooldown = hasCharges and chargeDur or nil,
|
||||||
|
})
|
||||||
|
|
||||||
|
if expiresIn > 0 then
|
||||||
|
self:ScheduleTimer(function()
|
||||||
|
local current = self:GetActiveCooldown(playerName, spellId)
|
||||||
|
if current and current._nonce == nonce then
|
||||||
|
self:ClearActiveCooldown(playerName, spellId)
|
||||||
|
self:TriggerTrackerUpdate()
|
||||||
|
end
|
||||||
|
end, expiresIn)
|
||||||
|
end
|
||||||
|
|
||||||
|
self:TriggerTrackerUpdate()
|
||||||
|
end
|
||||||
@@ -94,25 +94,6 @@ local PARTY_FRAME_PATTERNS = {
|
|||||||
|
|
||||||
local unitFrameCache = {}
|
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 function BuildAnchorLayoutSignature(settings, ordered, unitByPlayer)
|
||||||
local parts = {
|
local parts = {
|
||||||
settings.attachToPartyFrame == true and "attach" or "stack",
|
settings.attachToPartyFrame == true and "attach" or "stack",
|
||||||
@@ -222,55 +203,6 @@ local function ResolveUnitAnchorFrame(unitId)
|
|||||||
return nil
|
return nil
|
||||||
end
|
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)
|
local function GetTrackerLabel(tracker)
|
||||||
if type(tracker) ~= "table" then
|
if type(tracker) ~= "table" then
|
||||||
return "Tracker"
|
return "Tracker"
|
||||||
@@ -287,136 +219,6 @@ local function GetTrackerLabel(tracker)
|
|||||||
return "Tracker"
|
return "Tracker"
|
||||||
end
|
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)
|
local function SortTrackers(trackers)
|
||||||
table.sort(trackers, function(a, b)
|
table.sort(trackers, function(a, b)
|
||||||
local aId = tonumber(a and a.id) or 0
|
local aId = tonumber(a and a.id) or 0
|
||||||
@@ -467,13 +269,7 @@ Manager._shared.ShortName = ShortName
|
|||||||
Manager._shared.BuildAnchorLayoutSignature = BuildAnchorLayoutSignature
|
Manager._shared.BuildAnchorLayoutSignature = BuildAnchorLayoutSignature
|
||||||
Manager._shared.IsGroupTracker = IsGroupTracker
|
Manager._shared.IsGroupTracker = IsGroupTracker
|
||||||
Manager._shared.ResolveUnitAnchorFrame = ResolveUnitAnchorFrame
|
Manager._shared.ResolveUnitAnchorFrame = ResolveUnitAnchorFrame
|
||||||
Manager._shared.GetGroupPlayers = GetGroupPlayers
|
|
||||||
Manager._shared.GetTrackerLabel = GetTrackerLabel
|
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
|
Manager._shared.BuildGroupDisplaySignature = BuildGroupDisplaySignature
|
||||||
|
|
||||||
function Manager:GetTrackers()
|
function Manager:GetTrackers()
|
||||||
@@ -492,6 +288,13 @@ function Manager:GetTrackers()
|
|||||||
return self._trackerCache
|
return self._trackerCache
|
||||||
end
|
end
|
||||||
|
|
||||||
|
function Manager:GetTrackerFrameKey(tracker)
|
||||||
|
if type(tracker) == "table" then
|
||||||
|
return GetTrackerFrameKey(tracker.id)
|
||||||
|
end
|
||||||
|
return GetTrackerFrameKey(tracker)
|
||||||
|
end
|
||||||
|
|
||||||
function Manager:MarkTrackersDirty()
|
function Manager:MarkTrackersDirty()
|
||||||
self._trackerCache = nil
|
self._trackerCache = nil
|
||||||
self._trackerCacheSignature = nil
|
self._trackerCacheSignature = nil
|
||||||
@@ -648,12 +451,8 @@ function Manager:RefreshVisibleVisuals()
|
|||||||
break
|
break
|
||||||
end
|
end
|
||||||
local entries = byPlayer[playerName] or {}
|
local entries = byPlayer[playerName] or {}
|
||||||
if HMGT.FilterDisplayEntries then
|
local tickThis = false
|
||||||
entries = HMGT:FilterDisplayEntries(tracker, entries) or entries
|
entries, tickThis = HMGT:FinalizeTrackerEntries(tracker, entries, tracker.trackerKey or nil)
|
||||||
end
|
|
||||||
if HMGT.SortDisplayEntries then
|
|
||||||
HMGT:SortDisplayEntries(entries)
|
|
||||||
end
|
|
||||||
if #entries == 0 then
|
if #entries == 0 then
|
||||||
needsFullRefresh = true
|
needsFullRefresh = true
|
||||||
break
|
break
|
||||||
@@ -666,11 +465,8 @@ function Manager:RefreshVisibleVisuals()
|
|||||||
HMGT.TrackerFrame:UpdateFrame(frame, entries, true)
|
HMGT.TrackerFrame:UpdateFrame(frame, entries, true)
|
||||||
totalEntries = totalEntries + #entries
|
totalEntries = totalEntries + #entries
|
||||||
byPlayerFiltered[playerName] = entries
|
byPlayerFiltered[playerName] = entries
|
||||||
for _, entry in ipairs(entries) do
|
if tickThis then
|
||||||
if EntryNeedsVisualTicker(entry) then
|
|
||||||
shouldTick = true
|
shouldTick = true
|
||||||
break
|
|
||||||
end
|
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
local newSignature = BuildGroupDisplaySignature(currentOrder, byPlayerFiltered)
|
local newSignature = BuildGroupDisplaySignature(currentOrder, byPlayerFiltered)
|
||||||
@@ -686,12 +482,8 @@ function Manager:RefreshVisibleVisuals()
|
|||||||
if not shouldShow then
|
if not shouldShow then
|
||||||
needsFullRefresh = true
|
needsFullRefresh = true
|
||||||
else
|
else
|
||||||
if HMGT.FilterDisplayEntries then
|
local tickThis = false
|
||||||
entries = HMGT:FilterDisplayEntries(tracker, entries) or entries
|
entries, tickThis = HMGT:FinalizeTrackerEntries(tracker, entries, tracker.trackerKey or nil)
|
||||||
end
|
|
||||||
if HMGT.SortDisplayEntries then
|
|
||||||
HMGT:SortDisplayEntries(entries)
|
|
||||||
end
|
|
||||||
if #entries == 0 then
|
if #entries == 0 then
|
||||||
needsFullRefresh = true
|
needsFullRefresh = true
|
||||||
else
|
else
|
||||||
@@ -701,11 +493,8 @@ function Manager:RefreshVisibleVisuals()
|
|||||||
if self._displaySignatures[frameKey] ~= newSignature then
|
if self._displaySignatures[frameKey] ~= newSignature then
|
||||||
needsFullRefresh = true
|
needsFullRefresh = true
|
||||||
end
|
end
|
||||||
for _, entry in ipairs(entries) do
|
if tickThis then
|
||||||
if EntryNeedsVisualTicker(entry) then
|
|
||||||
shouldTick = true
|
shouldTick = true
|
||||||
break
|
|
||||||
end
|
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
@@ -751,12 +540,8 @@ function Manager:UpdateDisplay()
|
|||||||
local entries, shouldShow = self:BuildEntriesForTracker(tracker)
|
local entries, shouldShow = self:BuildEntriesForTracker(tracker)
|
||||||
|
|
||||||
if shouldShow then
|
if shouldShow then
|
||||||
if HMGT.FilterDisplayEntries then
|
local tickThis = false
|
||||||
entries = HMGT:FilterDisplayEntries(tracker, entries) or entries
|
entries, tickThis = HMGT:FinalizeTrackerEntries(tracker, entries, tracker.trackerKey or nil)
|
||||||
end
|
|
||||||
if HMGT.SortDisplayEntries then
|
|
||||||
HMGT:SortDisplayEntries(entries)
|
|
||||||
end
|
|
||||||
|
|
||||||
HMGT.TrackerFrame:UpdateFrame(frame, entries, true)
|
HMGT.TrackerFrame:UpdateFrame(frame, entries, true)
|
||||||
frame:Show()
|
frame:Show()
|
||||||
@@ -769,11 +554,8 @@ function Manager:UpdateDisplay()
|
|||||||
layoutDirty = true
|
layoutDirty = true
|
||||||
end
|
end
|
||||||
|
|
||||||
for _, entry in ipairs(entries) do
|
if tickThis then
|
||||||
if EntryNeedsVisualTicker(entry) then
|
|
||||||
shouldTick = true
|
shouldTick = true
|
||||||
break
|
|
||||||
end
|
|
||||||
end
|
end
|
||||||
else
|
else
|
||||||
frame:Hide()
|
frame:Hide()
|
||||||
|
|||||||
65
Modules/Tracker/TrackerPlayerState.lua
Normal file
65
Modules/Tracker/TrackerPlayerState.lua
Normal file
@@ -0,0 +1,65 @@
|
|||||||
|
local ADDON_NAME = "HailMaryGuildTools"
|
||||||
|
local HMGT = LibStub("AceAddon-3.0"):GetAddon(ADDON_NAME)
|
||||||
|
if not HMGT then return end
|
||||||
|
|
||||||
|
HMGT.TrackerPlayerState = HMGT.TrackerPlayerState or {}
|
||||||
|
|
||||||
|
local internals = HMGT.TrackerInternals or {}
|
||||||
|
local IsSpellKnownLocally = internals.IsSpellKnownLocally
|
||||||
|
|
||||||
|
function HMGT:CollectOwnAvailableTrackerSpells(classToken, specIndex)
|
||||||
|
local class = classToken or select(2, UnitClass("player"))
|
||||||
|
local spec = tonumber(specIndex) or tonumber(GetSpecialization())
|
||||||
|
if not class or not spec or spec <= 0 then
|
||||||
|
return {}
|
||||||
|
end
|
||||||
|
if not HMGT_SpellData or type(HMGT_SpellData.GetSpellsForSpec) ~= "function" then
|
||||||
|
return {}
|
||||||
|
end
|
||||||
|
|
||||||
|
local knownSpells = {}
|
||||||
|
for _, datasetName in ipairs({ "Interrupts", "RaidCooldowns", "GroupCooldowns" }) do
|
||||||
|
local dataset = HMGT_SpellData[datasetName]
|
||||||
|
if type(dataset) == "table" then
|
||||||
|
local spells = HMGT_SpellData.GetSpellsForSpec(class, spec, dataset)
|
||||||
|
for _, entry in ipairs(spells) do
|
||||||
|
local sid = tonumber(entry.spellId)
|
||||||
|
if sid and sid > 0 and IsSpellKnownLocally and IsSpellKnownLocally(sid) then
|
||||||
|
knownSpells[sid] = true
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
local ownName = self:NormalizePlayerName(UnitName("player"))
|
||||||
|
local ownCDs = ownName and self:GetPlayerCooldownMap(ownName, false)
|
||||||
|
if ownCDs then
|
||||||
|
for sid in pairs(ownCDs) do
|
||||||
|
sid = tonumber(sid)
|
||||||
|
if sid and sid > 0 then
|
||||||
|
knownSpells[sid] = true
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
return knownSpells
|
||||||
|
end
|
||||||
|
|
||||||
|
function HMGT:IsTrackedSpellKnownForPlayer(playerName, spellId)
|
||||||
|
local sid = tonumber(spellId)
|
||||||
|
if not sid or sid <= 0 then
|
||||||
|
return false
|
||||||
|
end
|
||||||
|
|
||||||
|
local normalizedName = self:NormalizePlayerName(playerName)
|
||||||
|
local ownName = self:NormalizePlayerName(UnitName("player"))
|
||||||
|
local pData = normalizedName and self.playerData[normalizedName]
|
||||||
|
if pData and type(pData.knownSpells) == "table" and pData.knownSpells[sid] == true then
|
||||||
|
return true
|
||||||
|
end
|
||||||
|
|
||||||
|
if normalizedName and ownName and normalizedName == ownName and IsSpellKnownLocally then
|
||||||
|
return IsSpellKnownLocally(sid)
|
||||||
|
end
|
||||||
|
|
||||||
|
return false
|
||||||
|
end
|
||||||
410
Modules/Tracker/TrackerState.lua
Normal file
410
Modules/Tracker/TrackerState.lua
Normal file
@@ -0,0 +1,410 @@
|
|||||||
|
local ADDON_NAME = "HailMaryGuildTools"
|
||||||
|
local HMGT = LibStub("AceAddon-3.0"):GetAddon(ADDON_NAME)
|
||||||
|
if not HMGT then return end
|
||||||
|
|
||||||
|
HMGT.TrackerState = HMGT.TrackerState or {}
|
||||||
|
|
||||||
|
function HMGT:EnsureTrackerStateTables()
|
||||||
|
self.playerData = self.playerData or {}
|
||||||
|
self.activeCDs = self.activeCDs or {}
|
||||||
|
self.availabilityStates = self.availabilityStates or {}
|
||||||
|
self.localSpellStateRevisions = self.localSpellStateRevisions or {}
|
||||||
|
self.remoteSpellStateRevisions = self.remoteSpellStateRevisions or {}
|
||||||
|
self.knownChargeInfo = self.knownChargeInfo or {}
|
||||||
|
end
|
||||||
|
|
||||||
|
function HMGT:ResetTrackerState()
|
||||||
|
self.playerData = {}
|
||||||
|
self.activeCDs = {}
|
||||||
|
self.availabilityStates = {}
|
||||||
|
self.localSpellStateRevisions = {}
|
||||||
|
self.remoteSpellStateRevisions = {}
|
||||||
|
self.knownChargeInfo = {}
|
||||||
|
end
|
||||||
|
|
||||||
|
function HMGT:GetPlayerCooldownMap(playerName, create)
|
||||||
|
local normalizedName = self:NormalizePlayerName(playerName)
|
||||||
|
if not normalizedName then
|
||||||
|
return nil
|
||||||
|
end
|
||||||
|
self:EnsureTrackerStateTables()
|
||||||
|
if create then
|
||||||
|
self.activeCDs[normalizedName] = self.activeCDs[normalizedName] or {}
|
||||||
|
end
|
||||||
|
return self.activeCDs[normalizedName]
|
||||||
|
end
|
||||||
|
|
||||||
|
function HMGT:GetAvailabilityStateMap(playerName, create)
|
||||||
|
local normalizedName = self:NormalizePlayerName(playerName)
|
||||||
|
if not normalizedName then
|
||||||
|
return nil
|
||||||
|
end
|
||||||
|
self:EnsureTrackerStateTables()
|
||||||
|
if create then
|
||||||
|
self.availabilityStates[normalizedName] = self.availabilityStates[normalizedName] or {}
|
||||||
|
end
|
||||||
|
return self.availabilityStates[normalizedName]
|
||||||
|
end
|
||||||
|
|
||||||
|
function HMGT:GetAvailabilityStateEntry(playerName, spellId)
|
||||||
|
local sid = tonumber(spellId)
|
||||||
|
local states = self:GetAvailabilityStateMap(playerName, false)
|
||||||
|
return states and sid and states[sid] or nil
|
||||||
|
end
|
||||||
|
|
||||||
|
function HMGT:SetAvailabilityStateEntry(playerName, spellId, stateData)
|
||||||
|
local sid = tonumber(spellId)
|
||||||
|
if not sid or sid <= 0 or type(stateData) ~= "table" then
|
||||||
|
return nil
|
||||||
|
end
|
||||||
|
local states = self:GetAvailabilityStateMap(playerName, true)
|
||||||
|
if not states then
|
||||||
|
return nil
|
||||||
|
end
|
||||||
|
states[sid] = stateData
|
||||||
|
return stateData
|
||||||
|
end
|
||||||
|
|
||||||
|
function HMGT:ClearAvailabilityState(playerName, spellId)
|
||||||
|
local sid = tonumber(spellId)
|
||||||
|
local normalizedName = self:NormalizePlayerName(playerName)
|
||||||
|
if not normalizedName or not sid or sid <= 0 then
|
||||||
|
return false
|
||||||
|
end
|
||||||
|
|
||||||
|
local states = self.availabilityStates and self.availabilityStates[normalizedName]
|
||||||
|
if not states or not states[sid] then
|
||||||
|
return false
|
||||||
|
end
|
||||||
|
|
||||||
|
states[sid] = nil
|
||||||
|
if not next(states) then
|
||||||
|
self.availabilityStates[normalizedName] = nil
|
||||||
|
end
|
||||||
|
return true
|
||||||
|
end
|
||||||
|
|
||||||
|
function HMGT:GetActiveCooldown(playerName, spellId)
|
||||||
|
local sid = tonumber(spellId)
|
||||||
|
local cooldowns = self:GetPlayerCooldownMap(playerName, false)
|
||||||
|
return cooldowns and sid and cooldowns[sid] or nil
|
||||||
|
end
|
||||||
|
|
||||||
|
function HMGT:SetActiveCooldown(playerName, spellId, cdData)
|
||||||
|
local sid = tonumber(spellId)
|
||||||
|
if not sid or sid <= 0 or type(cdData) ~= "table" then
|
||||||
|
return nil
|
||||||
|
end
|
||||||
|
local cooldowns = self:GetPlayerCooldownMap(playerName, true)
|
||||||
|
if not cooldowns then
|
||||||
|
return nil
|
||||||
|
end
|
||||||
|
cooldowns[sid] = cdData
|
||||||
|
return cdData
|
||||||
|
end
|
||||||
|
|
||||||
|
function HMGT:ClearActiveCooldown(playerName, spellId)
|
||||||
|
local sid = tonumber(spellId)
|
||||||
|
local normalizedName = self:NormalizePlayerName(playerName)
|
||||||
|
if not normalizedName or not sid or sid <= 0 then
|
||||||
|
return false
|
||||||
|
end
|
||||||
|
|
||||||
|
local cooldowns = self.activeCDs and self.activeCDs[normalizedName]
|
||||||
|
if not cooldowns or not cooldowns[sid] then
|
||||||
|
return false
|
||||||
|
end
|
||||||
|
|
||||||
|
cooldowns[sid] = nil
|
||||||
|
if not next(cooldowns) then
|
||||||
|
self.activeCDs[normalizedName] = nil
|
||||||
|
end
|
||||||
|
return true
|
||||||
|
end
|
||||||
|
|
||||||
|
function HMGT:ClearPlayerCooldowns(playerName)
|
||||||
|
local normalizedName = self:NormalizePlayerName(playerName)
|
||||||
|
if not normalizedName then
|
||||||
|
return false
|
||||||
|
end
|
||||||
|
if self.activeCDs and self.activeCDs[normalizedName] then
|
||||||
|
self.activeCDs[normalizedName] = nil
|
||||||
|
return true
|
||||||
|
end
|
||||||
|
return false
|
||||||
|
end
|
||||||
|
|
||||||
|
function HMGT:GetLocalSpellStateRevision(spellId)
|
||||||
|
local sid = tonumber(spellId)
|
||||||
|
if not sid or sid <= 0 then
|
||||||
|
return 0
|
||||||
|
end
|
||||||
|
self:EnsureTrackerStateTables()
|
||||||
|
return tonumber(self.localSpellStateRevisions[sid]) or 0
|
||||||
|
end
|
||||||
|
|
||||||
|
function HMGT:EnsureLocalSpellStateRevision(spellId)
|
||||||
|
local sid = tonumber(spellId)
|
||||||
|
if not sid or sid <= 0 then
|
||||||
|
return 0
|
||||||
|
end
|
||||||
|
self:EnsureTrackerStateTables()
|
||||||
|
local current = tonumber(self.localSpellStateRevisions[sid]) or 0
|
||||||
|
if current <= 0 then
|
||||||
|
current = 1
|
||||||
|
self.localSpellStateRevisions[sid] = current
|
||||||
|
end
|
||||||
|
return current
|
||||||
|
end
|
||||||
|
|
||||||
|
function HMGT:NextLocalSpellStateRevision(spellId)
|
||||||
|
local sid = tonumber(spellId)
|
||||||
|
if not sid or sid <= 0 then
|
||||||
|
return 0
|
||||||
|
end
|
||||||
|
self:EnsureTrackerStateTables()
|
||||||
|
local nextRevision = (tonumber(self.localSpellStateRevisions[sid]) or 0) + 1
|
||||||
|
self.localSpellStateRevisions[sid] = nextRevision
|
||||||
|
return nextRevision
|
||||||
|
end
|
||||||
|
|
||||||
|
function HMGT:GetRemoteSpellStateRevision(playerName, spellId)
|
||||||
|
local normalizedName = self:NormalizePlayerName(playerName)
|
||||||
|
local sid = tonumber(spellId)
|
||||||
|
local bySpell = normalizedName and self.remoteSpellStateRevisions[normalizedName]
|
||||||
|
return tonumber(bySpell and bySpell[sid]) or 0
|
||||||
|
end
|
||||||
|
|
||||||
|
function HMGT:SetRemoteSpellStateRevision(playerName, spellId, revision)
|
||||||
|
local normalizedName = self:NormalizePlayerName(playerName)
|
||||||
|
local sid = tonumber(spellId)
|
||||||
|
local rev = tonumber(revision) or 0
|
||||||
|
if not normalizedName or not sid or sid <= 0 or rev <= 0 then
|
||||||
|
return
|
||||||
|
end
|
||||||
|
self:EnsureTrackerStateTables()
|
||||||
|
self.remoteSpellStateRevisions[normalizedName] = self.remoteSpellStateRevisions[normalizedName] or {}
|
||||||
|
self.remoteSpellStateRevisions[normalizedName][sid] = rev
|
||||||
|
end
|
||||||
|
|
||||||
|
function HMGT:ClearRemoteSpellStateRevisions(playerName)
|
||||||
|
local normalizedName = self:NormalizePlayerName(playerName)
|
||||||
|
if not normalizedName then
|
||||||
|
return false
|
||||||
|
end
|
||||||
|
if self.remoteSpellStateRevisions and self.remoteSpellStateRevisions[normalizedName] then
|
||||||
|
self.remoteSpellStateRevisions[normalizedName] = nil
|
||||||
|
return true
|
||||||
|
end
|
||||||
|
return false
|
||||||
|
end
|
||||||
|
|
||||||
|
function HMGT:ClearTrackerStateForPlayer(playerName)
|
||||||
|
local normalizedName = self:NormalizePlayerName(playerName)
|
||||||
|
if not normalizedName then
|
||||||
|
return false
|
||||||
|
end
|
||||||
|
|
||||||
|
local changed = false
|
||||||
|
if self.activeCDs and self.activeCDs[normalizedName] then
|
||||||
|
self.activeCDs[normalizedName] = nil
|
||||||
|
changed = true
|
||||||
|
end
|
||||||
|
if self.availabilityStates and self.availabilityStates[normalizedName] then
|
||||||
|
self.availabilityStates[normalizedName] = nil
|
||||||
|
changed = true
|
||||||
|
end
|
||||||
|
if self.remoteSpellStateRevisions and self.remoteSpellStateRevisions[normalizedName] then
|
||||||
|
self.remoteSpellStateRevisions[normalizedName] = nil
|
||||||
|
changed = true
|
||||||
|
end
|
||||||
|
|
||||||
|
return changed
|
||||||
|
end
|
||||||
|
|
||||||
|
function HMGT:StoreKnownChargeInfo(spellId, maxCharges, chargeDuration)
|
||||||
|
local sid = tonumber(spellId)
|
||||||
|
local maxCount = tonumber(maxCharges)
|
||||||
|
if not sid or sid <= 0 or not maxCount or maxCount <= 1 then
|
||||||
|
return
|
||||||
|
end
|
||||||
|
|
||||||
|
self:EnsureTrackerStateTables()
|
||||||
|
self.knownChargeInfo[sid] = {
|
||||||
|
maxCharges = math.max(1, math.floor(maxCount + 0.5)),
|
||||||
|
chargeDuration = math.max(0, tonumber(chargeDuration) or 0),
|
||||||
|
updatedAt = GetTime(),
|
||||||
|
}
|
||||||
|
end
|
||||||
|
|
||||||
|
function HMGT:GetKnownChargeInfo(spellEntry, talents, spellId, fallbackChargeDuration)
|
||||||
|
local sid = tonumber(spellId or (spellEntry and spellEntry.spellId))
|
||||||
|
if not sid or sid <= 0 then
|
||||||
|
return 0, 0
|
||||||
|
end
|
||||||
|
|
||||||
|
local cached = self.knownChargeInfo and self.knownChargeInfo[sid]
|
||||||
|
local cachedMax = tonumber(cached and cached.maxCharges) or 0
|
||||||
|
local cachedDuration = tonumber(cached and cached.chargeDuration) or 0
|
||||||
|
|
||||||
|
local inferredMax, inferredDuration = HMGT_SpellData.GetEffectiveChargeInfo(
|
||||||
|
spellEntry,
|
||||||
|
talents or {},
|
||||||
|
(cachedMax > 0) and cachedMax or nil,
|
||||||
|
(cachedDuration > 0) and cachedDuration or fallbackChargeDuration
|
||||||
|
)
|
||||||
|
|
||||||
|
local maxCharges = math.max(cachedMax, tonumber(inferredMax) or 0)
|
||||||
|
local chargeDuration = math.max(
|
||||||
|
tonumber(inferredDuration) or 0,
|
||||||
|
cachedDuration,
|
||||||
|
tonumber(fallbackChargeDuration) or 0
|
||||||
|
)
|
||||||
|
|
||||||
|
if maxCharges > 1 then
|
||||||
|
self:StoreKnownChargeInfo(sid, maxCharges, chargeDuration)
|
||||||
|
end
|
||||||
|
|
||||||
|
return maxCharges, chargeDuration
|
||||||
|
end
|
||||||
|
|
||||||
|
function HMGT:PruneAvailabilityStates(playerName, knownSpells)
|
||||||
|
local normalizedName = self:NormalizePlayerName(playerName)
|
||||||
|
local states = normalizedName and self.availabilityStates[normalizedName]
|
||||||
|
if not states or type(knownSpells) ~= "table" then
|
||||||
|
return false
|
||||||
|
end
|
||||||
|
|
||||||
|
local changed = false
|
||||||
|
for sid in pairs(states) do
|
||||||
|
if not knownSpells[tonumber(sid)] then
|
||||||
|
states[sid] = nil
|
||||||
|
changed = true
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
if not next(states) then
|
||||||
|
self.availabilityStates[normalizedName] = nil
|
||||||
|
end
|
||||||
|
return changed
|
||||||
|
end
|
||||||
|
|
||||||
|
function HMGT:ResolveChargeState(cdData, now)
|
||||||
|
if type(cdData) ~= "table" then
|
||||||
|
return 0, 0, 0, 0
|
||||||
|
end
|
||||||
|
|
||||||
|
now = tonumber(now) or GetTime()
|
||||||
|
local maxCharges = math.max(0, tonumber(cdData.maxCharges) or 0)
|
||||||
|
local currentCharges = math.max(0, tonumber(cdData.currentCharges) or 0)
|
||||||
|
local chargeDuration = math.max(0, tonumber(cdData.chargeDuration) or 0)
|
||||||
|
local chargeStart = tonumber(cdData.chargeStart)
|
||||||
|
|
||||||
|
if maxCharges <= 0 then
|
||||||
|
return 0, chargeDuration, currentCharges, maxCharges
|
||||||
|
end
|
||||||
|
if currentCharges >= maxCharges or chargeDuration <= 0 or not chargeStart then
|
||||||
|
return 0, chargeDuration, math.min(currentCharges, maxCharges), maxCharges
|
||||||
|
end
|
||||||
|
|
||||||
|
local elapsed = math.max(0, now - chargeStart)
|
||||||
|
local gainedCharges = math.floor(elapsed / chargeDuration)
|
||||||
|
local remaining = chargeDuration - (elapsed % chargeDuration)
|
||||||
|
|
||||||
|
if gainedCharges > 0 then
|
||||||
|
currentCharges = math.min(maxCharges, currentCharges + gainedCharges)
|
||||||
|
if currentCharges >= maxCharges then
|
||||||
|
currentCharges = maxCharges
|
||||||
|
chargeStart = nil
|
||||||
|
remaining = 0
|
||||||
|
else
|
||||||
|
chargeStart = now - (elapsed % chargeDuration)
|
||||||
|
end
|
||||||
|
|
||||||
|
cdData.currentCharges = currentCharges
|
||||||
|
cdData.chargeStart = chargeStart
|
||||||
|
if currentCharges >= maxCharges then
|
||||||
|
cdData.startTime = now
|
||||||
|
cdData.duration = 0
|
||||||
|
else
|
||||||
|
local missing = maxCharges - currentCharges
|
||||||
|
cdData.startTime = chargeStart
|
||||||
|
cdData.duration = missing * chargeDuration
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
if currentCharges >= maxCharges then
|
||||||
|
return 0, chargeDuration, currentCharges, maxCharges
|
||||||
|
end
|
||||||
|
return math.max(0, remaining), chargeDuration, currentCharges, maxCharges
|
||||||
|
end
|
||||||
|
|
||||||
|
function HMGT:RefreshCooldownExpiryTimer(playerName, spellId, cdData)
|
||||||
|
if not cdData then return 0 end
|
||||||
|
local now = GetTime()
|
||||||
|
local duration = tonumber(cdData.duration) or 0
|
||||||
|
local startTime = tonumber(cdData.startTime) or now
|
||||||
|
local expiresIn = math.max(0, duration - (now - startTime))
|
||||||
|
|
||||||
|
self._cdNonce = (self._cdNonce or 0) + 1
|
||||||
|
local nonce = self._cdNonce
|
||||||
|
cdData._nonce = nonce
|
||||||
|
|
||||||
|
if expiresIn > 0 then
|
||||||
|
self:ScheduleTimer(function()
|
||||||
|
local current = self:GetActiveCooldown(playerName, spellId)
|
||||||
|
if current and current._nonce == nonce then
|
||||||
|
self:ClearActiveCooldown(playerName, spellId)
|
||||||
|
if playerName == self:NormalizePlayerName(UnitName("player")) then
|
||||||
|
self:PublishOwnSpellState(spellId)
|
||||||
|
end
|
||||||
|
self:TriggerTrackerUpdate()
|
||||||
|
end
|
||||||
|
end, expiresIn)
|
||||||
|
end
|
||||||
|
return expiresIn
|
||||||
|
end
|
||||||
|
|
||||||
|
function HMGT:CleanupStaleCooldowns()
|
||||||
|
local now = GetTime()
|
||||||
|
local ownName = self:NormalizePlayerName(UnitName("player"))
|
||||||
|
local removed = 0
|
||||||
|
for playerName, spells in pairs(self.activeCDs) do
|
||||||
|
for spellId, cdInfo in pairs(spells) do
|
||||||
|
local duration = tonumber(cdInfo.duration) or 0
|
||||||
|
local startTime = tonumber(cdInfo.startTime) or now
|
||||||
|
local rem = duration - (now - startTime)
|
||||||
|
local hasCharges = (tonumber(cdInfo.maxCharges) or 0) > 0
|
||||||
|
local currentCharges = tonumber(cdInfo.currentCharges) or 0
|
||||||
|
local maxCharges = tonumber(cdInfo.maxCharges) or 0
|
||||||
|
if hasCharges then
|
||||||
|
local _, _, cur, max = self:ResolveChargeState(cdInfo, now)
|
||||||
|
currentCharges = cur
|
||||||
|
maxCharges = max
|
||||||
|
end
|
||||||
|
local shouldDrop = false
|
||||||
|
if hasCharges then
|
||||||
|
if currentCharges >= maxCharges then
|
||||||
|
shouldDrop = true
|
||||||
|
elseif (tonumber(cdInfo.chargeDuration) or 0) <= 0 and rem <= -2 then
|
||||||
|
shouldDrop = true
|
||||||
|
end
|
||||||
|
elseif rem <= -2 then
|
||||||
|
shouldDrop = true
|
||||||
|
end
|
||||||
|
if shouldDrop then
|
||||||
|
spells[spellId] = nil
|
||||||
|
if playerName == ownName then
|
||||||
|
self:PublishOwnSpellState(spellId)
|
||||||
|
end
|
||||||
|
removed = removed + 1
|
||||||
|
end
|
||||||
|
end
|
||||||
|
if not next(spells) then
|
||||||
|
self.activeCDs[playerName] = nil
|
||||||
|
end
|
||||||
|
end
|
||||||
|
if removed > 0 then
|
||||||
|
self:Debug("verbose", "CleanupStaleCooldowns removed=%d", removed)
|
||||||
|
end
|
||||||
|
end
|
||||||
1041
Modules/Tracker/TrackerSync.lua
Normal file
1041
Modules/Tracker/TrackerSync.lua
Normal file
File diff suppressed because it is too large
Load Diff
Reference in New Issue
Block a user