HeroLib TTD?

main
ck 1 year ago
parent 3d203c280c
commit 4d0719b869
  1. 16
      src/APL/APL.lua
  2. 2
      src/Aura/Aura.lua
  3. 4
      src/Cacheable/Cacheable.lua
  4. 2
      src/Class/Class.lua
  5. 82
      src/Config/Config.lua
  6. 2
      src/Item/Item.lua
  7. 2
      src/ItemBook/ItemBook.lua
  8. 2
      src/Library/Library.lua
  9. 2
      src/MythicPlusUtils/MythicPlusUtils.lua
  10. 9
      src/ObjectManager/ObjectManager.lua
  11. 2
      src/Refreshable/Refreshable.lua
  12. 50
      src/Spell/Spell.lua
  13. 2
      src/SpellBook/SpellBook.lua
  14. 282
      src/TimeToDie/TimeToDie.lua
  15. 2
      src/Timer/Timer.lua
  16. 397
      src/Unit/Unit.lua
  17. 9
      src/UnitManager/UnitManager.lua
  18. 9
      src/Vector3/Vector3.lua
  19. 39
      src/_bastion.lua

@ -54,17 +54,17 @@ APLActor.__index = APLActor
function APLActor:New(actor)
local self = setmetatable({}, APLActor)
if actor.type == "spell" then
self.name = string.format("[%s] `%s`<%s>", actor.type, actor.spell:GetName(), actor.spell:GetID())
self.name = string.format("[%s] `%s`<%s>", "spell", actor.spell:GetName(), actor.spell:GetID())
elseif actor.type == "item" then
self.name = string.format("[%s] `%s`<%s>", actor.type, actor.item:GetName(), actor.item:GetID())
self.name = string.format("[%s] `%s`<%s>", "item", actor.item:GetName(), actor.item:GetID())
elseif actor.type == "apl" then
self.name = string.format("[%s] `%s`", actor.type, actor.apl.name)
self.name = string.format("[%s] `%s`", "apl", actor.apl.name)
elseif actor.type == "sequencer" then
self.name = string.format("[%s]", actor.type)
self.name = string.format("[%s]", "sequencer")
elseif actor.type == "variable" then
self.name = string.format("[%s] `%s`", actor.type, actor.variable)
self.name = string.format("[%s] `%s`", "variable", actor.variable)
elseif actor.type == "action" then
self.name = string.format("[%s] `%s`", actor.type, actor.action)
self.name = string.format("[%s] `%s`", "action", actor.action)
else
self.name = string.format("[%s] Unknown", actor.type)
end
@ -141,10 +141,10 @@ function APLActor:Execute()
---@cast actorTable APLActorItemTable
return actorTable.item:UsableIf(actorTable.usableFunc):Use(actorTable.target, actorTable.condition)
end
if actorTable.type == "action" then
if self:GetActor().type == "action" then
---@cast actorTable APLActorActionTable
-- print("Bastion: APL:Execute: Executing action " .. actorTable.action)
actorTable.cb(self)
self:GetActor().cb()
end
if actorTable.type == "variable" then
---@cast actorTable APLActorVariableTable

@ -1,5 +1,5 @@
-- Document with emmy lua: https://emmylua.github.io/
---@types Tinkr, Bastion
---@type Tinkr, Bastion
local Tinkr, Bastion = ...
-- Create a new Aura class

