---@type Tinkr, Bastion local Tinkr, Bastion = ... local Player = Bastion.UnitManager:Get("player") local Target = Bastion.UnitManager:Get("target") if not Bastion.Globals.UnitInfo then Bastion.Globals.UnitInfo = Bastion.Cache:New() end local Cache = Bastion.Globals.UnitInfo --- An attempt to integrate HeroLib TTD timers. ---@class Bastion.TimeToDie local TimeToDie = { Settings = { -- Refresh time (seconds) : min=0.1, max=2, default = 0.1 Refresh = 0.5, -- 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 }, Cache = {}, -- A cache of unused { time, value } tables to reduce garbage due to table creation ---@type table Units = {}, -- Used to track units, ---@type table ExistingUnits = {}, -- Used to track GUIDs of currently existing units (to be compared with tracked units) Throttle = 0 } function TimeToDie:IterableUnits() return Bastion.ObjectManager.enemies end function TimeToDie:Refresh() local currentTime = GetTime() local historyCount = TimeToDie.Settings.HistoryCount local historyTime = TimeToDie.Settings.HistoryTime local ttdCache = TimeToDie.Cache local iterableUnits = Bastion.ObjectManager.enemies local units = TimeToDie.Units local existingUnits = TimeToDie.ExistingUnits wipe(existingUnits) ---@param unit Bastion.Unit iterableUnits:each(function(unit) if unit:Exists() then local unitGUID = unit:GetGUID() -- Check if we didn't already scanned this unit. if unitGUID and not existingUnits[unitGUID] then existingUnits[unitGUID] = true local healthPercentage = unit:GetHealthPercent() -- Check if it's a valid unit if healthPercentage < 100 then local unitTable = units[unitGUID] -- Check if we have seen one time this unit, if we don't then initialize it. if not unitTable or healthPercentage > unitTable[1][1][2] then unitTable = { {}, currentTime } units[unitGUID] = unitTable end local values = unitTable[1] local time = currentTime - unitTable[2] -- Check if the % HP changed since the last check (or if there were none) if not values or healthPercentage ~= values[2] then local value local lastIndex = #ttdCache -- Check if we can re-use a table from the cache if lastIndex == 0 then value = { time, healthPercentage } else value = ttdCache[lastIndex] ttdCache[lastIndex] = nil value[1] = time value[2] = healthPercentage end table.insert(values, 1, value) local n = #values -- Delete values that are no longer valid while (n > historyCount) or (time - values[n][1] > historyTime) do ttdCache[#Cache + 1] = values[n] values[n] = nil n = n - 1 end end end end end return false end) -- Not sure if it's even worth it to do this here -- Ideally this should be event driven or done at least once a second if not less for key in pairs(units) do if not existingUnits[key] then units[key] = nil 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) local bossExists, maxTimeToDie for i = 1, 4 do local bossUnit = Bastion.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 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 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() >= 7777 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 >= 7777 then return false end return Bastion.Utils.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 return TimeToDie