Bastion aims to serve as a highly performant, simplisitic, and expandable World of Warcraft data visualization framework.
You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
Bastion/src/TimeToDie/TimeToDie.lua

337 lines
12 KiB

---@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<number, { time: number, percentage: number }>
Cache = {}, -- A cache of unused { time, value } tables to reduce garbage due to table creation
---@type table<string, { history: table<number, { time: number, percentage: number }>, time: number }>
Units = {},
---@type table<string, boolean>
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