---@type Tinkr local Tinkr, ---@class Bastion Bastion = ... --- An attempt to integrate HeroLib TTD timers. ---@class Bastion.TimeToDie local TimeToDie = { Refreshing = false, Settings = { -- Refresh time (seconds) : min=0.1, max=2, default = 0.1 Refresh = 0.2, -- History time (seconds) : min=5, max=120, default = 10+0.4 HistoryTime = 10 + 0.4, -- Max history count : min=20, max=500, default = 100 HistoryCount = 100, InactiveTime = 60, }, ---@type table Cache = {}, -- A cache of unused { time, value } tables to reduce garbage due to table creation ---@type table, time: number }> Units = {}, ---@type table ExistingUnits = {}, -- Used to track GUIDs of currently existing units (to be compared with tracked units) Throttle = 0, ---@enum Bastion.TimeToDie.Enums Enums = { --- No GUID NO_GUID = -1, -- 11111 --- Negative TTD NEGATIVE_TTD = -2, -- 9999 -- Not updated/Not enough samples NOT_UPDATED = -3, -- 8888 -- No DPS NO_DPS = -4, -- 7777 -- Dummy DUMMY = -5, -- 6666 -- Player PLAYER = -6, -- 25 DOES_NOT_EXIST = -7, }, IterableUnits = { "target", "focus", "mouseover", "boss1", "boss2", "boss3", "boss4", "boss5", "nameplate1", "nameplate2", "nameplate3", "nameplate4", "nameplate5", "nameplate6", "nameplate7", "nameplate8", "nameplate9", "nameplate10", "nameplate11", "nameplate12", "nameplate13", "nameplate14", "nameplate15", "nameplate16", "nameplate17", "nameplate18", "nameplate19", "nameplate20", "nameplate21", "nameplate22", "nameplate23", "nameplate24", "nameplate25", "nameplate26", "nameplate27", "nameplate28", "nameplate29", "nameplate30", "nameplate31", "nameplate32", "nameplate33", "nameplate34", "nameplate35", "nameplate36", "nameplate37", "nameplate38", "nameplate39", "nameplate40", }, NextUpdate = 0, LastStart = 0, Counter = 0, } function TimeToDie:IsRefreshing() return self.Refreshing end function TimeToDie:Init() if not Bastion.Globals.UnitInfo then Bastion.Globals.UnitInfo = Bastion.Cache:New() Bastion.Globals.EventManager:RegisterWoWEvent("UNIT_HEALTH", function(unitId) self:UNIT_HEALTH(unitId) end) end end function TimeToDie:IterableUnits2() return Bastion.ObjectManager.activeEnemies end ---@param force? boolean function TimeToDie:Refresh(force) if self:IsRefreshing() or (self.NextUpdate > GetTime() and not force) or (not Bastion.Enabled and not force) then return end local currentTime = GetTime() self.Refreshing = true self.LastStart = currentTime self.NextUpdate = self.LastStart + 2 local units = TimeToDie.Units for key, _ in pairs(units) do local unit = units[key] if unit.history and unit.history[1] then local lastTime = unit.history[1].time + unit.time if currentTime - lastTime > self.Settings.InactiveTime and Bastion.Globals.UnitManager:Get(key):InCombatOdds() < 80 then units[key] = nil end end end self.Refreshing = false end ---@param sourceGUID string function TimeToDie:UNIT_DIED(sourceGUID) if self.Units[sourceGUID] then self.Units[sourceGUID] = nil end end ---@param unitId UnitId function TimeToDie:UNIT_HEALTH(unitId) local currentTime = GetTime() if UnitExists(unitId) and UnitCanAttack("player", unitId) then local unitGUID = UnitGUID(unitId) if unitGUID then local unitTable = self.Units[unitGUID] local unitHealth = UnitHealth(unitId) local unitMaxHealth = UnitHealthMax(unitId) local unitHealthPerc = (unitHealth / unitMaxHealth) * 100 if unitHealthPerc < 100 then if not unitTable or unitHealthPerc > unitTable.history[1].percentage then unitTable = { history = {}, time = currentTime } self.Units[unitGUID] = unitTable end local history = unitTable.history local time = currentTime - unitTable.time if not history or not history[1] or unitHealthPerc ~= history[1].percentage then local val local lastIndex = #self.Cache if lastIndex == 0 then val = { time = time, percentage = unitHealthPerc } else val = self.Cache[lastIndex] self.Cache[lastIndex] = nil val.time = time val.percentage = unitHealthPerc end table.insert(history, 1, val) local n = #history while (n > self.Settings.HistoryCount) or (time - history[n].time > self.Settings.HistoryTime) do self.Cache[#self.Cache + 1] = history[n] history[n] = nil n = n - 1 end end end end end end TimeToDie.specialTTDPercentageData = { --- Dragonflight ----- Dungeons ----- --- Brackenhide Hollow -- Decatriarch Wratheye [186121] = 5, --- Shadowlands ----- Dungeons ----- --- De Other Side -- Mueh'zala leaves the fight at 10%. [166608] = 10, --- Mists of Tirna Scithe -- Tirnenns leaves the fight at 20%. [164929] = 20, -- Tirnenn Villager [164804] = 20, -- Droman Oulfarran --- Sanguine Depths -- General Kaal leaves the fight at 50%. [162099] = 50, ----- Castle Nathria ----- --- Stone Legion Generals -- General Kaal leaves the fight at 50% if General Grashaal has not fight yet. We take 49% as check value since it get -95% dmg reduction at 50% until intermission is over. ---@param self Bastion.Unit [168112] = function(self) return (not self:CheckHPFromBossList(168113, 99) and 49) or 0 end, --- Sun King's Salvation -- Shade of Kael'thas fight is 60% -> 45% and then 10% -> 0%. ---@param self Bastion.Unit [165805] = function(self) return (self:GetHealthPercent() > 20 and 45) or 0 end, ----- Sanctum of Domination ----- --- Eye of the Jailer leaves at 66% and 33% ---@param self Bastion.Unit [180018] = function(self) return (self:GetHealthPercent() > 66 and 66) or (self:GetHealthPercent() <= 66 and self:GetHealthPercent() > 33 and 33) or 0 end, --- Painsmith Raznal leaves at 70% and 40% ---@param self Bastion.Unit [176523] = function(self) return (self:GetHealthPercent() > 70 and 70) or (self:GetHealthPercent() <= 70 and self:GetHealthPercent() > 40 and 40) or 0 end, --- Fatescribe Roh-Kalo phases at 70% and 40% ---@param self Bastion.Unit [179390] = function(self) return (self:GetHealthPercent() > 70 and 70) or (self:GetHealthPercent() <= 70 and self:GetHealthPercent() > 40 and 40) or 0 end, --- Sylvanas Windrunner intermission at 83% and "dies" at 50% (45% in MM) ---@param self Bastion.Unit [180828] = function(self) local _, _, difficultyId = GetInstanceInfo() return (self:GetHealthPercent() > 83 and 83) or ((difficultyId == 16 and 45) or 50) end, --- Legion ----- Open World ----- --- Stormheim Invasion -- Lord Commander Alexius [118566] = 85, ----- Dungeons ----- --- Halls of Valor -- Hymdall leaves the fight at 10%. [94960] = 10, -- Fenryr leaves the fight at 60%. We take 50% as check value since it doesn't get immune at 60%. ---@param self Bastion.Unit [95674] = function(self) return (self:GetHealthPercent() > 50 and 60) or 0 end, -- Odyn leaves the fight at 80%. [95676] = 80, --- Maw of Souls -- Helya leaves the fight at 70%. [96759] = 70, ----- Trial of Valor ----- --- Odyn -- Hyrja & Hymdall leaves the fight at 25% during first stage and 85%/90% during second stage (HM/MM). ---@param self Bastion.Unit [114360] = function(self) local _, _, difficultyId = GetInstanceInfo() return (not self:CheckHPFromBossList(114263, 99) and 25) or (difficultyId == 16 and 85) or 90 end, ---@param self Bastion.Unit [114361] = function(self) local _, _, difficultyId = GetInstanceInfo() return (not self:CheckHPFromBossList(114263, 99) and 25) or (difficultyId == 16 and 85) or 90 end, -- Odyn leaves the fight at 10%. [114263] = 10, ----- Nighthold ----- --- Elisande leaves the fight two times at 10% then normally dies. She looses 50% power per stage (100 -> 50 -> 0). ---@param self Bastion.Unit [106643] = function(self) return (self:GetPower() > 0 and 10) or 0 end, --- Warlord of Draenor (WoD) ----- Dungeons ----- --- Shadowmoon Burial Grounds -- Carrion Worm doesn't die but leave the area at 10%. [88769] = 10, [76057] = 10, ----- HellFire Citadel ----- --- Hellfire Assault -- Mar'Tak doesn't die and leave fight at 50% (blocked at 1hp anyway). [93023] = 50, --- Classic ----- Dungeons ----- --- Uldaman -- Dwarves [184580] = 5, [184581] = 5, [184582] = 5, } -- Returns the max fight length of boss units, or the current selected target if no boss units ---@param enemies? Bastion.List ---@param bossOnly? boolean function TimeToDie.FightRemains(enemies, bossOnly) ---@type boolean, number local bossExists, maxTimeToDie for i = 1, 4 do local bossUnit = Bastion.Globals.UnitManager:Get(string.format("boss%d", i)) if bossUnit:Exists() then bossExists = true if not bossUnit:TimeToDieIsNotValid() then maxTimeToDie = math.max(maxTimeToDie or 0, bossUnit:TimeToDie2()) end end end if bossExists or bossOnly then -- If we have a boss list but no valid boss time, return invalid return maxTimeToDie or TimeToDie.Enums.NO_GUID -- 11111 end -- If we specify an AoE range, iterate through all the targets in the specified range if enemies then ---@param enemy Bastion.Unit enemies:each(function(enemy) if (enemy:InCombatOdds() > 80 or enemy:IsDummy()) and enemy:TimeToDieIsNotValid() then maxTimeToDie = math.max(maxTimeToDie or 0, enemy:TimeToDie2()) end return false end) if maxTimeToDie then return maxTimeToDie end end return Bastion.Globals.UnitManager:Get("target"):TimeToDie2() end -- Returns the max fight length of boss units, 11111 if not a boss fight function TimeToDie.BossFightRemains() return TimeToDie.FightRemains(nil, true) end -- Get if the Time To Die is Valid for a boss fight remains function TimeToDie.BossFightRemainsIsNotValid() return TimeToDie.BossFightRemains() < 0 end -- Returns if the current fight length meets the requirements. ---@param enemies? Bastion.List ---@param operator CompareThisTable ---@param value number ---@param checkIfValid boolean ---@param bossOnly boolean function TimeToDie.FilteredFightRemains(enemies, operator, value, checkIfValid, bossOnly) local fightRemains = TimeToDie.FightRemains(enemies, bossOnly) if checkIfValid and fightRemains < 0 then return false end return Bastion.Util:CompareThis(operator, fightRemains, value) or false end -- Returns if the current boss fight length meets the requirements, 11111 if not a boss fight. ---@param operator CompareThisTable ---@param value number ---@param checkIfValid boolean function TimeToDie.BossFilteredFightRemains(operator, value, checkIfValid) return TimeToDie.FilteredFightRemains(nil, operator, value, checkIfValid, true) end Bastion.TimeToDie = TimeToDie