main
jeffi 6 months ago
parent cb4c49b1b3
commit 6a48a2e7ac
  1. 2
      Examples/ExampleModule.lua
  2. 2
      Examples/Libraries/ExampleDependency.lua
  3. 42
      src/APL/APL.lua
  4. 114
      src/APLAction/APLAction.lua
  5. 31
      src/APLActor/APLActor.lua
  6. 22
      src/APLManager/APLManager.lua
  7. 2
      src/Aura/Aura.lua
  8. 6
      src/AuraTable/AuraTable.lua
  9. 299
      src/Bastion/Bastion.lua
  10. 5
      src/Command/Command.lua
  11. 24
      src/EventManager/EventManager.lua
  12. 68
      src/Item/Item.lua
  13. 2
      src/ItemBook/ItemBook.lua
  14. 4
      src/List/List.lua
  15. 4
      src/Missile/Missile.lua
  16. 30
      src/MissileManager/MissileManager.lua
  17. 13
      src/Module/Module.lua
  18. 12
      src/MythicPlusUtils/MythicPlusUtils.lua
  19. 3
      src/NotificationList/NotificationList.lua
  20. 0
      src/Object/Object.lua
  21. 97
      src/ObjectManager/ObjectManager.lua
  22. 362
      src/Spell/Spell.lua
  23. 78
      src/SpellBook/SpellBook.lua
  24. 150
      src/TimeToDie/TimeToDie.lua
  25. 127
      src/TimeToDieManager/TimeToDieManager.lua
  26. 120
      src/TimeToDieUnit/TimeToDieUnit.lua
  27. 13
      src/Timer/Timer.lua
  28. 436
      src/Unit/Unit.lua
  29. 166
      src/UnitManager/UnitManager.lua
  30. 8
      src/Util/Util.lua
  31. 4
      src/Vector3/Vector3.lua

@ -1,6 +1,6 @@
local Tinkr, Bastion = ... local Tinkr, Bastion = ...
local ExampleModule = Bastion.Module:New('ExampleModule') local ExampleModule = Bastion.Module:New('ExampleModule')
local Player = Bastion.UnitManager:Get('player') local Player = Bastion.Globals.UnitManager:Get('player')
-- Create a local spellbook. -- Create a local spellbook.
local SpellBook = Bastion.SpellBook:New() local SpellBook = Bastion.SpellBook:New()