@ -4,7 +4,7 @@ local Tinkr, Bastion = ...
-- Define a Cacheable class
---@class Cacheable<V>: { value: V, cache: Cache, callback?: fun(): V }
---@class Cacheable
---@class Cacheable<V>
local Cacheable = {
cache = nil,
callback = nil,
@ -40,7 +40,7 @@ end
-- Create
---@generic V : Cacheable, V
---@param value `V`
---@param value V
---@param cb fun(): V
function Cacheable:New(value, cb)
local self = setmetatable({}, Cacheable)

@ -1,4 +1,4 @@
---@types Tinkr, Bastion
---@type Tinkr, Bastion
local Tinkr, Bastion = ...
-- Create a new Class class

@ -0,0 +1,82 @@
---@type Tinkr, Bastion
local Tinkr, Bastion = ...
---@class Config
---@field instantiated boolean
---@field config Tinkr.Util.Config.Instance
---@field defaults nil | table
local Config = {
instantiated = false,
}
function Config:__index(k)
local response = Config[k]
if response == nil then
response = rawget(self, k)
end
if response == nil and self.instantiated then
response = self:Read(k)
end
return response
end
function Config:__newindex(key, value)
if self.instantiated then
self:Write(key, value)
else
rawset(self, key, value)
end
end
---@generic D
---@param name string
---@param defaults? D
function Config:New(name, defaults)
local self = setmetatable({}, Config)
self.config = Tinkr.Util.Config:New(name)
self.defaults = type(defaults) == "table" and defaults or {}
self.instantiated = true
return self
end
---@generic D
---@param key string
---@param default? D
---@return D
function Config:Read(key, default)
if type(default) == "nil" then
default = self.defaults[key]
end
return self.config:Read(key, default)
end
function Config:Reset()
if type(self.defaults) == "table" then
-- Clear all values currently in the config.
for key, _ in pairs(self.config.data) do
self:Write(key, nil)
end
-- Use default table to write new defaults.
for key, value in pairs(self.defaults) do
self:Write(key, value)
end
return true
end
return false
end
---@param key string
---@param value any
function Config:Write(key, value)
self.config:Write(key, value)
end
---@param key string
function Config:Sync(key)
self.config:Sync(key)
end
return Config

@ -122,7 +122,7 @@ end
---@return number
function Item:GetCooldownRemaining()
local start, duration = C_Container.GetItemCooldown(self:GetID())
return start + duration - GetTime()
return start == 0 and 0 or start + duration - GetTime()
end
-- Use the Item

@ -1,4 +1,4 @@
---@types Tinkr, Bastion
---@type Tinkr, Bastion
local Tinkr, Bastion = ...
-- Create a new ItemBook class

@ -1,4 +1,4 @@
---@types Tinkr, Bastion
---@type Tinkr, Bastion
local Tinkr, Bastion = ...
---@class Library

@ -1,4 +1,4 @@
---@types Tinkr, Bastion
---@type Tinkr, Bastion
local Tinkr, Bastion = ...
---@class MythicPlusUtils

@ -2,12 +2,13 @@
local Tinkr, Bastion = ...
---@class ObjectManager
---@field _lists table
---@field _lists table<string, { list: List, cb: fun(object: TinkrObjectReference): false | Unit }>
---@field enemies List
---@field friends List
---@field activeEnemies List
---@field explosives List
---@field incorporeal List
---@field others List
local ObjectManager = {}
ObjectManager.__index = ObjectManager
@ -21,13 +22,14 @@ function ObjectManager:New()
self.activeEnemies = Bastion.List:New()
self.explosives = Bastion.List:New()
self.incorporeal = Bastion.List:New()
self.others = Bastion.List:New()
return self
end
-- Register a custom list with a callback
---@param name string
---@param cb function
---@param cb fun(object: TinkrObjectReference): false | Unit
---@return List | false
function ObjectManager:RegisterList(name, cb)
if self._lists[name] then
@ -77,6 +79,7 @@ function ObjectManager:Refresh()
self.activeEnemies:clear()
self.explosives:clear()
self.incorporeal:clear()
self.others:clear()
self:ResetLists()
local objects = Objects()
@ -102,6 +105,8 @@ function ObjectManager:Refresh()
if unit:InCombatOdds() > 80 then
self.activeEnemies:push(unit)
end
else
self.others:push(unit)
end
end
end

@ -1,4 +1,4 @@
---@types Tinkr, Bastion
---@type Tinkr, Bastion
local Tinkr, Bastion = ...
-- Define a Refreshable class

@ -97,9 +97,10 @@ function Spell:PostCast(func)
end
-- Get the spells name
---@param byId? boolean
---@return string
function Spell:GetName()
return select(1, GetSpellInfo(self:GetID()))
function Spell:GetName(byId)
return select(1, GetSpellInfo((byId ~= nil and byId) and self:GetID() or self:GetID()))
end
-- Get the spells icon
@ -109,9 +110,10 @@ function Spell:GetIcon()
end
-- Get the spells cooldown
---@param byId? boolean
---@return number
function Spell:GetCooldown()
return select(2, GetSpellCooldown(self:GetID()))
function Spell:GetCooldown(byId)
return select(2, GetSpellCooldown((byId ~= nil and byId) and self:GetID() or self:GetName(byId)))
end
-- Return the castable function
@ -130,16 +132,21 @@ function Spell:GetOnCastFunction()
end
-- Get the spells cooldown remaining
---@param byId? boolean
---@return number
function Spell:GetCooldownRemaining()
local start, duration = GetSpellCooldown(self:GetID())
function Spell:GetCooldownRemaining(byId)
local start, duration = GetSpellCooldown((byId ~= nil and byId) and self:GetID() or self:GetName())
if start == 0 then
return 0
end
return start + duration - GetTime()
end
-- Get the spell count
---@param byId? boolean
---@return number
function Spell:GetCount()
return GetSpellCount(self:GetID())
function Spell:GetCount(byId)
return GetSpellCount((byId ~= nil and byId) and self:GetID() or self:GetName())
end
-- On cooldown
@ -267,22 +274,26 @@ function Spell:IsOnCooldown()
return select(2, GetSpellCooldown(spellIDName)) > 0
end
function Spell:IsUsableSpell()
return IsUsableSpell(self:GetID())
---@param byId? boolean
---@return boolean, boolean
function Spell:IsUsableSpell(byId)
return IsUsableSpell((byId ~= nil and byId) and self:GetID() or self:GetName())
end
-- Check if the spell is usable
---@param byId? boolean
---@return boolean
function Spell:IsUsable()
local usable, noMana = IsUsableSpell(self:GetID())
return usable or usableExcludes[self:GetID()] and not noMana
function Spell:IsUsable(byId)
local usable, noMana = self:IsUsableSpell(byId)
return usable and not noMana
end
-- Check if the spell is castable
---@param override? boolean
---@return boolean
function Spell:IsKnownAndUsable(override)
return self:IsKnown(override) and not self:IsOnCooldown() and self:IsUsable()
override = override ~= nil and override or false
return self:IsKnown(override) and not self:IsOnCooldown() and self:IsUsable(override and false or true)
end
-- Check if the spell is castable
@ -415,8 +426,11 @@ function Spell:GetTimeSinceLastCastAttempt()
return GetTime() - self.lastCastAttempt
end
function Spell:GetChargeInfo()
return GetSpellCharges(self:GetID())
end
-- Get the spells charges
---@return number
function Spell:GetCharges()
return select(1, GetSpellCharges(self:GetID()))
end
@ -433,7 +447,7 @@ end
-- Get the full cooldown (time until all charges are available)
---@return number
function Spell:GetFullRechargeTime()
local charges, maxCharges, _, duration = GetSpellCharges(self:GetID())
local charges, maxCharges, _, duration = self:GetChargeInfo()
if not charges or not maxCharges or charges == maxCharges then
return 0
end
@ -442,9 +456,9 @@ function Spell:GetFullRechargeTime()
end
function Spell:Recharge()
local charges, maxCharges, startTime, duration = GetSpellCharges(self:GetID())
local charges, maxCharges, startTime, duration = self:GetChargeInfo()
if charges == maxCharges then
return charges
return 0
end
local recharge = startTime + duration - GetTime()

@ -26,6 +26,7 @@ end
end ]]
-- Get a spell from the spellbook
---@param id integer
---@return Spell
function SpellBook:GetSpell(id)
local override = FindSpellOverrideByID(id)
@ -71,6 +72,7 @@ function SpellBook:GetSpellByName(name)
return self:GetSpell(spellID)
end
---@param id integer
---@return Spell
function SpellBook:GetIfRegistered(id)
return self.spells[id] or self.spells[FindSpellOverrideByID(id)]

