diff --git a/src/APL/APL.lua b/src/APL/APL.lua index aeae904..9e7bd29 100644 --- a/src/APL/APL.lua +++ b/src/APL/APL.lua @@ -7,8 +7,7 @@ Bastion = ... ---@class Bastion.APL ---@field apl Bastion.APLActor[] ---@field variables table ----@field name string ----@field last { successful: { name: string, time: number, index: number }, attempted: { name: string, time: number, index: number } } +---@field last { attempt: Bastion.APLActor, success: Bastion.APLActor, time: number, index: number } local APL = {} APL.__index = APL @@ -16,23 +15,19 @@ APL.__index = APL ---@param name string ---@return Bastion.APL function APL:New(name) - local self = setmetatable({}, APL) - - self.apl = {} - self.variables = {} - self.name = name - self.last = { - successful = { - name = "", + ---@class Bastion.APL + local self = setmetatable({ + active = false, + apl = {}, + variables = {}, + name = name, + last = { + success = {}, + attempt = {}, time = -1, index = -1, }, - attempted = { - name = "", - time = -1, - index = -1, - }, - } + }, APL) return self end @@ -54,7 +49,6 @@ end ---@class Bastion.APL.Actor.Variable.Table : Bastion.APLActor.Table.Base ---@field variable string ---@field cb fun(...):any ----@field _apl Bastion.APL -- Add variable ---@param name string @@ -65,7 +59,7 @@ function APL:AddVariable(name, cb) type = "variable", variable = name, cb = cb, - _apl = self, + parentAPL = self, }) table.insert(self.apl, actor) return actor @@ -74,16 +68,21 @@ end ---@class Bastion.APL.Actor.Action.Table : Bastion.APLActor.Table.Base ---@field action string ---@field cb fun(...):any +---@field args? any[] -- Add a manual action to the APL +---@generic A ---@param action string ----@param cb fun(...):any +---@param cb fun(...: A):any +---@param ... A ---@return Bastion.APLActor -function APL:AddAction(action, cb) +function APL:AddAction(action, cb, ...) local actor = Bastion.APLActor:New({ type = "action", action = action, cb = cb, + parentAPL = self, + args = SafePack(...), }) table.insert(self.apl, actor) return actor @@ -91,16 +90,18 @@ end ---@class Bastion.APL.Actor.Spell.Table : Bastion.APLActor.Table.Base ---@field spell Bastion.Spell ----@field condition? string|fun(self: Bastion.Spell): boolean +---@field condition? string|fun(self: Bastion.Spell, target: Bastion.Unit|false): boolean ---@field castableFunc false | fun(self: Bastion.Spell): boolean ---@field target Bastion.Unit | false ---@field onCastFunc false | fun(self: Bastion.Spell):any +---@field targetIf fun(self: Bastion.Spell): (Bastion.Unit) | false -- Add a spell to the APL ---@param spell Bastion.Spell ----@param condition? string|fun(self: Bastion.Spell):boolean +---@param condition? string|fun(self: Bastion.Spell, target: Bastion.Unit|false):boolean +---@param targetIf? fun(self: Bastion.Spell): Bastion.Unit ---@return Bastion.APLActor -function APL:AddSpell(spell, condition) +function APL:AddSpell(spell, condition, targetIf) local castableFunc = spell.CastableIfFunc local onCastFunc = spell.OnCastFunc local target = spell:GetTarget() @@ -112,6 +113,15 @@ function APL:AddSpell(spell, condition) castableFunc = castableFunc, target = target, onCastFunc = onCastFunc, + 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) + end }) table.insert(self.apl, actor) @@ -139,6 +149,7 @@ function APL:AddItem(item, condition) condition = condition, usableFunc = usableFunc, target = target, + parentAPL = self, }) table.insert(self.apl, actor) @@ -159,6 +170,7 @@ function APL:AddAPL(apl, condition) type = "apl", apl = apl, condition = condition, + parentAPL = self, }) table.insert(self.apl, actor) return actor @@ -175,27 +187,42 @@ function APL:ToggleAPL(name, enabled) end end -function APL:UpdateLastAttempted(name, index) - self.last.attempted.name = name - self.last.attempted.time = GetTime() - self.last.attempted.index = index +---@param actor Bastion.APLActor +function APL:UpdateLastAttempted(actor) + self.active = true + self.last.time = GetTime() + self.last.attempt = actor + self.last.index = actor.index +end + +function APL:UpdateLastSuccessful() + self.last.success = self.last.attempt +end + +function APL:GetCurrentActor() + if self.active then + return self.last.attempt + else + return false + end end -function APL:UpdateLastSuccessful(name, index) - self.last.successful.name = name - self.last.successful.time = GetTime() - self.last.successful.index = index +---@param active boolean +function APL:SetActive(active) + self.active = active end -- Execute the APL function APL:Execute() for i, actor in ipairs(self.apl) do - self:UpdateLastAttempted(actor.name, i) + self:UpdateLastAttempted(actor) if actor.enabled and (not actor:HasTraits() or actor:Evaluate()) and actor:Execute() then - self:UpdateLastSuccessful(actor.name, i) + self:UpdateLastSuccessful() return true end end + self.active = false + return false end ---@class Bastion.APL.Actor.Sequencer.Table : Bastion.APLActor.Table.Base @@ -211,6 +238,7 @@ function APL:AddSequence(sequencer, condition) type = "sequencer", sequencer = sequencer, condition = condition, + parentAPL = self, }) table.insert(self.apl, actor) return actor @@ -219,7 +247,7 @@ end -- tostring ---@return string function APL:__tostring() - return "Bastion.__APL(" .. self.name .. ")" + return string.format("APL(%s)", self.name) end Bastion.APL = APL diff --git a/src/APLActor/APLActor.lua b/src/APLActor/APLActor.lua index 79f9cbe..382ce8b 100644 --- a/src/APLActor/APLActor.lua +++ b/src/APLActor/APLActor.lua @@ -6,6 +6,9 @@ Bastion = ... ---@class Bastion.APLActor.Table.Base ---@field type "spell" | "item" | "apl" | "sequencer" | "variable" | "action" ---@field name string +---@field parentAPL Bastion.APL +---@field index number +---@field executor fun(self: Bastion.APLActor):boolean ---@alias Bastion.APLActor.Table Bastion.APL.Actor.Spell.Table | Bastion.APL.Actor.Item.Table | Bastion.APL.Actor.APL.Table | Bastion.APL.Actor.Sequencer.Table | Bastion.APL.Actor.Variable.Table | Bastion.APL.Actor.Action.Table @@ -21,25 +24,28 @@ APLActor.__index = APLActor -- Constructor ---@param actor Bastion.APLActor.Table function APLActor:New(actor) - local self = setmetatable({}, APLActor) + ---@class Bastion.APLActor + local self = setmetatable({ + index = #actor.parentAPL.apl + 1, + }, APLActor) 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`<%s>", "spell", name, id) + self.name = string.format("%s[%s]", name, id) 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`<%s>", "item", name, id) + self.name = string.format("%s[%s]", name, id) elseif actor.type == "apl" then - self.name = string.format("[%s] `%s`", "apl", actor.apl.name or "Unknown") + self.name = string.format("%s", actor.apl.name or "Unknown") elseif actor.type == "sequencer" then - self.name = string.format("[%s]", "sequencer") + self.name = string.format("") elseif actor.type == "variable" then - self.name = string.format("[%s] `%s`", "variable", actor.variable or "Unknown") + self.name = string.format("%s", actor.variable or "Unknown") elseif actor.type == "action" then - self.name = string.format("[%s] `%s`", "action", actor.action or "Unknown") + self.name = string.format("%s", actor.action or "Unknown") else - self.name = string.format("[%s] Unknown", actor.type or "Unknown") + self.name = string.format("<%s>", actor.type or "UNKNOWN") end self.actor = actor self.enabled = true @@ -61,7 +67,7 @@ end -- Get the actor ---@return Bastion.APLActor.Table -function APLActor:GetActor() +function APLActor:GetActorTable() return self.actor end @@ -69,7 +75,7 @@ end ---@return boolean function APLActor:Evaluate() for _, trait in ipairs(self.traits) do - if not trait:Evaluate(self:GetActor()) then + if not trait:Evaluate(self:GetActorTable()) then return false end end @@ -77,9 +83,13 @@ function APLActor:Evaluate() return true end +function APLActor:ExecuteActor() + return self:GetActorTable().executor(self) +end + -- Execute function APLActor:Execute() - local actorTable = self:GetActor() + local actorTable = self:GetActorTable() -- If the actor is a sequencer we don't want to continue executing the APL if the sequencer is not finished if actorTable.type == "sequencer" then ---@cast actorTable Bastion.APL.Actor.Sequencer.Table @@ -92,6 +102,7 @@ function APLActor:Execute() if actorTable.sequencer:ShouldReset() then actorTable.sequencer:Reset() end + return false end if actorTable.type == "apl" then ---@cast actorTable Bastion.APL.Actor.APL.Table @@ -101,23 +112,21 @@ function APLActor:Execute() end end if actorTable.type == "spell" then - ---@cast actorTable Bastion.APL.Actor.Spell.Table - return actorTable.spell:CastableIf(actorTable.castableFunc):OnCast(actorTable.onCastFunc):Cast(actorTable.target, - actorTable.condition) + return self:ExecuteActor() end if actorTable.type == "item" then ---@cast actorTable Bastion.APL.Actor.Item.Table - return actorTable.item:UsableIf(actorTable.usableFunc):Use(actorTable.target, actorTable.condition) + return ((not actorTable.target or actorTable.target:Exists()) and actorTable.item:UsableIf(actorTable.usableFunc):Use(actorTable.target, actorTable.condition)) end - if self:GetActor().type == "action" then + if self:GetActorTable().type == "action" then ---@cast actorTable Bastion.APL.Actor.Action.Table -- print("Bastion: APL:Execute: Executing action " .. actorTable.action) - self:GetActor().cb() + self:GetActorTable().cb(SafeUnpack(actorTable.args)) end if actorTable.type == "variable" then ---@cast actorTable Bastion.APL.Actor.Variable.Table -- print("Bastion: APL:Execute: Setting variable " .. actorTable.variable) - actorTable._apl.variables[actorTable.variable] = actorTable.cb(actorTable._apl) + actorTable.parentAPL.variables[actorTable.variable] = actorTable.cb(actorTable.parentAPL) end return false end @@ -131,7 +140,7 @@ end -- tostring ---@return string function APLActor:__tostring() - return string.format("Bastion.__APLActor(%s)", self.name) + return string.format("APLActor(%s)", self.name) end Bastion.APLActor = APLActor diff --git a/src/APLActor/APLActor2.lua b/src/APLActor/APLActor2.lua new file mode 100644 index 0000000..8491b4d --- /dev/null +++ b/src/APLActor/APLActor2.lua @@ -0,0 +1,56 @@ +---@type Tinkr +local Tinkr, +---@class Bastion +Bastion = ... + +---@class Bastion.APLActor2.Table.Base +---@field parentAPL Bastion.APL +---@field index number + +-- Create an APL actor for the APL class +---@class Bastion.APLActor2 +---@field traits Bastion.APLTrait[] +---@field table Bastion.APLActor2.Table.Base +---@field executor fun(self: Bastion.APLActor2): boolean +local APLActor = {} +APLActor.__index = APLActor + +---@param type string +function APLActor:New(type) + ---@class Bastion.APLActor2 + local self = setmetatable({ + type = type, + enabled = true, + traits = {}, + table = {}, + executor = function() return false end + }, APLActor) + return self +end + +---@param ... any +function APLActor:GetName(...) + local paramStrings = type(...) ~= "nil" and table.concat({ ... }, "-") or "" + return string.format("[%s]%s", self.type, paramStrings) +end + +-- Add a trait to the APL actor +---@param ... Bastion.APLTrait +---@return Bastion.APLActor2 +function APLActor:AddTraits(...) + for _, trait in ipairs({ ... }) do + table.insert(self.traits, trait) + end + + return self +end + +-- Get the actor +---@return Bastion.APLActor2.Table.Base +function APLActor:GetActorTable() + return self.table +end + +function APLActor:SetExecutor(executor) + self.executor = executor +end diff --git a/src/APLSpellActor/APLSpellActor.lua b/src/APLSpellActor/APLSpellActor.lua new file mode 100644 index 0000000..c3a63a0 --- /dev/null +++ b/src/APLSpellActor/APLSpellActor.lua @@ -0,0 +1,6 @@ +---@type Tinkr +local Tinkr, +---@class Bastion +Bastion = ... + +---@class Bastion.APL.SpellActor diff --git a/src/Bastion/Bastion.lua b/src/Bastion/Bastion.lua index 8ca8311..35d2913 100644 --- a/src/Bastion/Bastion.lua +++ b/src/Bastion/Bastion.lua @@ -5,8 +5,9 @@ local Tinkr = ... ---@class Bastion local Bastion = { + CombatEvents = {}, Enabled = false, - Interval = 0.01, + Interval = 0.15, Globals = { ---@type Bastion.Globals.SpellName SpellName = {} @@ -52,6 +53,8 @@ local bastionFiles = { "~/src/Module/Module", "~/src/UnitManager/UnitManager", "~/src/ObjectManager/ObjectManager", + "~/src/Missile/Missile", + "~/src/MissileManager/MissileManager", "~/src/Spell/Spell", "~/src/SpellBook/SpellBook", "~/src/Item/Item", @@ -184,16 +187,66 @@ function Bastion:Toggle(toggle) Bastion.Enabled = type(toggle) ~= "nil" and toggle or not Bastion.Enabled end +local combatEventSuffix = { + ["_DAMAGE"] = true, + ["_MISSED"] = true, + ["_ABSORBED"] = true, + ["_DRAIN"] = true, + ["_LEECH"] = true, + ["_INTERRUPT"] = true, + ["_DISPEL"] = true, + ["_DISPEL_FAILED"] = true, + ["_STOLEN"] = true, + ["_EXTRA_ATTACKS"] = true, + ["_AURA_APPLIED"] = true, + ["_AURA_REMOVED"] = true, + ["_AURA_APPLIED_DOSE"] = true, + ["_AURA_REMOVED_DOSE"] = true, + ["_AURA_REFRESH"] = true, + ["_AURA_BROKEN"] = true, + ["_AURA_BROKEN_SPELL"] = true, + ["_CAST_START"] = true, + ["_CAST_SUCCESS"] = true, + ["_CAST_FAILED"] = true, + ["_INSTAKILL"] = true, + ["_DURABILITY_DAMAGE"] = true, + ["_DURABILITY_DAMAGE_ALL"] = true, + ["_CREATE"] = true, + ["_SUMMON"] = true, + ["_RESURRECT"] = true, + ["_EMPOWER_START"] = true, + ["_EMPOWER_END"] = true, + ["_EMPOWER_INTERRUPT"] = true, +} + +local combatEventPrefix = { + "SWING", + "RANGE", + "SPELL", + "SPELL_PERIODIC", + "SPELL_BUILDING", + "ENVIRONMENTAL" +} + local loaded = false function Bastion:Load() if loaded then return self end + for suffix, _ in pairs(combatEventSuffix) do + for _, prefix in pairs(combatEventPrefix) do + Bastion.CombatEvents[prefix .. suffix] = true + end + end for i = 1, #bastionFiles do self:Require(bastionFiles[i]) end + self.Globals.EventManager:RegisterWoWEvent("PLAYER_ENTERING_WORLD", function() + self.UnitManager:ResetObjects() + end) + self.Globals.Command:Register('toggle', 'Toggle bastion on/off', function() self:Toggle() if self.Enabled then @@ -215,10 +268,9 @@ function Bastion:Load() ---@param unit UnitToken ---@param auras UnitAuraUpdateInfo self.Globals.EventManager:RegisterWoWEvent("UNIT_AURA", function(unit, auras) - ---@type Bastion.Unit | nil local u = self.UnitManager:Get(unit) - if u then + if u:Exists() then u:GetAuras():OnUpdate(auras) end end) @@ -240,36 +292,69 @@ function Bastion:Load() local playerGuid = UnitGUID("player") local missed = {} - self.Globals.EventManager:RegisterWoWEvent("COMBAT_LOG_EVENT_UNFILTERED", function() + 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() } + ---@type string local subEvent = args[2] + ---@type string local sourceGUID = args[4] + ---@type string local destGUID = args[8] + ---@type number local spellID = args[12] + ---@type string local spellName = args[13] if subEvent == "SPELL_CAST_SUCCESS" then - if (not self.Globals.SpellName[spellID] or self.Globals.SpellName[spellID] ~= spellName) then - self.Globals.SpellName[spellID] = spellName + if not Bastion.Globals.SpellName[spellID] then + Bastion.Globals.SpellName[spellID] = spellName end end - local u = self.UnitManager[sourceGUID] - local u2 = self.UnitManager[destGUID] - local t = GetTime() + local sourceUnit = Bastion.UnitManager:GetObject(sourceGUID) + + local destUnit = Bastion.UnitManager:GetObject(destGUID) + + --local t = GetTime() - if u then - u:SetLastCombatTime(t) + if sourceUnit and sourceUnit:IsAffectingCombat() then + sourceUnit:SetLastCombatTime() end - if u2 then - u2:SetLastCombatTime(t) + 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 + 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() + end + + --[[ if destUnit then if subEvent == "SPELL_MISSED" and sourceGUID == playerGuid and spellID == 408 then local missType = args[15] if missType == "IMMUNE" then - local castingSpell = u:GetCastingOrChannelingSpell() + local castingSpell = sourceUnit:GetCastingOrChannelingSpell() if castingSpell and type(castingSpell) == "table" then if not missed[castingSpell:GetID()] then @@ -278,16 +363,19 @@ function Bastion:Load() end end end - end + end ]] end) self.Ticker = C_Timer.NewTicker(self.Interval, function() self.Globals.CombatTimer:Check() - if Bastion.Enabled then + if self.Enabled then + self.MissileManager:Refresh() self.ObjectManager:Refresh() self.TimeToDie:Refresh() self:TickModules() + else + self.UnitManager:ResetObjects() end end) diff --git a/src/Cache/Cache.lua b/src/Cache/Cache.lua index 8f3608a..7bb61ab 100644 --- a/src/Cache/Cache.lua +++ b/src/Cache/Cache.lua @@ -42,7 +42,7 @@ function Cache:Get(key) return nil end ----@param key any +---@param key string|number|table ---@return boolean function Cache:IsCached(key) self.cache = self.cache or {} diff --git a/src/Missile/Missile.lua b/src/Missile/Missile.lua new file mode 100644 index 0000000..5f75ee2 --- /dev/null +++ b/src/Missile/Missile.lua @@ -0,0 +1,61 @@ +---@type Tinkr +local Tinkr, +---@class Bastion +Bastion = ... + +---@class Bastion.Missile : Tinkr.Missile +local Missile = { +} + +function Missile:__index(k) + if k == "_missile" then + return rawget(self, k) + end + + local response = rawget(self._missile, k) + + return response ~= nil and response or rawget(self, k) +end + +---@param missile Tinkr.Missile +function Missile:New(missile) + ---@class Bastion.Missile + local self = setmetatable({ + _missile = missile + }, Missile) + return self +end + +function Missile:GetSourceUnit() + return Bastion.UnitManager:Get(self.source) +end + +function Missile:GetTargetUnit() + return Bastion.UnitManager:Get(self.target) +end + +function Missile:GetCurrentVector() + return Bastion.Vector3:New(self.cx, self.cy, self.cz) +end + +function Missile:GetHitVector() + return Bastion.Vector3:New(self.hx, self.hy, self.hz) +end + +function Missile:GetInitialVector() + return Bastion.Vector3:New(self.ix, self.iy, self.iz) +end + +function Missile:GetModelVector() + return Bastion.Vector3:New(self.mx, self.my, self.mz) +end + +function Missile:GetPVector() + return Bastion.Vector3:New(self.px, self.py, self.pz) +end + +function Missile:GetUVector() + return Bastion.Vector3:New(self.ux, self.uy, self.uz) +end + +Bastion.Missile = Missile diff --git a/src/MissileManager/MissileManager.lua b/src/MissileManager/MissileManager.lua new file mode 100644 index 0000000..b6c1f8e --- /dev/null +++ b/src/MissileManager/MissileManager.lua @@ -0,0 +1,137 @@ +---@type Tinkr +local Tinkr, +---@class Bastion +Bastion = ... + +---@class Bastion.MissileManager.TrackingParams +---@field source? Bastion.Unit +---@field target? Bastion.Unit +---@field spellId? number +---@field spellVisualId? number +---@field callback fun(self: Bastion.Missile) + +---@class Bastion.MissileManager +---@field _lists table +---@field trackingParams Bastion.MissileManager.TrackingParams[] +---@field missiles Tinkr.Missile[] +local MissileManager = {} +MissileManager.__index = MissileManager + +function MissileManager:New() + ---@class Bastion.MissileManager + local self = setmetatable({}, MissileManager) + self.missiles = {} + self._lists = {} + self.trackedMissiles = Bastion.List:New() + self.allMissiles = Bastion.List:New() + self.trackingParams = {} + return self +end + +-- Register a custom list with a callback +---@param name string +---@param cb fun(missile: Tinkr.Missile): boolean | any +---@return Bastion.List | false +function MissileManager:RegisterList(name, cb) + if self._lists[name] then + return false + end + + self._lists[name] = { + list = Bastion.List:New(), + cb = cb, + } + + return self._lists[name].list +end + +-- reset custom lists +---@return nil +function MissileManager:ResetLists() + for _, list in pairs(self._lists) do + list.list:clear() + end +end + +function MissileManager:Reset() + self.missiles = {} + + self.trackedMissiles:clear() + self.allMissiles:clear() + self:ResetLists() +end + +-- Refresh custom lists +---@param missile Tinkr.Missile +---@return nil +function MissileManager:EnumLists(missile) + for _, list in pairs(self._lists) do + local r = list.cb(missile) + if r then + list.list:push(r) + end + end +end + +---@param params Bastion.MissileManager.TrackingParams +function MissileManager:TrackMissile(params) + table.insert(self.trackingParams, params) +end + +---@param missileObj Bastion.Missile +function MissileManager:EnumTrackingParams(missileObj) + local tracked = false + for i, trackingParam in ipairs(self.trackingParams) do + if (not trackingParam.source or missileObj:GetSourceUnit():IsUnit(trackingParam.source)) and + (not trackingParam.target or missileObj:GetTargetUnit():IsUnit(trackingParam.target)) and + (not trackingParam.spellId or missileObj._missile.spellId == trackingParam.spellId) and + (not trackingParam.spellVisualId or missileObj._missile.spellVisualId == trackingParam.spellVisualId) + then + tracked = true + trackingParam.callback(missileObj) + end + end + return tracked +end + +-- Get a list +---@param name string +---@return Bastion.List +function MissileManager:GetList(name) + return self._lists[name].list +end + +function MissileManager:Refresh() + self:Reset() + + local missiles = Missiles() + if type(missiles) == "table" then + for _, missile in ipairs(missiles) do + table.insert(self.missiles, missile) + self:EnumLists(missile) + local missileObj = Bastion.Missile:New(missile) + self.allMissiles:push(missileObj) + if self:EnumTrackingParams(missileObj) then + self.trackedMissiles:push(missileObj) + end + end + end +end + +---@param params { source?: Bastion.Unit, target?: Bastion.Unit, spellId?: number, spellVisualId?: number } +function MissileManager:GetMissiles(params) + ---@type Bastion.Missile[] + local missiles = {} + for _, missile in ipairs(self.trackedMissiles) do + if (not params.source or missile:GetSourceUnit():IsUnit(params.source)) and + (not params.target or missile:GetTargetUnit():IsUnit(params.target)) and + (not params.spellId or missile._missile.spellId == params.spellId) and + (not params.spellVisualId or missile._missile.spellVisualId == params.spellVisualId) + then + table.insert(missiles, missile) + end + end + return missiles +end + +Bastion.MissileManager = MissileManager:New() diff --git a/src/ObjectManager/ObjectManager.lua b/src/ObjectManager/ObjectManager.lua index 31f516f..414b577 100644 --- a/src/ObjectManager/ObjectManager.lua +++ b/src/ObjectManager/ObjectManager.lua @@ -22,8 +22,10 @@ function ObjectManager:New() self.explosives = Bastion.List:New() self.friends = Bastion.List:New() self.incorporeal = Bastion.List:New() + self.missiles = Bastion.List:New() self.others = Bastion.List:New() + return self end @@ -62,6 +64,7 @@ function ObjectManager:Reset() self.explosives:clear() self.friends:clear() self.incorporeal:clear() + self.missiles:clear() self.others:clear() self:ResetLists() end @@ -100,8 +103,7 @@ function ObjectManager:Refresh() if objectGUID then local unit = Bastion.UnitManager:GetObject(objectGUID) if not unit then - unit = Bastion.Unit:New(object) - Bastion.UnitManager:SetObject(unit) + unit = Bastion.UnitManager:SetObject(Bastion.Unit:New(object)) end if objectType == 5 and ObjectCreatureType(object) == 8 then @@ -116,7 +118,7 @@ function ObjectManager:Refresh() self.friends:push(unit) elseif unit:IsEnemy() then self.enemies:push(unit) - if unit:InCombatOdds() > 80 or unit:IsAffectingCombat() then + if unit:IsAffectingCombat() or unit:InCombatOdds() > 80 then self.activeEnemies:push(unit) end else @@ -125,6 +127,13 @@ function ObjectManager:Refresh() end end end + + local missiles = Missiles() + if type(missiles) == "table" then + for _, missile in pairs(missiles) do + self.missiles:push(missile) + end + end end Bastion.ObjectManager = ObjectManager:New() diff --git a/src/Spell/Spell.lua b/src/Spell/Spell.lua index 4d33370..5d37278 100644 --- a/src/Spell/Spell.lua +++ b/src/Spell/Spell.lua @@ -63,6 +63,7 @@ Bastion = ... ---@field target Bastion.Unit | false ---@field traits Bastion.Spell.Traits ---@field wasLooking boolean +---@field tickDuration false | number | fun(self: Bastion.Spell): number local Spell = { CastableIfFunc = false, damage = 0, @@ -132,6 +133,7 @@ function Spell:New(id) } self.target = false self.wasLooking = false + self.tickDuration = false return self end @@ -231,7 +233,7 @@ function Spell:AddOverrideSpell(spell, func) end -- Cast the spell ----@param unit Bastion.Unit +---@param unit? false | Bastion.Unit ---@param condition? string | fun(self:Bastion.Spell):boolean ---@return boolean function Spell:Cast(unit, condition) @@ -258,7 +260,7 @@ function Spell:Cast(unit, condition) -- if unit:GetOMToken() contains 'nameplate' then we need to use Object wrapper to cast - local u = unit and unit:GetOMToken() or self.traits.target.player and "none" or "none" + local 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) @@ -329,11 +331,16 @@ function Spell:IsKnown(includeOverrides) return isKnown end +function Spell:GetSpellLossOfControlCooldown() + local start, duration = 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 + return select(2, GetSpellCooldown(spellIDName)) > 0 or select(2, self:GetSpellLossOfControlCooldown()) > 0 end ---@param byId? boolean @@ -417,14 +424,8 @@ end -- Check if the spell is castable function Spell:Castable() - if not self:EvaluateTraits() then - return false - end - if self:GetCastableFunction() and not self:GetCastableFunction()(self) then - return false - end - - return self:IsKnownAndUsable(type(self.traits.cast.override) ~= nil and self.traits.cast.override or nil) + 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) end -- Set a script to check if the spell is castable @@ -800,4 +801,32 @@ function Spell:UpdateAura(spell, source, target) self.auras[spell:GetID()].lastApplied = GetTime() end +function Spell:GetTickDuration() + if type(self.tickDuration) == "function" then + return self.tickDuration(self) + else + return self.tickDuration + end +end + +---@param duration (number) | fun(self: Bastion.Spell): (number) +function Spell:TickDuration(duration) + self.tickDuration = duration +end + +---@param params { source: Bastion.Unit, target: Bastion.Unit, spellVisualId?: number } +function Spell:GetMissiles(params) + return Bastion.MissileManager:GetMissiles({ + source = params.source, + target = params.target, + spellId = self:GetID(), + spellVisualId = params.spellVisualId, + }) +end + +---@param params { source: Bastion.Unit, target: Bastion.Unit, spellVisualId?: number} +function Spell:InFlight(params) + return #self:GetMissiles(params) > 0 +end + Bastion.Spell = Spell diff --git a/src/TimeToDie/TimeToDie.lua b/src/TimeToDie/TimeToDie.lua index 3859da6..64de8fd 100644 --- a/src/TimeToDie/TimeToDie.lua +++ b/src/TimeToDie/TimeToDie.lua @@ -15,7 +15,7 @@ local Cache = Bastion.Globals.UnitInfo local TimeToDie = { Settings = { -- Refresh time (seconds) : min=0.1, max=2, default = 0.1 - Refresh = 0.5, + Refresh = 0.1, -- History time (seconds) : min=5, max=120, default = 10+0.4 HistoryTime = 10 + 0.4, -- Max history count : min=20, max=500, default = 100 @@ -84,11 +84,21 @@ function TimeToDie:Refresh() -- 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 - unitTable = { - history = {}, - time = currentTime - } - units[unitGUID] = unitTable + 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 end local history = unitTable.history local time = currentTime - unitTable.time diff --git a/src/Unit/Unit.lua b/src/Unit/Unit.lua index 7fcc0e9..d6a6f8e 100644 --- a/src/Unit/Unit.lua +++ b/src/Unit/Unit.lua @@ -92,7 +92,7 @@ end -- Check if the unit is valid ---@return boolean function Unit:IsValid() - return self:GetOMToken() ~= nil and self:Exists() + return (self:GetOMToken() ~= "none" and self:GetOMToken() ~= nil) and self:Exists() end -- Check if the unit exists @@ -291,7 +291,7 @@ function Unit:GetOMToken() if not self.unit then return "none" end - return self.unit:unit() + return self.unit:unit() or "none" end -- Is the unit a target @@ -1339,8 +1339,8 @@ end ---@return Bastion.Unit function Unit:Target() - return self:HasTarget() and Bastion.UnitManager:Get(ObjectTarget(self:GetOMToken()):unit()) or - Bastion.UnitManager:Get("none") + local objTarget = ObjectTarget(self:GetOMToken()) + return objTarget and Bastion.UnitManager:Get(objTarget:unit() or "none") or Bastion.UnitManager:Get("none") end local dummyUnits = { @@ -1581,7 +1581,7 @@ function Unit:TimeToDie2(minSamples) v = self:TimeToX(self:SpecialTTDPercentage(id), minSamples) if v >= 0 then ttd[minSamples] = v - Bastion.Globals.UnitInfo:Set(unitGuid, unitInfo, .5) + Bastion.Globals.UnitInfo:Set(unitGuid, unitInfo, .1) end end @@ -1646,4 +1646,144 @@ function Unit:BossTimeToDieIsNotValid(minSamples) return true end +---@param spell Bastion.Spell +---@param source? Bastion.Unit +function Unit:GetAuraTickRate(spell, source) + 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()) + if tooltipInfo and tooltipInfo.lines then + for _, line in ipairs(tooltipInfo.lines) do + if line.leftText then + local rate = line.leftText:gmatch("every (%d*%.?%d+) sec") + for tickRate in rate do + return tonumber(tickRate) or 0 + end + end + end + end + end + return false +end + +function Unit:Effects() + return UnitEffects(self:GetOMToken()) +end + +function Unit:IsSitting() + return UnitIsSitting(self:GetOMToken()) +end + +function Unit:GetAnimKit() + return GetUnitAnimKit(self:GetOMToken()) +end + +function Unit:IsEating() + local effects = self:Effects() + if not effects or #effects == 0 then + return false + end + for _, effect in ipairs(effects) do + if effect.spellId and effect.spellId == 396921 then -- https://www.wowhead.com/spell=396921 + return true + end + end + return false +end + +function Unit:IsDrinking() + local effects = self:Effects() + if not effects or #effects == 0 then + return false + end + for _, effect in ipairs(effects) do + if effect.spellId and effect.spellId == 396921 then -- https://www.wowhead.com/spell=396921 + return true + end + end + return false +end + +function Unit:IsEatingOrDrinking() + return self:IsEating() or self:IsDrinking() +end + +function Unit:IsSummoning() + local effects = self:Effects() + + if not effects or #effects == 0 then + return false + end + for _, effect in ipairs(effects) do + if effect.spellId and effect.spellId == 59782 then + return true + end + end + return false +end + +function Unit:GetLosssOfControlCount() + return C_LossOfControl.GetActiveLossOfControlDataCountByUnit(self:GetOMToken()) +end + +---@param index number +function Unit:LosssOfControlData(index) + return C_LossOfControl.GetActiveLossOfControlDataByUnit(self:GetOMToken(), index) +end + +function Unit:Available() + return self:Exists() + and self:IsAlive() + and self:GetLosssOfControlCount() == 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())) +end + +---@param params { target?: Bastion.Unit, spellId?: number, spellVisualId?: number } +function Unit:GetOutgoingMissles(params) + local missiles = Missiles() + ---@type Tinkr.Missile[] + local results = {} + if type(missiles) == "table" and self:IsValid() then + for i, missile in ipairs(missiles) do + local missileSource = missile.source:unit() + if missileSource then + if UnitIsUnit(self:GetOMToken(), missileSource) + and (not params.target or UnitIsUnit(missile.target:unit() or "none", params.target:GetOMToken())) + and (not params.spellId or params.spellId == missile.spellId) + and (not params.spellVisualId or params.spellVisualId == missile.spellVisualId) + then + table.insert(results, missile) + end + end + end + end + return results +end + +---@param params { source?: Bastion.Unit, spellId?: number, spellVisualId?: number } +function Unit:GetIncomingMissiles(params) + local missiles = Missiles() + ---@type Tinkr.Missile[] + local results = {} + if type(missiles) == "table" and self:IsValid() then + for i, missile in ipairs(missiles) do + local missileTarget = missile.target:unit() + if missileTarget then + if UnitIsUnit(self:GetOMToken(), missileTarget) + and (not params.source or UnitIsUnit(params.source:unit() or "none", self:GetOMToken())) + and (not params.spellId or params.spellId == missile.spellId) + and (not params.spellVisualId or params.spellVisualId == missile.spellVisualId) + then + table.insert(results, missile) + end + end + end + end + return results +end + Bastion.Unit = Unit diff --git a/src/UnitManager/UnitManager.lua b/src/UnitManager/UnitManager.lua index b1a1ce2..b2e7faa 100644 --- a/src/UnitManager/UnitManager.lua +++ b/src/UnitManager/UnitManager.lua @@ -16,10 +16,6 @@ local Unit = Bastion.Unit ---@field objects table ---@field cache Bastion.Cache local UnitManager = { - units = {}, - customUnits = {}, - objects = {}, - cache = {}, } ---@param k UnitId @@ -49,19 +45,11 @@ function UnitManager:__index(k) return self.objects[kguid] end - -- if not Validate(k) then - -- error("UnitManager:Get - Invalid token: " .. k) - -- end - if self.objects[kguid] == nil then local o = Object(k) if o then local unit = Unit:New(o) self:SetObject(unit) - - if self.objects[kguid] then - return self.objects[kguid] - end end end @@ -75,12 +63,13 @@ function UnitManager:New() local self = setmetatable({}, UnitManager) self.units = {} self.customUnits = {} + self.objects = {} self.cache = Bastion.Cache:New() return self end -- Get or create a unit ----@param token UnitId +---@param token UnitId | TinkrObjectReference function UnitManager:Get(token) -- if not Validate(token) then -- error("UnitManager:Get - Invalid token: " .. token) @@ -119,10 +108,31 @@ function UnitManager:GetObject(guid) return self.objects[guid] end +-- Get a unit by guid or create a new one if it does not exist. +---@param unit string | WowGameObject +---@return Bastion.Unit? +function UnitManager:GetOrCreateObject(unit) + local guid = type(unit) == "string" and unit or ObjectGUID(unit) + if guid then + local unitObj = self:GetObject(guid) + 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)) + end + end + return unitObj + end +end + -- Set a unit by guid ---@param unit Bastion.Unit function UnitManager:SetObject(unit) - self.objects[unit:GetGUID()] = unit + local guid = unit:GetGUID() + if guid then + self.objects[guid] = unit + return self.objects[guid] + end end -- Create a custom unit and cache it for .5 seconds @@ -146,6 +156,12 @@ function UnitManager:CreateCustomUnit(token, cb) return cachedUnit end +function UnitManager:ResetObjects() + for k, object in pairs(self.objects) do + self.objects[k] = nil + end +end + ---@description Enumerates all friendly units in the battlefield ---@param cb fun(unit: Bastion.Unit):boolean ---@return nil