@ -1,6 +1,6 @@
local Tinkr, Bastion = ... local Tinkr, Bastion = ...
local Player = Bastion.UnitManager:Get('player') local Player = Bastion.Globals.UnitManager:Get('player')
Bastion:RegisterLibrary(Bastion.Library:New({ Bastion:RegisterLibrary(Bastion.Library:New({
name = 'Dependable', name = 'Dependable',

@ -18,6 +18,7 @@ function APL:New(name)
---@class Bastion.APL ---@class Bastion.APL
local self = setmetatable({ local self = setmetatable({
active = false, active = false,
actions = {},
apl = {}, apl = {},
variables = {}, variables = {},
name = name, name = name,
@ -116,11 +117,9 @@ function APL:AddSpell(spell, condition, targetIf)
parentAPL = self, parentAPL = self,
targetIf = targetIf or false, targetIf = targetIf or false,
executor = function(actor) executor = function(actor)
local actorTable = actor.actor --[[@as Bastion.APL.Actor.Spell.Table]] local aTbl = actor.actor --[[@as Bastion.APL.Actor.Spell.Table]]
local spellTarget = actorTable.targetIf and actorTable.targetIf(actorTable.spell) or actorTable.target return aTbl.spell:CastableIf(aTbl.castableFunc):OnCast(aTbl.onCastFunc):Cast(
return (not spellTarget or spellTarget:Exists()) aTbl.targetIf and aTbl.targetIf(aTbl.spell) or aTbl.target or nil, aTbl.condition)
and (not actorTable.condition or actorTable.condition(actorTable.spell, spellTarget))
and actorTable.spell:CastableIf(actorTable.castableFunc):OnCast(actorTable.onCastFunc):Cast(spellTarget)
end end
}) })
@ -131,25 +130,34 @@ end
---@class Bastion.APL.Actor.Item.Table : Bastion.APLActor.Table.Base ---@class Bastion.APL.Actor.Item.Table : Bastion.APLActor.Table.Base
---@field item Bastion.Item ---@field item Bastion.Item
---@field condition? string | fun(self: Bastion.Item): boolean ---@field condition? string | fun(self: Bastion.Item, target: Bastion.Unit|false): boolean
---@field usableFunc false | fun(...): boolean ---@field usableFunc false | fun(...): boolean
---@field target Bastion.Unit | nil ---@field target Bastion.Unit | false
---@field onUseFunc false | fun(self: Bastion.Item):any
---@field targetIf fun(self: Bastion.Item): (Bastion.Unit) | false
-- Add an item to the APL -- Add an item to the APL
---@param item Bastion.Item ---@param item Bastion.Item
---@param condition? string | fun(self: Bastion.Item): boolean ---@param condition? string | fun(self: Bastion.Item, target: Bastion.Unit | false): boolean
---@param targetIf? fun(self: Bastion.Item): Bastion.Unit | false
---@return Bastion.APLActor ---@return Bastion.APLActor
function APL:AddItem(item, condition) function APL:AddItem(item, condition, targetIf)
local usableFunc = item.UsableIfFunc local usableFunc = item.UsableIfFunc
local target = item:GetTarget() local target = item:GetTarget()
local actor = Bastion.APLActor:New({ local actor = Bastion.APLActor:New({
type = "item", type = "item",
item = item, item = item,
condition = condition, condition = condition,
usableFunc = usableFunc, usableFunc = usableFunc,
target = target, target = target,
onUseFunc = item.OnUseFunc,
parentAPL = self, parentAPL = self,
targetIf = targetIf or false,
executor = function(actor)
local aTbl = actor.actor --[[@as Bastion.APL.Actor.Item.Table]]
return aTbl.item:UsableIf(aTbl.usableFunc):OnUse(aTbl.onUseFunc):Use(
aTbl.targetIf and aTbl.targetIf(aTbl.item) or aTbl.target or nil, aTbl.condition)
end
}) })
table.insert(self.apl, actor) table.insert(self.apl, actor)
@ -171,16 +179,19 @@ function APL:AddAPL(apl, condition)
apl = apl, apl = apl,
condition = condition, condition = condition,
parentAPL = self, parentAPL = self,
executor = function(actor)
return (not actor.actor.condition or actor.actor.condition()) and actor.actor.apl:Execute()
end
}) })
table.insert(self.apl, actor) table.insert(self.apl, actor)
return actor return actor
end end
---@param name string ---@param nameOrIndex string|number
---@param enabled? boolean ---@param enabled? boolean
function APL:ToggleAPL(name, enabled) function APL:ToggleAPL(nameOrIndex, enabled)
for _, actor in ipairs(self.apl) do for actorIndex, actor in ipairs(self.apl) do
if actor.actor.type == "apl" and actor.name == name then if actor.actor.type == "apl" and (actorIndex == nameOrIndex or actor.name == nameOrIndex) then
actor.enabled = type(enabled) == "boolean" and enabled or not actor.enabled actor.enabled = type(enabled) == "boolean" and enabled or not actor.enabled
break break
end end
@ -218,7 +229,10 @@ function APL:Execute()
self:UpdateLastAttempted(actor) self:UpdateLastAttempted(actor)
if actor.enabled and (not actor:HasTraits() or actor:Evaluate()) and actor:Execute() then if actor.enabled and (not actor:HasTraits() or actor:Evaluate()) and actor:Execute() then
self:UpdateLastSuccessful() self:UpdateLastSuccessful()
--print(string.format("%s Successful", actor.name))
return true return true
else
--print(string.format("%s Skipped", actor.name))
end end
end end
self.active = false self.active = false

@ -0,0 +1,114 @@
---@type Tinkr
local Tinkr,
---@class Bastion
Bastion = ...
---@alias Bastion.APL.Action.Type "spell" | "item" | "apl" | "sequence" | "variable" | "function"
---@alias Bastion.APL.Action.Object Bastion.Spell | Bastion.Item | Bastion.APL
-- APL Action class
---@class Bastion.APL.Action
local APLAction = {}
APLAction.__index = APLAction
---@class Bastion.APL.Action.New.Params
---@field parent Bastion.APL
---@field type Bastion.APL.Action.Type
---@field object Bastion.APL.Action.Object
---@field condition? false | string | fun(self: Bastion.APL.Action, object: Bastion.APL.Action.Object, target?: Bastion.Unit): boolean
---@field onActionFunc? false | fun(object: Bastion.APL.Action.Object, target?: Bastion.Unit): any
---@field actionUsableFunc? false | fun(object: Bastion.APL.Action.Object, target?: Bastion.Unit): boolean
---@field executor? fun(self: Bastion.APL.Action): boolean
---@field target? Bastion.Unit | false
---@field targetIf? false | fun(object: Bastion.APL.Action.Object): Bastion.Unit
---@param params Bastion.APL.Action.New.Params
function APLAction:New(params)
---@class Bastion.APL.Action
local self = setmetatable({}, APLAction)
self.parent = params.parent
self.index = #self.parent.actions + 1
self.type = params.type
self.object = params.object
self.condition = params.condition or false
self.onActionFunc = params.onActionFunc or false
self.actionUsableFunc = params.actionUsableFunc or false
self.executor = params.executor or false
self.target = params.target or false
self.targetIf = params.targetIf or false
return self
end
function APLAction:Execute()
return self.executor and self.executor(self) or false
end
---@class Bastion.APL.Action.New.Spell.Params
---@field parent Bastion.APL
---@field spell Bastion.Spell
---@field condition? false | string | fun(self: Bastion.APL.Action, object: Bastion.Spell, target?: Bastion.Unit): boolean
---@field target? Bastion.Unit
---@field targetIf? fun(self: Bastion.Spell): Bastion.Unit
---@param params Bastion.APL.Action.New.Spell.Params
function APLAction:NewSpell(params)
return self:New({
parent = params.parent,
type = "spell",
object = params.spell,
condition = params.condition or false,
target = params.target or params.spell:GetTarget(),
targetIf = params.targetIf or false,
onActionFunc = params.spell.OnCastFunc or false,
actionUsableFunc = params.spell.CastableIfFunc or false,
executor = function(this)
local target = this.targetIf and this.targetIf(this.object) or this.target or nil
return this.object:CastableIf(this.actionUsableFunc):OnCast(this.onActionFunc):Cast(target, this.condition)
end,
})
end
---@class Bastion.APL.Action.New.Item.Params
---@field parent Bastion.APL
---@field item Bastion.Item
---@field condition? false | string | fun(self: Bastion.APL.Action, object: Bastion.Item, target?: Bastion.Unit): boolean
---@field target? Bastion.Unit
---@field targetIf? fun(self: Bastion.Item): Bastion.Unit
---@param params Bastion.APL.Action.New.Item.Params
function APLAction:NewItem(params)
return self:New({
parent = params.parent,
type = "item",
object = params.item,
condition = params.condition or false,
target = params.target or params.item:GetTarget() or false,
targetIf = params.targetIf or false,
onActionFunc = params.item.OnUseFunc or false,
actionUsableFunc = params.item.UsableIfFunc or false,
executor = function(this)
return this.object:UsableIf(this.actionUsableFunc):OnUse(this.onActionFunc):Use(
this.targetIf and this.targetIf(this.object) or this.target or nil, this.condition)
end,
})
end
---@class Bastion.APL.Action.New.APL.Params
---@field parent Bastion.APL
---@field apl Bastion.APL
---@field condition? false | string | fun(self: Bastion.APL.Action, object: Bastion.APL): boolean
function APLAction:NewAPL(params)
return self:New({
parent = params.parent,
type = "apl",
object = params.apl,
condition = params.condition or false,
executor = function(this)
return this.object:Execute()
end,
})
end
Bastion.APLAction = APLAction

@ -28,24 +28,23 @@ function APLActor:New(actor)
local self = setmetatable({ local self = setmetatable({
index = #actor.parentAPL.apl + 1, index = #actor.parentAPL.apl + 1,
}, APLActor) }, APLActor)
self.name = string.format("[%s][%d]", actor.parentAPL.name, self.index)
if actor.type == "spell" then if actor.type == "spell" then
local name = actor.spell:GetName() or "Unknown" self.name = string.format("%s[<SPELL>%s[%s]]", self.name, actor.spell:GetName() or "Unknown",
local id = actor.spell:GetID() or 0 actor.spell:GetID() or 0)
self.name = string.format("<SPELL>%s[%s]", name, id)
elseif actor.type == "item" then elseif actor.type == "item" then
local name = actor.item:GetName() or "Unknown" self.name = string.format("%s[<ITEM>%s[%s]]", self.name, actor.item:GetName() or "Unknown",
local id = actor.item:GetID() or 0 actor.item:GetID() or 0)
self.name = string.format("<ITEM>%s[%s]", name, id)
elseif actor.type == "apl" then elseif actor.type == "apl" then
self.name = string.format("<APL>%s", actor.apl.name or "Unknown") self.name = string.format("%s[<APL>%s]", self.name, actor.apl.name or "Unknown")
elseif actor.type == "sequencer" then elseif actor.type == "sequencer" then
self.name = string.format("<SEQUENCER>") self.name = string.format("%s[<SEQUENCER>]", self.name)
elseif actor.type == "variable" then elseif actor.type == "variable" then
self.name = string.format("<VARIABLE>%s", actor.variable or "Unknown") self.name = string.format("%s[<VARIABLE>%s]", self.name, actor.variable or "Unknown")
elseif actor.type == "action" then elseif actor.type == "action" then
self.name = string.format("<ACTION>%s", actor.action or "Unknown") self.name = string.format("%s[<ACTION>%s]", self.name, actor.action or "Unknown")
else else
self.name = string.format("<%s>", actor.type or "UNKNOWN") self.name = string.format("%s[<%s>]", self.name, actor.type or "UNKNOWN")
end end
self.actor = actor self.actor = actor
self.enabled = true self.enabled = true
@ -105,19 +104,23 @@ function APLActor:Execute()
return false return false
end end
if actorTable.type == "apl" then if actorTable.type == "apl" then
---@cast actorTable Bastion.APL.Actor.APL.Table return self:ExecuteActor()
--[[ ---@cast actorTable Bastion.APL.Actor.APL.Table
if not actorTable.condition or actorTable.condition() then if not actorTable.condition or actorTable.condition() then
-- print("Bastion: APL:Execute: Executing sub APL " .. actorTable.apl.name) -- print("Bastion: APL:Execute: Executing sub APL " .. actorTable.apl.name)
return actorTable.apl:Execute() return actorTable.apl:Execute()
end end ]]
end end
if actorTable.type == "spell" then if actorTable.type == "spell" then
return self:ExecuteActor() return self:ExecuteActor()
end end
if actorTable.type == "item" then if actorTable.type == "item" then
return self:ExecuteActor()
end
--[[ if actorTable.type == "item" then
---@cast actorTable Bastion.APL.Actor.Item.Table ---@cast actorTable Bastion.APL.Actor.Item.Table
return ((not actorTable.target or actorTable.target:Exists()) and actorTable.item:UsableIf(actorTable.usableFunc):Use(actorTable.target, actorTable.condition)) return ((not actorTable.target or actorTable.target:Exists()) and actorTable.item:UsableIf(actorTable.usableFunc):Use(actorTable.target, actorTable.condition))
end end ]]
if self:GetActorTable().type == "action" then if self:GetActorTable().type == "action" then
---@cast actorTable Bastion.APL.Actor.Action.Table ---@cast actorTable Bastion.APL.Actor.Action.Table
-- print("Bastion: APL:Execute: Executing action " .. actorTable.action) -- print("Bastion: APL:Execute: Executing action " .. actorTable.action)

@ -0,0 +1,22 @@
---@type Tinkr
local Tinkr,
---@class Bastion
Bastion = ...
-- APL Manager
---@class Bastion.APLManager
local APLManager = {}
APLManager.__index = APLManager
APLManager.__metatable = false
function APLManager:New()
---@class Bastion.APLManager
local self = setmetatable({
APLs = {},
}, APLManager)
return self
end
Bastion.APLManager = APLManager

@ -301,7 +301,7 @@ end
-- Get the auras source -- Get the auras source
---@return Bastion.Unit ---@return Bastion.Unit
function Aura:GetSource() function Aura:GetSource()
return Bastion.UnitManager[self.aura.source] return Bastion.Globals.UnitManager[self.aura.source]
end end
-- Get the auras stealable status -- Get the auras stealable status

@ -112,7 +112,7 @@ function AuraTable:AddOrUpdateAuraInstanceID(instanceID, aura)
self.instanceIDLookup[instanceID] = spellId self.instanceIDLookup[instanceID] = spellId
if Bastion.UnitManager["player"] and Bastion.UnitManager["player"]:IsUnit(aura:GetSource()) then if Bastion.Globals.UnitManager["player"] and Bastion.Globals.UnitManager["player"]:IsUnit(aura:GetSource()) then
if not self.playerAuras[spellId] then if not self.playerAuras[spellId] then
self.playerAuras[spellId] = {} self.playerAuras[spellId] = {}
end end
@ -140,7 +140,7 @@ function AuraTable:GetUnitBuffs()
local spellId = aura:GetSpell():GetID() local spellId = aura:GetSpell():GetID()
if Bastion.UnitManager["player"]:IsUnit(aura:GetSource()) then if Bastion.Globals.UnitManager["player"]:IsUnit(aura:GetSource()) then
if not self.playerAuras[spellId] then if not self.playerAuras[spellId] then
self.playerAuras[spellId] = {} self.playerAuras[spellId] = {}
end end
@ -179,7 +179,7 @@ function AuraTable:GetUnitDebuffs()
local spellId = aura:GetSpell():GetID() local spellId = aura:GetSpell():GetID()
if Bastion.UnitManager["player"]:IsUnit(aura:GetSource()) then if Bastion.Globals.UnitManager["player"]:IsUnit(aura:GetSource()) then
if not self.playerAuras[spellId] then if not self.playerAuras[spellId] then
self.playerAuras[spellId] = {} self.playerAuras[spellId] = {}
end end

@ -1,13 +1,29 @@
---@type Tinkr ---@type Tinkr
local Tinkr = ... local Tinkr = ...
local startPrecision = GetTimePreciseSec()
---@class Bastion.Globals.SpellName : { [spellId]: string } ---@class Bastion.Globals.SpellName : { [spellId]: string }
---@class Bastion ---@class Bastion
local Bastion = { local Bastion = {
CombatEvents = {}, CombatEvents = {},
Enabled = false, Enabled = false,
Interval = 0.15, Interval = 0.10,
Created = GetTime(),
Loaded = 0,
NextUpdate = 0,
TimeSinceLastUpdate = 0,
TimeSinceLastOMUpdate = 0,
TimeSinceLastMissleManagerUpdate = 0,
TimeSinceLastTTDUpdate = 0,
TimeSinceLastCombatTimerUpdate = 0,
TimeSinceLastModuleUpdate = 0,
modulesTicking = false,
Updating = false,
LastStart = 0,
LastStop = 0,
UpdateDuration = 0,
Globals = { Globals = {
---@type Bastion.Globals.SpellName ---@type Bastion.Globals.SpellName
SpellName = {} SpellName = {}
@ -22,49 +38,52 @@ local Bastion = {
BastionScripts = "scripts/bastion/scripts", BastionScripts = "scripts/bastion/scripts",
ThirdParty = "scripts/BastionScripts", ThirdParty = "scripts/BastionScripts",
}, },
Log = function(message)
Log(string.format("%s [%.2f] [Bastion]: %s", date("%H:%M:%S"), GetTimePreciseSec() - startPrecision, message))
end,
} }
function Bastion:__index(key) --[[ function Bastion:__index(key)
if Bastion[key] then if Bastion[key] then
return Bastion[key] return Bastion[key]
end end
return rawget(self, key) return rawget(self, key)
end end ]]
local bastionFiles = { local bastionFiles = {
"~/src/ClassMagic/ClassMagic", "~/src/ClassMagic/ClassMagic",
"~/src/List/List", "~/src/APL/APL",
"~/src/Command/Command", "~/src/APLActor/APLActor",
"~/src/Util/Util", "~/src/APLTrait/APLTrait",
"~/src/Library/Library", "~/src/Aura/Aura",
"~/src/Notification/Notification", "~/src/AuraTable/AuraTable",
"~/src/NotificationList/NotificationList",
"~/src/Vector3/Vector3",
"~/src/Sequencer/Sequencer",
"~/src/Cache/Cache", "~/src/Cache/Cache",
"~/src/Cacheable/Cacheable", "~/src/Cacheable/Cacheable",
"~/src/Refreshable/Refreshable", "~/src/Class/Class",
"~/src/Command/Command",
"~/src/Config/Config",
"~/src/EventManager/EventManager", "~/src/EventManager/EventManager",
"~/src/Unit/Unit", "~/src/Item/Item",
"~/src/Aura/Aura", "~/src/ItemBook/ItemBook",
"~/src/APLTrait/APLTrait", "~/src/Library/Library",
"~/src/APLActor/APLActor", "~/src/List/List",
"~/src/APL/APL",
"~/src/Module/Module",
"~/src/UnitManager/UnitManager",
"~/src/ObjectManager/ObjectManager",
"~/src/Missile/Missile", "~/src/Missile/Missile",
"~/src/MissileManager/MissileManager", "~/src/MissileManager/MissileManager",
"~/src/Module/Module",
"~/src/MythicPlusUtils/MythicPlusUtils",
"~/src/Notification/Notification",
"~/src/NotificationList/NotificationList",
"~/src/ObjectManager/ObjectManager",
"~/src/Refreshable/Refreshable",
"~/src/Sequencer/Sequencer",
"~/src/Spell/Spell", "~/src/Spell/Spell",
"~/src/SpellBook/SpellBook", "~/src/SpellBook/SpellBook",
"~/src/Item/Item",
"~/src/ItemBook/ItemBook",
"~/src/AuraTable/AuraTable",
"~/src/Class/Class",
"~/src/Timer/Timer", "~/src/Timer/Timer",
"~/src/MythicPlusUtils/MythicPlusUtils",
"~/src/Config/Config",
"~/src/TimeToDie/TimeToDie", "~/src/TimeToDie/TimeToDie",
"~/src/Unit/Unit",
"~/src/UnitManager/UnitManager",
"~/src/Util/Util",
"~/src/Vector3/Vector3",
} }
---@param filePath string ---@param filePath string
@ -187,6 +206,19 @@ function Bastion:Toggle(toggle)
Bastion.Enabled = type(toggle) ~= "nil" and toggle or not Bastion.Enabled Bastion.Enabled = type(toggle) ~= "nil" and toggle or not Bastion.Enabled
end end
---@param interval number
function Bastion:SetInterval(interval)
self.Interval = type(interval) == "number" and interval > 0 and interval or self.Interval
--[[ Bastion.Ticker:Cancel()
Bastion.Ticker = C_Timer.NewTicker(Bastion.Interval, function()
Bastion.TickFunction(self)
end) ]]
end
function Bastion:GetInterval()
return Bastion.Interval
end
local combatEventSuffix = { local combatEventSuffix = {
["_DAMAGE"] = true, ["_DAMAGE"] = true,
["_MISSED"] = true, ["_MISSED"] = true,
@ -228,6 +260,47 @@ local combatEventPrefix = {
"ENVIRONMENTAL" "ENVIRONMENTAL"
} }
---@type Bastion.Module[]
local MODULES = {}
function Bastion:ModulesTicking()
return self.modulesTicking
end
---@param force? boolean
function Bastion:TickModules(force)
if not self.modulesTicking and (self.Enabled or force) then
self.modulesTicking = true
for i = 1, #MODULES do
MODULES[i]:Tick()
end
self.modulesTicking = false
end
end
-- Find a module by name
---@param name string
function Bastion:FindModule(name)
for i = 1, #MODULES do
if MODULES[i].name == name then
return MODULES[i]
end
end
return false
end
---@param module Bastion.Module
function Bastion:Register(module)
local foundModule = Bastion:FindModule(module.name)
if not foundModule then
table.insert(MODULES, module)
Bastion.Util:Print("Registered", module)
else
Bastion.Util:Print("Module already registered", module)
return
end
end
local loaded = false local loaded = false
function Bastion:Load() function Bastion:Load()
if loaded then if loaded then
@ -243,11 +316,43 @@ function Bastion:Load()
self:Require(bastionFiles[i]) self:Require(bastionFiles[i])
end end
self.Globals.EventManager:RegisterWoWEvent("PLAYER_ENTERING_WORLD", function() ---#region Init Start
self.UnitManager:ResetObjects() Bastion.Globals.Command = Bastion.Command:New("bastion")
Bastion.Globals.EventManager = Bastion.EventManager:New()
Bastion.Globals.ItemBook = Bastion.ItemBook:New()
Bastion.Globals.MissileManager = Bastion.MissileManager:New()
Bastion.TimeToDie:Init()
---@param args { [2]: string }
Bastion.Globals.Command:Register("module", "Toggle a module on/off", function(args)
local module = Bastion:FindModule(args[2])
if module then
module:Toggle()
if module.enabled then
Bastion.Util:Print("Enabled", module.name)
else
Bastion.Util:Print("Disabled", module.name)
end
else
Bastion.Util:Print("Module not found")
end
end) end)
self.Globals.Command:Register('toggle', 'Toggle bastion on/off', function() Bastion.Globals.MythicPlusUtils = Bastion.MythicPlusUtils:New()
Bastion.Globals.NotificationList = Bastion.NotificationList:New()
Bastion.ObjectManager = Bastion.ObjectManager:New()
Bastion.Globals.SpellBook = Bastion.SpellBook:New(true)
Bastion.Globals.UnitManager = Bastion.UnitManager:New()
Bastion.Globals.Command:Register("toggle", "Toggle bastion on/off", function()
Bastion.Util:TogglePrint()
end)
Bastion.Globals.Command:Register("debug", "Toggle debug mode on/off", function()
Bastion.Util:ToggleDebug()
end)
Bastion.Globals.EventManager:RegisterWoWEvent("PLAYER_ENTERING_WORLD", function()
Bastion.Globals.UnitManager:ResetObjects()
end)
Bastion.Globals.Command:Register('toggle', 'Toggle bastion on/off', function()
self:Toggle() self:Toggle()
if self.Enabled then if self.Enabled then
self.Util:Print("Enabled") self.Util:Print("Enabled")
@ -256,26 +361,23 @@ function Bastion:Load()
end end
end) end)
self.Globals.CombatTimer = self.Timer:New("combat", function() ---#endregion
return UnitAffectingCombat("player")
end)
---@param unitTarget UnitId Bastion.Globals.CombatTimer = self.Timer:New("combat", function()
self.Globals.EventManager:RegisterWoWEvent("UNIT_HEALTH", function(unitTarget) return UnitAffectingCombat("player")
--Bastion.UnitManager:Get(unitTarget):UpdateHealth()
end) end)
---@param unit UnitToken ---@param unit UnitToken
---@param auras UnitAuraUpdateInfo ---@param auras UnitAuraUpdateInfo
self.Globals.EventManager:RegisterWoWEvent("UNIT_AURA", function(unit, auras) Bastion.Globals.EventManager:RegisterWoWEvent("UNIT_AURA", function(unit, auras)
local u = self.UnitManager:Get(unit) local u = Bastion.Globals.UnitManager:Get(unit)
if u:Exists() then if u then
u:GetAuras():OnUpdate(auras) u:GetAuras():OnUpdate(auras)
end end
end) end)
self.Globals.EventManager:RegisterWoWEvent("UNIT_SPELLCAST_SUCCEEDED", function(...) Bastion.Globals.EventManager:RegisterWoWEvent("UNIT_SPELLCAST_SUCCEEDED", function(...)
---@type UnitIds, string, spellId ---@type UnitIds, string, spellId
local unit, castGUID, spellID = ... local unit, castGUID, spellID = ...
local spell = self.Globals.SpellBook:GetIfRegistered(spellID) local spell = self.Globals.SpellBook:GetIfRegistered(spellID)
@ -289,25 +391,11 @@ function Bastion:Load()
end end
end) end)
local playerGuid = UnitGUID("player")
local missed = {} local missed = {}
Bastion.Globals.EventManager:RegisterWoWEvent("UNIT_COMBAT",
---@param unitTarget UnitIds
---@param event string
---@param flagText string
---@param amount number
---@param schoolMask number
function(unitTarget, event, flagText, amount, schoolMask)
--[[ local unit = Bastion.UnitManager:Get(unitTarget)
if unit:IsAffectingCombat() then
unit:SetLastCombatTime()
end ]]
end)
Bastion.Globals.EventManager:RegisterWoWEvent("COMBAT_LOG_EVENT_UNFILTERED", function() Bastion.Globals.EventManager:RegisterWoWEvent("COMBAT_LOG_EVENT_UNFILTERED", function()
local args = { CombatLogGetCurrentEventInfo() } local args = { CombatLogGetCurrentEventInfo() }
local currTime = GetTime()
---@type string ---@type string
local subEvent = args[2] local subEvent = args[2]
---@type string ---@type string
@ -325,65 +413,95 @@ function Bastion:Load()
end end
end end
local sourceUnit = Bastion.UnitManager:GetObject(sourceGUID) local sourceUnit = Bastion.Globals.UnitManager:GetObject(sourceGUID)
if sourceUnit and sourceUnit:Exists() then
sourceUnit:SetLastCombatTime(currTime)
end
local destUnit = Bastion.UnitManager:GetObject(destGUID) local destUnit = Bastion.Globals.UnitManager:GetObject(destGUID)
--local t = GetTime() if destUnit and destUnit:Exists() then
destUnit:SetLastCombatTime(currTime)
end
if sourceUnit and sourceUnit:IsAffectingCombat() then if subEvent == "UNIT_DIED" or subEvent == "UNIT_DESTROYED" or subEvent == "PARTY_KILL" or subEvent == "SPELL_INSTAKILL" then
sourceUnit:SetLastCombatTime() Bastion.TimeToDie:UNIT_DIED(destGUID)
end
--[[ if sourceUnit and sourceUnit:IsValid() and sourceUnit:IsAffectingCombat() then
sourceUnit:SetLastCombatTime(currTime)
end end
local updateDestUnit = destUnit and destUnit:IsAffectingCombat() local updateDestUnit = destUnit and destUnit:IsAffectingCombat()
if Bastion.CombatEvents[subEvent] and not updateDestUnit or ((sourceUnit and sourceUnit:IsValid() and destUnit and destUnit:IsValid()) and not UnitThreatSituation(sourceUnit.unit:unit() --[[@as string]], destUnit.unit:unit() --[[@as string]])) then if Bastion.CombatEvents[subEvent] and not updateDestUnit or ((sourceUnit and destUnit) and not UnitThreatSituation(sourceUnit:GetOMToken(), destUnit:GetOMToken())) then
updateDestUnit = true updateDestUnit = true
--[[ for key, val in pairs(combatEvents) do
if val and subEvent:find(key) then
end end
if destUnit and updateDestUnit then
destUnit:SetLastCombatTime(currTime)
end ]] end ]]
end)
--[[ Bastion.Ticker = C_Timer.NewTicker(Bastion.Interval, function(this)
Bastion.TickFunction(self)
end) ]]
--[[
local currTime = GetTime()
if currTime >= self.NextUpdate then
self.NextUpdate = currTime + self.Interval
if not self.Globals.CombatTimer:IsRunning() and UnitAffectingCombat("player") then
self.Globals.CombatTimer:Start()
elseif self.Globals.CombatTimer:IsRunning() and not UnitAffectingCombat("player") then
self.Globals.CombatTimer:Reset()
end end
if destUnit and updateDestUnit then if self.Enabled then
destUnit:SetLastCombatTime() self.ObjectManager:Refresh()
self.UnitManager:ResetObjects()
self.Globals.MissileManager:Refresh()
self.TimeToDie:Refresh()
self:TickModules()
else
--Bastion.UnitManager:ResetObjects()
end end
end
]]
--[[ if destUnit then CreateFrame("Frame"):SetScript("OnUpdate", function(this, elapsed)
if subEvent == "SPELL_MISSED" and sourceGUID == playerGuid and spellID == 408 then self.TimeSinceLastUpdate = self.TimeSinceLastUpdate + elapsed
local missType = args[15]
if missType == "IMMUNE" then if self.TimeSinceLastUpdate >= self.Interval and not self.Updating then
local castingSpell = sourceUnit:GetCastingOrChannelingSpell() self.Updating = true
self.TimeSinceLastUpdate = 0
if castingSpell and type(castingSpell) == "table" then if not self.Globals.CombatTimer:IsRunning() and UnitAffectingCombat("player") then
if not missed[castingSpell:GetID()] then self.Globals.CombatTimer:Start()
missed[castingSpell:GetID()] = true elseif self.Globals.CombatTimer:IsRunning() and not UnitAffectingCombat("player") then
end self.Globals.CombatTimer:Reset()
end end
end
end
end ]]
end)
self.Ticker = C_Timer.NewTicker(self.Interval, function()
self.Globals.CombatTimer:Check()
if self.Enabled then
self.MissileManager:Refresh()
self.ObjectManager:Refresh() self.ObjectManager:Refresh()
self.Globals.UnitManager:ResetObjects()
self.Globals.MissileManager:Refresh()
self.TimeToDie:Refresh() self.TimeToDie:Refresh()
self:TickModules() self:TickModules()
else self.Updating = false
self.UnitManager:ResetObjects()
end end
end) end)
self.Globals.Command:Register("dumpspells", "Dump spells to a file", function() --Bastion.Globals.EventManager:SetOnUpdate(BastionOnUpdate)
--[[ Bastion.Ticker = C_Timer.NewTicker(Bastion.Interval, function()
Bastion.TickFunction(self)
end) ]]
Bastion.Globals.Command:Register("dumpspells", "Dump spells to a file", function()
local i = 1 local i = 1
local rand = math.random(100000, 999999) local rand = math.random(100000, 999999)
while true do while true do
local spellName, spellSubName = GetSpellBookItemName(i, BOOKTYPE_SPELL) local spellName, spellSubName = C_SpellBook.GetSpellBookItemName(i, BOOKTYPE_SPELL)
if not spellName then if not spellName then
do do
break break
@ -391,18 +509,18 @@ function Bastion:Load()
end end
-- use spellName and spellSubName here -- use spellName and spellSubName here
local spellID = select(7, GetSpellInfo(spellName)) local spellInfo = C_Spell.GetSpellInfo(spellName)
if spellID then if spellInfo and spellInfo.spellID then
spellName = spellName:gsub("[%W%s]", "") spellName = spellName:gsub("[%W%s]", "")
WriteFile("bastion-" .. UnitClass("player") .. "-" .. rand .. ".lua", WriteFile("bastion-" .. UnitClass("player") .. "-" .. rand .. ".lua",
"local " .. spellName .. " = Bastion.Globals.SpellBook:GetSpell(" .. spellID .. ")\n", true) "local " .. spellName .. " = Bastion.Globals.SpellBook:GetSpell(" .. spellInfo.spellID .. ")\n", true)
end end
i = i + 1 i = i + 1
end end
end) end)
self.Globals.Command:Register("missed", "Dump the list of immune kidney shot spells", function() Bastion.Globals.Command:Register("missed", "Dump the list of immune kidney shot spells", function()
for k, v in pairs(missed) do for k, v in pairs(missed) do
self.Util:Print(k) self.Util:Print(k)
end end
@ -414,6 +532,7 @@ function Bastion:Load()
LoadThird() LoadThird()
loaded = true loaded = true
self.Loaded = GetTime()
return self return self
end end

@ -5,7 +5,6 @@ Bastion = ...
-- Create a wow command handler class -- Create a wow command handler class
---@class Bastion.Command ---@class Bastion.Command
---@field command string
---@field commands Bastion.Command.Commands[] ---@field commands Bastion.Command.Commands[]
---@field prefix string ---@field prefix string
local Command = {} local Command = {}
@ -18,7 +17,7 @@ Command.__index = Command
---@return string ---@return string
function Command:__tostring() function Command:__tostring()
return "Command(" .. self.command .. ")" return "Command(" .. self.prefix .. ")"
end end
---@param prefix string ---@param prefix string
@ -82,4 +81,4 @@ function Command:PrintHelp()
end end
Bastion.Command = Command Bastion.Command = Command
Bastion.Globals.Command = Bastion.Command:New("bastion") --Bastion.Globals.Command = Bastion.Command:New("bastion")

@ -8,7 +8,7 @@ Bastion = ...
---@field frame Frame ---@field frame Frame
---@field events table<string, { [number]: fun(...) }> ---@field events table<string, { [number]: fun(...) }>
---@field eventHandlers table<string, { [number]: fun(...) }> ---@field eventHandlers table<string, { [number]: fun(...) }>
---@field wowEventHandlers table<WowEvent, { [number]: fun(...) }> ---@field wowEventHandlers table<WowEvent, { [number]: { sendEvent: boolean, fun: fun(...) } }>
---@field selfCombatEventHandlers table<string, { [number]: fun(...) }> ---@field selfCombatEventHandlers table<string, { [number]: fun(...) }>
---@field CombatEventHandlers table<string, { [number]: fun(...) }> ---@field CombatEventHandlers table<string, { [number]: fun(...) }>
---@field playerGUID string|boolean ---@field playerGUID string|boolean
@ -36,8 +36,12 @@ function EventManager:New()
self.frame:SetScript("OnEvent", function(f, event, ...) self.frame:SetScript("OnEvent", function(f, event, ...)
if self.wowEventHandlers[event] then if self.wowEventHandlers[event] then
for _, callback in ipairs(self.wowEventHandlers[event]) do for _, eventHandler in ipairs(self.wowEventHandlers[event]) do
callback(...) if eventHandler.sendEvent then
eventHandler.fun(event, ...)
else
eventHandler.fun(...)
end
end end
end end
end) end)
@ -64,7 +68,8 @@ end
-- Register a wow event -- Register a wow event
---@param events WowEvent | WowEvent[] ---@param events WowEvent | WowEvent[]
---@param handler fun(...) ---@param handler fun(...)
function EventManager:RegisterWoWEvent(events, handler) ---@param sendEvent? boolean
function EventManager:RegisterWoWEvent(events, handler, sendEvent)
if type(events) == "string" then if type(events) == "string" then
events = { events } events = { events }
end end
@ -75,7 +80,10 @@ function EventManager:RegisterWoWEvent(events, handler)
self.frame:RegisterEvent(event) self.frame:RegisterEvent(event)
end end
table.insert(self.wowEventHandlers[event], handler) table.insert(self.wowEventHandlers[event], {
fun = handler,
sendEvent = sendEvent or false,
})
end end
end end
@ -153,6 +161,10 @@ function EventManager:CLEUHandler(...)
end end
end end
function EventManager:SetOnUpdate(func)
self.frame:SetScript("OnUpdate", func)
end
Bastion.EventManager = EventManager Bastion.EventManager = EventManager
Bastion.Globals.EventManager = Bastion.EventManager:New() --Bastion.Globals.EventManager = Bastion.EventManager:New()

@ -3,12 +3,15 @@ local Tinkr,
---@class Bastion ---@class Bastion
Bastion = ... Bastion = ...
local Evaluator = Tinkr.Util.Evaluator
---@class Bastion.Item.Traits.Use ---@class Bastion.Item.Traits.Use
---@field moving? boolean ---@field moving? boolean
---@field dead? boolean ---@field dead? boolean
---@field casting? boolean ---@field casting? boolean
---@field channeling? boolean ---@field channeling? boolean
---@field byId? boolean ---@field byId? boolean
---@field delay? false | number
---@class Bastion.Item.Traits.Target ---@class Bastion.Item.Traits.Target
---@field exists? boolean ---@field exists? boolean
@ -41,11 +44,10 @@ local usableExcludes = {
} }
---@param itemId number | string ---@param itemId number | string
---@return number charges, number maxCharges, number start, number duration
local GetItemCharges = function(itemId) local GetItemCharges = function(itemId)
local _, spellId = C_Item.GetItemSpell(itemId) local _, spellId = C_Item.GetItemSpell(itemId)
local charges, maxCharges, start, duration, chargeModRate = GetSpellCharges(spellId) local chargeInfo = C_Spell.GetSpellCharges(spellId)
return charges, maxCharges, start, duration return chargeInfo
end end
function Item:__index(k) function Item:__index(k)
@ -90,6 +92,7 @@ function Item:New(id)
casting = false, casting = false,
channeling = false, channeling = false,
byId = false, byId = false,
delay = false,
}, },
target = { target = {
exists = true, exists = true,
@ -139,7 +142,7 @@ function Item:SetTraits(traits)
end end
function Item:EvaluateTraits() function Item:EvaluateTraits()
local player = Bastion.UnitManager:Get("player") local player = Bastion.Globals.UnitManager:Get("player")
if not self.traits.use.moving and player:IsMoving() then if not self.traits.use.moving and player:IsMoving() then
return false return false
@ -164,6 +167,12 @@ function Item:EvaluateTraits()
end end
end end
if self.traits.use.delay then
if self:GetTimeSinceLastUseAttempt() > 0 and self:GetTimeSinceLastUseAttempt() < self.traits.use.delay then
return false
end
end
return true return true
end end
@ -233,15 +242,14 @@ end
---@param unit? Bastion.Unit ---@param unit? Bastion.Unit
function Item:UseByID(unit) function Item:UseByID(unit)
local target = unit and unit:GetOMToken() or self.traits.target.player and "player" or "target" local target = unit and unit:GetOMToken() or self.traits.target.player and "player" or "target"
loadstringsecure(string.format("C_Macro.RunMacroText('/use [@%s] item:%d','')", target, self:GetID()))
RunMacroText(string.format("/use [@%s] item:%d", target, self:GetID()))
Bastion.Util:Debug("Using by id", self) Bastion.Util:Debug("Using by id", self)
end end
-- Use the Item -- Use the Item
---@param unit? Bastion.Unit ---@param unit? Bastion.Unit
---@param condition? string | fun(self:Bastion.Item):boolean ---@param condition? string | fun(self:Bastion.Item, target?: Bastion.Unit):boolean
---@return boolean ---@return boolean
function Item:Use(unit, condition) function Item:Use(unit, condition)
if not self:Usable() then if not self:Usable() then
@ -250,7 +258,7 @@ function Item:Use(unit, condition)
if condition then if condition then
if type(condition) == "string" and not self:EvaluateCondition(condition) then if type(condition) == "string" and not self:EvaluateCondition(condition) then
return false return false
elseif type(condition) == "function" and not condition(self) then elseif type(condition) == "function" and not condition(self, unit) then
return false return false
end end
end end
@ -260,14 +268,22 @@ function Item:Use(unit, condition)
self:GetPreUseFunction()(self) self:GetPreUseFunction()(self)
end 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 -- Check if the mouse was looking
self.wasLooking = IsMouselooking() --[[@as boolean]] self.wasLooking = IsMouselooking() --[[@as boolean]]
UseItemByName(self.traits.use.byId and self:GetID() or self:GetName(), target:GetOMToken())
---@type string|WowGameObject|false
local target = unit and unit:GetOMToken() or self.traits.target.exists and (self.traits.target.player and "player") or
self:TargetExists() and self:GetTarget():GetOMToken() or "none"
if type(target) == "string" and string.find(target, "^nameplate") then
target = Object(target) --[[@as string]]
end
if not target then
target = "none"
end
local useNameId = self.traits.use.byId and self:GetID() or self:GetName()
Evaluator:CallProtectedFunction("C_Item.UseItemByName", useNameId, target --[[@as string]])
Bastion.Util:Debug("Using", self) Bastion.Util:Debug("Using", self)
-- Set the last Use time -- Set the last Use time
@ -344,15 +360,14 @@ end
-- Check if the Item is usable -- Check if the Item is usable
---@return boolean ---@return boolean
function Item:IsUsable() function Item:IsUsable()
local usable, noMana = IsUsableItem(self:GetID()) local usable, noMana = C_Item.IsUsableItem(self:GetID())
return usable or usableExcludes[self:GetID()] return usable or usableExcludes[self:GetID()]
end end
-- Check if the Item is Usable -- Check if the Item is Usable
---@return boolean ---@return boolean
function Item:IsEquippedAndUsable() function Item:IsEquippedAndUsable()
return ((self:IsEquippable() and self:IsEquipped()) or (not self:IsEquippable() and self:IsUsable())) return (not self:IsEquippable() or self:IsEquipped()) and self:IsUsable() and not self:IsOnCooldown()
and not self:IsOnCooldown()
end end
-- Is equippable -- Is equippable
@ -363,14 +378,8 @@ end
-- Check if the Item is Usable -- Check if the Item is Usable
---@return boolean ---@return boolean
function Item:Usable() function Item:Usable()
if not self:EvaluateTraits() then return self:IsEquippedAndUsable() and self:EvaluateTraits() and
return false (not self:GetUsableFunction() or self:GetUsableFunction()(self))
end
if self:GetUsableFunction() and not self:GetUsableFunction()(self) then
return false
end
return self:IsEquippedAndUsable()
end end
-- Set a script to check if the Item is Usable -- Set a script to check if the Item is Usable
@ -497,7 +506,8 @@ end
-- Get the Items charges -- Get the Items charges
---@return number ---@return number
function Item:GetCharges() function Item:GetCharges()
return select(1, GetItemCharges(self:GetID())) local chargeInfo = GetItemCharges(self:GetID())
return chargeInfo and chargeInfo.currentCharges or 0
end end
function Item:GetCount() function Item:GetCount()
@ -507,8 +517,8 @@ end
-- Get the Items charges remaining -- Get the Items charges remaining
---@return number ---@return number
function Item:GetChargesRemaining() function Item:GetChargesRemaining()
local charges, maxCharges, start, duration = GetItemCharges(self:GetID()) local chargeInfo = GetItemCharges(self:GetID())
return charges return chargeInfo and chargeInfo.currentCharges or 0
end end
-- Create a condition for the Item -- Create a condition for the Item
@ -566,7 +576,7 @@ end
-- Get the Items target -- Get the Items target
function Item:GetTarget() function Item:GetTarget()
return self.traits.target.player and Bastion.UnitManager:Get("player") or self.target return self.traits.target.player and Bastion.Globals.UnitManager:Get("player") or self.target
end end
function Item:TargetExists() function Item:TargetExists()

@ -37,5 +37,5 @@ function ItemBook:GetItem(id)
return self.items[id] return self.items[id]
end end
Bastion.Globals.ItemBook = ItemBook:New() --Bastion.Globals.ItemBook = ItemBook:New()
Bastion.ItemBook = ItemBook Bastion.ItemBook = ItemBook

@ -156,7 +156,7 @@ end
---@generic R : any ---@generic R : any
---@generic V : any ---@generic V : any
---@param callback fun(result: R, value: V): R, boolean? ---@param callback fun(result: R, value: V): (R, boolean?)
---@param initialValue R ---@param initialValue R
---@return R ---@return R
function List:reduce(callback, initialValue) function List:reduce(callback, initialValue)
@ -254,7 +254,7 @@ end
---@return string ---@return string
function List:toString() function List:toString()
return self:join(", ") return string.format("Bastion.List(%d)", self:count()) --self:join(", ")
end end
Bastion.List = List Bastion.List = List

@ -27,11 +27,11 @@ function Missile:New(missile)
end end
function Missile:GetSourceUnit() function Missile:GetSourceUnit()
return Bastion.UnitManager:Get(self.source) return Bastion.Globals.UnitManager:Get(self.source)
end end
function Missile:GetTargetUnit() function Missile:GetTargetUnit()
return Bastion.UnitManager:Get(self.target) return Bastion.Globals.UnitManager:Get(self.target)
end end
function Missile:GetCurrentVector() function Missile:GetCurrentVector()

@ -17,7 +17,8 @@ Bastion = ...
local MissileManager = {} local MissileManager = {}
MissileManager.__index = MissileManager MissileManager.__index = MissileManager
function MissileManager:New() ---@param interval? number
function MissileManager:New(interval)
---@class Bastion.MissileManager ---@class Bastion.MissileManager
local self = setmetatable({}, MissileManager) local self = setmetatable({}, MissileManager)
self.missiles = {} self.missiles = {}
@ -25,6 +26,12 @@ function MissileManager:New()
self.trackedMissiles = Bastion.List:New() self.trackedMissiles = Bastion.List:New()
self.allMissiles = Bastion.List:New() self.allMissiles = Bastion.List:New()
self.trackingParams = {} self.trackingParams = {}
self.refreshing = false
self.interval = interval or 0.1
self.nextUpdate = 0
self.lastStart = 0
self.lastStop = 0
self.lastDuration = 0
return self return self
end end
@ -101,7 +108,16 @@ function MissileManager:GetList(name)
return self._lists[name].list return self._lists[name].list
end end
function MissileManager:Refresh() ---@param force? boolean
function MissileManager:Refresh(force)
if self:IsRefreshing() or (self.nextUpdate > GetTime() and not force) or (not Bastion.Enabled and not force) then
return
end
self.refreshing = true
self.lastStart = GetTime()
self.nextUpdate = self.lastStart + self.interval
self.refreshing = true
self:Reset() self:Reset()
local missiles = Missiles() local missiles = Missiles()
@ -116,6 +132,14 @@ function MissileManager:Refresh()
end end
end end
end end
self.lastStop = GetTime()
self.lastDuration = self.lastStop - self.lastStart
self.refreshing = false
end
function MissileManager:IsRefreshing()
return self.refreshing
end end
---@param params { source?: Bastion.Unit, target?: Bastion.Unit, spellId?: number, spellVisualId?: number } ---@param params { source?: Bastion.Unit, target?: Bastion.Unit, spellId?: number, spellVisualId?: number }
@ -134,4 +158,4 @@ function MissileManager:GetMissiles(params)
return missiles return missiles
end end
Bastion.MissileManager = MissileManager:New() Bastion.MissileManager = MissileManager

@ -27,12 +27,14 @@ end
---@param persist? boolean ---@param persist? boolean
---@return Bastion.Module ---@return Bastion.Module
function Module:New(name, interval, persist) function Module:New(name, interval, persist)
---@class Bastion.Module
local self = setmetatable({}, Module) local self = setmetatable({}, Module)
self.name = name self.name = name
self.enabled = false self.enabled = false
self.ticking = false
self.synced = {} self.synced = {}
self.interval = interval or 0.01 self.interval = interval or Bastion.Interval
self.nextTick = GetTime() + self.interval self.nextTick = GetTime() + self.interval
self.persist = persist or false self.persist = persist or false
return self return self
@ -52,7 +54,6 @@ end
function Module:NextTick() function Module:NextTick()
self.nextTick = GetTime() + self.interval self.nextTick = GetTime() + self.interval
return self
end end
-- Toggle the module -- Toggle the module
@ -87,6 +88,8 @@ end
-- Sync -- Sync
function Module:Tick() function Module:Tick()
if not self.ticking then
self.ticking = true
if self.enabled then if self.enabled then
if GetTime() >= self.nextTick then if GetTime() >= self.nextTick then
self:NextTick() self:NextTick()
@ -95,11 +98,12 @@ function Module:Tick()
end end
end end
end end
return self self.ticking = false
end
end end
Bastion.Module = Module Bastion.Module = Module
--[[
---@type Bastion.Module[] ---@type Bastion.Module[]
local MODULES = {} local MODULES = {}
@ -146,3 +150,4 @@ Bastion.Globals.Command:Register("module", "Toggle a module on/off", function(ar
Bastion.Util:Print("Module not found") Bastion.Util:Print("Module not found")
end end
end) end)
]]

@ -1226,8 +1226,8 @@ function MythicPlusUtils:New()
if self.loggedCasts[spellID] then if self.loggedCasts[spellID] then
return return
end end
local spellInfo = C_Spell.GetSpellInfo(spellID)
local name = GetSpellInfo(spellID) local name = spellInfo and spellInfo.name or "Unknown"
self.loggedCasts[spellID] = true self.loggedCasts[spellID] = true
@ -1248,7 +1248,8 @@ function MythicPlusUtils:New()
return return
end end
local name = GetSpellInfo(spellID) local spellInfo = C_Spell.GetSpellInfo(spellID)
local name = spellInfo and spellInfo.name or "Unknown"
self.loggedCasts[spellID] = true self.loggedCasts[spellID] = true
@ -1336,7 +1337,8 @@ function MythicPlusUtils:IsAOEBoss(unit)
return self.aoeBosses[unit:GetID()] return self.aoeBosses[unit:GetID()]
end end
Bastion.MythicPlusUtils = MythicPlusUtils:New() Bastion.MythicPlusUtils = MythicPlusUtils
--[[ Bastion.MythicPlusUtils = MythicPlusUtils:New()
Bastion.Globals.Command:Register("mplus", "Toggle m+ module on/off", function(args) Bastion.Globals.Command:Register("mplus", "Toggle m+ module on/off", function(args)
local cmd = args[2] local cmd = args[2]
@ -1356,4 +1358,4 @@ Bastion.Globals.Command:Register("mplus", "Toggle m+ module on/off", function(ar
Bastion.Util:Print("Available commands:") Bastion.Util:Print("Available commands:")
Bastion.Util:Print("debuffs") Bastion.Util:Print("debuffs")
Bastion.Util:Print("casts") Bastion.Util:Print("casts")
end) end) ]]

@ -90,6 +90,5 @@ function NotificationList:RemoveAllNotifications()
end end
end end
Bastion.Globals.Notifications = NotificationList:New()
Bastion.NotificationList = NotificationList Bastion.NotificationList = NotificationList
--Bastion.Globals.Notifications = Bastion.NotificationList:New()

@ -6,14 +6,25 @@ Bastion = ...
---@class Bastion.ObjectManager ---@class Bastion.ObjectManager
---@field _lists table<string, {list: Bastion.List, cb:fun(Object: WowGameObject): any}> ---@field _lists table<string, {list: Bastion.List, cb:fun(Object: WowGameObject): any}>
---@field objects WowGameObject[] ---@field objects WowGameObject[]
---@field objectList table<string, WowGameObject>
local ObjectManager = {} local ObjectManager = {}
ObjectManager.__index = ObjectManager ObjectManager.__index = ObjectManager
function ObjectManager:New() ---@param interval? number
function ObjectManager:New(interval)
---@class Bastion.ObjectManager ---@class Bastion.ObjectManager
local self = setmetatable({}, ObjectManager) local self = setmetatable({}, ObjectManager)
self.interval = interval or 0.1
self.nextUpdate = 0
self._lists = {} self._lists = {}
self.objects = {} self.objects = {}
self.objectList = {}
self.range = false
self.lastStart = 0
self.lastStop = 0
self.lastDuration = 0
self.refreshing = false
self.activeEnemies = Bastion.List:New() self.activeEnemies = Bastion.List:New()
self.afflicted = Bastion.List:New() self.afflicted = Bastion.List:New()
@ -24,11 +35,16 @@ function ObjectManager:New()
self.incorporeal = Bastion.List:New() self.incorporeal = Bastion.List:New()
self.missiles = Bastion.List:New() self.missiles = Bastion.List:New()
self.others = Bastion.List:New() self.others = Bastion.List:New()
self.outOfRange = Bastion.List:New()
return self return self
end end
---@param range number
function ObjectManager:SetRange(range)
self.range = type(range) == "number" and range or self.range
end
-- Register a custom list with a callback -- Register a custom list with a callback
---@param name string ---@param name string
---@param cb fun(object: TinkrObjectReference): boolean | any ---@param cb fun(object: TinkrObjectReference): boolean | any
@ -56,6 +72,7 @@ end
function ObjectManager:Reset() function ObjectManager:Reset()
self.objects = {} self.objects = {}
self.objectList = {}
self.activeEnemies:clear() self.activeEnemies:clear()
self.afflicted:clear() self.afflicted:clear()
@ -66,6 +83,7 @@ function ObjectManager:Reset()
self.incorporeal:clear() self.incorporeal:clear()
self.missiles:clear() self.missiles:clear()
self.others:clear() self.others:clear()
self.outOfRange:clear()
self:ResetLists() self:ResetLists()
end end
@ -88,52 +106,77 @@ function ObjectManager:GetList(name)
return self._lists[name].list return self._lists[name].list
end end
function ObjectManager:IsRefreshing()
return self.refreshing
end
function ObjectManager:InObjectList(objectGuid)
return self.objectList[objectGuid] ~= nil
end
-- Refresh all lists -- Refresh all lists
---@return nil ---@param force? boolean
function ObjectManager:Refresh() function ObjectManager:Refresh(force)
self:Reset() if self:IsRefreshing() or (self.nextUpdate > GetTime() and not force) or (not Bastion.Enabled and not force) then
return
end
self.refreshing = true
self.lastStart = GetTime()
self.nextUpdate = self.lastStart + self.interval
self:Reset()
local objects = Objects() local objects = Objects()
for _, object in pairs(objects) do for _, object in pairs(objects) do
local objectGuid = object:guid()
local objectType = object:type()
if objectGuid ~= false and objectType ~= false then
self.objectList[objectGuid] = object
table.insert(self.objects, object) table.insert(self.objects, object)
self:EnumLists(object) self:EnumLists(object)
local objectType = ObjectType(object)
if ({ [5] = true, [6] = true, [7] = true })[objectType] then if ({ [5] = true, [6] = true, [7] = true })[objectType] then
local objectGUID = ObjectGUID(object)
if objectGUID then
local unit = Bastion.UnitManager:GetObject(objectGUID)
if not unit then
unit = Bastion.UnitManager:SetObject(Bastion.Unit:New(object))
end
if objectType == 5 and ObjectCreatureType(object) == 8 then if objectType == 5 and ObjectCreatureType(object) == 8 then
self.critters:push(unit) self.critters:push(Bastion.Globals.UnitManager:GetObject(objectGuid) or
elseif unit:GetID() == 204560 then Bastion.Globals.UnitManager:SetObject(Bastion.Unit:New(object), objectGuid))
self.incorporeal:push(unit) elseif object:id() == 204560 --[[ unit:GetID() == 204560 ]] then
elseif unit:GetID() == 204773 then self.incorporeal:push(Bastion.Globals.UnitManager:GetObject(objectGuid) or
self.afflicted:push(unit) Bastion.Globals.UnitManager:SetObject(Bastion.Unit:New(object), objectGuid))
elseif unit:GetID() == 120651 then elseif object:id() == 204773 then
self.explosives:push(unit) self.afflicted:push(Bastion.Globals.UnitManager:GetObject(objectGuid) or
elseif (unit:IsInPartyOrRaid() or unit == Bastion.UnitManager["player"]) then Bastion.Globals.UnitManager:SetObject(Bastion.Unit:New(object), objectGuid))
self.friends:push(unit) elseif object:id() == 120651 then
elseif unit:IsEnemy() then self.explosives:push(Bastion.Globals.UnitManager:GetObject(objectGuid) or
Bastion.Globals.UnitManager:SetObject(Bastion.Unit:New(object), objectGuid))
elseif UnitInParty(object:unit()) or UnitInRaid(object:unit()) or UnitIsUnit("player", object:unit()) then
self.friends:push(Bastion.Globals.UnitManager:GetObject(objectGuid) or
Bastion.Globals.UnitManager:SetObject(Bastion.Unit:New(object), objectGuid))
elseif UnitCanAttack("player", object:unit()) then
local unit = Bastion.Globals.UnitManager:GetObject(objectGuid) or
Bastion.Globals.UnitManager:SetObject(Bastion.Unit:New(object), objectGuid)
self.enemies:push(unit) self.enemies:push(unit)
if unit:IsAffectingCombat() or unit:InCombatOdds() > 80 then if unit:IsAffectingCombat() or unit:InCombatOdds() > 80 then
self.activeEnemies:push(unit) self.activeEnemies:push(unit)
end end
else else
self.others:push(unit) self.others:push(object)
--[[ self.others:push(Bastion.UnitManager:GetObject(objectGuid) or
Bastion.UnitManager:SetObject(Bastion.Unit:New(object, objectGuid))) ]]
end end
end end
end end
end end
local missiles = Missiles() --[[ local missiles = Missiles()
if type(missiles) == "table" then if type(missiles) == "table" then
for _, missile in pairs(missiles) do for _, missile in pairs(missiles) do
self.missiles:push(missile) self.missiles:push(missile)
end end
end end ]]
self.lastStop = GetTime()
self.lastDuration = self.lastStop - self.lastStart
self.refreshing = false
end end
Bastion.ObjectManager = ObjectManager:New() Bastion.ObjectManager = ObjectManager

@ -12,6 +12,7 @@ Bastion = ...
---@field override boolean ---@field override boolean
---@field talent boolean | spellId ---@field talent boolean | spellId
---@field power boolean ---@field power boolean
---@field precast false | fun(self:Bastion.Spell):boolean
---@class Bastion.Spell.Traits.Cost ---@class Bastion.Spell.Traits.Cost
---@field type Enum.PowerType ---@field type Enum.PowerType
@ -24,20 +25,27 @@ Bastion = ...
---@class Bastion.Spell.Traits.Target ---@class Bastion.Spell.Traits.Target
---@field exists boolean ---@field exists boolean
---@field player boolean ---@field player boolean
---@field facing boolean
---@class Bastion.Spell.Traits.Aura
---@field track boolean
---@class Bastion.Spell.Traits ---@class Bastion.Spell.Traits
---@field cast Bastion.Spell.Traits.Cast ---@field cast Bastion.Spell.Traits.Cast
---@field target Bastion.Spell.Traits.Target ---@field target Bastion.Spell.Traits.Target
---@field cost Bastion.Spell.Traits.Cost[] ---@field cost Bastion.Spell.Traits.Cost[]
---@field aura Bastion.Spell.Traits.Aura
---@class Bastion.Spell.Traits.Cast.Params : Bastion.Spell.Traits.Cast, { [string]?: boolean } ---@class Bastion.Spell.Traits.Cast.Params : Bastion.Spell.Traits.Cast, { [string]?: boolean }
---@class Spell.Traits.Target.Params : Bastion.Spell.Traits.Target, { [string]?: boolean } ---@class Bastion.Spell.Traits.Target.Params : Bastion.Spell.Traits.Target, { [string]?: boolean }
---@class Spell.Traits.Cost.Params : Bastion.Spell.Traits.Cost[] ---@class Bastion.Spell.Traits.Cost.Params : Bastion.Spell.Traits.Cost[]
---@class Bastion.Spell.Traits.Aura.Params : Bastion.Spell.Traits.Aura
---@class Bastion.Spell.Traits.Params ---@class Bastion.Spell.Traits.Params
---@field cast? Bastion.Spell.Traits.Cast.Params ---@field cast? Bastion.Spell.Traits.Cast.Params
---@field target? Spell.Traits.Target.Params ---@field target? Bastion.Spell.Traits.Target.Params
---@field cost? Spell.Traits.Cost.Params ---@field cost? Bastion.Spell.Traits.Cost.Params
---@field aura? Bastion.Spell.Traits.Aura.Params
---@class Bastion.Spell.Aura ---@class Bastion.Spell.Aura
---@field spell Bastion.Spell ---@field spell Bastion.Spell
@ -110,10 +118,21 @@ end
function Spell:New(id) function Spell:New(id)
---@class Bastion.Spell ---@class Bastion.Spell
local self = setmetatable({}, Spell) local self = setmetatable({}, Spell)
local spellInfo = C_Spell.GetSpellInfo(id)
local maxRange = spellInfo and spellInfo.maxRange or 0
local minRange = spellInfo and spellInfo.minRange or 0
self.auras = {} self.auras = {}
self.overrides = {} self.overrides = {}
self.conditions = {} self.conditions = {}
self.spellID = id self.spellID = id
self.lastCastGUID = false
self.castGUID = false
self.lastCastSuccess = false
---@type 0 | 1 | 2 0 = SPELL_RANGE_DEFAULT, 1 = SPELL_RANGE_MELEE, 2 = SPELL_RANGE_RANGED
self.rangeEntry = 0
self.minRange = minRange
self.maxRange = maxRange
self.isMelee = minRange == 0 and maxRange == 0
self.traits = { self.traits = {
cast = { cast = {
moving = true, moving = true,
@ -124,12 +143,17 @@ function Spell:New(id)
override = false, override = false,
talent = false, talent = false,
power = false, power = false,
precast = false,
}, },
target = { target = {
exists = true, exists = true,
player = false, player = false,
facing = false,
}, },
cost = {}, cost = {},
aura = {
track = false,
},
} }
self.target = false self.target = false
self.wasLooking = false self.wasLooking = false
@ -138,8 +162,9 @@ function Spell:New(id)
return self return self
end end
---@return string
function Spell:GetDescription() function Spell:GetDescription()
return GetSpellDescription(self:GetID()) or "" return C_Spell.GetSpellDescription(self:GetID()) or ""
end end
-- Duplicator -- Duplicator
@ -161,23 +186,42 @@ function Spell:PostCast(func)
return self return self
end end
function Spell:GetSpellInfo()
return C_Spell.GetSpellInfo(self:GetID())
end
-- Get the spells name -- Get the spells name
---@return string ---@return string
function Spell:GetName() function Spell:GetName()
return Bastion.Globals.SpellName[self:GetID()] or select(1, GetSpellInfo(self:GetID())) local spellName = Bastion.Globals.SpellName[self:GetID()]
if not spellName then
local spellInfo = self:GetSpellInfo()
spellName = spellInfo and spellInfo.name or ""
end
return spellName
end end
-- Get the spells icon -- Get the spells icon
---@return number ---@return number
function Spell:GetIcon() function Spell:GetIcon()
return select(3, GetSpellInfo(self:GetID())) local spellInfo = self:GetSpellInfo()
return spellInfo and spellInfo.iconID or 0
end
---@param byId? boolean
function Spell:GetCooldownInfo(byId)
return C_Spell.GetSpellCooldown(byId and self:GetID() or self:GetName())
end end
-- Get the spells cooldown -- Get the spells cooldown
---@param byId? boolean ---@param byId? boolean
---@return number ---@return number
function Spell:GetCooldown(byId) function Spell:GetCooldown(byId)
return select(2, GetSpellCooldown(byId and self:GetID() or self:GetName())) local cdInfo = self:GetCooldownInfo(byId)
if cdInfo then
return cdInfo.duration
end
return 0
end end
-- Return the castable function -- Return the castable function
@ -199,18 +243,19 @@ end
---@param byId? boolean ---@param byId? boolean
---@return number ---@return number
function Spell:GetCooldownRemaining(byId) function Spell:GetCooldownRemaining(byId)
local start, duration = GetSpellCooldown(byId and self:GetID() or self:GetName()) local cdInfo = self:GetCooldownInfo(byId)
if start == 0 then if cdInfo then
return 0 if cdInfo.startTime == 0 then return 0 end
return cdInfo.startTime + cdInfo.duration - GetTime()
end end
return start + duration - GetTime() return 0
end end
-- Get the spell count -- Get the spell count
---@param byId? boolean ---@param byId? boolean
---@return number ---@return number
function Spell:GetCount(byId) function Spell:GetCount(byId)
return GetSpellCount((byId ~= nil and byId) and self:GetID() or self:GetName()) return C_Spell.GetSpellCastCount((byId ~= nil and byId) and self:GetID() or self:GetName())
end end
-- On cooldown -- On cooldown
@ -233,18 +278,18 @@ function Spell:AddOverrideSpell(spell, func)
end end
-- Cast the spell -- Cast the spell
---@param unit? false | Bastion.Unit ---@param unit? Bastion.Unit
---@param condition? string | fun(self:Bastion.Spell):boolean ---@param condition? string | fun(self:Bastion.Spell, target?: Bastion.Unit):boolean
---@return boolean ---@return boolean
function Spell:Cast(unit, condition) function Spell:Cast(unit, condition)
if not self:Castable() then if not self:Castable(unit) then
return false return false
end end
if condition then if condition then
if type(condition) == "string" and not self:EvaluateCondition(condition) then if type(condition) == "string" and not self:EvaluateCondition(condition) then
return false return false
elseif type(condition) == "function" and not condition(self) then elseif type(condition) == "function" and not condition(self, unit) then
return false return false
end end
end end
@ -259,15 +304,21 @@ function Spell:Cast(unit, condition)
self.wasLooking = IsMouselooking() --[[@as boolean]] self.wasLooking = IsMouselooking() --[[@as boolean]]
-- if unit:GetOMToken() contains 'nameplate' then we need to use Object wrapper to cast -- if unit:GetOMToken() contains 'nameplate' then we need to use Object wrapper to cast
local target = unit and unit:GetOMToken() or self.traits.target.player and "player" or "none"
local u = unit ~= false and unit:GetOMToken() or self.traits.target.player and "none" or "none" if type(target) == "string" and string.find(target, "nameplate") then
if type(u) == "string" and string.find(u, "nameplate") then target = Object(target):unit()
---@diagnostic disable-next-line: cast-local-type
u = Object(u)
end end
-- Cast the spell -- Cast the spell
CastSpellByName(self:GetName(), u) CastSpellByName(self:GetName(), target)
--[[ local tgt = Object(target)
local tgtname = target
if tgt then
tgtname = tgt:name() or target
target = UnitTokenFromGUID(tgt:guid() or "")
end
Tinkr.Util.Tools.Console.print("|cff1ebf3c[Cast]|r|cffFFCC00[" ..
self:GetName() .. "]|r->" .. tgtname .. "[" .. target .. "]") ]]
SpellCancelQueuedSpell() SpellCancelQueuedSpell()
Bastion.Util:Debug("Casting", self) Bastion.Util:Debug("Casting", self)
@ -291,14 +342,13 @@ function Spell:ForceCast(unit)
self.wasLooking = IsMouselooking() --[[@as boolean]] self.wasLooking = IsMouselooking() --[[@as boolean]]
-- if unit:GetOMToken() contains 'nameplate' then we need to use Object wrapper to cast -- if unit:GetOMToken() contains 'nameplate' then we need to use Object wrapper to cast
local u = unit and unit:GetOMToken() or self.traits.target.player and "none" or "none" local target = unit and unit:GetOMToken() or self.traits.target.player and "player" or "none"
if type(u) == "string" and string.find(u, "nameplate") then if type(target) == "string" and string.find(target, "nameplate") then
---@diagnostic disable-next-line: cast-local-type target = Object(target):unit()
u = Object(u)
end end
-- Cast the spell -- Cast the spell
CastSpellByName(self:GetName(), u) CastSpellByName(self:GetName(), target)
SpellCancelQueuedSpell() SpellCancelQueuedSpell()
Bastion.Util:Debug("Force Casting", self) Bastion.Util:Debug("Force Casting", self)
@ -315,7 +365,7 @@ function Spell:GetPostCastFunction()
end end
function Spell:OverrideSpellID() function Spell:OverrideSpellID()
return C_SpellBook.GetOverrideSpell(self.spellID) return C_Spell.GetOverrideSpell(self.spellID)
end end
function Spell:IsOverridden() function Spell:IsOverridden()
@ -326,27 +376,32 @@ end
---@param includeOverrides? boolean ---@param includeOverrides? boolean
---@return boolean ---@return boolean
function Spell:IsKnown(includeOverrides) function Spell:IsKnown(includeOverrides)
local isKnown = includeOverrides and IsSpellKnownOrOverridesKnown(self:GetID()) or IsSpellKnown(self:GetID()) or local isKnown = (includeOverrides or self.traits.cast.override) and IsSpellKnownOrOverridesKnown(self:GetID()) or
IsSpellKnown(self:GetID()) or
IsPlayerSpell(self:GetID()) IsPlayerSpell(self:GetID())
return isKnown return isKnown
end end
function Spell:GetSpellLossOfControlCooldown() function Spell:GetSpellLossOfControlCooldown()
local start, duration = GetSpellLossOfControlCooldown(self:IsOverridden() and self:GetName() or self:GetID()) or 0, 0 local start, duration =
C_Spell.GetSpellLossOfControlCooldown(self:IsOverridden() and self:GetName() or self:GetID()) or 0, 0
return start, duration return start, duration
end end
-- Check if the spell is on cooldown -- Check if the spell is on cooldown
---@return boolean ---@return boolean
function Spell:IsOnCooldown() function Spell:IsOnCooldown()
local spellIDName = self:IsOverridden() and self:GetName() or self:GetID() local spellCooldownInfo = self:GetCooldownInfo(self:IsOverridden() and false)
return select(2, GetSpellCooldown(spellIDName)) > 0 or select(2, self:GetSpellLossOfControlCooldown()) > 0 if spellCooldownInfo and spellCooldownInfo.duration then
return spellCooldownInfo.duration > 0 or select(2, self:GetSpellLossOfControlCooldown()) > 0
end
return false
end end
---@param byId? boolean ---@param byId? boolean
---@return boolean, boolean ---@return boolean, boolean
function Spell:IsUsableSpell(byId) function Spell:IsUsableSpell(byId)
return IsUsableSpell(byId and self:GetID() or self:GetName()) return C_Spell.IsSpellUsable(byId and self:GetID() or self:GetName())
end end
-- Check if the spell is usable -- Check if the spell is usable
@ -361,12 +416,13 @@ end
---@param override? boolean ---@param override? boolean
---@return boolean ---@return boolean
function Spell:IsKnownAndUsable(override) function Spell:IsKnownAndUsable(override)
return self:IsKnown(override) and not self:IsOnCooldown() and self:IsUsable(override ~= nil and false or true) return self:IsKnown(override or self.traits.cast.override) and not self:IsOnCooldown() and
self:IsUsable(override ~= nil and true or false)
end end
---@param traits Bastion.Spell.Traits.Params ---@param traits Bastion.Spell.Traits.Params
function Spell:SetTraits(traits) function Spell:SetTraits(traits)
for _, trait in pairs({ "cast", "target" }) do for _, trait in pairs({ "cast", "target", "aura" }) do
if type(traits[trait]) == "table" then if type(traits[trait]) == "table" then
for k, _ in pairs(self.traits[trait]) do for k, _ in pairs(self.traits[trait]) do
if traits[trait][k] ~= nil then if traits[trait][k] ~= nil then
@ -384,8 +440,17 @@ function Spell:SetTraits(traits)
return self return self
end end
function Spell:EvaluateTraits() ---@param target? Bastion.Unit
local player = Bastion.UnitManager:Get("player") function Spell:EvaluateTraits(target)
local player = Bastion.Globals.UnitManager:Get("player")
if self.traits.cast.precast and not self.traits.cast.precast(self) then
return false
end
if self.traits.target.facing and target and not player:IsFacing(target) then
return false
end
if self.traits.cast.power and not self:HasPower() then if self.traits.cast.power and not self:HasPower() then
return false return false
@ -411,11 +476,15 @@ function Spell:EvaluateTraits()
return false return false
end end
if self.traits.target.exists and not self:TargetExists() then if self.traits.target.exists then
if (self.traits.target.player and not player:Exists()) then
return false
elseif (target and (target == Bastion.Globals.UnitManager:Get("none") or not target:Exists())) or (not target and not self:TargetExists()) then
return false return false
end end
end
if self.traits.cast.talent and not IsPlayerSpell(type(self.traits.cast.talent) == "number" and tonumber(self.traits.cast.talent) or self:GetID()) then if self.traits.cast.talent and not IsPlayerSpell(tonumber(self.traits.cast.talent) or self:GetID()) then
return false return false
end end
@ -423,9 +492,10 @@ function Spell:EvaluateTraits()
end end
-- Check if the spell is castable -- Check if the spell is castable
function Spell:Castable() ---@param target? Bastion.Unit
return self:EvaluateTraits() and (not self:GetCastableFunction() or self:GetCastableFunction()(self)) and function Spell:Castable(target)
self:IsKnownAndUsable(type(self.traits.cast.override) ~= nil and self.traits.cast.override or nil) return self:IsKnownAndUsable(self.traits.cast.override) and self:EvaluateTraits(target) and
(not self:GetCastableFunction() or self:GetCastableFunction()(self))
end end
-- Set a script to check if the spell is castable -- Set a script to check if the spell is castable
@ -485,34 +555,66 @@ function Spell:Call(unit)
end end
-- Check if the spell is castable and cast it -- Check if the spell is castable and cast it
---@return boolean?
function Spell:HasRange() function Spell:HasRange()
return SpellHasRange(self:GetName()) return C_Spell.SpellHasRange(self:GetName())
end end
-- Get the range of the spell -- Get the range of the spell
---@return number ---@return number
---@return number ---@return number
function Spell:GetRange() function Spell:GetRange()
local name, rank, icon, castTime, minRange, maxRange, spellID, originalIcon = GetSpellInfo(self:GetID()) --local _, _, _, _, minRange, maxRange, _, _ = GetSpellInfo(self:GetID())
return maxRange, minRange return self.maxRange, self.minRange
end
---@param target? Bastion.Unit
function Spell:CheckRange(target)
end
---@param target? Bastion.Unit
function Spell:GetMinMaxRange(target)
local rangeMod, minRange, maxRange = 0.0, 0.0, 0.0
local unitCaster = Bastion.Globals.UnitManager:Get("player")
if self:HasRange() then
minRange, maxRange = self:GetRange()
-- probably melee
if self.isMelee then
rangeMod = unitCaster:GetMeleeRange(target or unitCaster)
else
local meleeRange = 0
--[[ if maxRange > 0 then
meleeRange = unitCaster:GetMeleeRange(target or unitCaster)
end ]]
minRange = (minRange == maxRange and minRange or not target and 0 or minRange)
end
end
maxRange = maxRange + rangeMod
return minRange, maxRange
end end
-- Check if the spell is in range of the unit -- Check if the spell is in range of the unit
---@param unit Bastion.Unit ---@param unit Bastion.Unit
---@return boolean ---@return boolean
function Spell:IsInRange(unit) function Spell:IsInRange(unit)
local hasRange = self:HasRange() if unit:IsUnit(Bastion.Globals.UnitManager:Get("player")) then
local inRange = IsSpellInRange(self:GetName(), unit:GetOMToken())
if hasRange == false then
return true return true
end end
if inRange == 1 then local hasRange = self:HasRange()
if hasRange == false then
return true return true
end end
return Bastion.UnitManager:Get("player"):InMelee(unit) local inRange = C_Spell.IsSpellInRange(self:GetName(), unit:GetOMToken()) or false
--[[ if inRange then
return true
end ]]
return inRange
--return Bastion.Globals.UnitManager:Get("player"):InMelee(unit)
end end
-- Get the last cast time -- Get the last cast time
@ -539,61 +641,88 @@ function Spell:GetTimeSinceLastCastAttempt()
return GetTime() - self.lastCastAttempt return GetTime() - self.lastCastAttempt
end end
function Spell:GetCastGuid()
return self.castGUID
end
---@param time number
---@param success boolean
---@param castGUID string
function Spell:SetLastCast(time, success, castGUID)
self.lastCastSuccess = success
if success and self.lastCastGUID and self.lastCastGUID == castGUID then
self.lastCastAt = self.lastCastAttempt
self.castGUID = self.lastCastGUID
end
end
---@param time number
---@param castGUID string
function Spell:SetLastCastAttempt(time, castGUID)
self.lastCastSuccess = false
self.lastCastAttempt = time
self.lastCastGUID = castGUID
end
function Spell:GetChargeInfo() function Spell:GetChargeInfo()
return GetSpellCharges(self:GetID()) return C_Spell.GetSpellCharges(self:GetID())
end end
-- Get the spells charges -- Get the spells charges
function Spell:GetCharges() function Spell:GetCharges()
return select(1, GetSpellCharges(self:GetID())) local chargeInfo = self:GetChargeInfo()
return chargeInfo and chargeInfo.currentCharges or 0
end end
function Spell:GetMaxCharges() function Spell:GetMaxCharges()
return select(2, GetSpellCharges(self:GetID())) local chargeInfo = self:GetChargeInfo()
return chargeInfo and chargeInfo.maxCharges or 0
end end
---@return number ---@return number
function Spell:GetCastLength() function Spell:GetCastLength()
return select(4, GetSpellInfo(self:GetID())) local spellInfo = self:GetSpellInfo()
return spellInfo and spellInfo.castTime or 0
end end
-- Get the full cooldown (time until all charges are available) -- Get the full cooldown (time until all charges are available)
---@return number ---@return number
function Spell:GetFullRechargeTime() function Spell:GetFullRechargeTime()
local charges, maxCharges, _, duration = self:GetChargeInfo() local spellChargeInfo = self:GetChargeInfo()
if not charges or not maxCharges or charges == maxCharges then if not spellChargeInfo or spellChargeInfo.currentCharges == spellChargeInfo.maxCharges then
return 0 return 0
end end
return (maxCharges - self:GetChargesFractional()) * duration return (spellChargeInfo.maxCharges - self:GetChargesFractional()) * spellChargeInfo.cooldownDuration
end end
function Spell:Recharge() function Spell:Recharge()
local charges, maxCharges, startTime, duration = self:GetChargeInfo() local spellChargeInfo = self:GetChargeInfo()
if charges == maxCharges then if not spellChargeInfo or spellChargeInfo.currentCharges == spellChargeInfo.maxCharges then
return 0 return 0
end end
local recharge = startTime + duration - GetTime() local recharge = spellChargeInfo.cooldownStartTime + spellChargeInfo.cooldownDuration - GetTime()
return recharge > 0 and recharge or 0 return recharge > 0 and recharge or 0
end end
-- Get the spells charges plus fractional -- Get the spells charges plus fractional
---@return number ---@return number
function Spell:GetChargesFractional() function Spell:GetChargesFractional()
local charges, maxCharges, _, duration = GetSpellCharges(self:GetID()) local spellChargeInfo = self:GetChargeInfo()
if charges == maxCharges then if not spellChargeInfo or spellChargeInfo.currentCharges == spellChargeInfo.maxCharges then
return charges return spellChargeInfo.currentCharges
end end
return charges + ((duration - self:Recharge()) / duration) return spellChargeInfo.currentCharges +
((spellChargeInfo.cooldownDuration - self:Recharge()) / spellChargeInfo.cooldownDuration)
end end
-- Get the spells charges remaining -- Get the spells charges remaining
---@return number ---@return number
function Spell:GetChargesRemaining() function Spell:GetChargesRemaining()
local charges, maxCharges, start, duration = GetSpellCharges(self:GetID()) local spellChargeInfo = self:GetChargeInfo()
return charges return spellChargeInfo and spellChargeInfo.currentCharges or 0
end end
-- Create a condition for the spell -- Create a condition for the spell
@ -705,16 +834,22 @@ function Spell:IsSpell(spell)
return self:GetID() == spell:GetID() return self:GetID() == spell:GetID()
end end
---@return SpellPowerCostInfo[]?
function Spell:GetCostInfo()
return C_Spell.GetSpellPowerCost(self:GetID())
end
function Spell:HasPower() function Spell:HasPower()
local costs = GetSpellPowerCost(self:GetID()) local costs = self:GetCostInfo()
local checked = {} local checked = {}
local hasPower = #costs > 0 and false or true local hasPower = costs and #costs > 0 and false or true
if not hasPower then if costs and not hasPower then
for _, cost in ipairs(costs) do for _, cost in ipairs(costs) do
if not checked[cost.type] then if not checked[cost.type] then
local powerCost = (cost.cost > cost.minCost and cost.minCost or cost.cost) local powerCost = (cost.cost > cost.minCost and cost.minCost or cost.cost)
if cost.hasRequiredAura or cost.requiredAuraID == 0 then if cost.hasRequiredAura or cost.requiredAuraID == 0 then
hasPower = powerCost == 0 or Bastion.UnitManager:Get("player"):GetPower(cost.type) >= powerCost hasPower = powerCost == 0 or
Bastion.Globals.UnitManager:Get("player"):GetPower(cost.type) >= powerCost
checked[cost.type] = true checked[cost.type] = true
if not hasPower then if not hasPower then
return false return false
@ -729,7 +864,7 @@ end
-- GetCost -- GetCost
---@return number ---@return number
function Spell:GetCost() function Spell:GetCost()
local cost = GetSpellPowerCost(self:GetID()) local cost = self:GetCostInfo()
return cost and cost[1] and cost[1].cost or 0 return cost and cost[1] and cost[1].cost or 0
end end
@ -757,10 +892,10 @@ end
---@param source? "any" | Bastion.Unit ---@param source? "any" | Bastion.Unit
function Spell:GetAura(target, source) function Spell:GetAura(target, source)
if type(target) == "nil" then if type(target) == "nil" then
target = Bastion.UnitManager:Get("player") target = Bastion.Globals.UnitManager:Get("player")
end end
if type(source) == "nil" then if type(source) == "nil" then
source = Bastion.UnitManager:Get("player") source = Bastion.Globals.UnitManager:Get("player")
end end
return target:GetAuras():FindFrom(self, source) return target:GetAuras():FindFrom(self, source)
@ -816,7 +951,7 @@ end
---@param params { source: Bastion.Unit, target: Bastion.Unit, spellVisualId?: number } ---@param params { source: Bastion.Unit, target: Bastion.Unit, spellVisualId?: number }
function Spell:GetMissiles(params) function Spell:GetMissiles(params)
return Bastion.MissileManager:GetMissiles({ return Bastion.Globals.MissileManager:GetMissiles({
source = params.source, source = params.source,
target = params.target, target = params.target,
spellId = self:GetID(), spellId = self:GetID(),
@ -829,4 +964,79 @@ function Spell:InFlight(params)
return #self:GetMissiles(params) > 0 return #self:GetMissiles(params) > 0
end end
--[[ ---@param target? Bastion.Unit
function Spell:IsMineUp(target)
target = target or Bastion.UnitManager:Get("player")
return target:GetAuras():FindMy(self):IsUp()
end
---@param target? Bastion.Unit
function Spell:IsMineRefreshable(target)
target = target or Bastion.UnitManager:Get("player")
return target:GetAuras():FindMy(self):Refreshable()
end
---@param spells Bastion.List
---@param source Bastion.Unit
---@param target Bastion.Unit
function Spell:IsAnyUp(spells, source, target)
local targetAuras = source:Exists() and target:GetAuras():FindAnyFrom(spells + self, source) or
target:GetAuras():FindAnyOf(spells + self)
return targetAuras:IsUp()
end
---@param source Bastion.Unit
---@params target Bastion.Unit
function Spell:IsUp(source, target)
local targetAuras = source:Exists() and target:GetAuras():FindFrom(self, source) or target:GetAuras():FindAny(self)
return targetAuras:IsUp()
end
---@param source Bastion.Unit
---@param target Bastion.Unit
function Spell:IsDown(source, target)
local targetAuras = source:Exists() and target:GetAuras():FindFrom(self, source) or target:GetAuras():FindAny(self)
return targetAuras:IsDown()
end
---@param spells Bastion.List
---@param target Bastion.Unit
function Spell:IsAnyOfMineUp(spells, target)
target = target or Bastion.UnitManager:Get("player")
return target:GetAuras():FindAnyOfMy(spells + self):IsUp()
end
---@param source Bastion.Unit
---@param target Bastion.Unit
function Spell:GetAuraCount(source, target)
return (source:Exists() and target:GetAuras():FindFrom(self, source) or target:GetAuras():FindAny(self)):GetCount()
end
---@param target? Bastion.Unit
function Spell:GetMyCount(target)
target = target or Bastion.UnitManager:Get("player")
return target:GetAuras():FindMy(self):GetCount()
end
---@param source Bastion.Unit
---@param target Bastion.Unit
function Spell:GetRemainingTime(source, target)
local targetAuras = source:Exists() and target:GetAuras():FindFrom(self, source) or target:GetAuras():FindAny(self)
return targetAuras:GetRemainingTime()
end
---@param target? Bastion.Unit
function Spell:GetMyRemainingTime(target)
target = target or Bastion.UnitManager:Get("player")
return target:GetAuras():FindMy(self):GetRemainingTime()
end
---@param spells Bastion.List
---@param source Bastion.Unit
---@param target Bastion.Unit
function Spell:GetAnyRemainingTime(spells, source, target)
return (source:Exists() and target:GetAuras():FindAnyFrom(spells + self, source) or target:GetAuras():FindAnyOf(spells + self))
:GetRemainingTime()
end ]]
Bastion.Spell = Spell Bastion.Spell = Spell

@ -11,25 +11,77 @@ local SpellBook = {}
SpellBook.__index = SpellBook SpellBook.__index = SpellBook
-- Constructor -- Constructor
---@param global? boolean
---@return Bastion.SpellBook ---@return Bastion.SpellBook
function SpellBook:New() function SpellBook:New(global)
---@class Bastion.SpellBook
local self = setmetatable({}, SpellBook) local self = setmetatable({}, SpellBook)
self.spells = {} self.spells = {}
--[[ Bastion.Globals.EventManager:RegisterWoWEvent("UNIT_SPELLCAST_SUCCEEDED", function(...) self.global = global or false
self:SpellCastSucceeded(...) --self:RegisterEvents()
end) ]]
return self return self
end end
--[[ function SpellBook:SpellCastSucceeded(...) function SpellBook:RegisterEvents()
local unit, castGUID, spellId = ... Bastion.Globals.EventManager:RegisterWoWEvent(
if self.spells[spellId] then { "UNIT_SPELLCAST_SENT", "UNIT_SPELLCAST_SUCCEEDED", "UNIT_SPELLCAST_FAILED", "UNIT_SPELLCAST_FAILED_QUIET",
self.spells[spellId].lastCastAt = GetTime() "UNIT_SPELLCAST_INTERRUPTED", "UNIT_SPELLCAST_START" },
function(...)
self:UnitSpellCast(...)
end, true)
end
---@param event "UNIT_SPELLCAST_SENT" | "UNIT_SPELLCAST_SUCCEEDED" | "UNIT_SPELLCAST_FAILED" | "UNIT_SPELLCAST_FAILED_QUIET" | "UNIT_SPELLCAST_INTERRUPTED"
function SpellBook:UnitSpellCast(event, ...)
local casterUnitId, targetUnitId, castGUID, spellId
local currentTime = GetTime()
if event == "UNIT_SPELLCAST_SENT" then
---@type UnitId, string, string, number
casterUnitId, targetUnitId, castGUID, spellId = ...
else
---@type UnitId, string, number
casterUnitId, castGUID, spellId = ...
end
local spell = self:GetIfRegistered(spellId) or self.global and self:GetSpell(spellId)
if spell then
if event == "UNIT_SPELLCAST_SENT" then
spell:SetLastCastAttempt(GetTime(), castGUID)
-- print(event, self.global, ...)
local casterUnit = Bastion.Globals.UnitManager:GetObject(casterUnitId)
if casterUnit and casterUnit:IsValid() and casterUnit.castGUID ~= castGUID then
casterUnit:SetLastSpell(spell, castGUID)
end
elseif event == "UNIT_SPELLCAST_SUCCEEDED" then
spell:SetLastCast(GetTime(), true, castGUID)
-- print(event, self.global, ...)
local casterUnit = Bastion.Globals.UnitManager:GetObject(casterUnitId)
if casterUnit and casterUnit:IsValid() then
casterUnit:SetSpell(false, castGUID)
casterUnit:SetLastCastInterrupted(false)
end
elseif event == "UNIT_SPELLCAST_FAILED" or event == "UNIT_SPELLCAST_FAILED_QUIET" then
if spell.castGUID == castGUID then
spell:SetLastCast(GetTime(), false, castGUID)
end
-- print(event, self.global, ...)
local casterUnit = Bastion.Globals.UnitManager:GetObject(casterUnitId)
if casterUnit and casterUnit:IsValid() then
casterUnit:SetSpell(false)
end
elseif event == "UNIT_SPELLCAST_INTERRUPTED" then
-- print(event, self.global, ...)
local casterUnit = Bastion.Globals.UnitManager:GetObject(casterUnitId)
if casterUnit and casterUnit:IsValid() then
casterUnit:SetSpell(false, castGUID)
casterUnit:SetLastCastInterrupted(true)
end
end
end
end end
end ]]
-- Get a spell from the spellbook -- Get a spell from the spellbook
---@param id integer ---@param id integer|string
---@return Bastion.Spell ---@return Bastion.Spell
function SpellBook:GetSpell(id) function SpellBook:GetSpell(id)
local override = FindSpellOverrideByID(id) local override = FindSpellOverrideByID(id)
@ -84,8 +136,8 @@ end
---@param name string ---@param name string
---@return Bastion.Spell ---@return Bastion.Spell
function SpellBook:GetSpellByName(name) function SpellBook:GetSpellByName(name)
local _, rank, icon, castTime, minRange, maxRange, spellID, originalIcon = GetSpellInfo(name) local spellInfo = C_Spell.GetSpellInfo(name)
return self:GetSpell(spellID) return self:GetSpell(spellInfo and spellInfo.spellID or 0)
end end
---@param id integer ---@param id integer
@ -101,5 +153,5 @@ function SpellBook:GetIfRegistered(id)
return false return false
end end
Bastion.Globals.SpellBook = SpellBook:New()
Bastion.SpellBook = SpellBook Bastion.SpellBook = SpellBook
--Bastion.Globals.SpellBook = Bastion.SpellBook:New(true)

@ -3,23 +3,19 @@ local Tinkr,
---@class Bastion ---@class Bastion
Bastion = ... Bastion = ...
local Target = Bastion.UnitManager:Get("target")
if not Bastion.Globals.UnitInfo then
Bastion.Globals.UnitInfo = Bastion.Cache:New()
end
local Cache = Bastion.Globals.UnitInfo
--- An attempt to integrate HeroLib TTD timers. --- An attempt to integrate HeroLib TTD timers.
---@class Bastion.TimeToDie ---@class Bastion.TimeToDie
local TimeToDie = { local TimeToDie = {
Refreshing = false,
Settings = { Settings = {
-- Refresh time (seconds) : min=0.1, max=2, default = 0.1 -- Refresh time (seconds) : min=0.1, max=2, default = 0.1
Refresh = 0.1, Refresh = 0.2,
-- History time (seconds) : min=5, max=120, default = 10+0.4 -- History time (seconds) : min=5, max=120, default = 10+0.4
HistoryTime = 10 + 0.4, HistoryTime = 10 + 0.4,
-- Max history count : min=20, max=500, default = 100 -- Max history count : min=20, max=500, default = 100
HistoryCount = 100 HistoryCount = 100,
InactiveTime = 60,
}, },
---@type table<number, { time: number, percentage: number }> ---@type table<number, { time: number, percentage: number }>
Cache = {}, -- A cache of unused { time, value } tables to reduce garbage due to table creation Cache = {}, -- A cache of unused { time, value } tables to reduce garbage due to table creation
@ -44,82 +40,106 @@ local TimeToDie = {
PLAYER = -6, -- 25 PLAYER = -6, -- 25
DOES_NOT_EXIST = -7, DOES_NOT_EXIST = -7,
}, },
NextRefresh = 0 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:IterableUnits() function TimeToDie:IsRefreshing()
return Bastion.ObjectManager.enemies return self.Refreshing
end end
function TimeToDie:Refresh() function TimeToDie:Init()
local currentTime = GetTime() 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
if currentTime >= TimeToDie.NextRefresh then function TimeToDie:IterableUnits2()
TimeToDie.NextRefresh = currentTime + TimeToDie.Settings.Refresh return Bastion.ObjectManager.activeEnemies
else 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 return
end end
local currentTime = GetTime()
local historyCount = TimeToDie.Settings.HistoryCount self.Refreshing = true
local historyTime = TimeToDie.Settings.HistoryTime self.LastStart = currentTime
local ttdCache = TimeToDie.Cache self.NextUpdate = self.LastStart + 2
local iterableUnits = TimeToDie:IterableUnits()
local units = TimeToDie.Units
local existingUnits = TimeToDie.ExistingUnits
wipe(existingUnits) 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 unit Bastion.Unit ---@param sourceGUID string
iterableUnits:each(function(unit) function TimeToDie:UNIT_DIED(sourceGUID)
if unit:Exists() then if self.Units[sourceGUID] then
local unitGUID = unit:GetGUID() self.Units[sourceGUID] = nil
-- Check if we didn't already scanned this unit.
if unitGUID and not existingUnits[unitGUID] then
existingUnits[unitGUID] = true
local healthPercentage = unit:GetHealthPercent()
-- Check if it's a valid unit
if healthPercentage < 100 then
--local unitTable = units[unitGUID]
local unitTable = units[unitGUID]
-- Check if we have seen one time this unit, if we don't then initialize it.
-- Also check to see if the unit's health percentage is higher than the last one we saw. If so, we recreate the table.
if not unitTable or healthPercentage > unitTable.history[1].percentage then
if unitTable and healthPercentage > unitTable.history[1].percentage then
for i = 1, #unitTable.history do
if healthPercentage > unitTable.history[i].percentage then
unitTable.history[i].percentage = healthPercentage
else
break
end end
end end
else
---@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 = { unitTable = {
history = {}, history = {},
time = currentTime time = currentTime
} }
units[unitGUID] = unitTable self.Units[unitGUID] = unitTable
end
end end
local history = unitTable.history local history = unitTable.history
local time = currentTime - unitTable.time local time = currentTime - unitTable.time
-- Check if the % HP changed since the last check (or if there were none) if not history or not history[1] or unitHealthPerc ~= history[1].percentage then
if not history or not history[1] or healthPercentage ~= history[1].percentage then
local val local val
local lastIndex = #ttdCache local lastIndex = #self.Cache
-- Check if we can re-use a table from the cache
if lastIndex == 0 then if lastIndex == 0 then
val = { time = time, percentage = healthPercentage } val = { time = time, percentage = unitHealthPerc }
else else
val = ttdCache[lastIndex] val = self.Cache[lastIndex]
ttdCache[lastIndex] = nil self.Cache[lastIndex] = nil
val.time = time val.time = time
val.percentage = healthPercentage val.percentage = unitHealthPerc
end end
table.insert(history, 1, val) table.insert(history, 1, val)
local n = #history local n = #history
-- Delete values that are no longer valid while (n > self.Settings.HistoryCount) or (time - history[n].time > self.Settings.HistoryTime) do
while (n > historyCount) or (time - history[n].time > historyTime) do self.Cache[#self.Cache + 1] = history[n]
ttdCache[#ttdCache + 1] = history[n]
history[n] = nil history[n] = nil
n = n - 1 n = n - 1
end end
@ -127,16 +147,6 @@ function TimeToDie:Refresh()
end 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 end
TimeToDie.specialTTDPercentageData = { TimeToDie.specialTTDPercentageData = {
@ -260,7 +270,7 @@ function TimeToDie.FightRemains(enemies, bossOnly)
---@type boolean, number ---@type boolean, number
local bossExists, maxTimeToDie local bossExists, maxTimeToDie
for i = 1, 4 do for i = 1, 4 do
local bossUnit = Bastion.UnitManager:Get(string.format("boss%d", i)) local bossUnit = Bastion.Globals.UnitManager:Get(string.format("boss%d", i))
if bossUnit:Exists() then if bossUnit:Exists() then
bossExists = true bossExists = true
if not bossUnit:TimeToDieIsNotValid() then if not bossUnit:TimeToDieIsNotValid() then
@ -288,7 +298,7 @@ function TimeToDie.FightRemains(enemies, bossOnly)
end end
end end
return Target:TimeToDie2() return Bastion.Globals.UnitManager:Get("target"):TimeToDie2()
end end
-- Returns the max fight length of boss units, 11111 if not a boss fight -- Returns the max fight length of boss units, 11111 if not a boss fight

@ -0,0 +1,127 @@
---@type Tinkr
local Tinkr,
---@class Bastion
Bastion = ...
---@class Bastion.TimeToDieManager
---@field units table<string, Bastion.TimeToDieUnit>
---@field recycle Bastion.TimeToDieUnit[]
local TimeToDieManager = {}
local tokenPrefixes = {
"^arena[1-5]",
"^boss[1-5]",
"^focus",
"^mouseover",
"^none",
"^party[1-4]",
"^partypet[1-4]",
"^pet",
"^player",
"^raid[1-40]",
"^raidpet[1-40]",
"^target",
"^vehicle",
"^nameplate[1-40]",
"^spectated",
}
---@param token string
local validateToken = function(token)
for i = 1, #tokenPrefixes do
if string.match(token, tokenPrefixes[i]) then
return true
end
end
end
function TimeToDieManager:__index(k)
if type(TimeToDieManager[k]) ~= "nil" then
return TimeToDieManager[k]
end
local kguid = type(k) == "string" and validateToken(k) and ObjectGUID(k) or k
if kguid and self.units[kguid] then
return self.units[kguid]
end
if self.units[kguid] == nil then
if Object(k) then
--return self:SetObject(Bastion.Unit:New(Object(k) or k))
end
end
end
function TimeToDieManager:New()
---@class Bastion.TimeToDieManager
local self = setmetatable({}, TimeToDieManager)
self.units = {}
self.recycle = {}
return self
end
---@param unitId UnitIds
function TimeToDieManager:NAME_PLATE_UNIT_ADDED(unitId)
print("NAME_PLATE_UNIT_ADDED", unitId)
end
---@param unitId UnitIds
function TimeToDieManager:NAME_PLATE_UNIT_REMOVED(unitId)
print("NAME_PLATE_UNIT_REMOVED", unitId)
end
---@param unitId UnitIds
function TimeToDieManager:UNIT_FLAGS(unitId)
print("UNIT_FLAGS", unitId)
end
function TimeToDieManager:RegisterEvents()
Bastion.EventManager:RegisterWoWEvent("NAME_PLATE_UNIT_ADDED", function(...)
self:NAME_PLATE_UNIT_ADDED(...)
end)
Bastion.EventManager:RegisterWoWEvent("NAME_PLATE_UNIT_REMOVED", function(...)
self:NAME_PLATE_UNIT_REMOVED(...)
end)
Bastion.EventManager:RegisterWoWEvent("UNIT_FLAGS", function(...)
self:UNIT_FLAGS(...)
end)
end
---@param unitGuid string
function TimeToDieManager:GetUnit(unitGuid)
if unitGuid and self.units[unitGuid] then
return self.units[unitGuid]
end
end
---@param unitGuid string
function TimeToDieManager:RemoveUnit(unitGuid)
if type(self.units[unitGuid]) ~= "nil" then
local oldUnit = self.units[unitGuid]
self.units[unitGuid] = nil
table.insert(self.recycle, oldUnit:Recycle())
end
end
---@param when number
---@param healthPct number
---@param unit? WowGameObject
function TimeToDieManager:RecycleUnit(when, healthPct, unit)
return Bastion.TimeToDieUnit:New(table.remove(self.recycle) or false, when, healthPct, unit)
end
---@param unitGuid string
---@param healthPct number
---@param when? number
function TimeToDieManager:UpdateUnit(unitGuid, healthPct, when)
local unitObject = self.units[unitGuid]
when = when or GetTime()
if not unitObject then
unitObject = self:GetUnit(unitGuid)
end
end
Bastion.TimeToDieManager = TimeToDieManager:New()

@ -0,0 +1,120 @@
---@type Tinkr
local Tinkr,
---@class Bastion
Bastion = ...
---@class Bastion.TimeToDieUnit
local TimeToDieUnit = {
Enums = {
Duration = {
DEFAULT_TTD = 30,
FOREVER = 300,
TRIVIAL = 5,
},
DeathPercent = {
[162099] = 0.5, -- General Kaal; Sanguine Depths
[166608] = 0.1, -- Mueh'zala; De Other Side
[164929] = 0.2, -- Tirnenn Villager; Mists of Tirna Scythe
[164804] = 0.2, -- Droman Oulfarran; Mists of Tirna Scythe
},
Excluded = {
[23775] = true, -- Head of the Horseman
[120651] = true, -- Explosives
[156227] = true, -- Neferset Denizen
[160966] = true, -- Thing from Beyond?
[161895] = true, -- Thing from Beyond?
[157452] = true, -- Nightmare Antigen in Carapace
[158041] = 310126, -- N'Zoth with Psychic Shell
[164698] = true, -- Tor'ghast Junk
[177117] = 355790, -- Ner'zhul: Orb of Torment (Protected by Eternal Torment)
[176581] = true, -- Painsmith: Spiked Ball
[186150] = true, -- Soul Fragment (Gavel of the First Arbiter)
[185685] = true, -- Season 3 Relics
[185680] = true, -- Season 3 Relics
[185683] = true, -- Season 3 Relics
[183501] = 367573, -- Xy'mox: Genesis Bulwark
[166969] = true, -- Frieda
[166970] = true, -- Stavros
[166971] = true, -- Niklaus
[168113] = 329606, -- Grashaal (when shielded)
[168112] = 329636, -- Kaal (when shielded)
[193760] = true, -- Surging Ruiner (Raszageth) -- gives bad range information.
[204560] = true, -- Incorporeal Being
}
},
}
TimeToDieUnit.__index = TimeToDieUnit
---@param recycledUnit Bastion.TimeToDieUnit | false
---@param when number
---@param healthPct number
---@param unit? WowGameObject
function TimeToDieUnit:New(recycledUnit, when, healthPct, unit)
---@class Bastion.TimeToDieUnit
local self = recycledUnit and recycledUnit:Recycle() or setmetatable({}, TimeToDieUnit)
self.firstSeen = when
self.firstHealthPct = healthPct
self.lastSeen = when
self.lastHealthPct = healthPct
self.unit = unit or false
self.rate = 0
self.n = 0
self.npcid = unit and unit:id() or false
self.deathPercent = self.npcid and TimeToDieUnit.Enums.DeathPercent[self.npcid] or 0
self.deathTime = self.unit and (UnitIsTrivial(self.unit:unit()) and UnitLevel(self.unit:unit()) > -1) and
TimeToDieUnit.Enums.TRIVIAL or TimeToDieUnit.Enums.DEFAULT_TTD
self.excluded = self.npcid and TimeToDieUnit.Enums.Excluded[self.npcid] or false
return self:Update(when, healthPct)
end
function TimeToDieUnit:Recycle()
self.firstSeen = 0
self.firstHealthPct = 0
self.lastSeen = 0
self.lastHealthPct = 0
self.unit = false
self.rate = 0
self.n = 0
self.npcid = 0
self.deathPercent = 0
self.deathTime = 0
self.excluded = false
return self
end
---@param when number
---@param healthPct number
function TimeToDieUnit:Update(when, healthPct)
local difference = self.lastHealthPct - healthPct
-- We don't recalculate the rate when enemies heal.
if difference > 0 then
local elapsed = when - self.lastSeen
-- If this is our first health difference, just store it.
if self.n == 0 then
self.rate = difference / elapsed
self.n = 1
else
local samples = min(self.n, 9)
local newRate = self.rate * samples + (difference / elapsed)
self.n = samples + 1
self.rate = newRate / self.n
end
self.deathTime = (healthPct - self.deathPercent) / self.rate
end
self.lastHealth = healthPct
self.lastSeen = when
return self
end
Bastion.TimeToDieUnit = TimeToDieUnit

@ -18,9 +18,11 @@ Timer.__index = Timer
---@param cb? fun():boolean ---@param cb? fun():boolean
---@return Bastion.Timer ---@return Bastion.Timer
function Timer:New(type, cb) function Timer:New(type, cb)
---@class Bastion.Timer
local self = setmetatable({}, Timer) local self = setmetatable({}, Timer)
self.startTime = nil self.startTime = nil
self.type = type self.type = type
self.checking = false
self.cb = cb self.cb = cb
return self return self
end end
@ -52,14 +54,21 @@ function Timer:Reset()
self.startTime = nil self.startTime = nil
end end
function Timer:IsChecking()
return self.checking
end
function Timer:Check() function Timer:Check()
self.checking = true
if self.cb then if self.cb then
if not self:IsRunning() and self.cb() then local cbResult = self.cb()
if not self:IsRunning() and cbResult then
self:Start() self:Start()
elseif self:IsRunning() and not self.cb() then elseif self:IsRunning() and not cbResult then
self:Reset() self:Reset()
end end
end end
self.checking = false
end end
Bastion.Timer = Timer Bastion.Timer = Timer

@ -8,7 +8,7 @@ Bastion = ...
---@field cache Bastion.Cache ---@field cache Bastion.Cache
---@field id false | number ---@field id false | number
---@field ttd_ticker false | cbObject ---@field ttd_ticker false | cbObject
---@field unit? TinkrObjectReference ---@field unit? WowGameObject
---@field aura_table Bastion.AuraTable | nil ---@field aura_table Bastion.AuraTable | nil
local Unit = { local Unit = {
---@type Bastion.Cache ---@type Bastion.Cache
@ -22,9 +22,9 @@ local Unit = {
unit = nil, unit = nil,
ttd_ticker = false, ttd_ticker = false,
ttd = 0, ttd = 0,
id = false, --[[ @asnumber ]] id = false,
watching_for_swings = false, watching_for_swings = false,
health = {} health = {},
} }
function Unit:UpdateHealth() function Unit:UpdateHealth()
@ -77,28 +77,168 @@ function Unit:__tostring()
end end
-- Constructor -- Constructor
---@param unit? TinkrObjectReference ---@param unit? WowGameObject
---@param guid? string
---@return Bastion.Unit ---@return Bastion.Unit
function Unit:New(unit) function Unit:New(unit, guid)
---@type Bastion.Unit ---@class Bastion.Unit
local self = setmetatable({}, Unit) local self = setmetatable({}, Unit)
self.unit = unit self.unit = unit
self.cache = Bastion.Cache:New() self.cache = Bastion.Cache:New()
self.aura_table = Bastion.AuraTable:New(self) self.aura_table = Bastion.AuraTable:New(self)
self.regression_history = {} self.regression_history = {}
self.spell = false
self.lastAttemptedSpell = false
self.castGUID = false
self.lastAttemptedCastGUID = false
self.lastCastInterrupted = false
self.cast = {
casting = false,
last = {
successful = {
time = false,
castGUID = false,
spell = false,
},
attempt = {
time = false,
castGUID = false,
spell = false,
interrupted = false,
}
},
current = {
time = false,
castGUID = false,
spell = false,
}
}
return self return self
end end
---@param interrupted boolean
function Unit:SetLastCastInterrupted(interrupted)
self.lastCastInterrupted = interrupted
end
---@param spell Bastion.Spell
---@param castGUID string
---@param time? number
function Unit:UnitSpellCastSent(spell, castGUID, time)
time = time or GetTime()
self.cast.last.attempt.time = self.cast.current.time
self.cast.last.attempt.castGUID = self.cast.current.castGUID
self.cast.last.attempt.spell = self.cast.current.spell
self.cast.last.attempt.interrupted = false
end
---@param spell Bastion.Spell
---@param castGUID string
---@param time? number
function Unit:UnitSpellCastSucceeded(spell, castGUID, time)
time = time or GetTime()
self.cast.last.successful.time = time
self.cast.last.successful.castGUID = castGUID
self.cast.last.successful.spell = spell
if self.cast.casting and self.cast.current.castGUID == castGUID then
self.cast.current.time = false
self.cast.current.castGUID = false
self.cast.current.spell = false
end
end
---@param spell Bastion.Spell
---@param castGUID string
---@param time? number
function Unit:UnitSpellCastFailed(spell, castGUID, time)
if self.cast.casting and self.cast.current.castGUID == castGUID then
self.cast.current.time = false
self.cast.current.castGUID = false
self.cast.current.spell = false
end
end
---@param spell Bastion.Spell
---@param castGUID string
---@param time? number
function Unit:UnitSpellCastFailedQuiet(spell, castGUID, time)
end
---@param spell Bastion.Spell
---@param castGUID string
---@param time? number
function Unit:UnitSpellCastInterrupted(spell, castGUID, time)
if self.cast.last.attempt.castGUID == castGUID then
self.cast.last.attempt.interrupted = true
end
end
---@param spell Bastion.Spell
---@param castGUID string
---@param time? number
function Unit:UnitSpellCastStart(spell, castGUID, time)
self.cast.casting = true
self.cast.current.time = time or GetTime()
self.cast.current.castGUID = castGUID
self.cast.current.spell = spell
end
---@param spell Bastion.Spell
---@param castGUID string
---@param time? number
function Unit:UnitSpellCastStop(spell, castGUID, time)
if self.cast.casting and self.cast.current.castGUID == castGUID then
self.cast.casting = false
self.cast.current.time = false
self.cast.current.castGUID = false
self.cast.current.spell = false
end
end
---@alias UNIT_SPELLCAST_EVENTS "UNIT_SPELLCAST_SENT" | "UNIT_SPELLCAST_CHANNEL_START" | "UNIT_SPELLCAST_CHANNEL_STOP" | "UNIT_SPELLCAST_CHANNEL_UPDATE" | "UNIT_SPELLCAST_DELAYED" | "UNIT_SPELLCAST_EMPOWER_START" | "UNIT_SPELLCAST_EMPOWER_STOP" | "UNIT_SPELLCAST_EMPOWER_UPDATE" | "UNIT_SPELLCAST_FAILED" | "UNIT_SPELLCAST_FAILED_QUIET" | "UNIT_SPELLCAST_INTERRUPTED" | "UNIT_SPELLCAST_INTERRUPTIBLE" | "UNIT_SPELLCAST_NOT_INTERRUPTIBLE" | "UNIT_SPELLCAST_RETICLE_CLEAR" | "UNIT_SPELLCAST_RETICLE_TARGET" | "UNIT_SPELLCAST_START" | "UNIT_SPELLCAST_STOP" | "UNIT_SPELLCAST_SUCCEEDED"
---@param event UNIT_SPELLCAST_EVENTS
---@param castGUID string
---@param time number
function Unit:UnitSpellCast(event, castGUID, time)
end
---@param spell false | Bastion.Spell
---@param castGUID false | string
---@param time? number
function Unit:SetLastSpell(spell, castGUID, time)
self.cast.last.attempt.time = time or GetTime()
self.cast.last.attempt.spell = spell
self.cast.last.attempt.castGUID = castGUID
self.lastAttemptedSpell = spell
self.lastAttemptedCastGUID = castGUID or false
if not self.spell or not self.castGUID or (self.castGUID and self.castGUID == castGUID) then
self:SetSpell(spell, castGUID)
end
end
---@param spell false | Bastion.Spell
---@param castGUID? false | string
function Unit:SetSpell(spell, castGUID)
self.spell = spell
if not spell and castGUID and self.castGUID and self.castGUID == castGUID then
self.lastCastInterrupted = false
end
self.castGUID = castGUID or false
end
-- Check if the unit is valid -- Check if the unit is valid
---@return boolean ---@return boolean
function Unit:IsValid() function Unit:IsValid()
return (self:GetOMToken() ~= "none" and self:GetOMToken() ~= nil) and self:Exists() return (self:GetOMToken() and self:GetOMToken() ~= "none") and self:Exists()
end end
-- Check if the unit exists -- Check if the unit exists
---@return boolean ---@return boolean
function Unit:Exists() function Unit:Exists()
return Object(self:GetOMToken()) ~= false return not self:IsUnit(Bastion.Globals.UnitManager:Get("none")) and Object(self:GetOMToken()) ~= false
end end
-- Get the units token -- Get the units token
@ -114,7 +254,7 @@ end
-- Get the units GUID -- Get the units GUID
function Unit:GetGUID() function Unit:GetGUID()
return ObjectGUID(self:GetOMToken()) return self.unit and self.unit:guid()
end end
-- Get the units health -- Get the units health
@ -132,7 +272,7 @@ end
-- Return Health Percent as an integer (0-100) -- Return Health Percent as an integer (0-100)
---@return number ---@return number
function Unit:GetHP() function Unit:GetHP()
return self:GetHealth() / self:GetMaxHealth() * 100 return (self:GetHealth() / self:GetMaxHealth()) * 100
end end
-- Get realized health -- Get realized health
@ -274,16 +414,7 @@ function Unit:IsBoss()
if UnitClassification(self:GetOMToken()) == "worldboss" then if UnitClassification(self:GetOMToken()) == "worldboss" then
return true return true
end end
return type(string.find(self.unit:unit() or "", "^boss[1-5]")) == "number"
for i = 1, 5 do
local bossGUID = UnitGUID("boss" .. i)
if self:GetGUID() == bossGUID then
return true
end
end
return false
end end
---@return UnitIds ---@return UnitIds
@ -419,37 +550,94 @@ function Unit:GetAttachmentPosition(attachmentId)
return Bastion.Vector3:New(x, y, z) return Bastion.Vector3:New(x, y, z)
end end
local attachmentId = {
ShieldMountMainItemVisual0 = 0,
HandRightItemVisual1 = 1,
HandLeftItemVisual2 = 2,
ElbowRightItemVisual3 = 3,
ElbowLeftItemVisual4 = 4,
ShoulderRight = 5,
ShoulderLeft = 6,
KneeRight = 7,
KneeLeft = 8,
HipRight = 9,
HipLeft = 10,
Helm = 11,
Back = 12,
ShoulderFlapRight = 13,
ShoulderFlapLeft = 14,
ChestBloodFront = 15,
ChestBloodBack = 16,
Breath = 17,
PlayerName = 18,
Base = 19,
Head = 20,
SpellLeftHand = 21,
SpellRightHand = 22,
Special1 = 23,
Special2 = 24,
Special3 = 25,
SheathMainHand = 26,
SheathOffHand = 27,
SheathShield = 28,
PlayerNameMounted = 29,
LargeWeaponLeft = 30,
LargeWeaponRight = 31,
HipWeaponLeft = 32,
HipWeaponRight = 33,
Chest = 34,
HandArrow = 35,
Bullet = 36,
SpellHandOmni = 37,
SpellHandDirected = 38,
VehicleSeat1 = 39,
VehicleSeat2 = 40,
VehicleSeat3 = 41,
VehicleSeat4 = 42,
VehicleSeat5 = 43,
VehicleSeat6 = 44,
VehicleSeat7 = 45,
VehicleSeat8 = 46,
LeftFoot = 47,
RightFoot = 48,
ShieldNoGlove = 49,
SpineLow = 50,
AlteredShoulderR = 51,
AlteredShoulderL = 52,
BeltBuckle = 53,
SheathCrossbow = 54,
HeadTop = 55,
VirtualSpellDirected = 56,
Backpack = 57,
Unknown1 = 68,
Unknown2 = 59,
Unknown = 60,
Unknown3 = 61,
Unknown4 = 62,
Unknown5 = 63,
Unknown6 = 64,
Unknown7 = 65,
Unknown8 = 66,
Unknown9 = 67,
Unknown10 = 68,
Unknown11 = 69,
Unknown12 = 70,
Unknown13 = 71,
Unknown14 = 72,
Unknown15 = 73,
Unknown16 = 74,
Unknown17 = 75,
}
local losFlag = bit.bor(0x1, 0x10, 0x100000)
-- Check if the unit can see another unit -- Check if the unit can see another unit
---@param unit Bastion.Unit ---@param unit Bastion.Unit
---@return boolean ---@return boolean
function Unit:CanSee(unit) 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 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 ax, ay, az = ObjectPosition(self:GetOMToken())
local ah = ObjectHeight(self:GetOMToken()) local ah = ObjectHeight(self:GetOMToken())
local attx, atty, attz = GetUnitAttachmentPosition(unit:GetOMToken(), 34) local attx, atty, attz = GetUnitAttachmentPosition(unit:GetOMToken(), attachmentId.Chest)
if not attx or not ax then if not attx or not ax then
return false return false
@ -463,11 +651,7 @@ function Unit:CanSee(unit)
return true return true
end end
if not attx or not ax then local x, y, z = TraceLine(ax, ay, az + ah, attx, atty, attz, losFlag)
return false
end
local x, y, z = TraceLine(ax, ay, az + ah, attx, atty, attz, 0)
if x ~= 0 or y ~= 0 or z ~= 0 then if x ~= 0 or y ~= 0 or z ~= 0 then
return false return false
else else
@ -475,6 +659,30 @@ function Unit:CanSee(unit)
end end
end end
---@param target? Bastion.Unit
function Unit:IsInWorld(target)
return UnitIsVisible(target and target:GetOMToken() or self:GetOMToken())
end
function Unit:PhaseReason()
return UnitPhaseReason(self:GetOMToken())
end
function Unit:GetHeight()
return ObjectHeight(self:GetOMToken())
end
function Unit:GetCollisionHeight()
return 0 --ObjectHeight(self:GetOMToken())
end
---@param target Bastion.Unit
function Unit:IsWithinLOS(target)
if target:IsInWorld() and not target:PhaseReason() then
local targetZ = target:GetHeight()
end
end
-- Check if the unit is casting a spell -- Check if the unit is casting a spell
---@return boolean ---@return boolean
function Unit:IsCasting() function Unit:IsCasting()
@ -520,6 +728,19 @@ function Unit:GetCastingOrChannelingSpell()
return false return false
end end
---@param target Bastion.Unit
function Unit:GetHitSpherePointFor(target)
local vThis = self:GetPosition()
local vObj = target:GetPosition()
local contactPoint = vThis +
(vObj - vThis):directionOrZero() * math.min(target:GetDistance(self), self:GetCombatReach())
return {
v = contactPoint,
o = contactPoint:GetAbsoluteAngle(contactPoint)
}
end
-- Get the end time of the cast or channel -- Get the end time of the cast or channel
---@return number ---@return number
function Unit:GetCastingOrChannelingEndTime() function Unit:GetCastingOrChannelingEndTime()
@ -553,8 +774,8 @@ end
---@return Bastion.Unit ---@return Bastion.Unit
function Unit:CastTarget() function Unit:CastTarget()
---@diagnostic disable-next-line: param-type-mismatch ---@diagnostic disable-next-line: param-type-mismatch
return self:IsCastingOrChanneling() and Bastion.UnitManager:Get(ObjectCastingTarget(self:GetOMToken())) or return self:IsCastingOrChanneling() and Bastion.Globals.UnitManager:Get(ObjectCastingTarget(self:GetOMToken())) or
Bastion.UnitManager:Get("none") Bastion.Globals.UnitManager:Get("none")
end end
---@param unit Bastion.Unit ---@param unit Bastion.Unit
@ -643,7 +864,7 @@ function Unit:GetEnemies(range)
local count = 0 local count = 0
Bastion.UnitManager:EnumEnemies(function(unit) Bastion.Globals.UnitManager:EnumEnemies(function(unit)
if not self:IsUnit(unit) and self:IsWithinCombatDistance(unit, range) and unit:IsAlive() and self:CanSee(unit) and unit:IsEnemy() then if not self:IsUnit(unit) and self:IsWithinCombatDistance(unit, range) and unit:IsAlive() and self:CanSee(unit) and unit:IsEnemy() then
count = count + 1 count = count + 1
end end
@ -665,7 +886,7 @@ function Unit:GetMeleeAttackers(facing)
local count = 0 local count = 0
Bastion.UnitManager:EnumEnemies(function(unit) Bastion.Globals.UnitManager:EnumEnemies(function(unit)
if not self:IsUnit(unit) and unit:IsAlive() and self:CanSee(unit) and self:InMelee(unit) and unit:IsEnemy() and (not facing or self:IsFacing(unit)) then if not self:IsUnit(unit) and unit:IsAlive() and self:CanSee(unit) and self:InMelee(unit) and unit:IsEnemy() and (not facing or self:IsFacing(unit)) then
count = count + 1 count = count + 1
end end
@ -682,7 +903,7 @@ end
function Unit:GetPartyHPAround(distance, percent) function Unit:GetPartyHPAround(distance, percent)
local count = 0 local count = 0
Bastion.UnitManager:EnumFriends(function(unit) Bastion.Globals.UnitManager:EnumFriends(function(unit)
if not self:IsUnit(unit) and unit:GetDistance(self) <= distance and unit:IsAlive() and self:CanSee(unit) and unit:GetHP() <= percent then if not self:IsUnit(unit) and unit:GetDistance(self) <= distance and unit:IsAlive() and self:CanSee(unit) and unit:GetHP() <= percent then
count = count + 1 count = count + 1
end end
@ -807,22 +1028,31 @@ function Unit:IsInfront(unit)
return not self:IsBehind(unit) return not self:IsBehind(unit)
end end
---@param target Bastion.Unit
function Unit:GetMeleeRange(target)
local range = self:GetCombatReach() + target:GetCombatReach() + 4.0 / 3.0
return math.max(range, 5.0)
end
---@return number ---@return number
function Unit:GetMeleeBoost() function Unit:GetMeleeBoost()
local meleeBoost = 0 local meleeBoost = 0
if IsPlayerSpell(197524) then return meleeBoost
--[[ if select(3, UnitClass("player")) == 11 and IsPlayerSpell(768) and GetShapeshiftForm() == 2 then
meleeBoost = meleeBoost + 3
end ]]
--[[ if IsPlayerSpell(197524) then
local astralInfluenceNode = C_Traits.GetNodeInfo(C_ClassTalents.GetActiveConfigID() or 0, 82210) local astralInfluenceNode = C_Traits.GetNodeInfo(C_ClassTalents.GetActiveConfigID() or 0, 82210)
local currentSpec = select(1, GetSpecializationInfo(GetSpecialization()))
if astralInfluenceNode then if astralInfluenceNode then
local currentRank = astralInfluenceNode.activeRank local currentRank = astralInfluenceNode.activeRank
if currentRank > 0 then if currentRank > 0 then
meleeBoost = ((currentSpec == 103 or currentSpec == 104) and 1 or 3) + (currentRank == 2 and 2 or 0) meleeBoost = meleeBoost + 5
end end
end end
elseif IsPlayerSpell(196924) then elseif IsPlayerSpell(196924) then
meleeBoost = 3 meleeBoost = 3
end end
return meleeBoost return meleeBoost ]]
end end
function Unit:GetModelId() function Unit:GetModelId()
@ -840,10 +1070,10 @@ end
---@param unit Bastion.Unit ---@param unit Bastion.Unit
---@return boolean ---@return boolean
function Unit:InMelee(unit) function Unit:InMelee(unit)
local x, y, z = ObjectPosition(self:GetOMToken()) local sX, sY, sZ = ObjectPosition(self:GetOMToken())
local x2, y2, z2 = ObjectPosition(unit:GetOMToken()) local tX, tY, tZ = ObjectPosition(unit:GetOMToken())
if not x or not x2 then if not sX or not tX then
return false return false
end end
@ -854,7 +1084,7 @@ function Unit:InMelee(unit)
return false return false
end end
local dist = math.sqrt((x - x2) ^ 2 + (y - y2) ^ 2 + (z - z2) ^ 2) local dist = math.sqrt((sX - tX) ^ 2 + (sY - tY) ^ 2 + (sZ - tZ) ^ 2)
local maxDist = math.max((scr + 1.3333) + ucr, 5.0) local maxDist = math.max((scr + 1.3333) + ucr, 5.0)
maxDist = maxDist + 1.0 + self:GetMeleeBoost() maxDist = maxDist + 1.0 + self:GetMeleeBoost()
@ -1031,20 +1261,22 @@ end
-- Get combat odds (if the last combat time is less than 1 minute ago return 1 / time, else return 0) -- Get combat odds (if the last combat time is less than 1 minute ago return 1 / time, else return 0)
-- the closer to 0 the more likely the unit is to be in combat (0 = 100%) 60 = 0% -- the closer to 0 the more likely the unit is to be in combat (0 = 100%) 60 = 0%
---@param seconds? number
---@return number ---@return number
function Unit:InCombatOdds() function Unit:InCombatOdds(seconds)
local time = self:GetCombatTime() local time = self:GetCombatTime()
local percent = 1 - (time / 60) local percent = (1 - (time / (seconds or 60))) * 100
return percent * 100 return percent > 0 and percent or 0
end end
-- Get units gcd time -- Get units gcd time
---@return number ---@return number
function Unit:GetGCD() function Unit:GetGCD()
local start, duration = GetSpellCooldown(61304) local cdInfo = C_Spell.GetSpellCooldown(61304)
local start = cdInfo and cdInfo.startTime or 0
local duration = cdInfo and cdInfo.duration or 0
return start == 0 and 0 or duration - (GetTime() - start) return start == 0 and 0 or duration - (GetTime() - start)
end end
@ -1340,7 +1572,8 @@ end
---@return Bastion.Unit ---@return Bastion.Unit
function Unit:Target() function Unit:Target()
local objTarget = ObjectTarget(self:GetOMToken()) local objTarget = ObjectTarget(self:GetOMToken())
return objTarget and Bastion.UnitManager:Get(objTarget:unit() or "none") or Bastion.UnitManager:Get("none") return objTarget and Bastion.Globals.UnitManager:Get(objTarget:unit() or "none") or
Bastion.Globals.UnitManager:Get("none")
end end
local dummyUnits = { local dummyUnits = {
@ -1448,7 +1681,7 @@ local dummyUnits = {
---@return boolean ---@return boolean
function Unit:IsDummy() function Unit:IsDummy()
local npcId = self:GetID() local npcId = self:GetID()
return npcId and npcId >= 0 and dummyUnits[npcId] == true or false return npcId and npcId >= 0 and dummyUnits[npcId] or false
end end
---@param npcId? number ---@param npcId? number
@ -1459,7 +1692,7 @@ function Unit:IsInBossList(npcId)
return false return false
end end
for i = 1, 4 do for i = 1, 4 do
local thisUnit = Bastion.UnitManager:Get(string.format("boss%d", i)) local thisUnit = Bastion.Globals.UnitManager:Get(string.format("boss%d", i))
if thisUnit:Exists() and thisUnit:GetID() == id then if thisUnit:Exists() and thisUnit:GetID() == id then
return true return true
end end
@ -1491,7 +1724,7 @@ function Unit:CheckHPFromBossList(npcId, hp)
local thisHP = hp or 100 local thisHP = hp or 100
for i = 1, 4 do for i = 1, 4 do
local thisUnit = Bastion.UnitManager:Get(string.format("boss%d", i)) local thisUnit = Bastion.Globals.UnitManager:Get(string.format("boss%d", i))
if thisUnit:Exists() and thisUnit:GetID() == id and thisUnit:GetHealthPercent() <= thisHP then if thisUnit:Exists() and thisUnit:GetID() == id and thisUnit:GetHealthPercent() <= thisHP then
return true return true
end end
@ -1505,8 +1738,11 @@ end
---@return Bastion.TimeToDie.Enums | integer ---@return Bastion.TimeToDie.Enums | integer
function Unit:TimeToX(percentage, minSamples) function Unit:TimeToX(percentage, minSamples)
--if self:IsDummy() then return 6666 end --if self:IsDummy() then return 6666 end
if self:IsPlayer() and Bastion.UnitManager:Get("player"):CanAttack(self) then return Bastion.TimeToDie.Enums.PLAYER end if self:IsPlayer() and Bastion.Globals.UnitManager:Get("player"):CanAttack(self) then
local seconds = 0 return Bastion.TimeToDie
.Enums.PLAYER
end
local seconds = Bastion.TimeToDie.Enums.NOT_UPDATED
local unitGuid = self:GetGUID() local unitGuid = self:GetGUID()
if not unitGuid then if not unitGuid then
return Bastion.TimeToDie.Enums.NO_GUID return Bastion.TimeToDie.Enums.NO_GUID
@ -1545,7 +1781,6 @@ function Unit:TimeToX(percentage, minSamples)
seconds = (percentage - a) / b seconds = (percentage - a) / b
-- Subtract current time to obtain "time remaining" -- Subtract current time to obtain "time remaining"
seconds = seconds - (GetTime() - unitTable.time) seconds = seconds - (GetTime() - unitTable.time)
if seconds < 0 then seconds = Bastion.TimeToDie.Enums.NEGATIVE_TTD end
end end
end end
end end
@ -1649,7 +1884,7 @@ end
---@param spell Bastion.Spell ---@param spell Bastion.Spell
---@param source? Bastion.Unit ---@param source? Bastion.Unit
function Unit:GetAuraTickRate(spell, source) function Unit:GetAuraTickRate(spell, source)
local unit = self.unit.unit() local unit = self.unit:unit()
local aura = source and self:GetAuras():FindFrom(spell, source) or self:GetAuras():Find(spell) local aura = source and self:GetAuras():FindFrom(spell, source) or self:GetAuras():Find(spell)
if aura:IsValid() and unit then if aura:IsValid() and unit then
local tooltipInfo = C_TooltipInfo.GetUnitDebuffByAuraInstanceID(unit, aura:GetAuraInstanceID()) local tooltipInfo = C_TooltipInfo.GetUnitDebuffByAuraInstanceID(unit, aura:GetAuraInstanceID())
@ -1723,23 +1958,64 @@ function Unit:IsSummoning()
return false return false
end end
function Unit:GetLosssOfControlCount() function Unit:GetLossOfControlCount()
return C_LossOfControl.GetActiveLossOfControlDataCountByUnit(self:GetOMToken()) return C_LossOfControl.GetActiveLossOfControlDataCountByUnit(self:GetOMToken())
end end
---@param index number ---@param index number
function Unit:LosssOfControlData(index) function Unit:LossOfControlData(index)
return C_LossOfControl.GetActiveLossOfControlDataByUnit(self:GetOMToken(), index) return C_LossOfControl.GetActiveLossOfControlDataByUnit(self:GetOMToken(), index)
end end
---@enum (key) LossOfControlType
local LossOfControlType = {
CHARM = "Charmed",
CONFUSE = "Confused",
DISARM = "Disarmed",
FEAR = "Feared",
FEAR_MECHANIC = "Feared",
NONE = "None",
PACIFY = "Pacified",
PACIFYSILENCE = "Pacified",
POSSESS = "Possessed",
ROOT = "Rooted",
SCHOOL_INTERRUPT = "Interrupted",
SILENCE = "Silenced",
STUN = "Stunned",
STUN_MECHANIC = "Stunned",
}
---@param locTypes LossOfControlType | LossOfControlType[]
function Unit:LossOfControlActive(locTypes)
locTypes = type(locTypes) == "table" and locTypes or type(locTypes) == "string" and { locTypes } or {}
local locCount = self:GetLossOfControlCount()
if locCount > 0 then
local returnCount = 0
for i = 1, locCount do
local locData = self:LossOfControlData(i)
if locData then
for _, locType in ipairs(locTypes) do
if locData.locType == locType then
returnCount = returnCount + 1
end
end
end
end
return returnCount
else
return 0
end
end
function Unit:Available() function Unit:Available()
return self:Exists() return self:Exists()
and self:IsAlive() and self:IsAlive()
and self:GetLosssOfControlCount() == 0 and self:LossOfControlActive({ "STUN", "STUN_MECHANIC" }) == 0
and not UnitOnTaxi(self:GetOMToken()) and not UnitOnTaxi(self:GetOMToken())
and not UnitInVehicle(self:GetOMToken()) and not UnitInVehicle(self:GetOMToken())
and not self:IsCastingOrChanneling() and not self:IsCastingOrChanneling()
and (self:IsAffectingCombat() or (not self:IsSitting() and not self:IsSummoning())) --[[ and (self:IsAffectingCombat() or (not self:IsSitting() and not self:IsSummoning())) ]]
end end
---@param params { target?: Bastion.Unit, spellId?: number, spellVisualId?: number } ---@param params { target?: Bastion.Unit, spellId?: number, spellVisualId?: number }

@ -3,8 +3,6 @@ local Tinkr,
---@class Bastion ---@class Bastion
Bastion = ... Bastion = ...
local Unit = Bastion.Unit
---@class Bastion.UnitManager.CustomUnit ---@class Bastion.UnitManager.CustomUnit
---@field unit Bastion.Cacheable | Bastion.Unit ---@field unit Bastion.Cacheable | Bastion.Unit
---@field cb fun(): Bastion.Unit ---@field cb fun(): Bastion.Unit
@ -13,47 +11,98 @@ local Unit = Bastion.Unit
---@class Bastion.UnitManager ---@class Bastion.UnitManager
---@field units table<string, Bastion.Unit> ---@field units table<string, Bastion.Unit>
---@field customUnits table<string, Bastion.UnitManager.CustomUnit> ---@field customUnits table<string, Bastion.UnitManager.CustomUnit>
---@field objects table<string | WowGameObject, Bastion.Unit> ---@field objects table<string, Bastion.Unit>
---@field cache Bastion.Cache ---@field cache Bastion.Cache
local UnitManager = { local UnitManager = {
} }
local tokenPrefixes = {
"^arena",
"^boss",
"^focus",
"^mouseover",
"^none",
"^party",
"^partypet",
"^pet",
"^player",
"^raid",
"^raidpet",
"^target",
"^vehicle",
"^nameplate",
"^spectated",
"^softenemy",
"^softfriend",
"^anyenemy",
"^anyfriend",
"^anyinteract",
"^npc",
"^questnpc",
}
---@param token string
local validateToken = function(token)
for i = 1, #tokenPrefixes do
if string.match(token, tokenPrefixes[i]) then
return true
end
end
return false
end
---@param k UnitId ---@param k UnitId
function UnitManager:__index(k) function UnitManager:__index(k)
if k == "none" then if k == "none" then
return self:Get("none") return self.objects.none
end end
if UnitManager[k] ~= nil then if UnitManager[k] ~= nil then
return UnitManager[k] return UnitManager[k]
end end
local k = k or "none" if rawget(self, k) ~= nil then
return rawget(self, k)
end
local key = k or "none"
-- if custom unit exists, return it it's cache expired return a new one -- if custom unit exists return it or a new one if the cache has expired.
if self.customUnits[k] then if self.customUnits[key] then
if not self.cache:IsCached(k) then if not self.cache:IsCached(key) then
self.customUnits[k].unit:Update() self.customUnits[key].unit:Update()
self.cache:Set(k, self.customUnits[k].unit, 0.5) self.cache:Set(key, self.customUnits[key].unit, 0.5)
end end
return self.customUnits[k].unit return self.customUnits[key].unit
end end
local kguid = ObjectGUID(k) local kguid = type(key) == "string" and validateToken(key) and ObjectGUID(key) or key
if kguid and self.objects[kguid] then if kguid ~= nil and self.objects[kguid] then
return self.objects[kguid] return self.objects[kguid]
end end
if kguid ~= nil and self.objects[kguid] == nil then
if self.objects[kguid] == nil then local obj = Object(kguid)
local o = Object(k) if obj then
if o then self.objects[kguid] = Bastion.Unit:New(obj, kguid)
local unit = Unit:New(o) return self:SetObject(Bastion.Unit:New(obj, kguid))
self:SetObject(unit)
end end
end end
return self.objects["none"] --if obj and objGuid then
--if self.objects[objGuid] == nil then
--self:SetObject(Bastion.Unit:New(obj, objGuid))
--end
--[[ return self:Get(objGuid) or self:SetObject(Bastion.Unit:New(obj), objGuid) ]]
--[[ if self.objects[obj:guid()] ~= nil then
return self.objects[obj:guid()]
else
return self:SetObject(Bastion.Unit:New(obj), obj:guid() or nil)
end ]]
--end
return self.objects.none
end end
-- Constructor -- Constructor
@ -63,38 +112,53 @@ function UnitManager:New()
local self = setmetatable({}, UnitManager) local self = setmetatable({}, UnitManager)
self.units = {} self.units = {}
self.customUnits = {} self.customUnits = {}
self.objects = {} self.objects = {
none = Bastion.Unit:New(),
}
self.cache = Bastion.Cache:New() self.cache = Bastion.Cache:New()
return self return self
end end
-- Get or create a unit -- Get or create a unit
---@param token UnitId | TinkrObjectReference ---@param token TinkrObjectReference
function UnitManager:Get(token) function UnitManager:Get(token)
-- if not Validate(token) then --[[ local unitObject = Object(token)
-- error("UnitManager:Get - Invalid token: " .. token) local tguid = unitObject and unitObject:guid() or "none"
-- end if self.objects[tguid] == nil then
if tguid == 'none' then
self.objects.none = Bastion.Unit:New()
else
self.objects[tguid] = Bastion.Unit:New(unitObject)
end
end
return Bastion.Refreshable:New(self.objects[tguid], function()
local tguid = ObjectGUID(token) or "none"
if self.objects[tguid] == nil then
if token == 'none' then
self.objects.none = Bastion.Unit:New()
else
self.objects[tguid] = Bastion.Unit:New(Object(tguid))
end
end
return self.objects[tguid]
end) ]]
local tguid = ObjectGUID(token) local tguid = ObjectGUID(token)
if tguid and self.objects[tguid] == nil then if tguid and self.objects[tguid] == nil then
if token == "none" then local obj = Object(tguid)
self.objects["none"] = Unit:New() if obj then
else self.objects[tguid] = Bastion.Unit:New(obj, tguid)
---@diagnostic disable-next-line: param-type-mismatch
self.objects[tguid] = Unit:New(Object(tguid))
end end
end end
return Bastion.Refreshable:New(self.objects[tguid], function() return Bastion.Refreshable:New(self.objects[tguid], function()
local tguid = ObjectGUID(token) or "none" local tguid = ObjectGUID(token) or "none"
if self.objects[tguid] == nil then if self.objects[tguid] == nil then
if token == "none" then local obj = Object(tguid)
self.objects["none"] = Unit:New() if obj then
else self.objects[tguid] = Bastion.Unit:New(obj, tguid)
---@diagnostic disable-next-line: param-type-mismatch
self.objects[tguid] = Unit:New(Object(tguid))
end end
end end
return self.objects[tguid] return self.objects[tguid]
@ -118,7 +182,7 @@ function UnitManager:GetOrCreateObject(unit)
if unitObj and not unitObj:IsValid() then if unitObj and not unitObj:IsValid() then
local object = type(unit) ~= "string" and unit or Object(guid) local object = type(unit) ~= "string" and unit or Object(guid)
if object then if object then
unitObj = Bastion.UnitManager:SetObject(Bastion.Unit:New(object)) unitObj = self:SetObject(Bastion.Unit:New(object, guid))
end end
end end
return unitObj return unitObj
@ -127,12 +191,18 @@ end
-- Set a unit by guid -- Set a unit by guid
---@param unit Bastion.Unit ---@param unit Bastion.Unit
function UnitManager:SetObject(unit) ---@param guid? string
local guid = unit:GetGUID() function UnitManager:SetObject(unit, guid)
if guid then local tguid = guid or unit:GetGUID()
self.objects[guid] = unit if tguid then
return self.objects[guid] self.objects[tguid] = unit
return self.objects[tguid]
end
end end
---@param unitGUID string
function UnitManager:ClearObject(unitGUID)
self.objects[unitGUID] = nil
end end
-- Create a custom unit and cache it for .5 seconds -- Create a custom unit and cache it for .5 seconds
@ -156,11 +226,21 @@ function UnitManager:CreateCustomUnit(token, cb)
return cachedUnit return cachedUnit
end end
function UnitManager:ClearInvalid()
for k, object in pairs(self.objects) do
if k ~= "none" and not self.objects[k]:IsValid() then
self.objects[k] = nil
end
end
end
function UnitManager:ResetObjects() function UnitManager:ResetObjects()
for k, object in pairs(self.objects) do for k, object in pairs(self.objects) do
if k ~= "none" and not Bastion.ObjectManager.objectList[k] then
self.objects[k] = nil self.objects[k] = nil
end end
end end
end
---@description Enumerates all friendly units in the battlefield ---@description Enumerates all friendly units in the battlefield
---@param cb fun(unit: Bastion.Unit):boolean ---@param cb fun(unit: Bastion.Unit):boolean
@ -383,4 +463,4 @@ function UnitManager:FindEnemiesCentroid(radius, range)
return centroid return centroid
end end
Bastion.UnitManager = UnitManager:New() Bastion.UnitManager = UnitManager

@ -52,10 +52,6 @@ function Util:GetPrintMode()
return PrintEnabled return PrintEnabled
end end
Bastion.Globals.Command:Register("toggle", "Toggle bastion on/off", function()
Util:TogglePrint()
end)
local DebugMode = false local DebugMode = false
function Util:ToggleDebug() function Util:ToggleDebug()
@ -83,8 +79,4 @@ function Util:Debug(...)
print(str) print(str)
end end
Bastion.Globals.Command:Register("debug", "Toggle debug mode on/off", function()
Util:ToggleDebug()
end)
Bastion.Util = Util Bastion.Util = Util

@ -359,12 +359,12 @@ end
---@param o number ---@param o number
function Vector3:NormalizeOrientation(o) function Vector3:NormalizeOrientation(o)
if o < 0 then if o < 0 then
mod = o * -1 local mod = o * -1
mod = math.fmod(mod, 2.0 * math.pi) mod = math.fmod(mod, 2.0 * math.pi)
mod = -mod + 2.0 * math.pi mod = -mod + 2.0 * math.pi
return mod return mod
end end
return math.fmod(o, 2.0 * math.pi) return math.fmod(o, 2.00 * math.pi)
end end
Bastion.Vector3 = Vector3 Bastion.Vector3 = Vector3

Loading…
Cancel
Save