Updates to TTD.

main
ck 11 months ago
parent ae0beddcca
commit fb3f8decb6
  1. 5
      src/APL/APL.lua
  2. 13
      src/APLActor/APLActor.lua
  3. 54
      src/EventManager/EventManager.lua
  4. 173
      src/Item/Item.lua
  5. 7
      src/ItemBook/ItemBook.lua
  6. 14
      src/Module/Module.lua
  7. 2
      src/MythicPlusUtils/MythicPlusUtils.lua
  8. 9
      src/ObjectManager/ObjectManager.lua
  9. 80
      src/Spell/Spell.lua
  10. 25
      src/TimeToDie/TimeToDie.lua
  11. 197
      src/Unit/Unit.lua
  12. 6
      src/UnitManager/UnitManager.lua
  13. 2
      src/Vector3/Vector3.lua
  14. 35
      src/_bastion.lua

@ -150,12 +150,9 @@ end
-- Add an APL to the APL (for sub APLs)
---@param apl Bastion.APL
---@param condition fun(...):boolean
---@param condition? fun(...):boolean
---@return Bastion.APLActor
function APL:AddAPL(apl, condition)
if not condition then
error("Bastion: APL:AddAPL: No condition for APL " .. apl.name)
end
local actor = Bastion.APLActor:New({
type = "apl",
apl = apl,

@ -80,12 +80,7 @@ function APLActor:Execute()
-- If the actor is a sequencer we don't want to continue executing the APL if the sequencer is not finished
if actorTable.type == "sequencer" then
---@cast actorTable Bastion.APL.Actor.Sequencer.Table
if actorTable.condition and actorTable.condition() and not actorTable.sequencer:Finished() then
actorTable.sequencer:Execute()
return true
end
if not actorTable.condition and not actorTable.sequencer:Finished() then
if (not actorTable.condition or actorTable.condition()) and not actorTable.sequencer:Finished() then
actorTable.sequencer:Execute()
return true
end
@ -97,11 +92,9 @@ function APLActor:Execute()
end
if actorTable.type == "apl" then
---@cast actorTable Bastion.APL.Actor.APL.Table
if actorTable.condition and actorTable.condition() then
if not actorTable.condition or actorTable.condition() then
-- print("Bastion: APL:Execute: Executing sub APL " .. actorTable.apl.name)
if actorTable.apl:Execute() then
return true
end
return actorTable.apl:Execute()
end
end
if actorTable.type == "spell" then

@ -6,9 +6,10 @@ local Tinkr, Bastion = ...
---@field frame Frame
---@field events table<string, { [number]: fun(...) }>
---@field eventHandlers table<string, { [number]: fun(...) }>
---@field wowEventHandlers table<string, { [number]: fun(...) }>
---@field wowEventHandlers table<WowEvent, { [number]: fun(...) }>
---@field selfCombatEventHandlers table<string, { [number]: fun(...) }>
---@field CombatEventHandlers table<string, { [number]: fun(...) }>
---@field playerGUID string|boolean
local EventManager = {
events = {},
eventHandlers = {},
@ -27,6 +28,7 @@ function EventManager:New()
self.wowEventHandlers = {}
self.selfCombatEventHandlers = {}
self.CombatEventHandlers = {}
self.playerGUID = UnitGUID("player") or false
-- Frame for wow events
self.frame = CreateFrame("Frame")
@ -59,20 +61,14 @@ function EventManager:RegisterEvent(event, handler)
end
-- Register a wow event
---@param event string | string[]
---@param events WowEvent | WowEvent[]
---@param handler fun(...)
---@return nil
function EventManager:RegisterWoWEvent(event, handler)
if type(event) == "table" then
for _, e in ipairs(event) do
if not self.wowEventHandlers[e] then
self.wowEventHandlers[e] = {}
self.frame:RegisterEvent(e)
function EventManager:RegisterWoWEvent(events, handler)
if type(events) == "string" then
events = { events }
end
table.insert(self.wowEventHandlers[e], handler)
end
else
for _, event in ipairs(events) do
if not self.wowEventHandlers[event] then
self.wowEventHandlers[event] = {}
self.frame:RegisterEvent(event)
@ -94,33 +90,33 @@ function EventManager:TriggerEvent(event, ...)
end
end
---@param subevent string | string[]
---@param subevents string | string[]
---@param handler fun(...)
function EventManager:RegisterSelfCombatEvent(subevent, handler)
if type(subevent) == "string" then
subevent = { subevent }
function EventManager:RegisterSelfCombatEvent(subevents, handler)
if type(subevents) == "string" then
subevents = { subevents }
end
for _, e in ipairs(subevent) do
if not self.selfCombatEventHandlers[e] then
self.selfCombatEventHandlers[e] = {}
for _, subevent in ipairs(subevents) do
if not self.selfCombatEventHandlers[subevent] then
self.selfCombatEventHandlers[subevent] = {}
end
table.insert(self.selfCombatEventHandlers[e], handler)
table.insert(self.selfCombatEventHandlers[subevent], handler)
end
end
---@param subevent string | string[]
---@param subevents string | string[]
---@param handler fun(...)
function EventManager:RegisterCombatEvent(subevent, handler)
if type(subevent) == "string" then
subevent = { subevent }
function EventManager:RegisterCombatEvent(subevents, handler)
if type(subevents) == "string" then
subevents = { subevents }
end
for _, e in ipairs(subevent) do
if not self.CombatEventHandlers[e] then
self.CombatEventHandlers[e] = {}
for _, subevent in ipairs(subevents) do
if not self.CombatEventHandlers[subevent] then
self.CombatEventHandlers[subevent] = {}
end
table.insert(self.CombatEventHandlers[e], handler)
table.insert(self.CombatEventHandlers[subevent], handler)
end
end
@ -128,7 +124,7 @@ end
---@param subevent string
---@param ... any
function EventManager:CLEUHandler(timestamp, subevent, ...)
if self.selfCombatEventHandlers[subevent] and select(2, ...) == UnitGUID("player") then
if self.selfCombatEventHandlers[subevent] and self.playerGUID and select(2, ...) == self.playerGUID then
for _, callback in ipairs(self.selfCombatEventHandlers[subevent]) do
callback(timestamp, subevent, ...)
end

@ -1,23 +1,37 @@
---@type Tinkr, Bastion
local Tinkr, Bastion = ...
---@class Bastion.Item.Traits.Use
---@field moving? boolean
---@field dead? boolean
---@field casting? boolean
---@field channeling? boolean
---@field byId? boolean
---@class Bastion.Item.Traits.Target
---@field exists? boolean
---@field player? boolean
-- Create a new Item class
---@class Bastion.Item
---@field ItemID number
---@field itemID number
---@field UsableIfFunc boolean | fun(self:Bastion.Item):boolean
---@field PreUseFunc boolean | fun(self:Bastion.Item)
---@field target boolean | Bastion.Unit
---@field target Bastion.Unit | false
---@field conditions table<string, { func: fun(self:Bastion.Item):boolean }>
---@field OnUseFunc boolean | fun(self:Bastion.Item)
---@field spellID number | nil
---@field lastUpdated boolean|number
---@field playerUsable boolean
---@field shouldUpdate boolean
local Item = {
UsableIfFunc = false,
PreUseFunc = false,
OnUseFunc = false,
wasLooking = false,
lastUpdated = false,
lastUseAttempt = 0,
conditions = {},
target = false,
}
local usableExcludes = {
@ -27,14 +41,13 @@ local usableExcludes = {
---@param itemId number | string
---@return number charges, number maxCharges, number start, number duration
local GetItemCharges = function(itemId)
local spellId = select(2, GetItemSpell(itemId))
local _, spellId = GetItemSpell(itemId)
local charges, maxCharges, start, duration, chargeModRate = GetSpellCharges(spellId)
return charges, maxCharges, start, duration
end
function Item:__index(k)
local response = Bastion.ClassMagic:Resolve(Item, k)
if response == nil then
response = rawget(self, k)
end
@ -62,24 +75,113 @@ end
-- Constructor
---@param id number
function Item:New(id)
---@class Bastion.Item
local self = setmetatable({}, Item)
self.itemID = id
self.spell = false
self.playerUsable = C_PlayerInfo.CanUseItem(id)
self.shouldUpdate = false
self.traits = {
use = {
moving = true,
dead = false,
casting = false,
channeling = false,
byId = false,
},
target = {
exists = true,
player = false,
}
}
self.ItemID = id
-- C_PlayerInfo.CanUseItem
-- Register spell in spellbook
if not C_Item.IsItemDataCachedByID(id) then
self.shouldUpdate = true
C_Item.RequestLoadItemDataByID(id)
else
local name, spellID = GetItemSpell(self:GetID())
if spellID then
self.spellID = spellID
Bastion.Globals.SpellBook:GetSpell(spellID)
end
end
return self
end
function Item:ShouldUpdate()
if self.shouldUpdate and not self.lastUpdated or GetTime() - self.lastUpdated > 30 then
return true
end
end
---@class Bastion.Item.Traits.Params
---@field use? Bastion.Item.Traits.Use
---@field target? Bastion.Item.Traits.Target
---@param traits Bastion.Item.Traits.Params
function Item:SetTraits(traits)
for _, itemTrait in pairs({ "use", "target" }) do
if type(traits[itemTrait]) == "table" then
local currentTrait = traits[itemTrait]
for traitKey, traitValue in pairs(self.traits[itemTrait]) do
if type(currentTrait[traitKey]) ~= "nil" then
self.traits[itemTrait][traitKey] = currentTrait[traitKey]
end
end
end
end
return self
end
function Item:EvaluateTraits()
local player = Bastion.UnitManager:Get("player")
if not self.traits.use.moving and player:IsMoving() then
return false
end
if not self.traits.use.dead and player:IsDead() then
return false
end
if not self.traits.use.casting and player:IsCasting() then
return false
end
if not self.traits.use.channeling and player:IsChanneling() then
return false
end
if self.traits.target.exists then
local target = self:GetTarget()
if type(target) ~= "boolean" and not target:Exists() or target == false then
return false
end
end
return true
end
function Item:Update()
if self:ShouldUpdate() then
if not self.spellID and self.playerUsable then
local name, spellID = GetItemSpell(self:GetID())
if spellID then
self.spellID = spellID
self.shouldUpdate = false
Bastion.Globals.SpellBook:GetSpell(spellID)
end
end
end
end
-- Get the Items id
---@return number
function Item:GetID()
return self.ItemID
return self.itemID
end
-- Get the Items name
@ -126,7 +228,17 @@ function Item:GetCooldownRemaining()
end
-- Use the Item
---@param unit Bastion.Unit
---@param unit? Bastion.Unit
function Item:UseByID(unit)
local target = unit and unit:GetOMToken() or self.traits.target.player and "player" or "target"
RunMacroText(string.format("/use [@%s] item:%d", target, self:GetID()))
Bastion:Debug("Using by id", self)
end
-- Use the Item
---@param unit? Bastion.Unit
---@param condition? string | fun(self:Bastion.Item):boolean
---@return boolean
function Item:Use(unit, condition)
@ -147,12 +259,14 @@ function Item:Use(unit, condition)
self:GetPreUseFunction()(self)
end
local target = unit or self.traits.target.player and Bastion.UnitManager:Get("player") or
self.traits.target.exists and self:TargetExists() and self:GetTarget() or false
if not target then
return false
end
-- Check if the mouse was looking
self.wasLooking = IsMouselooking()
-- Use the Item
UseItemByName(self:GetName(), unit:GetOMToken())
UseItemByName(self.traits.use.byId and self:GetID() or self:GetName(), target:GetOMToken())
Bastion:Debug("Using", self)
-- Set the last Use time
@ -186,7 +300,8 @@ end
-- Check if the Item is on cooldown
---@return boolean
function Item:IsOnCooldown()
return select(2, C_Container.GetItemCooldown(self:GetID())) > 0
local _, duration = C_Container.GetItemCooldown(self:GetID())
return duration > 0
end
-- Check if the Item is usable
@ -211,6 +326,9 @@ end
-- Check if the Item is Usable
---@return boolean
function Item:Usable()
if not self:EvaluateTraits() then
return false
end
if self:GetUsableFunction() then
return self:GetUsableFunction()(self)
end
@ -283,7 +401,18 @@ end
---@param unit Bastion.Unit
---@return boolean
function Item:IsInRange(unit)
local name, rank, icon, UseTime, Itemmin, Itemmax, ItemID = GetItemInfo(self:GetID())
if not ItemHasRange(self:GetID()) or not self.spellID then
return true
end
local itemSpell = self:GetSpell()
if itemSpell then
return itemSpell:IsInRange(unit)
end
return false
--[[ local name, rank, icon, UseTime, Itemmin, Itemmax, ItemID = GetItemInfo(self:GetID())
local them = Object(unit:GetOMToken())
@ -311,7 +440,7 @@ function Item:IsInRange(unit)
return true
end
return false
return false ]]
end
-- Get the last use time
@ -334,6 +463,10 @@ function Item:GetCharges()
return select(1, GetItemCharges(self:GetID()))
end
function Item:GetCount()
return GetItemCount(self:GetID(), false, false, false)
end
-- Get the Items charges remaining
---@return number
function Item:GetChargesRemaining()
@ -389,16 +522,18 @@ end
-- Set the Items target
---@param unit Bastion.Unit
---@return Bastion.Item
function Item:SetTarget(unit)
self.target = unit
return self
end
-- Get the Items target
---@return Bastion.Unit | boolean
function Item:GetTarget()
return self.target
return self.traits.target.player and Bastion.UnitManager:Get("player") or self.target
end
function Item:TargetExists()
return self:GetTarget() and self:GetTarget():Exists() or false
end
-- IsMagicDispel

@ -14,6 +14,13 @@ ItemBook.__index = ItemBook
function ItemBook:New()
local self = setmetatable({}, ItemBook)
self.items = {}
---@param itemId itemId
---@param success boolean
Bastion.Globals.EventManager:RegisterEvent("ITEM_DATA_LOAD_RESULT", function(itemId, success)
if itemId and success and self.items[itemId] then
self.items[itemId]:Update()
end
end)
return self
end

@ -1,9 +1,14 @@
---@type Tinkr, Bastion
local Tinkr, Bastion = ...
-- Create a module class for a bastion module
---@class Bastion.Module
---@field name string
---@field enabled boolean
---@field synced function[]
---@field interval? number
---@field nextTick number
local Module = {}
Module.__index = Module
@ -15,15 +20,17 @@ end
-- Constructor
---@param name string
---@param interval? number
---@return Bastion.Module
function Module:New(name)
function Module:New(name, interval)
local module = {}
setmetatable(module, Module)
module.name = name
module.enabled = false
module.synced = {}
module.interval = interval
module.nextTick = 0
return module
end
@ -67,10 +74,13 @@ end
-- Sync
function Module:Tick()
if self.enabled then
if Bastion.Tick >= self.nextTick then
self.nextTick = Bastion.Tick + (self.interval or 1)
for i = 1, #self.synced do
self.synced[i]()
end
end
end
end
return Module

@ -1300,7 +1300,7 @@ function MythicPlusUtils:CastingCriticalKick(unit, percent)
end
---@param unit Bastion.Unit
---@param percent number
---@param percent? number
---@return boolean
function MythicPlusUtils:CastingCriticalStun(unit, percent)
local castingSpell = unit:GetCastingOrChannelingSpell()

@ -92,8 +92,8 @@ function ObjectManager:Refresh()
for _, object in pairs(objects) do
self:EnumLists(object)
if ({ [5] = true, [6] = true, [7] = true })[ObjectType(object)] then
local objectType = ObjectType(object)
if ({ [5] = true, [6] = true, [7] = true })[objectType] then
local objectGUID = ObjectGUID(object)
if objectGUID then
local unit = Bastion.UnitManager:GetObject(objectGUID)
@ -102,7 +102,8 @@ function ObjectManager:Refresh()
Bastion.UnitManager:SetObject(unit)
end
if ObjectType(object) == 5 and ObjectCreatureType(object) == 8 then
local creatureType = ObjectCreatureType(object)
if objectType == 5 and creatureType == 8 then
self.critters:push(unit)
elseif unit:GetID() == 204560 then
self.incorporeal:push(unit)
@ -110,7 +111,7 @@ function ObjectManager:Refresh()
self.afflicted:push(unit)
elseif unit:GetID() == 120651 then
self.explosives:push(unit)
elseif unit:IsPlayer() and (unit:IsInParty() or unit == Bastion.UnitManager["player"]) then
elseif unit:IsPlayer() and (unit:IsInPartyOrRaid() or unit == Bastion.UnitManager["player"]) then
self.friends:push(unit)
elseif unit:IsEnemy() then
self.enemies:push(unit)

@ -9,6 +9,15 @@ local Tinkr, Bastion = ...
---@field channeling boolean
---@field override boolean
---@field talent boolean | spellId
---@field power boolean
---@class Bastion.Spell.Traits.Cost
---@field type Enum.PowerType
---@field cost number
---@field minCost number
---@field requiredAuraID number
---@field costPercent number
---@field costPerSecond number
---@class Bastion.Spell.Traits.Target
---@field exists boolean
@ -17,13 +26,16 @@ local Tinkr, Bastion = ...
---@class Bastion.Spell.Traits
---@field cast Bastion.Spell.Traits.Cast
---@field target Bastion.Spell.Traits.Target
---@field cost Bastion.Spell.Traits.Cost[]
---@class Bastion.Spell.Traits.Cast.Params : Bastion.Spell.Traits.Cast, { [string]?: boolean }
---@class Spell.Traits.Target.Params : Bastion.Spell.Traits.Target, { [string]?: boolean }
---@class Spell.Traits.Cost.Params : Bastion.Spell.Traits.Cost[]
---@class Bastion.Spell.Traits.Params
---@field cast? Bastion.Spell.Traits.Cast.Params
---@field target? Spell.Traits.Target.Params
---@field cost? Spell.Traits.Cost.Params
---@class Bastion.Spell.Aura
---@field spell Bastion.Spell
@ -49,7 +61,17 @@ local Tinkr, Bastion = ...
---@field target Bastion.Unit | false
---@field traits Bastion.Spell.Traits
---@field wasLooking boolean
local Spell = {}
local Spell = {
CastableIfFunc = false,
damage = 0,
damageFormula = false,
lastCastAt = false,
lastCastAttempt = false,
OnCastFunc = false,
PostCastFunc = false,
PreCastFunc = false,
release_at = false,
}
local usableExcludes = {
[18562] = true,
@ -86,16 +108,7 @@ function Spell:New(id)
---@class Bastion.Spell
local self = setmetatable({}, Spell)
self.auras = {}
self.CastableIfFunc = false
self.damage = 0
self.damageFormula = false
self.lastCastAt = false
self.lastCastAttempt = false
self.OnCastFunc = false
self.overrides = {}
self.PostCastFunc = false
self.PreCastFunc = false
self.release_at = false
self.conditions = {}
self.spellID = id
self.traits = {
@ -107,11 +120,13 @@ function Spell:New(id)
channeling = false,
override = false,
talent = false,
power = false,
},
target = {
exists = true,
player = false,
}
},
cost = {},
}
self.target = false
self.wasLooking = false
@ -218,6 +233,10 @@ end
---@param condition? string | fun(self:Bastion.Spell):boolean
---@return boolean
function Spell:Cast(unit, condition)
if not self:Castable() then
return false
end
if condition then
if type(condition) == "string" and not self:EvaluateCondition(condition) then
return false
@ -226,9 +245,6 @@ function Spell:Cast(unit, condition)
end
end
if not self:Castable() then
return false
end
-- Call pre cast function
if self:GetPreCastFunction() then
@ -351,12 +367,22 @@ function Spell:SetTraits(traits)
end
end
end
if traits.cost then
self.traits.cost = {}
for _, cost in ipairs(traits.cost) do
table.insert(self.traits.cost, cost)
end
end
return self
end
function Spell:EvaluateTraits()
local player = Bastion.UnitManager:Get("player")
if self.traits.cast.power and not self:HasPower() then
return false
end
if not self.traits.cast.global and player:GetGCD() > 0 then
return false
end
@ -678,6 +704,27 @@ function Spell:IsSpell(spell)
return self:GetID() == spell:GetID()
end
function Spell:HasPower()
local costs = GetSpellPowerCost(self:GetID())
local checked = {}
local hasPower = #costs > 0 and false or true
if not hasPower then
for _, cost in ipairs(costs) do
if not checked[cost.type] then
local powerCost = (cost.cost > cost.minCost and cost.minCost or cost.cost)
if cost.hasRequiredAura or cost.requiredAuraID == 0 then
hasPower = powerCost == 0 or Bastion.UnitManager:Get("player"):GetPower(cost.type) >= powerCost
checked[cost.type] = true
if not hasPower then
return false
end
end
end
end
end
return hasPower
end
-- GetCost
---@return number
function Spell:GetCost()
@ -705,9 +752,12 @@ function Spell:Damage()
end
end
---@param target Bastion.Unit
---@param target? Bastion.Unit
---@param source? "any" | Bastion.Unit
function Spell:GetAura(target, source)
if type(target) == "nil" then
target = Bastion.UnitManager:Get("player")
end
if type(source) == "nil" then
source = Bastion.UnitManager:Get("player")
end

@ -25,7 +25,23 @@ local TimeToDie = {
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
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,
},
}
function TimeToDie:IterableUnits()
@ -216,6 +232,7 @@ TimeToDie.specialTTDPercentageData = {
---@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.UnitManager:Get(string.format("boss%d", i))
@ -229,7 +246,7 @@ function TimeToDie.FightRemains(enemies, bossOnly)
if bossExists or bossOnly then
-- If we have a boss list but no valid boss time, return invalid
return maxTimeToDie or 11111
return maxTimeToDie or TimeToDie.Enums.NO_GUID -- 11111
end
-- If we specify an AoE range, iterate through all the targets in the specified range
@ -256,7 +273,7 @@ end
-- Get if the Time To Die is Valid for a boss fight remains
function TimeToDie.BossFightRemainsIsNotValid()
return TimeToDie.BossFightRemains() >= 7777
return TimeToDie.BossFightRemains() < 0
end
-- Returns if the current fight length meets the requirements.
@ -267,7 +284,7 @@ end
---@param bossOnly boolean
function TimeToDie.FilteredFightRemains(enemies, operator, value, checkIfValid, bossOnly)
local fightRemains = TimeToDie.FightRemains(enemies, bossOnly)
if checkIfValid and fightRemains >= 7777 then
if checkIfValid and fightRemains < 0 then
return false
end

@ -16,8 +16,23 @@ local Unit = {
ttd_ticker = false,
ttd = 0,
id = false, --[[ @asnumber ]]
watching_for_swings = false,
health = {}
}
function Unit:UpdateHealth()
if #self.health > 60 then
table.remove(self.health, 1)
end
table.insert(self.health, {
time = Bastion.Now,
percent = self:GetHP(),
health = self:GetHealth(),
maxHealth =
self:GetMaxHealth()
})
end
function Unit:__index(k)
local response = Bastion.ClassMagic:Resolve(Unit, k)
@ -56,6 +71,7 @@ end
-- Constructor
---@param unit? TinkrObjectReference
---@return Bastion.Unit
function Unit:New(unit)
---@class Bastion.Unit
local self = setmetatable({}, Unit)
@ -68,6 +84,7 @@ function Unit:New(unit)
self.cache = Bastion.Cache:New()
self.aura_table = Bastion.AuraTable:New(self)
self.regression_history = {}
self.health = {}
return self
end
@ -78,12 +95,12 @@ function Unit:IsValid()
end
-- Check if the unit exists
---@return boolean
function Unit:Exists()
return Object(self:GetOMToken()) ~= false
end
-- Get the units token
---@return string
function Unit:Token()
return self:GetOMToken()
end
@ -180,7 +197,6 @@ function Unit:GetPowerDeficit(powerType)
end
-- Get the units position
---@return Bastion.Vector3
function Unit:GetPosition()
local x, y, z = ObjectPosition(self:GetOMToken())
return Bastion.Vector3:New(x, y, z)
@ -311,11 +327,12 @@ function Unit:IsDamage()
end
-- Get the units role
---@return "TANK" | "HEALER" | "DAMAGER"
---@return "TANK" | "HEALER" | "DAMAGER" | "NONE"
function Unit:GetRole()
return UnitGroupRolesAssigned(self:GetOMToken())
end
---@return number | boolean
function Unit:GetSpecializationID()
if CanInspect(self:GetOMToken(), false) then
return ObjectSpecializationID(self:GetOMToken())
@ -324,10 +341,10 @@ function Unit:GetSpecializationID()
end
---@param fallback? boolean
---@return "TANK" | "HEALER" | "DAMAGER" | false
---@return "TANK" | "HEALER" | "DAMAGER" | "NONE" | false
function Unit:GetSpecializationRole(fallback)
local specID = self:GetSpecializationID()
if specID then
if type(specID) == "number" then
return GetSpecializationRoleByID(specID)
end
return fallback and self:GetRole() or false
@ -398,6 +415,15 @@ function Unit:CanSee(unit)
-- return false
-- end
-- end
local ignoreLoS = {
[98696] = true -- Illysanna Ravencrest (BRH)
}
if not unit:IsPlayer() then
local id = unit:GetID()
if id and ignoreLoS[id] then
return true
end
end
local ax, ay, az = ObjectPosition(self:GetOMToken())
local ah = ObjectHeight(self:GetOMToken())
local attx, atty, attz = GetUnitAttachmentPosition(unit:GetOMToken(), 34)
@ -432,6 +458,8 @@ function Unit:IsCasting()
return UnitCastingInfo(self:GetOMToken()) ~= nil
end
---@param percent number
---@return number
function Unit:GetTimeCastIsAt(percent)
local name, text, texture, startTimeMS, endTimeMS, isTradeSkill, castID, notInterruptible, spellId = UnitCastingInfo(
self:GetOMToken())
@ -453,7 +481,7 @@ function Unit:GetTimeCastIsAt(percent)
end
-- Get Casting or channeling spell
---@return Bastion.Spell | nil
---@return Bastion.Spell | boolean
function Unit:GetCastingOrChannelingSpell()
local name, text, texture, startTimeMS, endTimeMS, isTradeSkill, castID, notInterruptible, spellId = UnitCastingInfo(
self:GetOMToken())
@ -466,8 +494,7 @@ function Unit:GetCastingOrChannelingSpell()
if name then
return Bastion.Globals.SpellBook:GetSpell(spellId)
end
return nil
return false
end
-- Get the end time of the cast or channel
@ -500,6 +527,7 @@ function Unit:IsCastingOrChanneling()
return self:IsCasting() or self:IsChanneling()
end
---@return Bastion.Unit
function Unit:CastTarget()
---@diagnostic disable-next-line: param-type-mismatch
return self:IsCastingOrChanneling() and Bastion.UnitManager:Get(ObjectCastingTarget(self:GetOMToken())) or
@ -507,10 +535,12 @@ function Unit:CastTarget()
end
---@param unit Bastion.Unit
---@return boolean
function Unit:CastTargetIsUnit(unit)
return self:IsCastingOrChanneling() and self:CastTarget():IsUnit(unit)
end
---@return boolean
function Unit:IsImmobilized()
return bit.band(self:GetMovementFlag(), 0x400) > 0
end
@ -569,7 +599,7 @@ function Unit:IsInterruptibleAt(percent, ignoreInterruptible)
return false
end
local percent = percent or math.random(2, 5)
local percent = percent or math.random(2, 20)
local castPercent = self:GetChannelOrCastPercentComplete()
if castPercent >= percent then
@ -645,6 +675,7 @@ function Unit:IsMoving()
return GetUnitSpeed(self:GetOMToken()) > 0
end
---@return TinkrMovementFlags
function Unit:GetMovementFlag()
return ObjectMovementFlag(self:GetOMToken())
end
@ -786,15 +817,15 @@ end
---@param unit Bastion.Unit
---@return boolean
function Unit:InMelee(unit)
local x, y, z = ObjectPosition(self.unit)
local x2, y2, z2 = ObjectPosition(unit.unit)
local x, y, z = ObjectPosition(self:GetOMToken())
local x2, y2, z2 = ObjectPosition(unit:GetOMToken())
if not x or not x2 then
return false
end
local scr = ObjectCombatReach(self.unit)
local ucr = ObjectCombatReach(unit.unit)
local scr = ObjectCombatReach(self:GetOMToken())
local ucr = ObjectCombatReach(unit:GetOMToken())
if not scr or not ucr then
return false
@ -808,6 +839,7 @@ function Unit:InMelee(unit)
end
-- Get object id
---@return number
function Unit:GetID()
if self.id ~= false then
---@type number
@ -830,6 +862,7 @@ function Unit:IsInRaid()
return UnitInRaid(self:GetOMToken()) ~= nil
end
---@return boolean
function Unit:IsInPartyOrRaid()
return self:IsInParty() or self:IsInRaid()
end
@ -874,7 +907,7 @@ function Unit:PredictHealth(time)
table.remove(self.regression_history, 1)
end
table.insert(self.regression_history, { time = GetTime(), percent = self:GetHP() })
table.insert(self.regression_history, { time = Bastion.Now, percent = self:GetHP() })
for i = 1, #self.regression_history do
local entry = self.regression_history[i]
@ -883,7 +916,7 @@ function Unit:PredictHealth(time)
end
local slope, intercept = self:LinearRegression(x, y)
return slope * time + intercept
return slope * (Bastion.Now + time) + intercept
end
-- Use linear regression to guess the time until a given health percent
@ -897,7 +930,7 @@ function Unit:PredictTime(percent)
table.remove(self.regression_history, 1)
end
table.insert(self.regression_history, { time = GetTime(), percent = self:GetHP() })
table.insert(self.regression_history, { time = Bastion.Now, percent = self:GetHP() })
for i = 1, #self.regression_history do
local entry = self.regression_history[i]
@ -926,7 +959,7 @@ end
function Unit:TimeToDie()
if self:IsDead() then
self.regression_history = {}
if type(self.ttd_ticker) == "table" then
if self.ttd_ticker then
self.ttd_ticker:Cancel()
self.ttd_ticker = false
end
@ -955,6 +988,15 @@ function Unit:TimeToDie()
return self.ttd
end
---@param lower number
---@param upper? number
---@return boolean
function Unit:TTD(lower, upper)
upper = type(upper) == "nil" and lower or upper
local ttd = self:TimeToDie()
return ttd >= lower and ttd <= upper
end
-- Set combat time if affecting combat and return the difference between now and the last time
---@return number
function Unit:GetCombatTime()
@ -1053,8 +1095,8 @@ function Unit:GetSwingTimers()
return main_speed_remains, off_speed_remains
end
---@return nil
function Unit:WatchForSwings()
if not self.watching_for_swings then
Bastion.Globals.EventManager:RegisterWoWEvent("COMBAT_LOG_EVENT_UNFILTERED", function()
local _, subtype, _, sourceGUID, sourceName, _, _, destGUID, destName, destFlags, _, spellID, spellName, _, amount, interrupt, a, b, c, d, offhand, multistrike =
CombatLogGetCurrentEventInfo()
@ -1084,6 +1126,8 @@ function Unit:WatchForSwings()
end
end
end)
self.watching_for_swings = true
end
end
-- ismounted
@ -1137,6 +1181,7 @@ function Unit:GetStaggerPercent()
end
-- Get the units power regen rate
---@return number, number
function Unit:GetPowerRegen()
---@diagnostic disable-next-line: redundant-parameter
return GetPowerRegen(self:GetOMToken())
@ -1226,6 +1271,7 @@ function Unit:IsWithinCone(Target, Angle, Distance, rotation)
return diff <= Angle and self:GetDistance(Target) <= Distance
end
---@return number
function Unit:GetEmpoweredStage()
local stage = 0
local _, _, _, startTime, _, _, _, spellID, _, numStages = UnitChannelInfo(self:GetOMToken())
@ -1245,26 +1291,33 @@ function Unit:GetEmpoweredStage()
return stage
end
---@return boolean
function Unit:IsConnected()
return UnitIsConnected(self:GetOMToken())
end
---@return boolean
function Unit:HasIncomingRessurection()
---@diagnostic disable-next-line: return-type-mismatch
return self:IsDead() and UnitHasIncomingResurrection(self:GetOMToken())
end
---@return WowGameObject | false
function Unit:LootTarget()
return ObjectLootTarget(self:GetOMToken())
end
---@return boolean
function Unit:CanLoot()
return ObjectLootable(self:GetOMToken())
end
---@return boolean
function Unit:HasTarget()
return ObjectTarget(self:GetOMToken()) ~= false
end
---@return Bastion.Unit
function Unit:Target()
return self:HasTarget() and Bastion.UnitManager:Get(ObjectTarget(self:GetOMToken()):unit()) or
Bastion.UnitManager:Get("none")
@ -1355,7 +1408,7 @@ local dummyUnits = {
[174491] = true, -- Tanking Dummy
-- DargonFlight Valdrakken
[198594] = true, -- Cleave Training Dummy
[194648] = true, -- Training Dummy
[194648] = false, -- Training Dummy
[189632] = true, -- Animated Duelist
[194643] = true, -- Dungeoneer's Training Dummy
[194644] = true, -- Dungeoneer's Training Dummy
@ -1372,12 +1425,14 @@ local dummyUnits = {
[199057] = true, -- Black Dragon's Challenge Dummy (toy)
}
---@return boolean
function Unit:IsDummy()
local npcId = self:GetID()
return npcId and npcId >= 0 and dummyUnits[npcId] == true or false
end
---@param npcId? number
---@return boolean
function Unit:IsInBossList(npcId)
npcId = npcId or self:GetID()
for i = 1, 4 do
@ -1386,9 +1441,11 @@ function Unit:IsInBossList(npcId)
return true
end
end
return false
end
---@param npcId number
---@return number
function Unit:SpecialTTDPercentage(npcId)
local specialTTDPercentage = Bastion.TimeToDie.specialTTDPercentageData[npcId]
if not specialTTDPercentage then return 0 end
@ -1402,6 +1459,7 @@ end
---@param npcId? number
---@param hp? number
---@return boolean
function Unit:CheckHPFromBossList(npcId, hp)
npcId = npcId or self:GetID()
local thisHP = hp or 100
@ -1415,13 +1473,16 @@ function Unit:CheckHPFromBossList(npcId, hp)
return false
end
---@param percentage number
---@param minSamples? number
---@return Bastion.TimeToDie.Enums | integer
function Unit:TimeToX(percentage, minSamples)
if self:IsDummy() then return 6666 end
if self:IsPlayer() and Bastion.UnitManager:Get("player"):CanAttack(self) then return 25 end
local seconds = 8888
--if self:IsDummy() then return 6666 end
if self:IsPlayer() and Bastion.UnitManager:Get("player"):CanAttack(self) then return Bastion.TimeToDie.Enums.PLAYER end
local seconds = 0
local unitGuid = self:GetGUID()
if not unitGuid then
return seconds
return Bastion.TimeToDie.Enums.NO_GUID
end
local unitTable = Bastion.TimeToDie.Units[unitGuid]
-- Simple linear regression
@ -1456,8 +1517,8 @@ function Unit:TimeToX(percentage, minSamples)
-- 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
seconds = seconds - (GetTime() - unitTable[2])
if seconds < 0 then seconds = Bastion.TimeToDie.Enums.NEGATIVE_TTD end
end
end
end
@ -1465,12 +1526,15 @@ function Unit:TimeToX(percentage, minSamples)
end
---@param minSamples? number
---@return Bastion.TimeToDie.Enums | integer
function Unit:TimeToDie2(minSamples)
if not self:Exists() then
return 11111
return Bastion.TimeToDie.Enums.DOES_NOT_EXIST
end
local unitGuid = self:GetGUID()
if not unitGuid then return 11111 end
if not unitGuid then
return Bastion.TimeToDie.Enums.NO_GUID
end
minSamples = minSamples or 3
---@type {TTD: {[number]: number}}
@ -1482,23 +1546,28 @@ function Unit:TimeToDie2(minSamples)
ttd = {}
unitInfo.TTD = ttd
end
local v
if not ttd[minSamples] then
ttd[minSamples] = self:TimeToX(self:SpecialTTDPercentage(self:GetID()), minSamples)
v = self:TimeToX(self:SpecialTTDPercentage(self:GetID()), minSamples)
if v >= 0 then
ttd[minSamples] = v
Bastion.Globals.UnitInfo:Set(unitGuid, unitInfo, .5)
end
end
Bastion.Globals.UnitInfo:Set(unitGuid, unitInfo, .5)
return ttd[minSamples]
return ttd[minSamples] or v
end
-- Get the boss unit TimeToDie
---@param minSamples? number
---@return Bastion.TimeToDie.Enums | integer
function Unit:BossTimeToDie(minSamples)
if self:IsInBossList() or self:IsDummy() then
return self:TimeToDie2(minSamples)
end
return 11111
return Bastion.TimeToDie.Enums.DOES_NOT_EXIST
end
-- Get if the unit meets the TimeToDie requirements.
@ -1507,10 +1576,11 @@ end
---@param offset number
---@param valueThreshold number
---@param minSamples? number
---@return boolean
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
return TTD > -1 and TTD < valueThreshold and Bastion.Utils.CompareThis(operator, TTD + (offset or 0), value) or false
end
-- Get if the boss unit meets the TimeToDie requirements.
@ -1519,6 +1589,7 @@ end
---@param offset number
---@param valueThreshold number
---@param minSamples? number
---@return boolean
function Unit:BossFilteredTimeToDie(operator, value, offset, valueThreshold, minSamples)
if self:IsInBossList() or self:IsDummy() then
return self:FilteredTimeToDie(operator, value, offset, valueThreshold, minSamples)
@ -1529,12 +1600,14 @@ end
-- Get if the Time To Die is Valid for an Unit (i.e. not returning a warning code).
---@param minSamples? number
---@return boolean
function Unit:TimeToDieIsNotValid(minSamples)
return self:TimeToDie2(minSamples) >= 7777
return self:TimeToDie2(minSamples) > -1
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
---@return boolean
function Unit:BossTimeToDieIsNotValid(minSamples)
if self:IsInBossList() then
return self:TimeToDieIsNotValid(minSamples)
@ -1543,62 +1616,4 @@ function Unit:BossTimeToDieIsNotValid(minSamples)
return true
end
-- local empowering = {}
-- Bastion.EventManager:RegisterWoWEvent("UNIT_SPELLCAST_EMPOWER_START", function(...)
-- local unit, unk, id = ...
-- if not unit then return end
-- local guid = ObjectGUID(unit)
-- if not guid then return end
-- empowering[guid] = -1
-- end)
-- Bastion.EventManager:RegisterWoWEvent("UNIT_SPELLCAST_EMPOWER_STOP", function(...)
-- local unit, unk, id = ...
-- if not unit then return end
-- local guid = ObjectGUID(unit)
-- if not guid then return end
-- empowering[guid] = -1
-- end)
-- function Unit:GetUnitEmpowerStage()
-- local name, text, texture, startTime, endTime, isTradeSkill, notInterruptible, spellID, _, numStages =
-- UnitChannelInfo(self:GetOMToken());
-- if name and empowering[self:GetGUID()] == -1 then
-- empowering[self:GetGUID()] = numStages
-- end
-- if not name and empowering[self:GetGUID()] then
-- return empowering[self:GetGUID()]
-- end
-- if not name then
-- return empowering[self:GetGUID()]
-- end
-- local getStageDuration = function(stage)
-- if stage == numStages then
-- return GetUnitEmpowerHoldAtMaxTime(self:GetOMToken());
-- else
-- return GetUnitEmpowerStageDuration(self:GetOMToken(), stage - 1);
-- end
-- end
-- local time = GetTime() - (startTime / 1000);
-- local higheststage = 0
-- local sumdur = 0
-- for i = 1, numStages - 1, 1 do
-- local duration = getStageDuration(i) / 1000;
-- sumdur = sumdur + duration
-- if time > sumdur then
-- higheststage = i
-- end
-- end
-- return higheststage
-- end
return Unit

@ -11,7 +11,7 @@ local Unit = Bastion.Unit
---@class Bastion.UnitManager
---@field units table<string, Bastion.Unit>
---@field customUnits table<string, Bastion.UnitManager.CustomUnit>
---@field objects table<string, Bastion.Unit>
---@field objects table<string | WowGameObject, Bastion.Unit>
---@field cache Bastion.Cache
local UnitManager = {
units = {},
@ -29,7 +29,7 @@ function UnitManager:__index(k)
return UnitManager[k]
end
local k = k or "none"
k = k or "none"
-- if custom unit exists, return it it's cache expired return a new one
if self.customUnits[k] then
@ -109,7 +109,7 @@ end
-- Get a unit by guid
---@param guid string | WowGameObject
---@return Bastion.Unit
---@return Bastion.Unit?
function UnitManager:GetObject(guid)
return self.objects[guid]
end

@ -323,7 +323,7 @@ end
---@return Bastion.Vector3
function Vector3:Project(onNormal)
local num = onNormal:Dot(onNormal)
if num < 1.401298E-45 then
if num < 1.401298e-45 then
return Vector3:New(0, 0, 0)
end

@ -111,9 +111,8 @@ local exampleNames = {
"ExampleModule.lua",
}
---@param dir string
local function Load(dir)
local dir = dir
if dir:sub(1, 1) == "@" then
dir = dir:sub(2)
dir = string.format("%s/%s", BastionScriptsBase, dir)
@ -205,6 +204,11 @@ Bastion.Notifications = Bastion.NotificationList:New()
Bastion.Config = Bastion.require("Bastion.Config")
Bastion.TimeToDie = Bastion.require("Bastion.TimeToDie")
---@param unitTarget UnitId
Bastion.Globals.EventManager:RegisterWoWEvent("UNIT_HEALTH", function(unitTarget)
--Bastion.UnitManager:Get(unitTarget):UpdateHealth()
end)
---@enum (key) CompareThisTable
local compareThisTable = {
@ -227,12 +231,16 @@ Bastion.Utils = {
end
}
---@type table<string, Bastion.Library>
local LIBRARIES = {}
---@type Bastion.Module[]
local MODULES = {}
Bastion.Enabled = false
---@param unit UnitToken
---@param auras UnitAuraUpdateInfo
Bastion.Globals.EventManager:RegisterWoWEvent("UNIT_AURA", function(unit, auras)
---@type Bastion.Unit | nil
local u = Bastion.UnitManager:Get(unit)
@ -243,6 +251,7 @@ Bastion.Globals.EventManager:RegisterWoWEvent("UNIT_AURA", function(unit, auras)
end)
Bastion.Globals.EventManager:RegisterWoWEvent("UNIT_SPELLCAST_SUCCEEDED", function(...)
---@type UnitIds, string, spellId
local unit, castGUID, spellID = ...
local spell = Bastion.Globals.SpellBook:GetIfRegistered(spellID)
@ -255,7 +264,7 @@ Bastion.Globals.EventManager:RegisterWoWEvent("UNIT_SPELLCAST_SUCCEEDED", functi
end
end)
local pguid = UnitGUID("player")
local playerGuid = UnitGUID("player")
local missed = {}
---@class Bastion.Globals.SpellName : { [spellId]: string }
@ -303,7 +312,7 @@ Bastion.Globals.EventManager:RegisterWoWEvent("COMBAT_LOG_EVENT_UNFILTERED", fun
if u2 then
u2:SetLastCombatTime(t)
if subEvent == "SPELL_MISSED" and sourceGUID == pguid and spellID == 408 then
if subEvent == "SPELL_MISSED" and sourceGUID == playerGuid and spellID == 408 then
local missType = args[15]
if missType == "IMMUNE" then
@ -318,10 +327,18 @@ Bastion.Globals.EventManager:RegisterWoWEvent("COMBAT_LOG_EVENT_UNFILTERED", fun
end
end
end)
local Timer = {
TTD = 0
}
Bastion.Now = GetTime()
Bastion.Tick = GetGameTick()
Bastion.Ticker = C_Timer.NewTicker(0.1, function()
Bastion.Now = GetTime()
Bastion.Tick = GetGameTick()
if not Bastion.CombatTimer:IsRunning() and UnitAffectingCombat("player") then
Bastion.CombatTimer:Start()
elseif Bastion.CombatTimer:IsRunning() and not UnitAffectingCombat("player") then
@ -330,8 +347,8 @@ 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
if Bastion.Now > Timer.TTD then
Timer.TTD = Bastion.Now + Bastion.TimeToDie.Settings.Refresh
Bastion.TimeToDie:Refresh()
end
for i = 1, #MODULES do
@ -340,6 +357,7 @@ Bastion.Ticker = C_Timer.NewTicker(0.1, function()
end
end)
---@param module Bastion.Module
function Bastion:Register(module)
table.insert(MODULES, module)
Bastion:Print("Registered", module)
@ -352,11 +370,10 @@ function Bastion:FindModule(name)
return MODULES[i]
end
end
return nil
end
Bastion.PrintEnabled = false
function Bastion:Print(...)
if not Bastion.PrintEnabled then
return
@ -494,6 +511,7 @@ function Bastion:CheckLibraryDependencies()
return true
end
---@param library string
function Bastion:Import(library)
local lib = self:GetLibrary(library)
@ -504,6 +522,7 @@ function Bastion:Import(library)
return lib:Resolve()
end
---@param name string
function Bastion:GetLibrary(name)
if not LIBRARIES[name] then
error("Library " .. name .. " not found")

Loading…
Cancel
Save