---@class DBMCoreNamespace local private = select(2, ...) local L = DBM_CORE_L local DBMScheduler = private:GetModule("DBMScheduler") local stringUtils = private:GetPrototype("StringUtils") local tableUtils = private:GetPrototype("TableUtils") ---@class DBM local DBM = private:GetPrototype("DBM") ---@class Announce local announcePrototype = private:GetPrototype("Announce") ---@class DBMMod local bossModPrototype = private:GetPrototype("DBMMod") local pformat = stringUtils.pformat local removeEntry = tableUtils.removeEntry ---@class Timer local timerPrototype = private:GetPrototype("Timer") local mt = {__index = timerPrototype} local countvoice1, countvoice2, countvoice3, countvoice4 local countvoice1max, countvoice2max, countvoice3max, countvoice4max = 5, 5, 5, 5 local countpath1, countpath2, countpath3, countpath4 --Merged countdown object for timers with build-in countdown function DBM:BuildVoiceCountdownCache() countvoice1 = self.Options.CountdownVoice countvoice2 = self.Options.CountdownVoice2 countvoice3 = self.Options.CountdownVoice3 countvoice4 = self.Options.PullVoice for _, count in pairs(DBM:GetCountSounds()) do if count.value == countvoice1 then countpath1 = count.path countvoice1max = count.max end if count.value == countvoice2 then countpath2 = count.path countvoice2max = count.max end if count.value == countvoice3 then countpath3 = count.path countvoice3max = count.max end if count.value == countvoice4 then countpath4 = count.path countvoice4max = count.max end end end local function playCountSound(_, path, requiresCombat) -- timerId, path if requiresCombat and not (InCombatLockdown() or UnitAffectingCombat("player")) then return end DBM:PlaySoundFile(path) end local function playCountdown(timerId, timer, voice, count, requiresCombat) if DBM.Options.DontPlayCountdowns then return end timer = timer or 10 count = count or 4 voice = voice or 1 if timer <= count then count = math.floor(timer) end if not countpath1 or not countpath2 or not countpath3 then DBM:Debug("Voice cache not built at time of playCountdown. On fly caching.", 3) DBM:BuildVoiceCountdownCache() end local maxCount, path if type(voice) == "string" then maxCount = 5--Safe to assume if it's not one of the built ins, it's likely heroes/OW, which has a max of 5 path = voice elseif voice == 2 then maxCount = countvoice2max or 10 path = countpath2 or "Interface\\AddOns\\DBM-Core\\Sounds\\Kolt\\" elseif voice == 3 then maxCount = countvoice3max or 5 path = countpath3 or "Interface\\AddOns\\DBM-Core\\Sounds\\Smooth\\" elseif voice == 4 then maxCount = countvoice4max or 10 path = countpath4 or "Interface\\AddOns\\DBM-Core\\Sounds\\Corsica\\" else maxCount = countvoice1max or 10 path = countpath1 or "Interface\\AddOns\\DBM-Core\\Sounds\\Corsica\\" end if not path then--Should not happen but apparently it does somehow DBM:Debug("Voice path failed in countdownProtoType:Start.") return end if count == 0 then--If a count of 0 is passed,then it's a "Countout" timer, not "Countdown" for i = 1, timer do if i < maxCount then DBM:Schedule(i, playCountSound, timerId, path .. i .. ".ogg", requiresCombat) end end else for i = count, 1, -1 do if i <= maxCount then DBM:Schedule(timer - i, playCountSound, timerId, path .. i .. ".ogg", requiresCombat) end end end end --"break" and "pull" timers have custom classifications that are straight forward and not in this table local timerTypeSimplification = { --All cooldown times, be they approx cd or next exact, or even AI timers, map to "CD" ["cdcount"] = "cd", ["cdsource"] = "cd", ["nextcount"] = "cd", ["nextsource"] = "cd", ["cdspecial"] = "cd", ["nextspecial"] = "cd", ["ai"] = "cd", ["adds"] = "cd", ["addscustom"] = "cd", ["cdnp"] = "cd", ["nextnp"] = "cd", --RPs all map to "warmup" ["roleplay"] = "warmup", ["combat"] = "warmup", --all stage types will map to "stage" ["achievement"] = "stage", ["stagecount"] = "stage", ["stagecountcycle"] = "stage", ["stagecontext"] = "stage", ["stagecontextcount"] = "stage", ["intermission"] = "stage", ["intermissioncount"] = "stage", --Target Bars such as buff/debuff on another player, on self, or on the boss, RPs all map to "target" ["targetcount"] = "target", ["fades"] = "target",--Fades is usually used as a personal target timer. So like debuff on other player is "debuff (targetname)" but on self it's just "debuff fades" --All cast bar types map to "cast" ["active"] = "cast",--Active bars are usually things like Whirlwind is active on the boss, or a channeled cast is being done. so effectively it's for channeled casts, as upposed to regular casts ["castsource"] = "cast", ["castcount"] = "cast", } --Very similar to above but more specific to key replacement and not type replacement, to match BW behavior for unification of WAs local waKeyOverrides = { ["combat"] = "warmup", ["roleplay"] = "warmup", ["achievement"] = "stages", ["stagecount"] = "stages", ["stagecountcycle"] = "stages", ["stagecontext"] = "stages", ["stagecontextcount"] = "stages", ["intermission"] = "stages", ["intermissioncount"] = "stages", } function timerPrototype:Start(timer, ...) if not self.mod.isDummyMod then--Don't apply following rulesets to pull timers and such if DBM.Options.DontShowBossTimers and not self.mod.isTrashMod then return end if DBM.Options.DontShowTrashTimers and self.mod.isTrashMod then return end end if timer and type(timer) ~= "number" then return self:Start(nil, timer, ...) -- first argument is optional! end if not self.option or self.mod.Options[self.option] then local isCountTimer = false if self.type and (self.type == "cdcount" or self.type == "nextcount" or self.type == "stagecount" or self.type == "stagecontextcount" or self.type == "stagecountcycle" or self.type == "intermissioncount") then isCountTimer = true end if isCountTimer and not self.allowdouble then--remove previous timer. for i = #self.startedTimers, 1, -1 do if DBM.Options.BadTimerAlert or DBM.Options.DebugMode and DBM.Options.DebugLevel > 1 then local bar = DBT:GetBar(self.startedTimers[i]) if bar then local remaining = ("%.1f"):format(bar.timer) local ttext = _G[bar.frame:GetName() .. "BarName"]:GetText() or "" ttext = ttext .. "(" .. self.id .. ")" if bar.timer > 0.2 then local phaseText = self.mod.vb.phase and " (" .. SCENARIO_STAGE:format(self.mod.vb.phase) .. ")" or "" if DBM.Options.BadTimerAlert and bar.timer > 1 then--If greater than 1 seconds off, report this out of debug mode to all users DBM:AddMsg("Timer " .. ttext .. phaseText .. " refreshed before expired. Remaining time is : " .. remaining .. ". Please report this bug", nil, nil, nil, true) DBM:FireEvent("DBM_Debug", "Timer " .. ttext .. phaseText .. " refreshed before expired. Remaining time is : " .. remaining .. ". Please report this bug", 2) else DBM:Debug("Timer " .. ttext .. phaseText .. " refreshed before expired. Remaining time is : " .. remaining, 2, true) end end end end DBT:CancelBar(self.startedTimers[i]) DBM:Unschedule(playCountSound, self.startedTimers[i]) DBM:FireEvent("DBM_TimerStop", self.startedTimers[i]) tremove(self.startedTimers, i) end end timer = timer and ((timer > 0 and timer) or self.timer + timer) or self.timer local id = self.id .. pformat((("\t%s"):rep(select("#", ...))), ...) --AI timer api: --Starting ai timer with (1) indicates it's a first timer after pull --Starting timer with (2) or (3) indicates it's a stage 2 or stage 3 first timer --Starting AI timer with anything above 3 indicarets it's a regular timer and to use shortest time in between two regular casts if self.type == "ai" then--A learning timer if not DBM.Options.AITimer then return end if timer > 5 then--Normal behavior. local newPhase = false for i = 1, 5 do --Check for any phase timers that are strings, if a string it means last cast of this ability was first case of a given stage if self["phase" .. i .. "CastTimer"] and type(self["phase" .. i .. "CastTimer"]) == "string" then--This is first cast of spell, we need to generate self.firstPullTimer self["phase" .. i .. "CastTimer"] = tonumber(self["phase" .. i .. "CastTimer"]) self["phase" .. i .. "CastTimer"] = GetTime() - self["phase" .. i .. "CastTimer"]--We have generated a self.phase1CastTimer! Next pull, DBM should know timer for first cast next pull. FANCY! DBM:Debug("AI timer learned a first timer for current phase of " .. self["phase" .. i .. "CastTimer"], 2) newPhase = true end end if self.lastCast and not newPhase then--We have a GetTime() on last cast and it's not affected by a phase change local timeLastCast = GetTime() - self.lastCast--Get time between current cast and last cast if timeLastCast > 5 then--Prevent infinite loop cpu hang. Plus anything shorter than 5 seconds doesn't need a timer if not self.lowestSeenCast or (self.lowestSeenCast and self.lowestSeenCast > timeLastCast) then--Always use lowest seen cast for a timer self.lowestSeenCast = timeLastCast DBM:Debug("AI timer learned a new lowest timer of " .. self.lowestSeenCast, 2) end end end self.lastCast = GetTime() if self.lowestSeenCast then--Always use lowest seen cast for timer timer = self.lowestSeenCast else return--Don't start the bogus timer shoved into timer field in the mod end else--AI timer passed with 5 or less is indicating phase change, with timer as phase number if not private.isRetail then timer = math.floor(timer)--Floor inprecise timers in classic because combat is mostly caused by PLAYER_REGEN in dungeons end if self["phase" .. timer .. "CastTimer"] and type(self["phase" .. timer .. "CastTimer"]) == "number" then --Check if timer is shorter than previous learned first timer by scanning remaining time on existing bar local bar = DBT:GetBar(id) if bar then local remaining = ("%.1f"):format(bar.timer) if bar.timer > 0.2 then self["phase" .. timer .. "CastTimer"] = self["phase" .. timer .. "CastTimer"] - remaining DBM:Debug("AI timer learned a lower first timer for current phase of " .. self["phase" .. timer .. "CastTimer"], 2) end end timer = self["phase" .. timer .. "CastTimer"] else--No first pull timer generated yet, set it to GetTime, as a string self["phase" .. timer .. "CastTimer"] = tostring(GetTime()) return--Don't start the x second timer end end end if DBM.Options.BadTimerAlert or DBM.Options.DebugMode and DBM.Options.DebugLevel > 1 then if not self.type or (self.type ~= "target" and self.type ~= "active" and self.type ~= "fades" and self.type ~= "ai") and not self.allowdouble then local bar = DBT:GetBar(id) if bar then local remaining = ("%.1f"):format(bar.timer) local ttext = _G[bar.frame:GetName() .. "BarName"]:GetText() or "" ttext = ttext .. "(" .. self.id .. ")" if bar.timer > 0.2 then local phaseText = self.mod.vb.phase and " (" .. SCENARIO_STAGE:format(self.mod.vb.phase) .. ")" or "" if DBM.Options.BadTimerAlert and bar.timer > 1 then--If greater than 1 seconds off, report this out of debug mode to all users DBM:AddMsg("Timer " .. ttext .. phaseText .. " refreshed before expired. Remaining time is : " .. remaining .. ". Please report this bug", nil, nil, nil, true) DBM:FireEvent("DBM_Debug", "Timer " .. ttext .. phaseText .. " refreshed before expired. Remaining time is : " .. remaining .. ". Please report this bug", 2) else DBM:Debug("Timer " .. ttext .. phaseText .. " refreshed before expired. Remaining time is : " .. remaining, 2, true) end end end end end local colorId if self.option then colorId = self.mod.Options[self.option .. "TColor"] elseif self.colorType and type(self.colorType) == "string" then--No option for specific timer, but another bool option given that tells us where to look for TColor (for mods such as trio boss for valentines day in events mods) colorId = self.mod.Options[self.colorType .. "TColor"] else--No option, or secondary option, set colorId to hardcoded color type colorId = self.colorType or 0 end local countVoice, countVoiceMax = 0, self.countdownMax or 4 if self.option then countVoice = self.mod.Options[self.option .. "CVoice"] if not self.fade and (type(countVoice) == "string" or countVoice > 0) then--Started without faded and has count voice assigned playCountdown(id, timer, countVoice, countVoiceMax, self.requiresCombat)--timerId, timer, voice, count end end local bar = DBT:CreateBar(timer, id, self.icon, self.startLarge, nil, nil, nil, colorId, nil, self.keep, self.fade, countVoice, countVoiceMax, self.simpType == "cd") if not bar then return false, "error" -- creating the timer failed somehow, maybe hit the hard-coded timer limit of 15 end local msg if self.type and not self.text then msg = pformat(self.mod:GetLocalizedTimerText(self.type, self.spellId, self.name), ...) else if type(self.text) == "number" then--spellId passed in timer text, it's a timer with short text msg = pformat(self.mod:GetLocalizedTimerText(self.type, self.text, self.name), ...) else msg = pformat(self.text, ...) end end msg = msg:gsub(">.-<", stringUtils.stripServerName) bar:SetText(msg, self.inlineIcon) --ID: Internal DBM timer ID --msg: Timer Text (Do not use msg has an event trigger, it varies language to language or based on user timer options. Use this to DISPLAY only (such as timer replacement UI). use spellId field 99% of time --timer: Raw timer value (number). --Icon: Texture Path for Icon --type: Timer type, which is one of only 7 possible types: "cd" for coolodwns, "target" for target bars such as debuff on a player, "stage" for any kind of stage timer (stage ends, next stage, or even just a warmup timer like "fight begins"), and then "cast" timer which is used for both a regular cast and a channeled cast (ie boss is casting frostbolt, or boss is channeling whirlwind). Lastly, break, pull, and berserk timers are "breaK", "pull", and "berserk" respectively --spellId: Raw spellid if available (most timers will have spellId or EJ ID unless it's a specific timer not tied to ability such as pull or combat start or rez timers. EJ id will be in format ej%d --colorID: Type classification (1-Add, 2-Aoe, 3-targeted ability, 4-Interrupt, 5-Role, 6-Stage, 7-User(custom)) --Mod ID: Encounter ID as string, or a generic string for mods that don't have encounter ID (such as trash, dummy/test mods) --Keep: true or nil, whether or not to keep bar on screen when it expires (if true, timer should be retained until an actual TimerStop occurs or a new TimerStart with same barId happens (in which case you replace bar with new one) --fade: true or nil, whether or not to fade a bar (set alpha to usersetting/2) --spellName: Sent so users can use a spell name instead of spellId, if they choose. Mostly to be more classic wow friendly, spellID is still preferred method (even for classic) --MobGUID if it could be parsed out of args --timerCount if current timer is a count timer. Returns number (count value) needed to have weak auras that trigger off a specific timer count without using localized message text local guid, timerCount if select("#", ...) > 0 then--If timer has args for i = 1, select("#", ...) do local v = select(i, ...) if DBM:IsNonPlayableGUID(v) then--Then scan them for a mob guid guid = v--If found, guid will be passed in DBM_TimerStart callback end --Not most efficient way to do it, but since it's already being done for guid, it's best not to repeat the work if isCountTimer and type(v) == "number" then timerCount = v end end end --Mods that have specifically flagged that it's safe to assume all timers from that boss mod belong to boss1 --This check is performed secondary to args scan so that no adds guids are overwritten if not guid and self.mod.sendMainBossGUID and not DBM.Options.DontSendBossGUIDs and (self.type == "cd" or self.type == "next" or self.type == "cdcount" or self.type == "nextcount" or self.type == "cdspecial" or self.type == "ai") then guid = UnitGUID("boss1") end DBM:FireEvent("DBM_TimerStart", id, msg, timer, self.icon, self.simpType, self.waSpecialKey or self.spellId, colorId, self.mod.id, self.keep, self.fade, self.name, guid, timerCount) --Bssically tops bar from starting if it's being put on a plater nameplate, to give plater users option to have nameplate CDs without actually using the bars --This filter will only apply to trash mods though, boss timers will always be shown due to need to have them exist for Pause, Resume, Update, and GetTime/GetRemaining methods if guid and (self.type == "cdnp" or self.type == "nextnp") and not (DBM.Options.DebugMode and DBM.Options.DebugLevel > 1) then DBT:CancelBar(id)--Cancel bar without stop callback return false, "disabled" end if not tContains(self.startedTimers, id) then--Make sure timer doesn't exist already before adding it tinsert(self.startedTimers, id) end if not self.keep then--Don't ever remove startedTimers on a schedule, if it's a keep timer self.mod:Unschedule(removeEntry, self.startedTimers, id) self.mod:Schedule(timer, removeEntry, self.startedTimers, id) end return bar else return false, "disabled" end end timerPrototype.Show = timerPrototype.Start --A way to set the fade to yes or no, overriding hardcoded value in NewTimer object with temporary one --If this method is used, it WILL persist until reload or changing it back function timerPrototype:SetFade(fadeOn, ...) --Done this way so SetFade can be used with :Start without needless performance cost (ie, ApplyStyle won't run unless it needs to) if fadeOn and not self.fade then self.fade = true--set timer object metatable, which will make sure next bar started uses fade --Find and Update an existing bar that's already started local id = self.id .. pformat((("\t%s"):rep(select("#", ...))), ...) local bar = DBT:GetBar(id) if bar and not bar.fade then DBM:FireEvent("DBM_TimerFadeUpdate", id, self.spellId, self.mod.id, true, self.name)--Timer ID, spellId, modId, true/nil, spellName (new callback only needed if we update an existing timers fade, self.fade is passed in timer start object for new timers) bar.fade = true--Set bar object metatable, which is copied from timer metatable at bar start only bar:ApplyStyle() DBM:Unschedule(playCountSound, id)--Don't even need to check option, it's faster cpu wise to just unschedule countdown either way end elseif not fadeOn and self.fade then self.fade = nil--set timer object metatable, which will make sure next bar started does NOT use fade --Find and Update an existing bar that's already started local id = self.id .. pformat((("\t%s"):rep(select("#", ...))), ...) local bar = DBT:GetBar(id) if bar and bar.fade then DBM:FireEvent("DBM_TimerFadeUpdate", id, self.spellId, self.mod.id, nil, self.name)--Timer ID, spellId, modId, true/nil, spellName (new callback only needed if we update an existing timers fade, self.fade is passed in timer start object for new timers) bar.fade = nil--Set bar object metatable, which is copied from timer metatable at bar start only bar:ApplyStyle() if self.option then local countVoice = self.mod.Options[self.option .. "CVoice"] or 0 if (type(countVoice) == "string" or countVoice > 0) then--Unfading bar, start countdown DBM:Unschedule(playCountSound, id) playCountdown(id, bar.timer, countVoice, self.countdownMax, self.requiresCombat)--timerId, timer, voice, count DBM:Debug("Re-enabling a countdown on bar ID: " .. id .. " after a SetFade disable call") end end end end end --This version does NOT set timer object meta, only started bar meta --Use this if you only want to alter an already STARTED temporarily --As such it also only needs fadeOn. fadeoff isn't needed since this temp alter never affects newly started bars function timerPrototype:SetSTFade(fadeOn, ...) local id = self.id .. pformat((("\t%s"):rep(select("#", ...))), ...) local bar = DBT:GetBar(id) if bar then if fadeOn and not bar.fade then DBM:FireEvent("DBM_TimerFadeUpdate", id, self.spellId, self.mod.id, true, self.name)--Timer ID, spellId, modId, true/nil, spellName (new callback only needed if we update an existing timers fade, self.fade is passed in timer start object for new timers) bar.fade = true--Set bar object metatable, which is copied from timer metatable at bar start only bar:ApplyStyle() DBM:Unschedule(playCountSound, id) elseif not fadeOn and bar.fade then DBM:FireEvent("DBM_TimerFadeUpdate", id, self.spellId, self.mod.id, nil, self.name) bar.fade = false bar:ApplyStyle() if self.option then local countVoice = self.mod.Options[self.option .. "CVoice"] or 0 if (type(countVoice) == "string" or countVoice > 0) then--Unfading bar, start countdown DBM:Unschedule(playCountSound, id) playCountdown(id, bar.timer, countVoice, self.countdownMax, self.requiresCombat)--timerId, timer, voice, count DBM:Debug("Re-enabling a countdown on bar ID: " .. id .. " after a SetSTFade disable call") end end end end end function timerPrototype:SetSTKeep(keepOn, ...) local id = self.id .. pformat((("\t%s"):rep(select("#", ...))), ...) local bar = DBT:GetBar(id) if bar then if keepOn and not bar.keep then bar.keep = true--Set bar object metatable, which is copied from timer metatable at bar start only bar:ApplyStyle() elseif not keepOn and bar.keep then DBM:FireEvent("DBM_TimerFadeUpdate", id, self.spellId, self.mod.id, nil) bar.keep = false bar:ApplyStyle() end end end function timerPrototype:DelayedStart(delay, ...) DBMScheduler:Unschedule(self.Start, self.mod, self, ...) DBMScheduler:Schedule(delay or 0.5, self.Start, self.mod, self, ...) end timerPrototype.DelayedShow = timerPrototype.DelayedStart function timerPrototype:Schedule(t, ...) return DBMScheduler:Schedule(t, self.Start, self.mod, self, ...) end function timerPrototype:Unschedule(...) return DBMScheduler:Unschedule(self.Start, self.mod, self, ...) end --TODO, figure out why this function doesn't properly stop count timers when calling stop without count on count timers function timerPrototype:Stop(...) if select("#", ...) == 0 then for i = #self.startedTimers, 1, -1 do DBM:FireEvent("DBM_TimerStop", self.startedTimers[i]) DBT:CancelBar(self.startedTimers[i]) DBM:Unschedule(playCountSound, self.startedTimers[i])--Unschedule countdown by timerId tremove(self.startedTimers, i) end else local id = self.id .. pformat((("\t%s"):rep(select("#", ...))), ...) for i = #self.startedTimers, 1, -1 do if self.startedTimers[i] == id then local guid for j = 1, select("#", ...) do local v = select(j, ...) if DBM:IsNonPlayableGUID(v) then--Then scan them for a mob guid guid = v--If found, guid will be passed in DBM_TimerStart callback end end --Mods that have specifically flagged that it's safe to assume all timers from that boss mod belong to boss1 --This check is performed secondary to args scan so that no adds guids are overwritten if not guid and self.mod.sendMainBossGUID and not DBM.Options.DontSendBossGUIDs and (self.type == "cd" or self.type == "next" or self.type == "cdcount" or self.type == "nextcount" or self.type == "cdspecial" or self.type == "ai") then guid = UnitGUID("boss1") end DBM:FireEvent("DBM_TimerStop", id, guid) DBT:CancelBar(id) DBM:Unschedule(playCountSound, id)--Unschedule countdown by timerId tremove(self.startedTimers, i) end end end if self.type == "ai" then--A learning timer if not DBM.Options.AITimer then return end self.lastCast = nil for i = 1, 4 do --Check for any phase timers that are strings and never got a chance to become AI timers, then wipe them if self["phase" .. i .. "CastTimer"] and type(self["phase" .. i .. "CastTimer"]) == "string" then self["phase" .. i .. "CastTimer"] = nil DBM:Debug("Wiping incomplete new timer of stage " .. i, 2) end end end end --HardStop is a method used when you want to force stop all varients of a timer by ID, period, but still pass a GUID for callbacks --This is especially useful for count timers where guid is 2nd arg and count is 1st --where Stop(guid) would mismatch object and not stop a bar and calling stop on every possible count is silly and stop without args wouldn't send GUID function timerPrototype:HardStop(guid) --Mods that have specifically flagged that it's safe to assume all timers from that boss mod belong to boss1 --This check is performed secondary to args scan so that no adds guids are overwritten if not guid and self.mod.sendMainBossGUID and not DBM.Options.DontSendBossGUIDs and (self.type == "cd" or self.type == "next" or self.type == "cdcount" or self.type == "nextcount" or self.type == "cdspecial" or self.type == "ai") then guid = UnitGUID("boss1") end for i = #self.startedTimers, 1, -1 do DBM:FireEvent("DBM_TimerStop", self.startedTimers[i], guid) DBT:CancelBar(self.startedTimers[i]) DBM:Unschedule(playCountSound, self.startedTimers[i])--Unschedule countdown by timerId tremove(self.startedTimers, i) end end --In past boss mods have always had to manually call Stop just to restart a timer, to avoid triggering false debug messages --This function should simplify boss mod creation by allowing you to "Restart" a timer with one call in mod instead of 2 function timerPrototype:Restart(timer, ...) if self.type and (self.type == "cdcount" or self.type == "nextcount") and not self.allowdouble then self:Stop()--Cleanup any count timers left over on a restart else self:Stop(...) end self:Unschedule(...)--Also unschedules not yet started timers that used timer:Schedule() self:Start(timer, ...) end timerPrototype.Reboot = timerPrototype.Restart function timerPrototype:Cancel(...) self:Stop(...) self:Unschedule(...)--Also unschedules not yet started timers that used timer:Schedule() end function timerPrototype:GetTime(...) local id = self.id .. pformat((("\t%s"):rep(select("#", ...))), ...) local bar = DBT:GetBar(id) return bar and (bar.totalTime - bar.timer) or 0, (bar and bar.totalTime) or 0 end function timerPrototype:GetRemaining(...) local id = self.id .. pformat((("\t%s"):rep(select("#", ...))), ...) local bar = DBT:GetBar(id) return bar and bar.timer or 0 end function timerPrototype:IsStarted(...) local id = self.id .. pformat((("\t%s"):rep(select("#", ...))), ...) local bar = DBT:GetBar(id) return bar and true end function timerPrototype:SetTimer(timer) self.timer = timer end function timerPrototype:Update(elapsed, totalTime, ...) if DBM.Options.DontShowBossTimers and not self.mod.isTrashMod then return end if DBM.Options.DontShowTrashTimers and self.mod.isTrashMod then return end local id = self.id .. pformat((("\t%s"):rep(select("#", ...))), ...) ---@type DBTBar|boolean|nil local bar = DBT:GetBar(id) if not bar then bar = self:Start(totalTime, ...) end if bar then -- still need to check as :Start() can return nil instead of actually starting the timer DBM:FireEvent("DBM_TimerUpdate", id, elapsed, totalTime) local newRemaining = totalTime - elapsed if not bar.keep and newRemaining > 0 then --Correct table for tracked timer objects for adjusted time, or else timers may get stuck if stop is called on them self.mod:Unschedule(removeEntry, self.startedTimers, id) self.mod:Schedule(newRemaining, removeEntry, self.startedTimers, id) end if self.option then local countVoice = self.mod.Options[self.option .. "CVoice"] or 0 if (type(countVoice) == "string" or countVoice > 0) then if not bar.fade then--Don't start countdown voice if it's faded bar if newRemaining > 2 then --Can't be called early beacuse then it won't unschedule countdown triggered by :Start if it was called --Also doesn't need to be called early like it does in AddTime and RemoveTime since those early return DBM:Unschedule(playCountSound, id) playCountdown(id, newRemaining, countVoice, self.countdownMax, self.requiresCombat)--timerId, timer, voice, count DBM:Debug("Updating a countdown after a timer Update call for timer ID:" .. id) end end end end return DBT:UpdateBar(id, elapsed, totalTime) end end function timerPrototype:AddTime(extendAmount, ...) if DBM.Options.DontShowBossTimers and not self.mod.isTrashMod then return end if DBM.Options.DontShowTrashTimers and self.mod.isTrashMod then return end local id = self.id .. pformat((("\t%s"):rep(select("#", ...))), ...) DBM:Unschedule(playCountSound, id)--Needs to be unscheduled early in case Start is called instead of Update local bar = DBT:GetBar(id) if not bar then return self:Start(extendAmount, ...) else local elapsed, total = (bar.totalTime - bar.timer), bar.totalTime if elapsed and total then local newRemaining = (total + extendAmount) - elapsed if not bar.keep then --Correct table for tracked timer objects for adjusted time, or else timers may get stuck if stop is called on them self.mod:Unschedule(removeEntry, self.startedTimers, id) self.mod:Schedule(newRemaining, removeEntry, self.startedTimers, id) end if self.option then local countVoice = self.mod.Options[self.option .. "CVoice"] or 0 if (type(countVoice) == "string" or countVoice > 0) then if not bar.fade then--Don't start countdown voice if it's faded bar playCountdown(id, newRemaining, countVoice, self.countdownMax, self.requiresCombat)--timerId, timer, voice, count DBM:Debug("Updating a countdown after a timer AddTime call for timer ID:" .. id) end end end DBM:FireEvent("DBM_TimerUpdate", id, elapsed, total + extendAmount) return DBT:UpdateBar(id, elapsed, total + extendAmount) end end end function timerPrototype:RemoveTime(reduceAmount, ...) if DBM.Options.DontShowBossTimers and not self.mod.isTrashMod then return end if DBM.Options.DontShowTrashTimers and self.mod.isTrashMod then return end local id = self.id .. pformat((("\t%s"):rep(select("#", ...))), ...) DBM:Unschedule(playCountSound, id)--Needs to be unscheduled here, or countdown might not be canceled if removing time made it cease to have a > 0 value local bar = DBT:GetBar(id) if not bar then return--Do nothing else if not bar.keep then self.mod:Unschedule(removeEntry, self.startedTimers, id)--Needs to be unscheduled here, or the entry might just get left in table until original expire time, if new expire time is less than 0 end local elapsed, total = (bar.totalTime - bar.timer), bar.totalTime if elapsed and total then local newRemaining = (total - reduceAmount) - elapsed if newRemaining > 0 then --Correct table for tracked timer objects for adjusted time, or else timers may get stuck if stop is called on them if not bar.keep then self.mod:Schedule(newRemaining, removeEntry, self.startedTimers, id) end if self.option and newRemaining > 2 then local countVoice = self.mod.Options[self.option .. "CVoice"] or 0 if (type(countVoice) == "string" or countVoice > 0) then if not bar.fade then--Don't start countdown voice if it's faded bar if newRemaining > 2 then playCountdown(id, newRemaining, countVoice, self.countdownMax, self.requiresCombat)--timerId, timer, voice, count DBM:Debug("Updating a countdown after a timer RemoveTime call for timer ID:" .. id) end end end end DBM:FireEvent("DBM_TimerUpdate", id, elapsed, total - reduceAmount) return DBT:UpdateBar(id, elapsed, total - reduceAmount) else--New remaining less than 0 DBM:FireEvent("DBM_TimerStop", id) removeEntry(self.startedTimers, id) return DBT:CancelBar(id) end end end end function timerPrototype:Pause(...) local id = self.id .. pformat((("\t%s"):rep(select("#", ...))), ...) local bar = DBT:GetBar(id) DBM:Unschedule(playCountSound, id)--Kill countdown on pause if bar then if not bar.keep then self.mod:Unschedule(removeEntry, self.startedTimers, id)--Prevent removal from startedTimers table while bar is paused end DBM:FireEvent("DBM_TimerPause", id) return bar:Pause() end end function timerPrototype:Resume(...) local id = self.id .. pformat((("\t%s"):rep(select("#", ...))), ...) local bar = DBT:GetBar(id) if bar then local elapsed, total = (bar.totalTime - bar.timer), bar.totalTime if elapsed and total then local remaining = total - elapsed if not bar.keep then self.mod:Schedule(remaining, removeEntry, self.startedTimers, id)--Re-schedule the auto remove entry stuff end --Have to check if paused bar had a countdown on resume so we can restore it if self.option and not bar.fade then local countVoice = self.mod.Options[self.option .. "CVoice"] or 0 if (type(countVoice) == "string" or countVoice > 0) then playCountdown(id, remaining, countVoice, self.countdownMax, self.requiresCombat)--timerId, timer, voice, count DBM:Debug("Updating a countdown after a timer Resume call for timer ID:" .. id) end end end DBM:FireEvent("DBM_TimerResume", id) return bar:Resume() end end function timerPrototype:UpdateIcon(icon, ...) local id = self.id .. pformat((("\t%s"):rep(select("#", ...))), ...) local bar = DBT:GetBar(id) if bar then icon = DBM:ParseSpellIcon(icon) DBM:FireEvent("DBM_TimerUpdateIcon", id, icon) return bar:SetIcon(icon) end end --This function changes the spellname and callback key (but not option key) of timer object --This is needed for faction bosses where we need to swap out a spell key/name on fly after a boss is engaged function timerPrototype:UpdateKey(altSpellId, ...) --Check if existing bar first, local id = self.id .. pformat((("\t%s"):rep(select("#", ...))), ...) local bar = DBT:GetBar(id) self.spellId = altSpellId self.icon = DBM:ParseSpellIcon(altSpellId, self.type, self.icon) self.name = nil--By wiping name, it becomes uncached and can get replaced by GetLocalizedTimerText in :Start if bar then --If a bar exists while updating key we" --Get remainig, kill old timer, start new one with ID/name replacement applied local remaining = bar.timer self:Stop(...) self:Unschedule(...) DBM:Unschedule(playCountSound, id) self:Start(remaining, ...)--Restart it end end function timerPrototype:UpdateInline(newInline, ...) local id = self.id .. pformat((("\t%s"):rep(select("#", ...))), ...) local bar = DBT:GetBar(id) if bar then local ttext = _G[bar.frame:GetName() .. "BarName"]:GetText() or "" return bar:SetText(ttext, newInline or self.inlineIcon) end end function timerPrototype:UpdateName(name, ...) local id = self.id .. pformat((("\t%s"):rep(select("#", ...))), ...) local bar = DBT:GetBar(id) if bar then return bar:SetText(name, self.inlineIcon) end end function timerPrototype:SetColor(c, ...) local id = self.id .. pformat((("\t%s"):rep(select("#", ...))), ...) local bar = DBT:GetBar(id) if bar then return bar:SetColor(c) end end function timerPrototype:DisableEnlarge(...) local id = self.id .. pformat((("\t%s"):rep(select("#", ...))), ...) local bar = DBT:GetBar(id) if bar then bar.small = true end end ---@param optionDefault SpecFlags|boolean? function timerPrototype:AddOption(optionDefault, optionName, colorType, countdown, spellId, optionType, waCustomName) if optionName ~= false then self.option = optionName or self.id self.mod:AddBoolOption(self.option, optionDefault, "timer", nil, colorType, countdown, spellId, optionType, waCustomName) end end --If a new countdown default is added to a NewTimer object, change optionName of timer to reset a new default function bossModPrototype:NewTimer(timer, name, icon, optionDefault, optionName, colorType, inlineIcon, keep, countdown, countdownMax, r, g, b, spellId, requiresCombat, waCustomName, customType) if r and type(r) == "string" then DBM:Debug("|cffff0000r probably has inline icon in it and needs to be fixed for |r" .. name .. r) r = nil--Fix it for users end if inlineIcon and type(inlineIcon) == "number" then DBM:Debug("|cffff0000spellID texture path or colorType is in inlineIcon field and needs to be fixed for |r" .. name .. inlineIcon) inlineIcon = nil--Fix it for users end icon = DBM:ParseSpellIcon(icon) local waSpecialKey, simpType if customType then simpType = timerTypeSimplification[customType] or customType waSpecialKey = waKeyOverrides[customType] end ---@class Timer local obj = setmetatable( { text = self.localization.timers[name], type = customType or "cd",--Auto assign simpType = simpType or "cd", waSpecialKey = waSpecialKey, spellId = spellId,--Allows Localized timer text to still have a spellId arg weak auras can latch onto timer = timer, id = name, icon = icon, colorType = colorType or 0, inlineIcon = inlineIcon, keep = keep, countdown = countdown, countdownMax = countdownMax, r = r, g = g, b = b, requiresCombat = requiresCombat, startedTimers = {}, mod = self, startLarge = nil, }, mt ) obj:AddOption(optionDefault, optionName, colorType, countdown, spellId, nil, waCustomName) tinsert(self.timers, obj) return obj end -- new constructor for the new auto-localized timer types -- note that the function might look unclear because it needs to handle different timer types, especially achievement timers need special treatment -- If a new countdown is added to an existing timer that didn't have one before, use optionName (number) to force timer to reset defaults by assigning it a new variable ---@param timerType string local function newTimer(self, timerType, timer, spellId, timerText, optionDefault, optionName, colorType, texture, inlineIcon, keep, countdown, countdownMax, r, g, b, requiresCombat) if type(timer) == "string" and timer:match("OptionVersion") then error("OptionVersion hack deprecated, remove it from: " .. spellId) end if type(colorType) == "number" and colorType > 8 then DBM:AddMsg("|cffff0000texture is in the colorType arg for: |r" .. spellId) end --Use option optionName for optionVersion as well, no reason to split. --This ensures that remaining arg positions match for auto generated and regular NewTimer local optionVersion if type(optionName) == "number" then optionVersion = optionName optionName = nil end local allowdouble if type(timer) == "string" and timer:match("d%d+") then allowdouble = true timer = tonumber(string.sub(timer, 2)) end local spellName, icon spellName = DBM:ParseSpellName(spellId, timerType) local unparsedId = spellId if timerType == "achievement" then icon = DBM:ParseSpellIcon(texture or spellId, timerType) elseif timerType == "cdspecial" or timerType == "nextspecial" or timerType == "stage" or timerType == "stagecount" or timerType == "stagecountcycle" or timerType == "stagecontext" or timerType == "stagecontextcount" or timerType == "intermission" or timerType == "intermissioncount" then icon = DBM:ParseSpellIcon(texture or spellId, timerType) if timerType == "stage" or timerType == "stagecount" or timerType == "stagecountcycle" or timerType == "stagecontext" or timerType == "stagecontextcount" or timerType == "intermission" or timerType == "intermissioncount" then colorType = 6 end elseif timerType == "roleplay" then icon = DBM:ParseSpellIcon(texture or spellId, timerType, private.isRetail and 237538 or 136106) colorType = 6 elseif timerType == "adds" or timerType == "addscustom" then icon = DBM:ParseSpellIcon(texture or spellId, timerType, 136116) colorType = 1 else icon = DBM:ParseSpellIcon(texture or spellId, timerType) colorType = colorType or 0 end local timerTextValue if timerText then --First check if it's shorttext if DBM.Options.ShortTimerText then --If timertext is a number, accept it as a secondary auto translate spellid if type(timerText) == "number" then timerTextValue = timerText spellName = DBM:GetSpellName(timerText or 0)--Override Cached spell Name --Interpret it literal with no restrictions, first checking mod local table, then just taking timerText directly else timerTextValue = self.localization.timers[timerText]--Check timers table first, otherwise accept it as literal timer text end else--Short text is off, we want to be more aggressive in NOT setting short text if we can help it --Short text is off, and spellId does exist, only accept timerText if it's in mods localization tables, cause then it's not short text, it's hard localization if spellId and type(spellId) == "number" then --Only use timerText if it's full localized, cause that's not shorttext timerTextValue = rawget(self.localization.timers, timerText) else--If no spellID, then we allow hard setting timerText because it's only translation timer object has timerTextValue = self.localization.timers[timerText] end end end local id = "Timer" .. (spellId or 0) .. timerType .. (optionVersion or "") local simpType = timerTypeSimplification[timerType] or timerType local waSpecialKey = waKeyOverrides[timerType] ---@class Timer local obj = setmetatable( { text = timerTextValue, type = timerType, simpType = simpType, waSpecialKey = waSpecialKey,--Not same as simpType, this overrides option key spellId = spellId, name = spellName,--If name gets stored as nil, it'll be corrected later in Timer start, if spell name returns in a later attempt timer = timer, id = id, icon = icon, colorType = colorType, inlineIcon = inlineIcon, keep = keep, countdown = countdown, countdownMax = countdownMax, r = r, g = g, b = b, requiresCombat = requiresCombat, allowdouble = allowdouble, startedTimers = {}, mod = self, }, mt ) obj:AddOption(optionDefault, optionName, colorType, countdown, spellId, timerType) tinsert(self.timers, obj) -- todo: move the string creation to the GUI with SetFormattedString... if not self.localization.options[id] or self.localization.options[id] == id then if timerType == "achievement" then self.localization.options[id] = L.AUTO_TIMER_OPTIONS[timerType]:format((GetAchievementLink(spellId) or ""):gsub("%[(.+)%]", "%1")) elseif timerType == "cdspecial" or timerType == "nextspecial" or timerType == "stage" or timerType == "stagecount" or timerType == "stagecountcycle" or timerType == "intermission" or timerType == "intermissioncount" or timerType == "roleplay" then--Timers without spellid, generic (do not add stagecontext here, it has spellname parsing) self.localization.options[id] = L.AUTO_TIMER_OPTIONS[timerType]--Using more than 1 stage timer or more than 1 special timer will break this, fortunately you should NEVER use more than 1 of either in a mod else self.localization.options[id] = L.AUTO_TIMER_OPTIONS[timerType]:format(unparsedId) end end return obj end ---@overload fun(self: DBMMod, timer: number|string, spellId: number|string?, timerText: number|string?, optionDefault: SpecFlags|boolean?, optionName: string|number|boolean?, colorType: number?, texture: number|string?, inlineIcon: string?, keep: boolean?, countdown: number?, countdownMax: number?, r: number?, g: number?, b: number?, requiresCombat: boolean?): Timer function bossModPrototype:NewTargetTimer(...) return newTimer(self, "target", ...) end ---@overload fun(self: DBMMod, timer: number|string, spellId: number|string?, timerText: number|string?, optionDefault: SpecFlags|boolean?, optionName: string|number|boolean?, colorType: number?, texture: number|string?, inlineIcon: string?, keep: boolean?, countdown: number?, countdownMax: number?, r: number?, g: number?, b: number?, requiresCombat: boolean?): Timer function bossModPrototype:NewTargetCountTimer(...) return newTimer(self, "targetcount", ...) end --Buff/Debuff/event on boss ---@overload fun(self: DBMMod, timer: number|string, spellId: number|string?, timerText: number|string?, optionDefault: SpecFlags|boolean?, optionName: string|number|boolean?, colorType: number?, texture: number|string?, inlineIcon: string?, keep: boolean?, countdown: number?, countdownMax: number?, r: number?, g: number?, b: number?, requiresCombat: boolean?): Timer function bossModPrototype:NewBuffActiveTimer(...) return newTimer(self, "active", ...) end ----Buff/Debuff on players ---@overload fun(self: DBMMod, timer: number|string, spellId: number|string?, timerText: number|string?, optionDefault: SpecFlags|boolean?, optionName: string|number|boolean?, colorType: number?, texture: number|string?, inlineIcon: string?, keep: boolean?, countdown: number?, countdownMax: number?, r: number?, g: number?, b: number?, requiresCombat: boolean?): Timer function bossModPrototype:NewBuffFadesTimer(...) return newTimer(self, "fades", ...) end ---@overload fun(self: DBMMod, timer: number|string, spellId: number|string?, timerText: number|string?, optionDefault: SpecFlags|boolean?, optionName: string|number|boolean?, colorType: number?, texture: number|string?, inlineIcon: string?, keep: boolean?, countdown: number?, countdownMax: number?, r: number?, g: number?, b: number?, requiresCombat: boolean?): Timer function bossModPrototype:NewCastTimer(timer, ...) if tonumber(timer) and timer > 1000 then -- hehe :) best hack in DBM. This makes the first argument optional, so we can omit it to use the cast time from the spell id ;) local spellId = timer timer = select(4, DBM:GetSpellInfo(spellId)) or 1000 -- GetSpellInfo takes YOUR spell haste into account...WTF? local spellHaste = select(4, DBM:GetSpellInfo(10059)) / 10000 -- 10059 = Stormwind Portal, should have 10000 ms cast time timer = timer / spellHaste -- calculate the real cast time of the spell... return self:NewCastTimer(timer / 1000, spellId, ...) end return newTimer(self, "cast", timer, ...) end ---@overload fun(self: DBMMod, timer: number|string, spellId: number|string?, timerText: number|string?, optionDefault: SpecFlags|boolean?, optionName: string|number|boolean?, colorType: number?, texture: number|string?, inlineIcon: string?, keep: boolean?, countdown: number?, countdownMax: number?, r: number?, g: number?, b: number?, requiresCombat: boolean?): Timer function bossModPrototype:NewCastCountTimer(timer, ...) if tonumber(timer) and timer > 1000 then -- hehe :) best hack in DBM. This makes the first argument optional, so we can omit it to use the cast time from the spell id ;) local spellId = timer timer = select(4, DBM:GetSpellInfo(spellId)) or 1000 -- GetSpellInfo takes YOUR spell haste into account...WTF? local spellHaste = select(4, DBM:GetSpellInfo(10059)) / 10000 -- 10059 = Stormwind Portal, should have 10000 ms cast time timer = timer / spellHaste -- calculate the real cast time of the spell... return self:NewCastTimer(timer / 1000, spellId, ...) end return newTimer(self, "castcount", timer, ...) end ---@overload fun(self: DBMMod, timer: number|string, spellId: number|string?, timerText: number|string?, optionDefault: SpecFlags|boolean?, optionName: string|number|boolean?, colorType: number?, texture: number|string?, inlineIcon: string?, keep: boolean?, countdown: number?, countdownMax: number?, r: number?, g: number?, b: number?, requiresCombat: boolean?): Timer function bossModPrototype:NewCastSourceTimer(timer, ...) if tonumber(timer) and timer > 1000 then -- hehe :) best hack in DBM. This makes the first argument optional, so we can omit it to use the cast time from the spell id ;) local spellId = timer timer = select(4, DBM:GetSpellInfo(spellId)) or 1000 -- GetSpellInfo takes YOUR spell haste into account...WTF? local spellHaste = select(4, DBM:GetSpellInfo(10059)) / 10000 -- 10059 = Stormwind Portal, should have 10000 ms cast time timer = timer / spellHaste -- calculate the real cast time of the spell... return self:NewCastSourceTimer(timer / 1000, spellId, ...) end return newTimer(self, "castsource", timer, ...) end ---@overload fun(self: DBMMod, timer: number|string, spellId: number|string?, timerText: number|string?, optionDefault: SpecFlags|boolean?, optionName: string|number|boolean?, colorType: number?, texture: number|string?, inlineIcon: string?, keep: boolean?, countdown: number?, countdownMax: number?, r: number?, g: number?, b: number?, requiresCombat: boolean?): Timer function bossModPrototype:NewCDTimer(...) return newTimer(self, "cd", ...) end ---@overload fun(self: DBMMod, timer: number|string, spellId: number|string?, timerText: number|string?, optionDefault: SpecFlags|boolean?, optionName: string|number|boolean?, colorType: number?, texture: number|string?, inlineIcon: string?, keep: boolean?, countdown: number?, countdownMax: number?, r: number?, g: number?, b: number?, requiresCombat: boolean?): Timer function bossModPrototype:NewCDCountTimer(...) return newTimer(self, "cdcount", ...) end ---@overload fun(self: DBMMod, timer: number|string, spellId: number|string?, timerText: number|string?, optionDefault: SpecFlags|boolean?, optionName: string|number|boolean?, colorType: number?, texture: number|string?, inlineIcon: string?, keep: boolean?, countdown: number?, countdownMax: number?, r: number?, g: number?, b: number?, requiresCombat: boolean?): Timer function bossModPrototype:NewCDSourceTimer(...) return newTimer(self, "cdsource", ...) end ---@overload fun(self: DBMMod, timer: number|string, spellId: number|string?, timerText: number|string?, optionDefault: SpecFlags|boolean?, optionName: string|number|boolean?, colorType: number?, texture: number|string?, inlineIcon: string?, keep: boolean?, countdown: number?, countdownMax: number?, r: number?, g: number?, b: number?, requiresCombat: boolean?): Timer function bossModPrototype:NewNextTimer(...) return newTimer(self, "next", ...) end ---@overload fun(self: DBMMod, timer: number|string, spellId: number|string?, timerText: number|string?, optionDefault: SpecFlags|boolean?, optionName: string|number|boolean?, colorType: number?, texture: number|string?, inlineIcon: string?, keep: boolean?, countdown: number?, countdownMax: number?, r: number?, g: number?, b: number?, requiresCombat: boolean?): Timer function bossModPrototype:NewNextCountTimer(...) return newTimer(self, "nextcount", ...) end ---@overload fun(self: DBMMod, timer: number|string, spellId: number|string?, timerText: number|string?, optionDefault: SpecFlags|boolean?, optionName: string|number|boolean?, colorType: number?, texture: number|string?, inlineIcon: string?, keep: boolean?, countdown: number?, countdownMax: number?, r: number?, g: number?, b: number?, requiresCombat: boolean?): Timer function bossModPrototype:NewNextSourceTimer(...) return newTimer(self, "nextsource", ...) end ---@overload fun(self: DBMMod, timer: number|string, spellId: number|string?, timerText: number|string?, optionDefault: SpecFlags|boolean?, optionName: string|number|boolean?, colorType: number?, texture: number|string?, inlineIcon: string?, keep: boolean?, countdown: number?, countdownMax: number?, r: number?, g: number?, b: number?, requiresCombat: boolean?): Timer function bossModPrototype:NewAchievementTimer(...) return newTimer(self, "achievement", ...) end ---@overload fun(self: DBMMod, timer: number|string, spellId: number|string?, timerText: number|string?, optionDefault: SpecFlags|boolean?, optionName: string|number|boolean?, colorType: number?, texture: number|string?, inlineIcon: string?, keep: boolean?, countdown: number?, countdownMax: number?, r: number?, g: number?, b: number?, requiresCombat: boolean?): Timer function bossModPrototype:NewCDSpecialTimer(...) return newTimer(self, "cdspecial", ...) end ---@overload fun(self: DBMMod, timer: number|string, spellId: number|string?, timerText: number|string?, optionDefault: SpecFlags|boolean?, optionName: string|number|boolean?, colorType: number?, texture: number|string?, inlineIcon: string?, keep: boolean?, countdown: number?, countdownMax: number?, r: number?, g: number?, b: number?, requiresCombat: boolean?): Timer function bossModPrototype:NewNextSpecialTimer(...) return newTimer(self, "nextspecial", ...) end ---@overload fun(self: DBMMod, timer: number|string, spellId: number|string?, timerText: number|string?, optionDefault: SpecFlags|boolean?, optionName: string|number|boolean?, colorType: number?, texture: number|string?, inlineIcon: string?, keep: boolean?, countdown: number?, countdownMax: number?, r: number?, g: number?, b: number?, requiresCombat: boolean?): Timer function bossModPrototype:NewCDComboTimer(...) return newTimer(self, "cdcombo", ...) end ---@overload fun(self: DBMMod, timer: number|string, spellId: number|string?, timerText: number|string?, optionDefault: SpecFlags|boolean?, optionName: string|number|boolean?, colorType: number?, texture: number|string?, inlineIcon: string?, keep: boolean?, countdown: number?, countdownMax: number?, r: number?, g: number?, b: number?, requiresCombat: boolean?): Timer function bossModPrototype:NewNextComboTimer(...) return newTimer(self, "nextcombo", ...) end ---@overload fun(self: DBMMod, timer: number|string, spellId: number|string?, timerText: number|string?, optionDefault: SpecFlags|boolean?, optionName: string|number|boolean?, colorType: number?, texture: number|string?, inlineIcon: string?, keep: boolean?, countdown: number?, countdownMax: number?, r: number?, g: number?, b: number?, requiresCombat: boolean?): Timer function bossModPrototype:NewStageTimer(...) return newTimer(self, "stage", ...) end bossModPrototype.NewPhaseTimer = bossModPrototype.NewStageTimer--Deprecated naming, once all mods are converted over, NewPhaseTimer will be wiped out for NewStageTimer ---@overload fun(self: DBMMod, timer: number|string, spellId: number|string?, timerText: number|string?, optionDefault: SpecFlags|boolean?, optionName: string|number|boolean?, colorType: number?, texture: number|string?, inlineIcon: string?, keep: boolean?, countdown: number?, countdownMax: number?, r: number?, g: number?, b: number?, requiresCombat: boolean?): Timer function bossModPrototype:NewStageCountTimer(...) return newTimer(self, "stagecount", ...) end --Used mainly for compat with BW/LW timers where they use "stages" but then use the spell/journal descriptor instead of "stage d" --Basically, it's a generic spellName timer for "stages" callback ---@overload fun(self: DBMMod, timer: number|string, spellId: number|string?, timerText: number|string?, optionDefault: SpecFlags|boolean?, optionName: string|number|boolean?, colorType: number?, texture: number|string?, inlineIcon: string?, keep: boolean?, countdown: number?, countdownMax: number?, r: number?, g: number?, b: number?, requiresCombat: boolean?): Timer function bossModPrototype:NewStageContextTimer(...) return newTimer(self, "stagecontext", ...) end --Same as NewStageContextTimer, with count ---@overload fun(self: DBMMod, timer: number|string, spellId: number|string?, timerText: number|string?, optionDefault: SpecFlags|boolean?, optionName: string|number|boolean?, colorType: number?, texture: number|string?, inlineIcon: string?, keep: boolean?, countdown: number?, countdownMax: number?, r: number?, g: number?, b: number?, requiresCombat: boolean?): Timer function bossModPrototype:NewStageContextCountTimer(...) return newTimer(self, "stagecontextcount", ...) end ---@overload fun(self: DBMMod, timer: number|string, spellId: number|string?, timerText: number|string?, optionDefault: SpecFlags|boolean?, optionName: string|number|boolean?, colorType: number?, texture: number|string?, inlineIcon: string?, keep: boolean?, countdown: number?, countdownMax: number?, r: number?, g: number?, b: number?, requiresCombat: boolean?): Timer function bossModPrototype:NewStageCountCycleTimer(...) return newTimer(self, "stagecountcycle", ...) end ---@overload fun(self: DBMMod, timer: number|string, spellId: number|string?, timerText: number|string?, optionDefault: SpecFlags|boolean?, optionName: string|number|boolean?, colorType: number?, texture: number|string?, inlineIcon: string?, keep: boolean?, countdown: number?, countdownMax: number?, r: number?, g: number?, b: number?, requiresCombat: boolean?): Timer function bossModPrototype:NewIntermissionTimer(...) return newTimer(self, "intermission", ...) end ---@overload fun(self: DBMMod, timer: number|string, spellId: number|string?, timerText: number|string?, optionDefault: SpecFlags|boolean?, optionName: string|number|boolean?, colorType: number?, texture: number|string?, inlineIcon: string?, keep: boolean?, countdown: number?, countdownMax: number?, r: number?, g: number?, b: number?, requiresCombat: boolean?): Timer function bossModPrototype:NewIntermissionCountTimer(...) return newTimer(self, "intermissioncount", ...) end ---@overload fun(self: DBMMod, timer: number|string, spellId: number|string?, timerText: number|string?, optionDefault: SpecFlags|boolean?, optionName: string|number|boolean?, colorType: number?, texture: number|string?, inlineIcon: string?, keep: boolean?, countdown: number?, countdownMax: number?, r: number?, g: number?, b: number?, requiresCombat: boolean?): Timer function bossModPrototype:NewRPTimer(...) return newTimer(self, "roleplay", ...) end function bossModPrototype:NewCombatTimer(timer) return newTimer(self, "combat", timer, nil, nil, nil, nil, 0, "132349", nil, nil, 1, 5) end ---@overload fun(self: DBMMod, timer: number|string, spellId: number|string?, timerText: number|string?, optionDefault: SpecFlags|boolean?, optionName: string|number|boolean?, colorType: number?, texture: number|string?, inlineIcon: string?, keep: boolean?, countdown: number?, countdownMax: number?, r: number?, g: number?, b: number?, requiresCombat: boolean?): Timer function bossModPrototype:NewAddsTimer(...) return newTimer(self, "adds", ...) end ---@overload fun(self: DBMMod, timer: number|string, spellId: number|string?, timerText: number|string?, optionDefault: SpecFlags|boolean?, optionName: string|number|boolean?, colorType: number?, texture: number|string?, inlineIcon: string?, keep: boolean?, countdown: number?, countdownMax: number?, r: number?, g: number?, b: number?, requiresCombat: boolean?): Timer function bossModPrototype:NewAddsCustomTimer(...) return newTimer(self, "addscustom", ...) end ---@overload fun(self: DBMMod, timer: number|string, spellId: number|string?, timerText: number|string?, optionDefault: SpecFlags|boolean?, optionName: string|number|boolean?, colorType: number?, texture: number|string?, inlineIcon: string?, keep: boolean?, countdown: number?, countdownMax: number?, r: number?, g: number?, b: number?, requiresCombat: boolean?): Timer function bossModPrototype:NewCDNPTimer(...) return newTimer(self, "cdnp", ...) end ---@overload fun(self: DBMMod, timer: number|string, spellId: number|string?, timerText: number|string?, optionDefault: SpecFlags|boolean?, optionName: string|number|boolean?, colorType: number?, texture: number|string?, inlineIcon: string?, keep: boolean?, countdown: number?, countdownMax: number?, r: number?, g: number?, b: number?, requiresCombat: boolean?): Timer function bossModPrototype:NewNextNPTimer(...) return newTimer(self, "nextnp", ...) end ---@overload fun(self: DBMMod, timer: number|string, spellId: number|string?, timerText: number|string?, optionDefault: SpecFlags|boolean?, optionName: string|number|boolean?, colorType: number?, texture: number|string?, inlineIcon: string?, keep: boolean?, countdown: number?, countdownMax: number?, r: number?, g: number?, b: number?, requiresCombat: boolean?): Timer function bossModPrototype:NewCDCountNPTimer(...) return newTimer(self, "cdcountnp", ...) end ---@overload fun(self: DBMMod, timer: number|string, spellId: number|string?, timerText: number|string?, optionDefault: SpecFlags|boolean?, optionName: string|number|boolean?, colorType: number?, texture: number|string?, inlineIcon: string?, keep: boolean?, countdown: number?, countdownMax: number?, r: number?, g: number?, b: number?, requiresCombat: boolean?): Timer function bossModPrototype:NewNextCountNPTimer(...) return newTimer(self, "nextcountnp", ...) end ---@overload fun(self: DBMMod, timer: number|string, spellId: number|string?, timerText: number|string?, optionDefault: SpecFlags|boolean?, optionName: string|number|boolean?, colorType: number?, texture: number|string?, inlineIcon: string?, keep: boolean?, countdown: number?, countdownMax: number?, r: number?, g: number?, b: number?, requiresCombat: boolean?): Timer function bossModPrototype:NewAITimer(...) return newTimer(self, "ai", ...) end function bossModPrototype:GetLocalizedTimerText(timerType, spellId, Name) local spellName if Name then spellName = Name--Pull from name stored in object elseif spellId then DBM:Debug("|cffff0000GetLocalizedTimerText fallback, this should not happen and is a bug. this fallback should be deleted if this message is never seen after async code is live|r") spellName = DBM:ParseSpellName(spellId, timerType) --Name wasn't provided, but we succeeded in getting a name, generate one into object now for caching purposes --This would really only happen if GetSpellName failed to return spell name on first attempt (which now happens in 9.0) if spellName then self.name = spellName end end return pformat(L.AUTO_TIMER_TEXTS[timerType], spellName) end