525 lines
21 KiB
Lua
525 lines
21 KiB
Lua
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",
|
|
"TrackerState",
|
|
"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
|