@ -0,0 +1,282 @@
---@type Tinkr, Bastion
local Tinkr, Bastion = ...
local Cache = Bastion.Caches
local Player = Bastion.UnitManager:Get("player")
local Target = Bastion.UnitManager:Get("target")
--- An attempt to integrate HeroLib TTD timers.
---@class TimeToDie
local TimeToDie = {
Settings = {
-- Refresh time (seconds) : min=0.1, max=2, default = 0.1
Refresh = 0.1,
-- 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<string, {[1]: {[1]: number[], [2]: number}, [2]: number }>
Units = {}, -- Used to track units,
---@type table<string, boolean>
ExistingUnits = {}, -- Used to track GUIDs of currently existing units (to be compared with tracked units)
Throttle = 0
}
function TimeToDie:IterableUnits()
return Bastion.List:New():concat(Bastion.ObjectManager.enemies):concat(Bastion.ObjectManager.explosives):concat(
Bastion.ObjectManager.incorporeal)
end
function TimeToDie:Refresh()
local currentTime = GetTime()
local historyCount = self.Settings.HistoryCount
local historyTime = self.Settings.HistoryTime
local ttdCache = self.Cache
local iterableUnits = self:IterableUnits()
local units = self.Units
local existingUnits = self.ExistingUnits
wipe(existingUnits)
local thisUnit
---@param unit Unit
iterableUnits:each(function(unit)
thisUnit = unit
if thisUnit:Exists() then
local unitGUID = thisUnit:GetGUID()
-- Check if we didn't already scanned this unit.
if unitGUID and not existingUnits[unitGUID] then
existingUnits[unitGUID] = true
local healthPercentage = thisUnit:GetHealthPercent()
-- Check if it's a valid unit
if Player:CanAttack(thisUnit) and 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
tableinsert(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 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 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 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 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 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 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 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 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 Unit
[114361] = function(self)
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 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? 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 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? 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 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

@ -1,4 +1,4 @@
---@types Tinkr, Bastion
---@type Tinkr, Bastion
local Tinkr, Bastion = ...
-- Create a new Timer class

@ -89,9 +89,10 @@ function Unit:Token()
end
-- Get the units name
---@return string
---@return string?
function Unit:GetName()
return select(1, UnitName(self:GetOMToken()))
local unitName, realm = UnitName(self:GetOMToken())
return unitName
end
-- Get the units GUID
@ -503,7 +504,8 @@ local attachmentOverride = {
function Unit:GetHitSphere()
--local srcX, srcY, srcZ = GetUnitAttachmentPosition(self:GetOMToken(), attachmentOverride[self:GetID()] or AttachmentPoisitions.PlayerName)
--local srcHeight = UnitIsMounted(self:GetOMToken()) and 3.081099 or 2.43808
return Bastion.Vector3:New(GetUnitAttachmentPosition(self:GetOMToken(), attachmentOverride[self:GetID()] or AttachmentPoisitions.PlayerName))
return Bastion.Vector3:New(GetUnitAttachmentPosition(self:GetOMToken(),
attachmentOverride[self:GetID()] or AttachmentPoisitions.PlayerName))
end
function Unit:GetLOSSourcePosition()
@ -513,47 +515,61 @@ function Unit:GetLOSSourcePosition()
end
local losFlag = bit.bor(0x10)
-- Check if the unit can see another unit
---@param unit Unit
---@param targetUnit Unit
---@return boolean
function Unit:CanSee(unit)
-- mechagon smoke cloud
-- local mechagonID = 2097
-- local smokecloud = 298602
-- local name, instanceType, difficultyID, difficultyName, maxPlayers, dynamicDifficulty, isDynamic, instanceID, instanceGroupSize, LfgDungeonID =
-- GetInstanceInfo()
-- otherUnit = otherUnit and otherUnit or "player"
-- if instanceID == 2097 then
-- if (self:debuff(smokecloud, unit) and not self:debuff(smokecloud, otherUnit))
-- or (self:debuff(smokecloud, otherUnit) and not self:debuff(smokecloud, unit))
-- then
-- return false
-- end
-- end
local ax, ay, az = ObjectPosition(self:GetOMToken())
local ah = ObjectHeight(self:GetOMToken())
local attx, atty, attz = GetUnitAttachmentPosition(unit:GetOMToken(), 34)
if not attx or not ax then
return false
function Unit:CanSee2(targetUnit)
local npcId = targetUnit:GetID()
if npcId and losBlacklist[npcId] then
return true
end
if not ah then
return false
end
local src = self:GetLOSSourcePosition()
local dst = targetUnit:GetHitSphere()
if (ax == 0 and ay == 0 and az == 0) or (attx == 0 and atty == 0 and attz == 0) then
if (src.x == 0 and src.y == 0 and src.z == 0) or (dst.x == 0 and dst.y == 0 and dst.z == 0) then
return true
end
if not attx or not ax then
local contactPoint = src +
(dst - src):directionOrZero() * math.min(targetUnit:GetDistance(self), self:GetCombatReach())
local x, y, z = TraceLine(src.x, src.y, src.z, contactPoint.x, contactPoint.y, contactPoint.z, losFlag)
if x ~= 0 or y ~= 0 or z ~= 0 then
return false
else
return true
end
end
---@param destination Unit
---@return Vector3
function Unit:GetHitSpherePointFor(destination)
local vThis = Bastion.Vector3:New(self:GetPosition().x, self:GetPosition().y,
self:GetPosition().z + ObjectHeight(self:GetOMToken()))
local vObj = Bastion.Vector3:New(destination:GetPosition().x, destination:GetPosition().y,
destination:GetPosition().z)
local contactPoint = vThis +
(vObj - vThis):directionOrZero() * math.min(destination:GetDistance(self), self:GetCombatReach())
return contactPoint
end
---@param destinationUnit Unit
function Unit:CanSee(destinationUnit)
local src = self:GetPosition()
local dst = destinationUnit:GetPosition()
if ObjectType(self:GetOMToken()) == 6 then
src.z = src.z + ObjectHeight(self:GetOMToken())
else
dst = self:GetHitSpherePointFor(destinationUnit)
end
if (src.x == 0 and src.y == 0 and src.z == 0) or (dst.x == 0 and dst.y == 0 and dst.z == 0) then
return true
end
local x, y, z = TraceLine(src.x, src.y, src.z, dst.x, dst.y, dst.z, losFlag)
local x, y, z = TraceLine(ax, ay, az + ah, attx, atty, attz, losFlag)
if x ~= 0 or y ~= 0 or z ~= 0 then
return false
else
@ -568,10 +584,12 @@ function Unit:IsCasting()
end
function Unit:GetTimeCastIsAt(percent)
local name, text, texture, startTimeMS, endTimeMS, isTradeSkill, castID, notInterruptible, spellId = UnitCastingInfo(self:GetOMToken())
local name, text, texture, startTimeMS, endTimeMS, isTradeSkill, castID, notInterruptible, spellId = UnitCastingInfo(
self:GetOMToken())
if not name then
name, text, texture, startTimeMS, endTimeMS, isTradeSkill, notInterruptible, spellId = UnitChannelInfo(self:GetOMToken())
name, text, texture, startTimeMS, endTimeMS, isTradeSkill, notInterruptible, spellId = UnitChannelInfo(self
:GetOMToken())
end
if name and startTimeMS and endTimeMS then
@ -588,10 +606,12 @@ end
-- Get Casting or channeling spell
---@return Spell | nil
function Unit:GetCastingOrChannelingSpell()
local name, text, texture, startTimeMS, endTimeMS, isTradeSkill, castID, notInterruptible, spellId = UnitCastingInfo(self:GetOMToken())
local name, text, texture, startTimeMS, endTimeMS, isTradeSkill, castID, notInterruptible, spellId = UnitCastingInfo(
self:GetOMToken())
if not name then
name, text, texture, startTimeMS, endTimeMS, isTradeSkill, notInterruptible, spellId = UnitChannelInfo(self:GetOMToken())
name, text, texture, startTimeMS, endTimeMS, isTradeSkill, notInterruptible, spellId = UnitChannelInfo(self
:GetOMToken())
end
if name then
@ -604,10 +624,12 @@ end
-- Get the end time of the cast or channel
---@return number
function Unit:GetCastingOrChannelingEndTime()
local name, text, texture, startTimeMS, endTimeMS, isTradeSkill, castID, notInterruptible, spellId = UnitCastingInfo(self:GetOMToken())
local name, text, texture, startTimeMS, endTimeMS, isTradeSkill, castID, notInterruptible, spellId = UnitCastingInfo(
self:GetOMToken())
if not name then
name, text, texture, startTimeMS, endTimeMS, isTradeSkill, notInterruptible, spellId = UnitChannelInfo(self:GetOMToken())
name, text, texture, startTimeMS, endTimeMS, isTradeSkill, notInterruptible, spellId = UnitChannelInfo(self
:GetOMToken())
end
if name then
@ -651,10 +673,12 @@ end
---@return number
function Unit:GetChannelOrCastPercentComplete()
local name, text, texture, startTimeMS, endTimeMS, isTradeSkill, castID, notInterruptible, spellId = UnitCastingInfo(self:GetOMToken())
local name, text, texture, startTimeMS, endTimeMS, isTradeSkill, castID, notInterruptible, spellId = UnitCastingInfo(
self:GetOMToken())
if not name then
name, text, texture, startTimeMS, endTimeMS, isTradeSkill, notInterruptible, spellId = UnitChannelInfo(self:GetOMToken())
name, text, texture, startTimeMS, endTimeMS, isTradeSkill, notInterruptible, spellId = UnitChannelInfo(self
:GetOMToken())
end
if name and startTimeMS and endTimeMS then
@ -670,10 +694,12 @@ end
-- Check if unit is interruptible
---@return boolean
function Unit:IsInterruptible()
local name, text, texture, startTimeMS, endTimeMS, isTradeSkill, castID, notInterruptible, spellId = UnitCastingInfo(self:GetOMToken())
local name, text, texture, startTimeMS, endTimeMS, isTradeSkill, castID, notInterruptible, spellId = UnitCastingInfo(
self:GetOMToken())
if not name then
name, text, texture, startTimeMS, endTimeMS, isTradeSkill, notInterruptible, spellId = UnitChannelInfo(self:GetOMToken())
name, text, texture, startTimeMS, endTimeMS, isTradeSkill, notInterruptible, spellId = UnitChannelInfo(self
:GetOMToken())
end
if name then
@ -819,7 +845,8 @@ end
---@param unit Unit
---@return boolean
function Unit:IsTanking(unit)
local isTanking, status, threatpct, rawthreatpct, threatvalue = UnitDetailedThreatSituation(self:GetOMToken(), unit:GetOMToken())
local isTanking, status, threatpct, rawthreatpct, threatvalue = UnitDetailedThreatSituation(self:GetOMToken(),
unit:GetOMToken())
return isTanking
end
@ -1064,7 +1091,8 @@ function Unit:TimeToDie()
-- if the unit has more than 5 million health but there's not enough data to make a prediction we can assume there's roughly 250000 damage per second and estimate the time to die
if #self.regression_history < 5 and self:GetMaxHealth() > 5000000 then
return self:GetMaxHealth() / 250000 -- 250000 is an estimate of the average damage per second a well geared group will average
return self:GetMaxHealth() /
250000 -- 250000 is an estimate of the average damage per second a well geared group will average
end
if self.ttd ~= self.ttd or self.ttd < 0 or self.ttd == math.huge then
@ -1374,17 +1402,294 @@ end
function Unit:HasIncomingRessurection()
return self:IsDead() and UnitHasIncomingResurrection(self:GetOMToken())
end
function Unit:LootTarget()
return ObjectLootTarget(self:GetOMToken())
end
function Unit:CanLoot()
return ObjectLootable(self:GetOMToken())
end
function Unit:HasTarget()
return ObjectTarget(self:GetOMToken()) ~= false
end
function Unit:Target()
return self:HasTarget() and Bastion.UnitManager:Get(ObjectTarget(self:GetOMToken()):unit()) or Bastion.UnitManager:Get("none")
return self:HasTarget() and Bastion.UnitManager:Get(ObjectTarget(self:GetOMToken()):unit()) or
Bastion.UnitManager:Get("none")
end
local dummyUnits = {
-- City (SW, Orgri, ...)
[31146] = true, -- Raider's Training Dummy
[31144] = true, -- Training Dummy
[32666] = true, -- Training Dummy
[32667] = true, -- Training Dummy
[46647] = true, -- Training Dummy
[114832] = true, -- PvP Training Dummy
[153285] = true, -- Training Dummy
[153292] = true, -- Training Dummy
-- MoP Shrine of Two Moons
[67127] = true, -- Training Dummy
-- WoD Alliance Garrison
[87317] = true, -- Mage Tower Damage Training Dummy
[87318] = true, -- Mage Tower Damage Dungeoneer's Training Dummy (& Garrison)
[87320] = true, -- Mage Tower Damage Raider's Training Dummy
[88314] = true, -- Tanking Dungeoneer's Training Dummy
[88316] = true, -- Healing Training Dummy ----> FRIENDLY
-- WoD Horde Garrison
[87760] = true, -- Mage Tower Damage Training Dummy
[87761] = true, -- Mage Tower Damage Dungeoneer's Training Dummy (& Garrison)
[87762] = true, -- Mage Tower Damage Raider's Training Dummy
[88288] = true, -- Tanking Dungeoneer's Training Dummy
[88289] = true, -- Healing Training Dummy ----> FRIENDLY
-- Legion Druid Class Order Hall
[113964] = true, -- Raider's Training Dummy
[113966] = true, -- Dungeoneer's Training Dummy
-- Legion Mage Class Order Hall
[103397] = true, -- Greater Bullwark Construct
[103404] = true, -- Bullwark Construct
[103402] = true, -- Lesser Bullwark Construct
-- Legion Priest Class Order Hall
[107555] = true, -- Bound void Wraith
[107556] = true, -- Bound void Walker
-- Legion Rogue Class Order Hall
[92164] = true, -- Training Dummy
[92165] = true, -- Dungeoneer's Training Dummy
[92166] = true, -- Raider's Training Dummy
-- Legion Warlock Class Order Hall
[101956] = true, -- Rebellious Fel Lord
[102045] = true, -- Rebellious WrathGuard
[102048] = true, -- Rebellious Felguard
[102052] = true, -- Rebellious imp
-- BfA Dazar'Alor
[144081] = true, -- Training Dummy
[144082] = true, -- Training Dummy
[144085] = true, -- Training Dummy
[144086] = true, -- Raider's Training Dummy
-- BfA Boralus
[126781] = true, -- Training Dummy
[131983] = true, -- Raider's Training Dummy
[131989] = true, -- Training Dummy
[131992] = true, -- Dungeoneer's Training Dummy
-- Shadowlands Kyrian
[154564] = true, -- Valiant's Humility
[154567] = true, -- Purity's Cleaning
[154580] = true, -- Reinforced Guardian
[154583] = true, -- Starlwart Guardian
[154585] = true, -- Valiant's Resolve
[154586] = true, -- Stalwart Phalanx
[160325] = true, -- Humility's Obedience
-- Shadowlands Venthyr
[173942] = true, -- Training Dummy
[175449] = true, -- Raider's Training Dummy
[175450] = true, -- Dungeoneer's Training Dummy
[175451] = true, -- Dungeoneer's Tanking Dummy
[175452] = true, -- Raider's Tanking Dummy
[175455] = true, -- Cleave Training Dummy
[175456] = true, -- Swarm Training Dummy
[175462] = true, -- Sinfall Fiend
-- Shadowlands Night Fae
[174565] = true, -- Dungeoneer's Tanking Dummy
[174566] = true, -- Raider's Tanking Dummy
[174567] = true, -- Raider's Training Dummy
[174568] = true, -- Dungeoneer's Training Dummy
[174569] = true, -- Training Dummy
[174570] = true, -- Swarm Training Dummy
[174571] = true, -- Cleave Training Dummy
-- Shadowlands Necrolord
[174484] = true, -- Dungeoneer's Training Dummy
[174487] = true, -- Training Dummy
[174488] = true, -- Raider's Training Dummy
[174491] = true, -- Tanking Dummy
-- DargonFlight Valdrakken
[198594] = true, -- Cleave Training Dummy
[194648] = true, -- Training Dummy
[189632] = true, -- Animated Duelist
[194643] = true, -- Dungeoneer's Training Dummy
[194644] = true, -- Dungeoneer's Training Dummy
[197833] = true, -- PvP Training Dummy
[189617] = true, -- Boulderfist
[194649] = true, -- Normal Tank Dummy
-- DargonFlight Iskaara
[193563] = true, -- Training Dummy
-- Other
[65310] = true, -- Turnip Punching Bag (toy)
[66374] = true, -- Anatomical Dummy (toy)
[196394] = true, -- Tuskarr Training Dummy (toy)
[196406] = true, -- Rubbery Fish Head (toy)
[199057] = true, -- Black Dragon's Challenge Dummy (toy)
}
function Unit:IsDummy()
local npcId = self:GetID()
return npcId >= 0 and dummyUnits[npcId] == true
end
---@param npcId? number
function Unit:IsInBossList(npcId)
npcId = npcId or self:GetID()
for i = 1, 4 do
local thisUnit = Bastion.UnitManager:Get(string.format("boss%d", i))
if thisUnit:Exists() and thisUnit:GetID() == npcId then
return true
end
end
end
---@param npcId number
function Unit:SpecialTTDPercentage(npcId)
local specialTTDPercentage = Bastion.TimeToDie.specialTTDPercentageData[npcId]
if not specialTTDPercentage then return 0 end
if type(specialTTDPercentage) == "number" then
return specialTTDPercentage
end
return specialTTDPercentage(self)
end
---@param npcId? number
---@param hp? number
function Unit:CheckHPFromBossList(npcId, hp)
npcId = npcId or self:GetID()
local thisHP = hp or 100
for i = 1, 4 do
local thisUnit = Bastion.UnitManager:Get(string.format("boss%d", i))
if thisUnit:Exists() and thisUnit:GetID() == npcId and thisUnit:GetHealthPercent() <= thisHP then
return true
end
end
return false
end
function Unit:TimeToX(percentage, minSamples)
if self:IsDummy() then return 6666 end
if self:IsPlayer() and Player:CanAttack(self) then return 25 end
local seconds = 8888
local unitGuid = self:GetGUID()
if not unitGuid then
return seconds
end
local unitTable = Bastion.TimeToDie.Units[unitGuid]
-- Simple linear regression
-- ( E(x^2) E(x) ) ( a ) ( E(xy) )
-- ( E(x) n ) ( b ) = ( E(y) )
-- Format of the above: ( 2x2 Matrix ) * ( 2x1 Vector ) = ( 2x1 Vector )
-- Solve to find a and b, satisfying y = a + bx
-- Matrix arithmetic has been expanded and solved to make the following operation as fast as possible
if unitTable then
minSamples = minSamples or 3
local values = unitTable[1]
local n = #values
if n > minSamples then
local a, b = 0, 0
local ex2, ex, exy, ey = 0, 0, 0, 0
for i = 1, n do
local value = values[i]
local x, y = value[1], value[2]
ex2 = ex2 + x * x
ex = ex + x
exy = exy + x * y
ey = ey + y
end
-- Invariant to find matrix inverse
local invariant = 1 / (ex2 * n - ex * ex)
-- Solve for a and b
a = (-ex * exy * invariant) + (ex2 * ey * invariant)
b = (n * exy * invariant) - (ex * ey * invariant)
if b ~= 0 then
-- Use best fit line to calculate estimated time to reach target health
seconds = (percentage - a) / b
-- Subtract current time to obtain "time remaining"
seconds = math.min(7777, seconds - (GetTime() - unitTable[2]))
if seconds < 0 then seconds = 9999 end
end
end
end
return seconds
end
---@param minSamples? number
function Unit:TimeToDie2(minSamples)
if not self:Exists() then
return 11111
end
local unitGuid = self:GetGUID()
if not unitGuid then return 11111 end
minSamples = minSamples or 3
---@type {TTD: {[number]: number}}
local unitInfo = Cache.UnitInfo:IsCached(unitGuid) and Cache.UnitInfo:Get(unitGuid) or {}
local ttd = unitInfo.TTD
if not ttd then
ttd = {}
unitInfo.TTD = ttd
end
if not ttd[minSamples] then
ttd[minSamples] = self:TimeToX(self:SpecialTTDPercentage(self:GetID()), minSamples)
end
Bastion.Caches.UnitInfo:Set(unitGuid, unitInfo, .5)
return ttd[minSamples]
end
-- Get the boss unit TimeToDie
---@param minSamples? number
function Unit:BossTimeToDie(minSamples)
if self:IsInBossList() or self:IsDummy() then
return self:TimeToDie2(minSamples)
end
return 11111
end
-- Get if the unit meets the TimeToDie requirements.
---@param operator CompareThisTable
---@param value number
---@param offset number
---@param valueThreshold number
---@param minSamples? number
function Unit:FilteredTimeToDie(operator, value, offset, valueThreshold, minSamples)
local TTD = self:TimeToDie2(minSamples)
return TTD < (valueThreshold or 7777) and Bastion.Utils.CompareThis(operator, TTD + (offset or 0), value) or false
end
-- Get if the boss unit meets the TimeToDie requirements.
---@param operator CompareThisTable
---@param value number
---@param offset number
---@param valueThreshold number
---@param minSamples? number
function Unit:BossFilteredTimeToDie(operator, value, offset, valueThreshold, minSamples)
if self:IsInBossList() or self:IsDummy() then
return self:FilteredTimeToDie(operator, value, offset, valueThreshold, minSamples)
end
return false
end
-- Get if the Time To Die is Valid for an Unit (i.e. not returning a warning code).
---@param minSamples? number
function Unit:TimeToDieIsNotValid(minSamples)
return self:TimeToDie2(minSamples) >= 7777
end
-- Get if the Time To Die is Valid for a boss Unit (i.e. not returning a warning code or not being a boss).
---@param minSamples? number
function Unit:BossTimeToDieIsNotValid(minSamples)
if self:IsInBossList() then
return self:TimeToDieIsNotValid(minSamples)
end
return true
end
-- local empowering = {}

@ -5,12 +5,12 @@ local ObjectManager = Tinkr.Util.ObjectManager
local Unit = Bastion.Unit
---@class CacheableUnit<U> : Cacheable<U>
---@class CacheableUnit<U> : Cacheable<Unit>
-- Create a new UnitManager class
---@class UnitManager : { [UnitId]: Unit }
---@field units table<string, Unit>
---@field customUnits table<string, { unit: CacheableUnit, cb: fun(unit: Unit): Unit }>
---@field customUnits table<string, { unit: Cacheable<Unit>, cb: fun(unit: Unit): Unit }>
---@field objects table<string, Unit>
---@field cache Cache
local UnitManager = {
@ -121,8 +121,8 @@ end
-- Create a custom unit and cache it for .5 seconds
---@param token string
---@param cb fun(): Unit
---@return Cacheable<Unit>
---@param cb fun(unit: Unit?): Unit
---@return Unit
function UnitManager:CreateCustomUnit(token, cb)
local unit = cb()
local cachedUnit = Bastion.Cacheable:New(unit, cb)
@ -269,6 +269,7 @@ function UnitManager:GetFriendWithMostFriends(radius)
end
-- Get the enemy with the most enemies within a given radius
---@return Unit|nil, Unit[]
function UnitManager:GetEnemiesWithMostEnemies(radius)
local unit = nil
local count = 0

@ -2,10 +2,11 @@
---@class Vector3
---@operator add(Vector3): Vector3
---@operator sub(Vector3): Vector3
---@operator mul(Vector3): Vector3
---@operator div(Vector3): Vector3
---@operator sub(Vector3|number): Vector3
---@operator mul(number): Vector3
---@operator div(number): Vector3
---@operator unm(): Vector3
---@operator len(): number
local Vector3 = {}
Vector3.__index = Vector3
@ -20,7 +21,7 @@ function Vector3:__add(other)
return Vector3:New(self.x + other.x, self.y + other.y, self.z + other.z)
end
---@param other Vector3
---@param other Vector3 | number
---@return Vector3
function Vector3:__sub(other)
if type(other) == "number" then

@ -124,6 +124,33 @@ Bastion.Timer = Bastion.require("Timer")
Bastion.CombatTimer = Bastion.Timer:New("combat")
Bastion.MythicPlusUtils = Bastion.require("MythicPlusUtils"):New()
Bastion.Notifications = Bastion.NotificationsList:New()
Bastion.Config = Bastion.require("Config")
Bastion.TimeToDie = Bastion.require("TimeToDie")
Bastion.Caches = {
UnitInfo = Bastion.Cache:New()
}
---@enum (key) CompareThisTable
local compareThisTable = {
[">"] = function(A, B) return A > B end,
["<"] = function(A, B) return A < B end,
[">="] = function(A, B) return A >= B end,
["<="] = function(A, B) return A <= B end,
["=="] = function(A, B) return A == B end,
["min"] = function(A, B) return A < B end,
["max"] = function(A, B) return A > B end,
}
Bastion.Utils = {
---@generic A
---@param operator CompareThisTable
---@param a A
---@param b A
CompareThis = function(operator, a, b)
return compareThisTable[operator](a, b)
end
}
local LIBRARIES = {}
---@type Module[]
@ -202,7 +229,9 @@ Bastion.Globals.EventManager:RegisterWoWEvent("COMBAT_LOG_EVENT_UNFILTERED", fun
end
end
end)
local Timer = {
TTD = 0
}
Bastion.Ticker = C_Timer.NewTicker(0.1, function()
if not Bastion.CombatTimer:IsRunning() and UnitAffectingCombat("player") then
Bastion.CombatTimer:Start()
@ -212,6 +241,10 @@ Bastion.Ticker = C_Timer.NewTicker(0.1, function()
if Bastion.Enabled then
Bastion.ObjectManager:Refresh()
if GetTime() > Timer.TTD then
Timer.TTD = GetTime() + Bastion.TimeToDie.Settings.Refresh
Bastion.TimeToDie:Refresh()
end
for i = 1, #MODULES do
MODULES[i]:Tick()
end
@ -233,6 +266,7 @@ function Bastion:FindModule(name)
return nil
end
Bastion.PrintEnabled = false
function Bastion:Print(...)
if not Bastion.PrintEnabled then
@ -294,7 +328,8 @@ Command:Register("dumpspells", "Dump spells to a file", function()
if spellID then
spellName = spellName:gsub("[%W%s]", "")
WriteFile("bastion-" .. UnitClass("player") .. "-" .. rand .. ".lua", "local " .. spellName .. " = Bastion.Globals.SpellBook:GetSpell(" .. spellID .. ")\n", true)
WriteFile("bastion-" .. UnitClass("player") .. "-" .. rand .. ".lua",
"local " .. spellName .. " = Bastion.Globals.SpellBook:GetSpell(" .. spellID .. ")\n", true)
end
i = i + 1
end

Loading…
Cancel
Save