forked from jeffi/Bastion
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.
337 lines
12 KiB
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
|
|
|