From 6a48a2e7acad89dd699931766eda67cc93972436 Mon Sep 17 00:00:00 2001 From: jeffi Date: Fri, 26 Jul 2024 03:14:45 -0500 Subject: [PATCH] Updoots. --- Examples/ExampleModule.lua | 2 +- Examples/Libraries/ExampleDependency.lua | 2 +- src/APL/APL.lua | 42 ++- src/APLAction/APLAction.lua | 114 ++++++ src/APLActor/APLActor.lua | 31 +- src/APLManager/APLManager.lua | 22 ++ src/Aura/Aura.lua | 2 +- src/AuraTable/AuraTable.lua | 6 +- src/Bastion/Bastion.lua | 301 ++++++++++----- src/Command/Command.lua | 5 +- src/EventManager/EventManager.lua | 24 +- src/Item/Item.lua | 68 ++-- src/ItemBook/ItemBook.lua | 2 +- src/List/List.lua | 4 +- src/Missile/Missile.lua | 4 +- src/MissileManager/MissileManager.lua | 30 +- src/Module/Module.lua | 23 +- src/MythicPlusUtils/MythicPlusUtils.lua | 12 +- src/NotificationList/NotificationList.lua | 3 +- src/Object/Object.lua | 0 src/ObjectManager/ObjectManager.lua | 103 +++-- src/Spell/Spell.lua | 364 ++++++++++++++---- src/SpellBook/SpellBook.lua | 78 +++- src/TimeToDie/TimeToDie.lua | 182 ++++----- src/TimeToDieManager/TimeToDieManager.lua | 127 +++++++ src/TimeToDieUnit/TimeToDieUnit.lua | 120 ++++++ src/Timer/Timer.lua | 13 +- src/Unit/Unit.lua | 436 ++++++++++++++++++---- src/UnitManager/UnitManager.lua | 168 ++++++--- src/Util/Util.lua | 8 - src/Vector3/Vector3.lua | 4 +- 31 files changed, 1771 insertions(+), 529 deletions(-) create mode 100644 src/APLAction/APLAction.lua create mode 100644 src/APLManager/APLManager.lua create mode 100644 src/Object/Object.lua create mode 100644 src/TimeToDieManager/TimeToDieManager.lua create mode 100644 src/TimeToDieUnit/TimeToDieUnit.lua diff --git a/Examples/ExampleModule.lua b/Examples/ExampleModule.lua index 592228c..77673c9 100644 --- a/Examples/ExampleModule.lua +++ b/Examples/ExampleModule.lua @@ -1,6 +1,6 @@ local Tinkr, Bastion = ... local ExampleModule = Bastion.Module:New('ExampleModule') -local Player = Bastion.UnitManager:Get('player') +local Player = Bastion.Globals.UnitManager:Get('player') -- Create a local spellbook. local SpellBook = Bastion.SpellBook:New() diff --git a/Examples/Libraries/ExampleDependency.lua b/Examples/Libraries/ExampleDependency.lua index 87c1449..225b050 100644 --- a/Examples/Libraries/ExampleDependency.lua +++ b/Examples/Libraries/ExampleDependency.lua @@ -1,6 +1,6 @@ local Tinkr, Bastion = ... -local Player = Bastion.UnitManager:Get('player') +local Player = Bastion.Globals.UnitManager:Get('player') Bastion:RegisterLibrary(Bastion.Library:New({ name = 'Dependable', diff --git a/src/APL/APL.lua b/src/APL/APL.lua index 9e7bd29..ff0cf7e 100644 --- a/src/APL/APL.lua +++ b/src/APL/APL.lua @@ -18,6 +18,7 @@ function APL:New(name) ---@class Bastion.APL local self = setmetatable({ active = false, + actions = {}, apl = {}, variables = {}, name = name, @@ -116,11 +117,9 @@ function APL:AddSpell(spell, condition, targetIf) parentAPL = self, targetIf = targetIf or false, executor = function(actor) - local actorTable = actor.actor --[[@as Bastion.APL.Actor.Spell.Table]] - local spellTarget = actorTable.targetIf and actorTable.targetIf(actorTable.spell) or actorTable.target - return (not spellTarget or spellTarget:Exists()) - and (not actorTable.condition or actorTable.condition(actorTable.spell, spellTarget)) - and actorTable.spell:CastableIf(actorTable.castableFunc):OnCast(actorTable.onCastFunc):Cast(spellTarget) + local aTbl = actor.actor --[[@as Bastion.APL.Actor.Spell.Table]] + return aTbl.spell:CastableIf(aTbl.castableFunc):OnCast(aTbl.onCastFunc):Cast( + aTbl.targetIf and aTbl.targetIf(aTbl.spell) or aTbl.target or nil, aTbl.condition) end }) @@ -131,25 +130,34 @@ end ---@class Bastion.APL.Actor.Item.Table : Bastion.APLActor.Table.Base ---@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 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 ---@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 -function APL:AddItem(item, condition) +function APL:AddItem(item, condition, targetIf) local usableFunc = item.UsableIfFunc local target = item:GetTarget() - local actor = Bastion.APLActor:New({ type = "item", item = item, condition = condition, usableFunc = usableFunc, target = target, + onUseFunc = item.OnUseFunc, 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) @@ -171,16 +179,19 @@ function APL:AddAPL(apl, condition) apl = apl, condition = condition, 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) return actor end ----@param name string +---@param nameOrIndex string|number ---@param enabled? boolean -function APL:ToggleAPL(name, enabled) - for _, actor in ipairs(self.apl) do - if actor.actor.type == "apl" and actor.name == name then +function APL:ToggleAPL(nameOrIndex, enabled) + for actorIndex, actor in ipairs(self.apl) do + if actor.actor.type == "apl" and (actorIndex == nameOrIndex or actor.name == nameOrIndex) then actor.enabled = type(enabled) == "boolean" and enabled or not actor.enabled break end @@ -218,7 +229,10 @@ function APL:Execute() self:UpdateLastAttempted(actor) if actor.enabled and (not actor:HasTraits() or actor:Evaluate()) and actor:Execute() then self:UpdateLastSuccessful() + --print(string.format("%s Successful", actor.name)) return true + else + --print(string.format("%s Skipped", actor.name)) end end self.active = false diff --git a/src/APLAction/APLAction.lua b/src/APLAction/APLAction.lua new file mode 100644 index 0000000..02c35b8 --- /dev/null +++ b/src/APLAction/APLAction.lua @@ -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 diff --git a/src/APLActor/APLActor.lua b/src/APLActor/APLActor.lua index 382ce8b..0c30c8a 100644 --- a/src/APLActor/APLActor.lua +++ b/src/APLActor/APLActor.lua @@ -28,24 +28,23 @@ function APLActor:New(actor) local self = setmetatable({ index = #actor.parentAPL.apl + 1, }, APLActor) + self.name = string.format("[%s][%d]", actor.parentAPL.name, self.index) if actor.type == "spell" then - local name = actor.spell:GetName() or "Unknown" - local id = actor.spell:GetID() or 0 - self.name = string.format("%s[%s]", name, id) + self.name = string.format("%s[%s[%s]]", self.name, actor.spell:GetName() or "Unknown", + actor.spell:GetID() or 0) elseif actor.type == "item" then - local name = actor.item:GetName() or "Unknown" - local id = actor.item:GetID() or 0 - self.name = string.format("%s[%s]", name, id) + self.name = string.format("%s[%s[%s]]", self.name, actor.item:GetName() or "Unknown", + actor.item:GetID() or 0) elseif actor.type == "apl" then - self.name = string.format("%s", actor.apl.name or "Unknown") + self.name = string.format("%s[%s]", self.name, actor.apl.name or "Unknown") elseif actor.type == "sequencer" then - self.name = string.format("") + self.name = string.format("%s[]", self.name) elseif actor.type == "variable" then - self.name = string.format("%s", actor.variable or "Unknown") + self.name = string.format("%s[%s]", self.name, actor.variable or "Unknown") elseif actor.type == "action" then - self.name = string.format("%s", actor.action or "Unknown") + self.name = string.format("%s[%s]", self.name, actor.action or "Unknown") else - self.name = string.format("<%s>", actor.type or "UNKNOWN") + self.name = string.format("%s[<%s>]", self.name, actor.type or "UNKNOWN") end self.actor = actor self.enabled = true @@ -105,19 +104,23 @@ function APLActor:Execute() return false end 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 -- print("Bastion: APL:Execute: Executing sub APL " .. actorTable.apl.name) return actorTable.apl:Execute() - end + end ]] end if actorTable.type == "spell" then return self:ExecuteActor() end if actorTable.type == "item" then + return self:ExecuteActor() + end + --[[ if actorTable.type == "item" then ---@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)) - end + end ]] if self:GetActorTable().type == "action" then ---@cast actorTable Bastion.APL.Actor.Action.Table -- print("Bastion: APL:Execute: Executing action " .. actorTable.action) diff --git a/src/APLManager/APLManager.lua b/src/APLManager/APLManager.lua new file mode 100644 index 0000000..717a65f --- /dev/null +++ b/src/APLManager/APLManager.lua @@ -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 diff --git a/src/Aura/Aura.lua b/src/Aura/Aura.lua index 26039b3..aa1335e 100644 --- a/src/Aura/Aura.lua +++ b/src/Aura/Aura.lua @@ -301,7 +301,7 @@ end -- Get the auras source ---@return Bastion.Unit function Aura:GetSource() - return Bastion.UnitManager[self.aura.source] + return Bastion.Globals.UnitManager[self.aura.source] end -- Get the auras stealable status diff --git a/src/AuraTable/AuraTable.lua b/src/AuraTable/AuraTable.lua index 80b80c4..6b0c7b5 100644 --- a/src/AuraTable/AuraTable.lua +++ b/src/AuraTable/AuraTable.lua @@ -112,7 +112,7 @@ function AuraTable:AddOrUpdateAuraInstanceID(instanceID, aura) 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 self.playerAuras[spellId] = {} end @@ -140,7 +140,7 @@ function AuraTable:GetUnitBuffs() 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 self.playerAuras[spellId] = {} end @@ -179,7 +179,7 @@ function AuraTable:GetUnitDebuffs() 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 self.playerAuras[spellId] = {} end diff --git a/src/Bastion/Bastion.lua b/src/Bastion/Bastion.lua index 35d2913..b8fd9a4 100644 --- a/src/Bastion/Bastion.lua +++ b/src/Bastion/Bastion.lua @@ -1,13 +1,29 @@ ---@type Tinkr local Tinkr = ... +local startPrecision = GetTimePreciseSec() + ---@class Bastion.Globals.SpellName : { [spellId]: string } ---@class Bastion local Bastion = { CombatEvents = {}, 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 = { ---@type Bastion.Globals.SpellName SpellName = {} @@ -22,49 +38,52 @@ local Bastion = { BastionScripts = "scripts/bastion/scripts", 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 return Bastion[key] end return rawget(self, key) -end +end ]] local bastionFiles = { "~/src/ClassMagic/ClassMagic", - "~/src/List/List", - "~/src/Command/Command", - "~/src/Util/Util", - "~/src/Library/Library", - "~/src/Notification/Notification", - "~/src/NotificationList/NotificationList", - "~/src/Vector3/Vector3", - "~/src/Sequencer/Sequencer", + "~/src/APL/APL", + "~/src/APLActor/APLActor", + "~/src/APLTrait/APLTrait", + "~/src/Aura/Aura", + "~/src/AuraTable/AuraTable", "~/src/Cache/Cache", "~/src/Cacheable/Cacheable", - "~/src/Refreshable/Refreshable", + "~/src/Class/Class", + "~/src/Command/Command", + "~/src/Config/Config", "~/src/EventManager/EventManager", - "~/src/Unit/Unit", - "~/src/Aura/Aura", - "~/src/APLTrait/APLTrait", - "~/src/APLActor/APLActor", - "~/src/APL/APL", - "~/src/Module/Module", - "~/src/UnitManager/UnitManager", - "~/src/ObjectManager/ObjectManager", + "~/src/Item/Item", + "~/src/ItemBook/ItemBook", + "~/src/Library/Library", + "~/src/List/List", "~/src/Missile/Missile", "~/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/SpellBook/SpellBook", - "~/src/Item/Item", - "~/src/ItemBook/ItemBook", - "~/src/AuraTable/AuraTable", - "~/src/Class/Class", "~/src/Timer/Timer", - "~/src/MythicPlusUtils/MythicPlusUtils", - "~/src/Config/Config", "~/src/TimeToDie/TimeToDie", + "~/src/Unit/Unit", + "~/src/UnitManager/UnitManager", + "~/src/Util/Util", + "~/src/Vector3/Vector3", } ---@param filePath string @@ -187,6 +206,19 @@ function Bastion:Toggle(toggle) Bastion.Enabled = type(toggle) ~= "nil" and toggle or not Bastion.Enabled 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 = { ["_DAMAGE"] = true, ["_MISSED"] = true, @@ -228,6 +260,47 @@ local combatEventPrefix = { "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 function Bastion:Load() if loaded then @@ -243,11 +316,43 @@ function Bastion:Load() self:Require(bastionFiles[i]) end - self.Globals.EventManager:RegisterWoWEvent("PLAYER_ENTERING_WORLD", function() - self.UnitManager:ResetObjects() + ---#region Init Start + 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) - 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() if self.Enabled then self.Util:Print("Enabled") @@ -256,26 +361,23 @@ function Bastion:Load() end end) - self.Globals.CombatTimer = self.Timer:New("combat", function() - return UnitAffectingCombat("player") - end) + ---#endregion - ---@param unitTarget UnitId - self.Globals.EventManager:RegisterWoWEvent("UNIT_HEALTH", function(unitTarget) - --Bastion.UnitManager:Get(unitTarget):UpdateHealth() + Bastion.Globals.CombatTimer = self.Timer:New("combat", function() + return UnitAffectingCombat("player") end) ---@param unit UnitToken ---@param auras UnitAuraUpdateInfo - self.Globals.EventManager:RegisterWoWEvent("UNIT_AURA", function(unit, auras) - local u = self.UnitManager:Get(unit) + Bastion.Globals.EventManager:RegisterWoWEvent("UNIT_AURA", function(unit, auras) + local u = Bastion.Globals.UnitManager:Get(unit) - if u:Exists() then + if u then u:GetAuras():OnUpdate(auras) end end) - self.Globals.EventManager:RegisterWoWEvent("UNIT_SPELLCAST_SUCCEEDED", function(...) + Bastion.Globals.EventManager:RegisterWoWEvent("UNIT_SPELLCAST_SUCCEEDED", function(...) ---@type UnitIds, string, spellId local unit, castGUID, spellID = ... local spell = self.Globals.SpellBook:GetIfRegistered(spellID) @@ -289,25 +391,11 @@ function Bastion:Load() end end) - local playerGuid = UnitGUID("player") 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() local args = { CombatLogGetCurrentEventInfo() } - + local currTime = GetTime() ---@type string local subEvent = args[2] ---@type string @@ -325,65 +413,95 @@ function Bastion:Load() 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.Globals.UnitManager:GetObject(destGUID) + + if destUnit and destUnit:Exists() then + destUnit:SetLastCombatTime(currTime) + end - local destUnit = Bastion.UnitManager:GetObject(destGUID) + if subEvent == "UNIT_DIED" or subEvent == "UNIT_DESTROYED" or subEvent == "PARTY_KILL" or subEvent == "SPELL_INSTAKILL" then + Bastion.TimeToDie:UNIT_DIED(destGUID) + end - --local t = GetTime() - if sourceUnit and sourceUnit:IsAffectingCombat() then - sourceUnit:SetLastCombatTime() + --[[ if sourceUnit and sourceUnit:IsValid() and sourceUnit:IsAffectingCombat() then + sourceUnit:SetLastCombatTime(currTime) end 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 - --[[ for key, val in pairs(combatEvents) do - if val and subEvent:find(key) then - end - end ]] end if destUnit and updateDestUnit then - destUnit:SetLastCombatTime() + destUnit:SetLastCombatTime(currTime) + 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 + + if self.Enabled then + self.ObjectManager:Refresh() + self.UnitManager:ResetObjects() + self.Globals.MissileManager:Refresh() + self.TimeToDie:Refresh() + self:TickModules() + else + --Bastion.UnitManager:ResetObjects() + end end + ]] - --[[ if destUnit then - if subEvent == "SPELL_MISSED" and sourceGUID == playerGuid and spellID == 408 then - local missType = args[15] + CreateFrame("Frame"):SetScript("OnUpdate", function(this, elapsed) + self.TimeSinceLastUpdate = self.TimeSinceLastUpdate + elapsed - if missType == "IMMUNE" then - local castingSpell = sourceUnit:GetCastingOrChannelingSpell() + if self.TimeSinceLastUpdate >= self.Interval and not self.Updating then + self.Updating = true + self.TimeSinceLastUpdate = 0 - if castingSpell and type(castingSpell) == "table" then - if not missed[castingSpell:GetID()] then - missed[castingSpell:GetID()] = true - end - end - end + 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 ]] - end) - - self.Ticker = C_Timer.NewTicker(self.Interval, function() - self.Globals.CombatTimer:Check() - if self.Enabled then - self.MissileManager:Refresh() self.ObjectManager:Refresh() + self.Globals.UnitManager:ResetObjects() + self.Globals.MissileManager:Refresh() self.TimeToDie:Refresh() self:TickModules() - else - self.UnitManager:ResetObjects() + self.Updating = false 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 rand = math.random(100000, 999999) while true do - local spellName, spellSubName = GetSpellBookItemName(i, BOOKTYPE_SPELL) + local spellName, spellSubName = C_SpellBook.GetSpellBookItemName(i, BOOKTYPE_SPELL) if not spellName then do break @@ -391,18 +509,18 @@ function Bastion:Load() end -- 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]", "") 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 i = i + 1 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 self.Util:Print(k) end @@ -414,6 +532,7 @@ function Bastion:Load() LoadThird() loaded = true + self.Loaded = GetTime() return self end diff --git a/src/Command/Command.lua b/src/Command/Command.lua index f4380b9..a2005fe 100644 --- a/src/Command/Command.lua +++ b/src/Command/Command.lua @@ -5,7 +5,6 @@ Bastion = ... -- Create a wow command handler class ---@class Bastion.Command ----@field command string ---@field commands Bastion.Command.Commands[] ---@field prefix string local Command = {} @@ -18,7 +17,7 @@ Command.__index = Command ---@return string function Command:__tostring() - return "Command(" .. self.command .. ")" + return "Command(" .. self.prefix .. ")" end ---@param prefix string @@ -82,4 +81,4 @@ function Command:PrintHelp() end Bastion.Command = Command -Bastion.Globals.Command = Bastion.Command:New("bastion") +--Bastion.Globals.Command = Bastion.Command:New("bastion") diff --git a/src/EventManager/EventManager.lua b/src/EventManager/EventManager.lua index 81b2983..ba679df 100644 --- a/src/EventManager/EventManager.lua +++ b/src/EventManager/EventManager.lua @@ -8,7 +8,7 @@ Bastion = ... ---@field frame Frame ---@field events table ---@field eventHandlers table ----@field wowEventHandlers table +---@field wowEventHandlers table ---@field selfCombatEventHandlers table ---@field CombatEventHandlers table ---@field playerGUID string|boolean @@ -36,8 +36,12 @@ function EventManager:New() self.frame:SetScript("OnEvent", function(f, event, ...) if self.wowEventHandlers[event] then - for _, callback in ipairs(self.wowEventHandlers[event]) do - callback(...) + for _, eventHandler in ipairs(self.wowEventHandlers[event]) do + if eventHandler.sendEvent then + eventHandler.fun(event, ...) + else + eventHandler.fun(...) + end end end end) @@ -64,7 +68,8 @@ end -- Register a wow event ---@param events WowEvent | WowEvent[] ---@param handler fun(...) -function EventManager:RegisterWoWEvent(events, handler) +---@param sendEvent? boolean +function EventManager:RegisterWoWEvent(events, handler, sendEvent) if type(events) == "string" then events = { events } end @@ -75,7 +80,10 @@ function EventManager:RegisterWoWEvent(events, handler) self.frame:RegisterEvent(event) end - table.insert(self.wowEventHandlers[event], handler) + table.insert(self.wowEventHandlers[event], { + fun = handler, + sendEvent = sendEvent or false, + }) end end @@ -153,6 +161,10 @@ function EventManager:CLEUHandler(...) end end +function EventManager:SetOnUpdate(func) + self.frame:SetScript("OnUpdate", func) +end + Bastion.EventManager = EventManager -Bastion.Globals.EventManager = Bastion.EventManager:New() +--Bastion.Globals.EventManager = Bastion.EventManager:New() diff --git a/src/Item/Item.lua b/src/Item/Item.lua index 6361abc..4d39f31 100644 --- a/src/Item/Item.lua +++ b/src/Item/Item.lua @@ -3,12 +3,15 @@ local Tinkr, ---@class Bastion Bastion = ... +local Evaluator = Tinkr.Util.Evaluator + ---@class Bastion.Item.Traits.Use ---@field moving? boolean ---@field dead? boolean ---@field casting? boolean ---@field channeling? boolean ---@field byId? boolean +---@field delay? false | number ---@class Bastion.Item.Traits.Target ---@field exists? boolean @@ -41,11 +44,10 @@ local usableExcludes = { } ---@param itemId number | string ----@return number charges, number maxCharges, number start, number duration local GetItemCharges = function(itemId) local _, spellId = C_Item.GetItemSpell(itemId) - local charges, maxCharges, start, duration, chargeModRate = GetSpellCharges(spellId) - return charges, maxCharges, start, duration + local chargeInfo = C_Spell.GetSpellCharges(spellId) + return chargeInfo end function Item:__index(k) @@ -90,6 +92,7 @@ function Item:New(id) casting = false, channeling = false, byId = false, + delay = false, }, target = { exists = true, @@ -139,7 +142,7 @@ function Item:SetTraits(traits) end 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 return false @@ -164,6 +167,12 @@ function Item:EvaluateTraits() 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 end @@ -233,15 +242,14 @@ end ---@param unit? Bastion.Unit function Item:UseByID(unit) local target = unit and unit:GetOMToken() or self.traits.target.player and "player" or "target" - - RunMacroText(string.format("/use [@%s] item:%d", target, self:GetID())) + loadstringsecure(string.format("C_Macro.RunMacroText('/use [@%s] item:%d','')", target, self:GetID())) Bastion.Util:Debug("Using by id", self) end -- Use the Item ---@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 function Item:Use(unit, condition) if not self:Usable() then @@ -250,7 +258,7 @@ function Item:Use(unit, condition) if condition then if type(condition) == "string" and not self:EvaluateCondition(condition) then return false - elseif type(condition) == "function" and not condition(self) then + elseif type(condition) == "function" and not condition(self, unit) then return false end end @@ -260,14 +268,22 @@ function Item:Use(unit, condition) self:GetPreUseFunction()(self) end - local target = unit or self.traits.target.player and Bastion.UnitManager:Get("player") or - self.traits.target.exists and self:TargetExists() and self:GetTarget() or false - if not target then - return false - end -- Check if the mouse was looking self.wasLooking = IsMouselooking() --[[@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) -- Set the last Use time @@ -344,15 +360,14 @@ end -- Check if the Item is usable ---@return boolean function Item:IsUsable() - local usable, noMana = IsUsableItem(self:GetID()) + local usable, noMana = C_Item.IsUsableItem(self:GetID()) return usable or usableExcludes[self:GetID()] end -- Check if the Item is Usable ---@return boolean function Item:IsEquippedAndUsable() - return ((self:IsEquippable() and self:IsEquipped()) or (not self:IsEquippable() and self:IsUsable())) - and not self:IsOnCooldown() + return (not self:IsEquippable() or self:IsEquipped()) and self:IsUsable() and not self:IsOnCooldown() end -- Is equippable @@ -363,14 +378,8 @@ end -- Check if the Item is Usable ---@return boolean function Item:Usable() - if not self:EvaluateTraits() then - return false - end - if self:GetUsableFunction() and not self:GetUsableFunction()(self) then - return false - end - - return self:IsEquippedAndUsable() + return self:IsEquippedAndUsable() and self:EvaluateTraits() and + (not self:GetUsableFunction() or self:GetUsableFunction()(self)) end -- Set a script to check if the Item is Usable @@ -497,7 +506,8 @@ end -- Get the Items charges ---@return number function Item:GetCharges() - return select(1, GetItemCharges(self:GetID())) + local chargeInfo = GetItemCharges(self:GetID()) + return chargeInfo and chargeInfo.currentCharges or 0 end function Item:GetCount() @@ -507,8 +517,8 @@ end -- Get the Items charges remaining ---@return number function Item:GetChargesRemaining() - local charges, maxCharges, start, duration = GetItemCharges(self:GetID()) - return charges + local chargeInfo = GetItemCharges(self:GetID()) + return chargeInfo and chargeInfo.currentCharges or 0 end -- Create a condition for the Item @@ -566,7 +576,7 @@ end -- Get the Items target 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 function Item:TargetExists() diff --git a/src/ItemBook/ItemBook.lua b/src/ItemBook/ItemBook.lua index 5c821f3..69f6eb0 100644 --- a/src/ItemBook/ItemBook.lua +++ b/src/ItemBook/ItemBook.lua @@ -37,5 +37,5 @@ function ItemBook:GetItem(id) return self.items[id] end -Bastion.Globals.ItemBook = ItemBook:New() +--Bastion.Globals.ItemBook = ItemBook:New() Bastion.ItemBook = ItemBook diff --git a/src/List/List.lua b/src/List/List.lua index 607652c..997c576 100644 --- a/src/List/List.lua +++ b/src/List/List.lua @@ -156,7 +156,7 @@ end ---@generic R : 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 ---@return R function List:reduce(callback, initialValue) @@ -254,7 +254,7 @@ end ---@return string function List:toString() - return self:join(", ") + return string.format("Bastion.List(%d)", self:count()) --self:join(", ") end Bastion.List = List diff --git a/src/Missile/Missile.lua b/src/Missile/Missile.lua index 5f75ee2..749bd38 100644 --- a/src/Missile/Missile.lua +++ b/src/Missile/Missile.lua @@ -27,11 +27,11 @@ function Missile:New(missile) end function Missile:GetSourceUnit() - return Bastion.UnitManager:Get(self.source) + return Bastion.Globals.UnitManager:Get(self.source) end function Missile:GetTargetUnit() - return Bastion.UnitManager:Get(self.target) + return Bastion.Globals.UnitManager:Get(self.target) end function Missile:GetCurrentVector() diff --git a/src/MissileManager/MissileManager.lua b/src/MissileManager/MissileManager.lua index a6d44c3..4cd17c7 100644 --- a/src/MissileManager/MissileManager.lua +++ b/src/MissileManager/MissileManager.lua @@ -17,7 +17,8 @@ Bastion = ... local MissileManager = {} MissileManager.__index = MissileManager -function MissileManager:New() +---@param interval? number +function MissileManager:New(interval) ---@class Bastion.MissileManager local self = setmetatable({}, MissileManager) self.missiles = {} @@ -25,6 +26,12 @@ function MissileManager:New() self.trackedMissiles = Bastion.List:New() self.allMissiles = Bastion.List:New() 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 end @@ -101,7 +108,16 @@ function MissileManager:GetList(name) return self._lists[name].list 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() local missiles = Missiles() @@ -116,6 +132,14 @@ function MissileManager:Refresh() end end end + + self.lastStop = GetTime() + self.lastDuration = self.lastStop - self.lastStart + self.refreshing = false +end + +function MissileManager:IsRefreshing() + return self.refreshing end ---@param params { source?: Bastion.Unit, target?: Bastion.Unit, spellId?: number, spellVisualId?: number } @@ -134,4 +158,4 @@ function MissileManager:GetMissiles(params) return missiles end -Bastion.MissileManager = MissileManager:New() +Bastion.MissileManager = MissileManager diff --git a/src/Module/Module.lua b/src/Module/Module.lua index fd27129..f1b80a6 100644 --- a/src/Module/Module.lua +++ b/src/Module/Module.lua @@ -27,12 +27,14 @@ end ---@param persist? boolean ---@return Bastion.Module function Module:New(name, interval, persist) + ---@class Bastion.Module local self = setmetatable({}, Module) self.name = name self.enabled = false + self.ticking = false self.synced = {} - self.interval = interval or 0.01 + self.interval = interval or Bastion.Interval self.nextTick = GetTime() + self.interval self.persist = persist or false return self @@ -52,7 +54,6 @@ end function Module:NextTick() self.nextTick = GetTime() + self.interval - return self end -- Toggle the module @@ -87,19 +88,22 @@ end -- Sync function Module:Tick() - if self.enabled then - if GetTime() >= self.nextTick then - self:NextTick() - for i = 1, #self.synced do - self.synced[i]() + if not self.ticking then + self.ticking = true + if self.enabled then + if GetTime() >= self.nextTick then + self:NextTick() + for i = 1, #self.synced do + self.synced[i]() + end end end + self.ticking = false end - return self end Bastion.Module = Module - +--[[ ---@type Bastion.Module[] local MODULES = {} @@ -146,3 +150,4 @@ Bastion.Globals.Command:Register("module", "Toggle a module on/off", function(ar Bastion.Util:Print("Module not found") end end) + ]] diff --git a/src/MythicPlusUtils/MythicPlusUtils.lua b/src/MythicPlusUtils/MythicPlusUtils.lua index 72ee869..0d995a6 100644 --- a/src/MythicPlusUtils/MythicPlusUtils.lua +++ b/src/MythicPlusUtils/MythicPlusUtils.lua @@ -1226,8 +1226,8 @@ function MythicPlusUtils:New() if self.loggedCasts[spellID] then return end - - local name = GetSpellInfo(spellID) + local spellInfo = C_Spell.GetSpellInfo(spellID) + local name = spellInfo and spellInfo.name or "Unknown" self.loggedCasts[spellID] = true @@ -1248,7 +1248,8 @@ function MythicPlusUtils:New() return end - local name = GetSpellInfo(spellID) + local spellInfo = C_Spell.GetSpellInfo(spellID) + local name = spellInfo and spellInfo.name or "Unknown" self.loggedCasts[spellID] = true @@ -1336,7 +1337,8 @@ function MythicPlusUtils:IsAOEBoss(unit) return self.aoeBosses[unit:GetID()] end -Bastion.MythicPlusUtils = MythicPlusUtils:New() +Bastion.MythicPlusUtils = MythicPlusUtils +--[[ Bastion.MythicPlusUtils = MythicPlusUtils:New() Bastion.Globals.Command:Register("mplus", "Toggle m+ module on/off", function(args) 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("debuffs") Bastion.Util:Print("casts") -end) +end) ]] diff --git a/src/NotificationList/NotificationList.lua b/src/NotificationList/NotificationList.lua index a7c7988..1dcddea 100644 --- a/src/NotificationList/NotificationList.lua +++ b/src/NotificationList/NotificationList.lua @@ -90,6 +90,5 @@ function NotificationList:RemoveAllNotifications() end end -Bastion.Globals.Notifications = NotificationList:New() - Bastion.NotificationList = NotificationList +--Bastion.Globals.Notifications = Bastion.NotificationList:New() diff --git a/src/Object/Object.lua b/src/Object/Object.lua new file mode 100644 index 0000000..e69de29 diff --git a/src/ObjectManager/ObjectManager.lua b/src/ObjectManager/ObjectManager.lua index 414b577..91b3844 100644 --- a/src/ObjectManager/ObjectManager.lua +++ b/src/ObjectManager/ObjectManager.lua @@ -6,14 +6,25 @@ Bastion = ... ---@class Bastion.ObjectManager ---@field _lists table ---@field objects WowGameObject[] +---@field objectList table local ObjectManager = {} ObjectManager.__index = ObjectManager -function ObjectManager:New() +---@param interval? number +function ObjectManager:New(interval) ---@class Bastion.ObjectManager local self = setmetatable({}, ObjectManager) + self.interval = interval or 0.1 + self.nextUpdate = 0 self._lists = {} 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.afflicted = Bastion.List:New() @@ -24,11 +35,16 @@ function ObjectManager:New() self.incorporeal = Bastion.List:New() self.missiles = Bastion.List:New() self.others = Bastion.List:New() - + self.outOfRange = Bastion.List:New() return self 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 ---@param name string ---@param cb fun(object: TinkrObjectReference): boolean | any @@ -56,6 +72,7 @@ end function ObjectManager:Reset() self.objects = {} + self.objectList = {} self.activeEnemies:clear() self.afflicted:clear() @@ -66,6 +83,7 @@ function ObjectManager:Reset() self.incorporeal:clear() self.missiles:clear() self.others:clear() + self.outOfRange:clear() self:ResetLists() end @@ -88,52 +106,77 @@ function ObjectManager:GetList(name) return self._lists[name].list end +function ObjectManager:IsRefreshing() + return self.refreshing +end + +function ObjectManager:InObjectList(objectGuid) + return self.objectList[objectGuid] ~= nil +end + -- Refresh all lists ----@return nil -function ObjectManager:Refresh() - self:Reset() +---@param force? boolean +function ObjectManager: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:Reset() local objects = Objects() - for _, object in pairs(objects) do - table.insert(self.objects, object) - self:EnumLists(object) - local objectType = ObjectType(object) - if ({ [5] = true, [6] = true, [7] = true })[objectType] then - local objectGUID = ObjectGUID(object) - if objectGUID then - local unit = Bastion.UnitManager:GetObject(objectGUID) - if not unit then - unit = Bastion.UnitManager:SetObject(Bastion.Unit:New(object)) - end + 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) + self:EnumLists(object) + if ({ [5] = true, [6] = true, [7] = true })[objectType] then if objectType == 5 and ObjectCreatureType(object) == 8 then - self.critters:push(unit) - elseif unit:GetID() == 204560 then - self.incorporeal:push(unit) - elseif unit:GetID() == 204773 then - self.afflicted:push(unit) - elseif unit:GetID() == 120651 then - self.explosives:push(unit) - elseif (unit:IsInPartyOrRaid() or unit == Bastion.UnitManager["player"]) then - self.friends:push(unit) - elseif unit:IsEnemy() then + self.critters:push(Bastion.Globals.UnitManager:GetObject(objectGuid) or + Bastion.Globals.UnitManager:SetObject(Bastion.Unit:New(object), objectGuid)) + elseif object:id() == 204560 --[[ unit:GetID() == 204560 ]] then + self.incorporeal:push(Bastion.Globals.UnitManager:GetObject(objectGuid) or + Bastion.Globals.UnitManager:SetObject(Bastion.Unit:New(object), objectGuid)) + elseif object:id() == 204773 then + self.afflicted:push(Bastion.Globals.UnitManager:GetObject(objectGuid) or + Bastion.Globals.UnitManager:SetObject(Bastion.Unit:New(object), objectGuid)) + elseif object:id() == 120651 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) if unit:IsAffectingCombat() or unit:InCombatOdds() > 80 then self.activeEnemies:push(unit) end 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 - local missiles = Missiles() + --[[ local missiles = Missiles() if type(missiles) == "table" then for _, missile in pairs(missiles) do self.missiles:push(missile) end - end + end ]] + self.lastStop = GetTime() + self.lastDuration = self.lastStop - self.lastStart + self.refreshing = false end -Bastion.ObjectManager = ObjectManager:New() +Bastion.ObjectManager = ObjectManager diff --git a/src/Spell/Spell.lua b/src/Spell/Spell.lua index 5d37278..144c366 100644 --- a/src/Spell/Spell.lua +++ b/src/Spell/Spell.lua @@ -12,6 +12,7 @@ Bastion = ... ---@field override boolean ---@field talent boolean | spellId ---@field power boolean +---@field precast false | fun(self:Bastion.Spell):boolean ---@class Bastion.Spell.Traits.Cost ---@field type Enum.PowerType @@ -24,20 +25,27 @@ Bastion = ... ---@class Bastion.Spell.Traits.Target ---@field exists boolean ---@field player boolean +---@field facing boolean + +---@class Bastion.Spell.Traits.Aura +---@field track boolean ---@class Bastion.Spell.Traits ---@field cast Bastion.Spell.Traits.Cast ---@field target Bastion.Spell.Traits.Target ---@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 Spell.Traits.Target.Params : Bastion.Spell.Traits.Target, { [string]?: boolean } ----@class Spell.Traits.Cost.Params : Bastion.Spell.Traits.Cost[] +---@class Bastion.Spell.Traits.Target.Params : Bastion.Spell.Traits.Target, { [string]?: boolean } +---@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 ---@field cast? Bastion.Spell.Traits.Cast.Params ----@field target? Spell.Traits.Target.Params ----@field cost? Spell.Traits.Cost.Params +---@field target? Bastion.Spell.Traits.Target.Params +---@field cost? Bastion.Spell.Traits.Cost.Params +---@field aura? Bastion.Spell.Traits.Aura.Params ---@class Bastion.Spell.Aura ---@field spell Bastion.Spell @@ -110,10 +118,21 @@ end function Spell:New(id) ---@class Bastion.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.overrides = {} self.conditions = {} 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 = { cast = { moving = true, @@ -124,12 +143,17 @@ function Spell:New(id) override = false, talent = false, power = false, + precast = false, }, target = { exists = true, player = false, + facing = false, }, cost = {}, + aura = { + track = false, + }, } self.target = false self.wasLooking = false @@ -138,8 +162,9 @@ function Spell:New(id) return self end +---@return string function Spell:GetDescription() - return GetSpellDescription(self:GetID()) or "" + return C_Spell.GetSpellDescription(self:GetID()) or "" end -- Duplicator @@ -161,23 +186,42 @@ function Spell:PostCast(func) return self end +function Spell:GetSpellInfo() + return C_Spell.GetSpellInfo(self:GetID()) +end + -- Get the spells name ---@return string 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 -- Get the spells icon ---@return number 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 -- Get the spells cooldown ---@param byId? boolean ---@return number 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 -- Return the castable function @@ -199,18 +243,19 @@ end ---@param byId? boolean ---@return number function Spell:GetCooldownRemaining(byId) - local start, duration = GetSpellCooldown(byId and self:GetID() or self:GetName()) - if start == 0 then - return 0 + local cdInfo = self:GetCooldownInfo(byId) + if cdInfo then + if cdInfo.startTime == 0 then return 0 end + return cdInfo.startTime + cdInfo.duration - GetTime() end - return start + duration - GetTime() + return 0 end -- Get the spell count ---@param byId? boolean ---@return number 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 -- On cooldown @@ -233,18 +278,18 @@ function Spell:AddOverrideSpell(spell, func) end -- Cast the spell ----@param unit? false | Bastion.Unit ----@param condition? string | fun(self:Bastion.Spell):boolean +---@param unit? Bastion.Unit +---@param condition? string | fun(self:Bastion.Spell, target?: Bastion.Unit):boolean ---@return boolean function Spell:Cast(unit, condition) - if not self:Castable() then + if not self:Castable(unit) then return false end if condition then if type(condition) == "string" and not self:EvaluateCondition(condition) then return false - elseif type(condition) == "function" and not condition(self) then + elseif type(condition) == "function" and not condition(self, unit) then return false end end @@ -259,15 +304,21 @@ function Spell:Cast(unit, condition) self.wasLooking = IsMouselooking() --[[@as boolean]] -- if unit:GetOMToken() contains 'nameplate' then we need to use Object wrapper to cast - - local u = unit ~= false and unit:GetOMToken() or self.traits.target.player and "none" or "none" - if type(u) == "string" and string.find(u, "nameplate") then - ---@diagnostic disable-next-line: cast-local-type - u = Object(u) + local target = unit and unit:GetOMToken() or self.traits.target.player and "player" or "none" + if type(target) == "string" and string.find(target, "nameplate") then + target = Object(target):unit() end -- 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() Bastion.Util:Debug("Casting", self) @@ -291,14 +342,13 @@ function Spell:ForceCast(unit) self.wasLooking = IsMouselooking() --[[@as boolean]] -- 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" - if type(u) == "string" and string.find(u, "nameplate") then - ---@diagnostic disable-next-line: cast-local-type - u = Object(u) + local target = unit and unit:GetOMToken() or self.traits.target.player and "player" or "none" + if type(target) == "string" and string.find(target, "nameplate") then + target = Object(target):unit() end -- Cast the spell - CastSpellByName(self:GetName(), u) + CastSpellByName(self:GetName(), target) SpellCancelQueuedSpell() Bastion.Util:Debug("Force Casting", self) @@ -315,7 +365,7 @@ function Spell:GetPostCastFunction() end function Spell:OverrideSpellID() - return C_SpellBook.GetOverrideSpell(self.spellID) + return C_Spell.GetOverrideSpell(self.spellID) end function Spell:IsOverridden() @@ -326,27 +376,32 @@ end ---@param includeOverrides? boolean ---@return boolean 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()) return isKnown end 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 end -- Check if the spell is on cooldown ---@return boolean function Spell:IsOnCooldown() - local spellIDName = self:IsOverridden() and self:GetName() or self:GetID() - return select(2, GetSpellCooldown(spellIDName)) > 0 or select(2, self:GetSpellLossOfControlCooldown()) > 0 + local spellCooldownInfo = self:GetCooldownInfo(self:IsOverridden() and false) + if spellCooldownInfo and spellCooldownInfo.duration then + return spellCooldownInfo.duration > 0 or select(2, self:GetSpellLossOfControlCooldown()) > 0 + end + return false end ---@param byId? boolean ---@return boolean, boolean 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 -- Check if the spell is usable @@ -361,12 +416,13 @@ end ---@param override? boolean ---@return boolean 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 ---@param traits Bastion.Spell.Traits.Params 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 for k, _ in pairs(self.traits[trait]) do if traits[trait][k] ~= nil then @@ -384,8 +440,17 @@ function Spell:SetTraits(traits) return self end -function Spell:EvaluateTraits() - local player = Bastion.UnitManager:Get("player") +---@param target? Bastion.Unit +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 return false @@ -411,11 +476,15 @@ function Spell:EvaluateTraits() return false end - if self.traits.target.exists and not self:TargetExists() then - return false + 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 + 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 end @@ -423,9 +492,10 @@ function Spell:EvaluateTraits() end -- Check if the spell is castable -function Spell:Castable() - return self:EvaluateTraits() and (not self:GetCastableFunction() or self:GetCastableFunction()(self)) and - self:IsKnownAndUsable(type(self.traits.cast.override) ~= nil and self.traits.cast.override or nil) +---@param target? Bastion.Unit +function Spell:Castable(target) + return self:IsKnownAndUsable(self.traits.cast.override) and self:EvaluateTraits(target) and + (not self:GetCastableFunction() or self:GetCastableFunction()(self)) end -- Set a script to check if the spell is castable @@ -485,34 +555,66 @@ function Spell:Call(unit) end -- Check if the spell is castable and cast it +---@return boolean? function Spell:HasRange() - return SpellHasRange(self:GetName()) + return C_Spell.SpellHasRange(self:GetName()) end -- Get the range of the spell ---@return number ---@return number function Spell:GetRange() - local name, rank, icon, castTime, minRange, maxRange, spellID, originalIcon = GetSpellInfo(self:GetID()) - return maxRange, minRange + --local _, _, _, _, minRange, maxRange, _, _ = GetSpellInfo(self:GetID()) + 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 -- Check if the spell is in range of the unit ---@param unit Bastion.Unit ---@return boolean function Spell:IsInRange(unit) - local hasRange = self:HasRange() - local inRange = IsSpellInRange(self:GetName(), unit:GetOMToken()) - - if hasRange == false then + if unit:IsUnit(Bastion.Globals.UnitManager:Get("player")) then return true end - if inRange == 1 then + local hasRange = self:HasRange() + if hasRange == false then return true 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 -- Get the last cast time @@ -539,61 +641,88 @@ function Spell:GetTimeSinceLastCastAttempt() return GetTime() - self.lastCastAttempt 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() - return GetSpellCharges(self:GetID()) + return C_Spell.GetSpellCharges(self:GetID()) end -- Get the spells charges function Spell:GetCharges() - return select(1, GetSpellCharges(self:GetID())) + local chargeInfo = self:GetChargeInfo() + return chargeInfo and chargeInfo.currentCharges or 0 end function Spell:GetMaxCharges() - return select(2, GetSpellCharges(self:GetID())) + local chargeInfo = self:GetChargeInfo() + return chargeInfo and chargeInfo.maxCharges or 0 end ---@return number function Spell:GetCastLength() - return select(4, GetSpellInfo(self:GetID())) + local spellInfo = self:GetSpellInfo() + return spellInfo and spellInfo.castTime or 0 end -- Get the full cooldown (time until all charges are available) ---@return number function Spell:GetFullRechargeTime() - local charges, maxCharges, _, duration = self:GetChargeInfo() - if not charges or not maxCharges or charges == maxCharges then + local spellChargeInfo = self:GetChargeInfo() + if not spellChargeInfo or spellChargeInfo.currentCharges == spellChargeInfo.maxCharges then return 0 end - return (maxCharges - self:GetChargesFractional()) * duration + return (spellChargeInfo.maxCharges - self:GetChargesFractional()) * spellChargeInfo.cooldownDuration end function Spell:Recharge() - local charges, maxCharges, startTime, duration = self:GetChargeInfo() - if charges == maxCharges then + local spellChargeInfo = self:GetChargeInfo() + if not spellChargeInfo or spellChargeInfo.currentCharges == spellChargeInfo.maxCharges then return 0 end - local recharge = startTime + duration - GetTime() + local recharge = spellChargeInfo.cooldownStartTime + spellChargeInfo.cooldownDuration - GetTime() return recharge > 0 and recharge or 0 end -- Get the spells charges plus fractional ---@return number function Spell:GetChargesFractional() - local charges, maxCharges, _, duration = GetSpellCharges(self:GetID()) + local spellChargeInfo = self:GetChargeInfo() - if charges == maxCharges then - return charges + if not spellChargeInfo or spellChargeInfo.currentCharges == spellChargeInfo.maxCharges then + return spellChargeInfo.currentCharges end - return charges + ((duration - self:Recharge()) / duration) + return spellChargeInfo.currentCharges + + ((spellChargeInfo.cooldownDuration - self:Recharge()) / spellChargeInfo.cooldownDuration) end -- Get the spells charges remaining ---@return number function Spell:GetChargesRemaining() - local charges, maxCharges, start, duration = GetSpellCharges(self:GetID()) - return charges + local spellChargeInfo = self:GetChargeInfo() + return spellChargeInfo and spellChargeInfo.currentCharges or 0 end -- Create a condition for the spell @@ -705,16 +834,22 @@ function Spell:IsSpell(spell) return self:GetID() == spell:GetID() end +---@return SpellPowerCostInfo[]? +function Spell:GetCostInfo() + return C_Spell.GetSpellPowerCost(self:GetID()) +end + function Spell:HasPower() - local costs = GetSpellPowerCost(self:GetID()) + local costs = self:GetCostInfo() local checked = {} - local hasPower = #costs > 0 and false or true - if not hasPower then + local hasPower = costs and #costs > 0 and false or true + if costs and not hasPower then for _, cost in ipairs(costs) do if not checked[cost.type] then local powerCost = (cost.cost > cost.minCost and cost.minCost or cost.cost) if cost.hasRequiredAura or cost.requiredAuraID == 0 then - hasPower = powerCost == 0 or Bastion.UnitManager:Get("player"):GetPower(cost.type) >= powerCost + hasPower = powerCost == 0 or + Bastion.Globals.UnitManager:Get("player"):GetPower(cost.type) >= powerCost checked[cost.type] = true if not hasPower then return false @@ -729,7 +864,7 @@ end -- GetCost ---@return number function Spell:GetCost() - local cost = GetSpellPowerCost(self:GetID()) + local cost = self:GetCostInfo() return cost and cost[1] and cost[1].cost or 0 end @@ -757,10 +892,10 @@ end ---@param source? "any" | Bastion.Unit function Spell:GetAura(target, source) if type(target) == "nil" then - target = Bastion.UnitManager:Get("player") + target = Bastion.Globals.UnitManager:Get("player") end if type(source) == "nil" then - source = Bastion.UnitManager:Get("player") + source = Bastion.Globals.UnitManager:Get("player") end return target:GetAuras():FindFrom(self, source) @@ -816,7 +951,7 @@ end ---@param params { source: Bastion.Unit, target: Bastion.Unit, spellVisualId?: number } function Spell:GetMissiles(params) - return Bastion.MissileManager:GetMissiles({ + return Bastion.Globals.MissileManager:GetMissiles({ source = params.source, target = params.target, spellId = self:GetID(), @@ -829,4 +964,79 @@ function Spell:InFlight(params) return #self:GetMissiles(params) > 0 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 diff --git a/src/SpellBook/SpellBook.lua b/src/SpellBook/SpellBook.lua index 8fad902..1dd1ea7 100644 --- a/src/SpellBook/SpellBook.lua +++ b/src/SpellBook/SpellBook.lua @@ -11,25 +11,77 @@ local SpellBook = {} SpellBook.__index = SpellBook -- Constructor +---@param global? boolean ---@return Bastion.SpellBook -function SpellBook:New() +function SpellBook:New(global) + ---@class Bastion.SpellBook local self = setmetatable({}, SpellBook) self.spells = {} - --[[ Bastion.Globals.EventManager:RegisterWoWEvent("UNIT_SPELLCAST_SUCCEEDED", function(...) - self:SpellCastSucceeded(...) - end) ]] + self.global = global or false + --self:RegisterEvents() return self end ---[[ function SpellBook:SpellCastSucceeded(...) - local unit, castGUID, spellId = ... - if self.spells[spellId] then - self.spells[spellId].lastCastAt = GetTime() +function SpellBook:RegisterEvents() + Bastion.Globals.EventManager:RegisterWoWEvent( + { "UNIT_SPELLCAST_SENT", "UNIT_SPELLCAST_SUCCEEDED", "UNIT_SPELLCAST_FAILED", "UNIT_SPELLCAST_FAILED_QUIET", + "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 -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 -- Get a spell from the spellbook ----@param id integer +---@param id integer|string ---@return Bastion.Spell function SpellBook:GetSpell(id) local override = FindSpellOverrideByID(id) @@ -84,8 +136,8 @@ end ---@param name string ---@return Bastion.Spell function SpellBook:GetSpellByName(name) - local _, rank, icon, castTime, minRange, maxRange, spellID, originalIcon = GetSpellInfo(name) - return self:GetSpell(spellID) + local spellInfo = C_Spell.GetSpellInfo(name) + return self:GetSpell(spellInfo and spellInfo.spellID or 0) end ---@param id integer @@ -101,5 +153,5 @@ function SpellBook:GetIfRegistered(id) return false end -Bastion.Globals.SpellBook = SpellBook:New() Bastion.SpellBook = SpellBook +--Bastion.Globals.SpellBook = Bastion.SpellBook:New(true) diff --git a/src/TimeToDie/TimeToDie.lua b/src/TimeToDie/TimeToDie.lua index 64de8fd..e39ad5b 100644 --- a/src/TimeToDie/TimeToDie.lua +++ b/src/TimeToDie/TimeToDie.lua @@ -3,23 +3,19 @@ local Tinkr, ---@class 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. ---@class Bastion.TimeToDie local TimeToDie = { + Refreshing = false, Settings = { -- 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 HistoryTime = 10 + 0.4, -- Max history count : min=20, max=500, default = 100 - HistoryCount = 100 + HistoryCount = 100, + InactiveTime = 60, }, ---@type table Cache = {}, -- A cache of unused { time, value } tables to reduce garbage due to table creation @@ -44,98 +40,112 @@ local TimeToDie = { PLAYER = -6, -- 25 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() - return Bastion.ObjectManager.enemies +function TimeToDie:IsRefreshing() + return self.Refreshing end -function TimeToDie:Refresh() - local currentTime = GetTime() +function TimeToDie:Init() + if not Bastion.Globals.UnitInfo then + Bastion.Globals.UnitInfo = Bastion.Cache:New() + Bastion.Globals.EventManager:RegisterWoWEvent("UNIT_HEALTH", function(unitId) + self:UNIT_HEALTH(unitId) + end) + end +end + +function TimeToDie:IterableUnits2() + return Bastion.ObjectManager.activeEnemies +end - if currentTime >= TimeToDie.NextRefresh then - TimeToDie.NextRefresh = currentTime + TimeToDie.Settings.Refresh - else +---@param force? boolean +function TimeToDie:Refresh(force) + if self:IsRefreshing() or (self.NextUpdate > GetTime() and not force) or (not Bastion.Enabled and not force) then return end + local currentTime = GetTime() + + self.Refreshing = true + self.LastStart = currentTime + self.NextUpdate = self.LastStart + 2 - local historyCount = TimeToDie.Settings.HistoryCount - local historyTime = TimeToDie.Settings.HistoryTime - local ttdCache = TimeToDie.Cache - local iterableUnits = TimeToDie:IterableUnits() local units = TimeToDie.Units - local existingUnits = TimeToDie.ExistingUnits + 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 - wipe(existingUnits) +---@param sourceGUID string +function TimeToDie:UNIT_DIED(sourceGUID) + if self.Units[sourceGUID] then + self.Units[sourceGUID] = nil + end +end - ---@param unit Bastion.Unit - iterableUnits:each(function(unit) - if unit:Exists() then - local unitGUID = unit:GetGUID() - -- 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 - else - unitTable = { - history = {}, - time = currentTime - } - units[unitGUID] = unitTable - end +---@param unitId UnitId +function TimeToDie:UNIT_HEALTH(unitId) + local currentTime = GetTime() + if UnitExists(unitId) and UnitCanAttack("player", unitId) then + local unitGUID = UnitGUID(unitId) + if unitGUID then + local unitTable = self.Units[unitGUID] + local unitHealth = UnitHealth(unitId) + local unitMaxHealth = UnitHealthMax(unitId) + local unitHealthPerc = (unitHealth / unitMaxHealth) * 100 + if unitHealthPerc < 100 then + if not unitTable or unitHealthPerc > unitTable.history[1].percentage then + unitTable = { + history = {}, + time = currentTime + } + self.Units[unitGUID] = unitTable + end + local history = unitTable.history + local time = currentTime - unitTable.time + if not history or not history[1] or unitHealthPerc ~= history[1].percentage then + local val + local lastIndex = #self.Cache + if lastIndex == 0 then + val = { time = time, percentage = unitHealthPerc } + else + val = self.Cache[lastIndex] + self.Cache[lastIndex] = nil + val.time = time + val.percentage = unitHealthPerc end - local history = unitTable.history - 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 healthPercentage ~= history[1].percentage then - local val - local lastIndex = #ttdCache - -- Check if we can re-use a table from the cache - if lastIndex == 0 then - val = { time = time, percentage = healthPercentage } - else - val = ttdCache[lastIndex] - ttdCache[lastIndex] = nil - val.time = time - val.percentage = healthPercentage - end - table.insert(history, 1, val) - local n = #history - -- Delete values that are no longer valid - while (n > historyCount) or (time - history[n].time > historyTime) do - ttdCache[#ttdCache + 1] = history[n] - history[n] = nil - n = n - 1 - end + table.insert(history, 1, val) + local n = #history + while (n > self.Settings.HistoryCount) or (time - history[n].time > self.Settings.HistoryTime) do + self.Cache[#self.Cache + 1] = history[n] + history[n] = nil + n = n - 1 end end end end - 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 @@ -260,7 +270,7 @@ function TimeToDie.FightRemains(enemies, bossOnly) ---@type boolean, number local bossExists, maxTimeToDie for i = 1, 4 do - local bossUnit = Bastion.UnitManager:Get(string.format("boss%d", i)) + local bossUnit = Bastion.Globals.UnitManager:Get(string.format("boss%d", i)) if bossUnit:Exists() then bossExists = true if not bossUnit:TimeToDieIsNotValid() then @@ -288,7 +298,7 @@ function TimeToDie.FightRemains(enemies, bossOnly) end end - return Target:TimeToDie2() + return Bastion.Globals.UnitManager:Get("target"):TimeToDie2() end -- Returns the max fight length of boss units, 11111 if not a boss fight diff --git a/src/TimeToDieManager/TimeToDieManager.lua b/src/TimeToDieManager/TimeToDieManager.lua new file mode 100644 index 0000000..8dee5b7 --- /dev/null +++ b/src/TimeToDieManager/TimeToDieManager.lua @@ -0,0 +1,127 @@ +---@type Tinkr +local Tinkr, +---@class Bastion +Bastion = ... + +---@class Bastion.TimeToDieManager +---@field units table +---@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() diff --git a/src/TimeToDieUnit/TimeToDieUnit.lua b/src/TimeToDieUnit/TimeToDieUnit.lua new file mode 100644 index 0000000..7dd9579 --- /dev/null +++ b/src/TimeToDieUnit/TimeToDieUnit.lua @@ -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 diff --git a/src/Timer/Timer.lua b/src/Timer/Timer.lua index f8a0cc0..8c6eaf2 100644 --- a/src/Timer/Timer.lua +++ b/src/Timer/Timer.lua @@ -18,9 +18,11 @@ Timer.__index = Timer ---@param cb? fun():boolean ---@return Bastion.Timer function Timer:New(type, cb) + ---@class Bastion.Timer local self = setmetatable({}, Timer) self.startTime = nil self.type = type + self.checking = false self.cb = cb return self end @@ -52,14 +54,21 @@ function Timer:Reset() self.startTime = nil end +function Timer:IsChecking() + return self.checking +end + function Timer:Check() + self.checking = true 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() - elseif self:IsRunning() and not self.cb() then + elseif self:IsRunning() and not cbResult then self:Reset() end end + self.checking = false end Bastion.Timer = Timer diff --git a/src/Unit/Unit.lua b/src/Unit/Unit.lua index d6a6f8e..c778a36 100644 --- a/src/Unit/Unit.lua +++ b/src/Unit/Unit.lua @@ -8,7 +8,7 @@ Bastion = ... ---@field cache Bastion.Cache ---@field id false | number ---@field ttd_ticker false | cbObject ----@field unit? TinkrObjectReference +---@field unit? WowGameObject ---@field aura_table Bastion.AuraTable | nil local Unit = { ---@type Bastion.Cache @@ -22,9 +22,9 @@ local Unit = { unit = nil, ttd_ticker = false, ttd = 0, - id = false, --[[ @asnumber ]] + id = false, watching_for_swings = false, - health = {} + health = {}, } function Unit:UpdateHealth() @@ -77,28 +77,168 @@ function Unit:__tostring() end -- Constructor ----@param unit? TinkrObjectReference +---@param unit? WowGameObject +---@param guid? string ---@return Bastion.Unit -function Unit:New(unit) - ---@type Bastion.Unit +function Unit:New(unit, guid) + ---@class Bastion.Unit local self = setmetatable({}, Unit) self.unit = unit self.cache = Bastion.Cache:New() self.aura_table = Bastion.AuraTable:New(self) 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 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 ---@return boolean 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 -- Check if the unit exists ---@return boolean function Unit:Exists() - return Object(self:GetOMToken()) ~= false + return not self:IsUnit(Bastion.Globals.UnitManager:Get("none")) and Object(self:GetOMToken()) ~= false end -- Get the units token @@ -114,7 +254,7 @@ end -- Get the units GUID function Unit:GetGUID() - return ObjectGUID(self:GetOMToken()) + return self.unit and self.unit:guid() end -- Get the units health @@ -132,7 +272,7 @@ end -- Return Health Percent as an integer (0-100) ---@return number function Unit:GetHP() - return self:GetHealth() / self:GetMaxHealth() * 100 + return (self:GetHealth() / self:GetMaxHealth()) * 100 end -- Get realized health @@ -274,16 +414,7 @@ function Unit:IsBoss() if UnitClassification(self:GetOMToken()) == "worldboss" then return true end - - for i = 1, 5 do - local bossGUID = UnitGUID("boss" .. i) - - if self:GetGUID() == bossGUID then - return true - end - end - - return false + return type(string.find(self.unit:unit() or "", "^boss[1-5]")) == "number" end ---@return UnitIds @@ -419,37 +550,94 @@ function Unit:GetAttachmentPosition(attachmentId) return Bastion.Vector3:New(x, y, z) 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 ---@param unit Bastion.Unit ---@return boolean function Unit:CanSee(unit) - -- mechagon smoke cloud - -- local mechagonID = 2097 - -- local smokecloud = 298602 - - -- local name, instanceType, difficultyID, difficultyName, maxPlayers, dynamicDifficulty, isDynamic, instanceID, instanceGroupSize, LfgDungeonID = - -- GetInstanceInfo() - - -- otherUnit = otherUnit and otherUnit or "player" - -- if instanceID == 2097 then - -- if (self:debuff(smokecloud, unit) and not self:debuff(smokecloud, otherUnit)) - -- or (self:debuff(smokecloud, otherUnit) and not self:debuff(smokecloud, unit)) - -- then - -- return false - -- end - -- end - local ignoreLoS = { - [98696] = true -- Illysanna Ravencrest (BRH) - } - --[[ if not unit:IsPlayer() then - local id = unit:GetID() - if id and ignoreLoS[id] then - return true - end - end ]] local ax, ay, az = ObjectPosition(self:GetOMToken()) local ah = ObjectHeight(self:GetOMToken()) - local attx, atty, attz = GetUnitAttachmentPosition(unit:GetOMToken(), 34) + local attx, atty, attz = GetUnitAttachmentPosition(unit:GetOMToken(), attachmentId.Chest) + if not attx or not ax then return false @@ -463,11 +651,7 @@ function Unit:CanSee(unit) return true end - if not attx or not ax then - return false - end - - local x, y, z = TraceLine(ax, ay, az + ah, attx, atty, attz, 0) + local x, y, z = TraceLine(ax, ay, az + ah, attx, atty, attz, losFlag) if x ~= 0 or y ~= 0 or z ~= 0 then return false else @@ -475,6 +659,30 @@ function Unit:CanSee(unit) 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 ---@return boolean function Unit:IsCasting() @@ -520,6 +728,19 @@ function Unit:GetCastingOrChannelingSpell() return false 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 ---@return number function Unit:GetCastingOrChannelingEndTime() @@ -553,8 +774,8 @@ end ---@return Bastion.Unit function Unit:CastTarget() ---@diagnostic disable-next-line: param-type-mismatch - return self:IsCastingOrChanneling() and Bastion.UnitManager:Get(ObjectCastingTarget(self:GetOMToken())) or - Bastion.UnitManager:Get("none") + return self:IsCastingOrChanneling() and Bastion.Globals.UnitManager:Get(ObjectCastingTarget(self:GetOMToken())) or + Bastion.Globals.UnitManager:Get("none") end ---@param unit Bastion.Unit @@ -643,7 +864,7 @@ function Unit:GetEnemies(range) 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 count = count + 1 end @@ -665,7 +886,7 @@ function Unit:GetMeleeAttackers(facing) 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 count = count + 1 end @@ -682,7 +903,7 @@ end function Unit:GetPartyHPAround(distance, percent) 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 count = count + 1 end @@ -807,22 +1028,31 @@ function Unit:IsInfront(unit) return not self:IsBehind(unit) 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 function Unit:GetMeleeBoost() 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 currentSpec = select(1, GetSpecializationInfo(GetSpecialization())) if astralInfluenceNode then local currentRank = astralInfluenceNode.activeRank 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 elseif IsPlayerSpell(196924) then meleeBoost = 3 end - return meleeBoost + return meleeBoost ]] end function Unit:GetModelId() @@ -840,10 +1070,10 @@ end ---@param unit Bastion.Unit ---@return boolean function Unit:InMelee(unit) - local x, y, z = ObjectPosition(self:GetOMToken()) - local x2, y2, z2 = ObjectPosition(unit:GetOMToken()) + local sX, sY, sZ = ObjectPosition(self:GetOMToken()) + local tX, tY, tZ = ObjectPosition(unit:GetOMToken()) - if not x or not x2 then + if not sX or not tX then return false end @@ -854,7 +1084,7 @@ function Unit:InMelee(unit) return false 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) 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) -- the closer to 0 the more likely the unit is to be in combat (0 = 100%) 60 = 0% +---@param seconds? number ---@return number -function Unit:InCombatOdds() +function Unit:InCombatOdds(seconds) 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 -- Get units gcd time ---@return number 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) end @@ -1340,7 +1572,8 @@ end ---@return Bastion.Unit function Unit:Target() 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 local dummyUnits = { @@ -1448,7 +1681,7 @@ local dummyUnits = { ---@return boolean function Unit:IsDummy() 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 ---@param npcId? number @@ -1459,7 +1692,7 @@ function Unit:IsInBossList(npcId) return false end 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 return true end @@ -1491,7 +1724,7 @@ function Unit:CheckHPFromBossList(npcId, hp) local thisHP = hp or 100 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 return true end @@ -1505,8 +1738,11 @@ end ---@return Bastion.TimeToDie.Enums | integer function Unit:TimeToX(percentage, minSamples) --if self:IsDummy() then return 6666 end - if self:IsPlayer() and Bastion.UnitManager:Get("player"):CanAttack(self) then return Bastion.TimeToDie.Enums.PLAYER end - local seconds = 0 + if self:IsPlayer() and Bastion.Globals.UnitManager:Get("player"):CanAttack(self) then + return Bastion.TimeToDie + .Enums.PLAYER + end + local seconds = Bastion.TimeToDie.Enums.NOT_UPDATED local unitGuid = self:GetGUID() if not unitGuid then return Bastion.TimeToDie.Enums.NO_GUID @@ -1545,7 +1781,6 @@ function Unit:TimeToX(percentage, minSamples) seconds = (percentage - a) / b -- Subtract current time to obtain "time remaining" seconds = seconds - (GetTime() - unitTable.time) - if seconds < 0 then seconds = Bastion.TimeToDie.Enums.NEGATIVE_TTD end end end end @@ -1649,7 +1884,7 @@ end ---@param spell Bastion.Spell ---@param source? Bastion.Unit 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) if aura:IsValid() and unit then local tooltipInfo = C_TooltipInfo.GetUnitDebuffByAuraInstanceID(unit, aura:GetAuraInstanceID()) @@ -1723,23 +1958,64 @@ function Unit:IsSummoning() return false end -function Unit:GetLosssOfControlCount() +function Unit:GetLossOfControlCount() return C_LossOfControl.GetActiveLossOfControlDataCountByUnit(self:GetOMToken()) end ---@param index number -function Unit:LosssOfControlData(index) +function Unit:LossOfControlData(index) return C_LossOfControl.GetActiveLossOfControlDataByUnit(self:GetOMToken(), index) 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() return self:Exists() and self:IsAlive() - and self:GetLosssOfControlCount() == 0 + and self:LossOfControlActive({ "STUN", "STUN_MECHANIC" }) == 0 and not UnitOnTaxi(self:GetOMToken()) and not UnitInVehicle(self:GetOMToken()) 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 ---@param params { target?: Bastion.Unit, spellId?: number, spellVisualId?: number } diff --git a/src/UnitManager/UnitManager.lua b/src/UnitManager/UnitManager.lua index b2e7faa..b5a44de 100644 --- a/src/UnitManager/UnitManager.lua +++ b/src/UnitManager/UnitManager.lua @@ -3,8 +3,6 @@ local Tinkr, ---@class Bastion Bastion = ... -local Unit = Bastion.Unit - ---@class Bastion.UnitManager.CustomUnit ---@field unit Bastion.Cacheable | Bastion.Unit ---@field cb fun(): Bastion.Unit @@ -13,47 +11,98 @@ local Unit = Bastion.Unit ---@class Bastion.UnitManager ---@field units table ---@field customUnits table ----@field objects table +---@field objects table ---@field cache Bastion.Cache 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 function UnitManager:__index(k) if k == "none" then - return self:Get("none") + return self.objects.none end + if UnitManager[k] ~= nil then return UnitManager[k] 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 self.customUnits[k] then - if not self.cache:IsCached(k) then - self.customUnits[k].unit:Update() - self.cache:Set(k, self.customUnits[k].unit, 0.5) + -- if custom unit exists return it or a new one if the cache has expired. + if self.customUnits[key] then + if not self.cache:IsCached(key) then + self.customUnits[key].unit:Update() + self.cache:Set(key, self.customUnits[key].unit, 0.5) end - return self.customUnits[k].unit + return self.customUnits[key].unit 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] end - - if self.objects[kguid] == nil then - local o = Object(k) - if o then - local unit = Unit:New(o) - self:SetObject(unit) + if kguid ~= nil and self.objects[kguid] == nil then + local obj = Object(kguid) + if obj then + self.objects[kguid] = Bastion.Unit:New(obj, kguid) + return self:SetObject(Bastion.Unit:New(obj, kguid)) 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 -- Constructor @@ -63,38 +112,53 @@ function UnitManager:New() local self = setmetatable({}, UnitManager) self.units = {} self.customUnits = {} - self.objects = {} + self.objects = { + none = Bastion.Unit:New(), + } self.cache = Bastion.Cache:New() return self end -- Get or create a unit ----@param token UnitId | TinkrObjectReference +---@param token TinkrObjectReference function UnitManager:Get(token) - -- if not Validate(token) then - -- error("UnitManager:Get - Invalid token: " .. token) - -- end + --[[ local unitObject = Object(token) + local tguid = unitObject and unitObject:guid() or "none" + 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) if tguid and self.objects[tguid] == nil then - if token == "none" then - self.objects["none"] = Unit:New() - else - ---@diagnostic disable-next-line: param-type-mismatch - self.objects[tguid] = Unit:New(Object(tguid)) + local obj = Object(tguid) + if obj then + self.objects[tguid] = Bastion.Unit:New(obj, tguid) 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"] = Unit:New() - else - ---@diagnostic disable-next-line: param-type-mismatch - self.objects[tguid] = Unit:New(Object(tguid)) + local obj = Object(tguid) + if obj then + self.objects[tguid] = Bastion.Unit:New(obj, tguid) end end return self.objects[tguid] @@ -118,7 +182,7 @@ function UnitManager:GetOrCreateObject(unit) if unitObj and not unitObj:IsValid() then local object = type(unit) ~= "string" and unit or Object(guid) if object then - unitObj = Bastion.UnitManager:SetObject(Bastion.Unit:New(object)) + unitObj = self:SetObject(Bastion.Unit:New(object, guid)) end end return unitObj @@ -127,14 +191,20 @@ end -- Set a unit by guid ---@param unit Bastion.Unit -function UnitManager:SetObject(unit) - local guid = unit:GetGUID() - if guid then - self.objects[guid] = unit - return self.objects[guid] +---@param guid? string +function UnitManager:SetObject(unit, guid) + local tguid = guid or unit:GetGUID() + if tguid then + self.objects[tguid] = unit + return self.objects[tguid] end end +---@param unitGUID string +function UnitManager:ClearObject(unitGUID) + self.objects[unitGUID] = nil +end + -- Create a custom unit and cache it for .5 seconds ---@generic V :Bastion.Unit ---@param token string @@ -156,9 +226,19 @@ function UnitManager:CreateCustomUnit(token, cb) return cachedUnit 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() for k, object in pairs(self.objects) do - self.objects[k] = nil + if k ~= "none" and not Bastion.ObjectManager.objectList[k] then + self.objects[k] = nil + end end end @@ -383,4 +463,4 @@ function UnitManager:FindEnemiesCentroid(radius, range) return centroid end -Bastion.UnitManager = UnitManager:New() +Bastion.UnitManager = UnitManager diff --git a/src/Util/Util.lua b/src/Util/Util.lua index 71c6eb2..f6ba5f0 100644 --- a/src/Util/Util.lua +++ b/src/Util/Util.lua @@ -52,10 +52,6 @@ function Util:GetPrintMode() return PrintEnabled end -Bastion.Globals.Command:Register("toggle", "Toggle bastion on/off", function() - Util:TogglePrint() -end) - local DebugMode = false function Util:ToggleDebug() @@ -83,8 +79,4 @@ function Util:Debug(...) print(str) end -Bastion.Globals.Command:Register("debug", "Toggle debug mode on/off", function() - Util:ToggleDebug() -end) - Bastion.Util = Util diff --git a/src/Vector3/Vector3.lua b/src/Vector3/Vector3.lua index 98cf291..341cbcb 100644 --- a/src/Vector3/Vector3.lua +++ b/src/Vector3/Vector3.lua @@ -359,12 +359,12 @@ end ---@param o number function Vector3:NormalizeOrientation(o) if o < 0 then - mod = o * -1 + local mod = o * -1 mod = math.fmod(mod, 2.0 * math.pi) mod = -mod + 2.0 * math.pi return mod end - return math.fmod(o, 2.0 * math.pi) + return math.fmod(o, 2.00 * math.pi) end Bastion.Vector3 = Vector3