From 4d0719b86911434833b262f42c73e2a329ad1b8f Mon Sep 17 00:00:00 2001 From: ck Date: Sun, 31 Dec 2023 10:24:44 -0600 Subject: [PATCH] HeroLib TTD? --- src/APL/APL.lua | 16 +- src/Aura/Aura.lua | 2 +- src/Cacheable/Cacheable.lua | 4 +- src/Class/Class.lua | 2 +- src/Config/Config.lua | 82 +++++ src/Item/Item.lua | 2 +- src/ItemBook/ItemBook.lua | 2 +- src/Library/Library.lua | 2 +- src/MythicPlusUtils/MythicPlusUtils.lua | 2 +- src/ObjectManager/ObjectManager.lua | 9 +- src/Refreshable/Refreshable.lua | 2 +- src/Spell/Spell.lua | 50 +-- src/SpellBook/SpellBook.lua | 2 + src/TimeToDie/TimeToDie.lua | 282 +++++++++++++++++ src/Timer/Timer.lua | 2 +- src/Unit/Unit.lua | 399 +++++++++++++++++++++--- src/UnitManager/UnitManager.lua | 9 +- src/Vector3/Vector3.lua | 9 +- src/_bastion.lua | 39 ++- 19 files changed, 822 insertions(+), 95 deletions(-) create mode 100644 src/Config/Config.lua create mode 100644 src/TimeToDie/TimeToDie.lua diff --git a/src/APL/APL.lua b/src/APL/APL.lua index 001b580..5aa83f0 100644 --- a/src/APL/APL.lua +++ b/src/APL/APL.lua @@ -54,17 +54,17 @@ APLActor.__index = APLActor function APLActor:New(actor) local self = setmetatable({}, APLActor) if actor.type == "spell" then - self.name = string.format("[%s] `%s`<%s>", actor.type, actor.spell:GetName(), actor.spell:GetID()) + self.name = string.format("[%s] `%s`<%s>", "spell", actor.spell:GetName(), actor.spell:GetID()) elseif actor.type == "item" then - self.name = string.format("[%s] `%s`<%s>", actor.type, actor.item:GetName(), actor.item:GetID()) + self.name = string.format("[%s] `%s`<%s>", "item", actor.item:GetName(), actor.item:GetID()) elseif actor.type == "apl" then - self.name = string.format("[%s] `%s`", actor.type, actor.apl.name) + self.name = string.format("[%s] `%s`", "apl", actor.apl.name) elseif actor.type == "sequencer" then - self.name = string.format("[%s]", actor.type) + self.name = string.format("[%s]", "sequencer") elseif actor.type == "variable" then - self.name = string.format("[%s] `%s`", actor.type, actor.variable) + self.name = string.format("[%s] `%s`", "variable", actor.variable) elseif actor.type == "action" then - self.name = string.format("[%s] `%s`", actor.type, actor.action) + self.name = string.format("[%s] `%s`", "action", actor.action) else self.name = string.format("[%s] Unknown", actor.type) end @@ -141,10 +141,10 @@ function APLActor:Execute() ---@cast actorTable APLActorItemTable return actorTable.item:UsableIf(actorTable.usableFunc):Use(actorTable.target, actorTable.condition) end - if actorTable.type == "action" then + if self:GetActor().type == "action" then ---@cast actorTable APLActorActionTable -- print("Bastion: APL:Execute: Executing action " .. actorTable.action) - actorTable.cb(self) + self:GetActor().cb() end if actorTable.type == "variable" then ---@cast actorTable APLActorVariableTable diff --git a/src/Aura/Aura.lua b/src/Aura/Aura.lua index 16b502d..2a71e3f 100644 --- a/src/Aura/Aura.lua +++ b/src/Aura/Aura.lua @@ -1,5 +1,5 @@ -- Document with emmy lua: https://emmylua.github.io/ ----@types Tinkr, Bastion +---@type Tinkr, Bastion local Tinkr, Bastion = ... -- Create a new Aura class diff --git a/src/Cacheable/Cacheable.lua b/src/Cacheable/Cacheable.lua index eae502b..1694e9f 100644 --- a/src/Cacheable/Cacheable.lua +++ b/src/Cacheable/Cacheable.lua @@ -4,7 +4,7 @@ local Tinkr, Bastion = ... -- Define a Cacheable class ---@class Cacheable: { value: V, cache: Cache, callback?: fun(): V } ----@class Cacheable +---@class Cacheable local Cacheable = { cache = nil, callback = nil, @@ -40,7 +40,7 @@ end -- Create ---@generic V : Cacheable, V ----@param value `V` +---@param value V ---@param cb fun(): V function Cacheable:New(value, cb) local self = setmetatable({}, Cacheable) diff --git a/src/Class/Class.lua b/src/Class/Class.lua index a7e2abc..04bda47 100644 --- a/src/Class/Class.lua +++ b/src/Class/Class.lua @@ -1,4 +1,4 @@ ----@types Tinkr, Bastion +---@type Tinkr, Bastion local Tinkr, Bastion = ... -- Create a new Class class diff --git a/src/Config/Config.lua b/src/Config/Config.lua new file mode 100644 index 0000000..8d1f08b --- /dev/null +++ b/src/Config/Config.lua @@ -0,0 +1,82 @@ +---@type Tinkr, Bastion +local Tinkr, Bastion = ... + +---@class Config +---@field instantiated boolean +---@field config Tinkr.Util.Config.Instance +---@field defaults nil | table +local Config = { + instantiated = false, +} + +function Config:__index(k) + local response = Config[k] + + if response == nil then + response = rawget(self, k) + end + + if response == nil and self.instantiated then + response = self:Read(k) + end + + return response +end + +function Config:__newindex(key, value) + if self.instantiated then + self:Write(key, value) + else + rawset(self, key, value) + end +end + +---@generic D +---@param name string +---@param defaults? D +function Config:New(name, defaults) + local self = setmetatable({}, Config) + self.config = Tinkr.Util.Config:New(name) + self.defaults = type(defaults) == "table" and defaults or {} + self.instantiated = true + return self +end + +---@generic D +---@param key string +---@param default? D +---@return D +function Config:Read(key, default) + if type(default) == "nil" then + default = self.defaults[key] + end + return self.config:Read(key, default) +end + +function Config:Reset() + if type(self.defaults) == "table" then + -- Clear all values currently in the config. + for key, _ in pairs(self.config.data) do + self:Write(key, nil) + end + -- Use default table to write new defaults. + for key, value in pairs(self.defaults) do + self:Write(key, value) + end + return true + end + return false +end + +---@param key string +---@param value any +function Config:Write(key, value) + self.config:Write(key, value) +end + +---@param key string +function Config:Sync(key) + self.config:Sync(key) +end + +return Config diff --git a/src/Item/Item.lua b/src/Item/Item.lua index 72de71b..91414bb 100644 --- a/src/Item/Item.lua +++ b/src/Item/Item.lua @@ -122,7 +122,7 @@ end ---@return number function Item:GetCooldownRemaining() local start, duration = C_Container.GetItemCooldown(self:GetID()) - return start + duration - GetTime() + return start == 0 and 0 or start + duration - GetTime() end -- Use the Item diff --git a/src/ItemBook/ItemBook.lua b/src/ItemBook/ItemBook.lua index 1000d6e..c4f43d2 100644 --- a/src/ItemBook/ItemBook.lua +++ b/src/ItemBook/ItemBook.lua @@ -1,4 +1,4 @@ ----@types Tinkr, Bastion +---@type Tinkr, Bastion local Tinkr, Bastion = ... -- Create a new ItemBook class diff --git a/src/Library/Library.lua b/src/Library/Library.lua index e5dca10..568353f 100644 --- a/src/Library/Library.lua +++ b/src/Library/Library.lua @@ -1,4 +1,4 @@ ----@types Tinkr, Bastion +---@type Tinkr, Bastion local Tinkr, Bastion = ... ---@class Library diff --git a/src/MythicPlusUtils/MythicPlusUtils.lua b/src/MythicPlusUtils/MythicPlusUtils.lua index f54a062..2d73b1f 100644 --- a/src/MythicPlusUtils/MythicPlusUtils.lua +++ b/src/MythicPlusUtils/MythicPlusUtils.lua @@ -1,4 +1,4 @@ ----@types Tinkr, Bastion +---@type Tinkr, Bastion local Tinkr, Bastion = ... ---@class MythicPlusUtils diff --git a/src/ObjectManager/ObjectManager.lua b/src/ObjectManager/ObjectManager.lua index 3984a74..c632552 100644 --- a/src/ObjectManager/ObjectManager.lua +++ b/src/ObjectManager/ObjectManager.lua @@ -2,12 +2,13 @@ local Tinkr, Bastion = ... ---@class ObjectManager ----@field _lists table +---@field _lists table ---@field enemies List ---@field friends List ---@field activeEnemies List ---@field explosives List ---@field incorporeal List +---@field others List local ObjectManager = {} ObjectManager.__index = ObjectManager @@ -21,13 +22,14 @@ function ObjectManager:New() self.activeEnemies = Bastion.List:New() self.explosives = Bastion.List:New() self.incorporeal = Bastion.List:New() + self.others = Bastion.List:New() return self end -- Register a custom list with a callback ---@param name string ----@param cb function +---@param cb fun(object: TinkrObjectReference): false | Unit ---@return List | false function ObjectManager:RegisterList(name, cb) if self._lists[name] then @@ -77,6 +79,7 @@ function ObjectManager:Refresh() self.activeEnemies:clear() self.explosives:clear() self.incorporeal:clear() + self.others:clear() self:ResetLists() local objects = Objects() @@ -102,6 +105,8 @@ function ObjectManager:Refresh() if unit:InCombatOdds() > 80 then self.activeEnemies:push(unit) end + else + self.others:push(unit) end end end diff --git a/src/Refreshable/Refreshable.lua b/src/Refreshable/Refreshable.lua index 8a53698..8f26844 100644 --- a/src/Refreshable/Refreshable.lua +++ b/src/Refreshable/Refreshable.lua @@ -1,4 +1,4 @@ ----@types Tinkr, Bastion +---@type Tinkr, Bastion local Tinkr, Bastion = ... -- Define a Refreshable class diff --git a/src/Spell/Spell.lua b/src/Spell/Spell.lua index 13be388..7928afc 100644 --- a/src/Spell/Spell.lua +++ b/src/Spell/Spell.lua @@ -97,9 +97,10 @@ function Spell:PostCast(func) end -- Get the spells name +---@param byId? boolean ---@return string -function Spell:GetName() - return select(1, GetSpellInfo(self:GetID())) +function Spell:GetName(byId) + return select(1, GetSpellInfo((byId ~= nil and byId) and self:GetID() or self:GetID())) end -- Get the spells icon @@ -109,9 +110,10 @@ function Spell:GetIcon() end -- Get the spells cooldown +---@param byId? boolean ---@return number -function Spell:GetCooldown() - return select(2, GetSpellCooldown(self:GetID())) +function Spell:GetCooldown(byId) + return select(2, GetSpellCooldown((byId ~= nil and byId) and self:GetID() or self:GetName(byId))) end -- Return the castable function @@ -130,16 +132,21 @@ function Spell:GetOnCastFunction() end -- Get the spells cooldown remaining +---@param byId? boolean ---@return number -function Spell:GetCooldownRemaining() - local start, duration = GetSpellCooldown(self:GetID()) +function Spell:GetCooldownRemaining(byId) + local start, duration = GetSpellCooldown((byId ~= nil and byId) and self:GetID() or self:GetName()) + if start == 0 then + return 0 + end return start + duration - GetTime() end -- Get the spell count +---@param byId? boolean ---@return number -function Spell:GetCount() - return GetSpellCount(self:GetID()) +function Spell:GetCount(byId) + return GetSpellCount((byId ~= nil and byId) and self:GetID() or self:GetName()) end -- On cooldown @@ -267,22 +274,26 @@ function Spell:IsOnCooldown() return select(2, GetSpellCooldown(spellIDName)) > 0 end -function Spell:IsUsableSpell() - return IsUsableSpell(self:GetID()) +---@param byId? boolean +---@return boolean, boolean +function Spell:IsUsableSpell(byId) + return IsUsableSpell((byId ~= nil and byId) and self:GetID() or self:GetName()) end -- Check if the spell is usable +---@param byId? boolean ---@return boolean -function Spell:IsUsable() - local usable, noMana = IsUsableSpell(self:GetID()) - return usable or usableExcludes[self:GetID()] and not noMana +function Spell:IsUsable(byId) + local usable, noMana = self:IsUsableSpell(byId) + return usable and not noMana end -- Check if the spell is castable ---@param override? boolean ---@return boolean function Spell:IsKnownAndUsable(override) - return self:IsKnown(override) and not self:IsOnCooldown() and self:IsUsable() + override = override ~= nil and override or false + return self:IsKnown(override) and not self:IsOnCooldown() and self:IsUsable(override and false or true) end -- Check if the spell is castable @@ -415,8 +426,11 @@ function Spell:GetTimeSinceLastCastAttempt() return GetTime() - self.lastCastAttempt end +function Spell:GetChargeInfo() + return GetSpellCharges(self:GetID()) +end + -- Get the spells charges ----@return number function Spell:GetCharges() return select(1, GetSpellCharges(self:GetID())) end @@ -433,7 +447,7 @@ end -- Get the full cooldown (time until all charges are available) ---@return number function Spell:GetFullRechargeTime() - local charges, maxCharges, _, duration = GetSpellCharges(self:GetID()) + local charges, maxCharges, _, duration = self:GetChargeInfo() if not charges or not maxCharges or charges == maxCharges then return 0 end @@ -442,9 +456,9 @@ function Spell:GetFullRechargeTime() end function Spell:Recharge() - local charges, maxCharges, startTime, duration = GetSpellCharges(self:GetID()) + local charges, maxCharges, startTime, duration = self:GetChargeInfo() if charges == maxCharges then - return charges + return 0 end local recharge = startTime + duration - GetTime() diff --git a/src/SpellBook/SpellBook.lua b/src/SpellBook/SpellBook.lua index a525998..ce70796 100644 --- a/src/SpellBook/SpellBook.lua +++ b/src/SpellBook/SpellBook.lua @@ -26,6 +26,7 @@ end end ]] -- Get a spell from the spellbook +---@param id integer ---@return Spell function SpellBook:GetSpell(id) local override = FindSpellOverrideByID(id) @@ -71,6 +72,7 @@ function SpellBook:GetSpellByName(name) return self:GetSpell(spellID) end +---@param id integer ---@return Spell function SpellBook:GetIfRegistered(id) return self.spells[id] or self.spells[FindSpellOverrideByID(id)] diff --git a/src/TimeToDie/TimeToDie.lua b/src/TimeToDie/TimeToDie.lua new file mode 100644 index 0000000..8de1b8d --- /dev/null +++ b/src/TimeToDie/TimeToDie.lua @@ -0,0 +1,282 @@ +---@type Tinkr, Bastion +local Tinkr, Bastion = ... +local Cache = Bastion.Caches +local Player = Bastion.UnitManager:Get("player") +local Target = Bastion.UnitManager:Get("target") + +--- An attempt to integrate HeroLib TTD timers. +---@class TimeToDie +local TimeToDie = { + Settings = { + -- Refresh time (seconds) : min=0.1, max=2, default = 0.1 + Refresh = 0.1, + -- History time (seconds) : min=5, max=120, default = 10+0.4 + HistoryTime = 10 + 0.4, + -- Max history count : min=20, max=500, default = 100 + HistoryCount = 100 + }, + Cache = {}, -- A cache of unused { time, value } tables to reduce garbage due to table creation + ---@type table + Units = {}, -- Used to track units, + ---@type table + ExistingUnits = {}, -- Used to track GUIDs of currently existing units (to be compared with tracked units) + Throttle = 0 +} + +function TimeToDie:IterableUnits() + return Bastion.List:New():concat(Bastion.ObjectManager.enemies):concat(Bastion.ObjectManager.explosives):concat( + Bastion.ObjectManager.incorporeal) +end + +function TimeToDie:Refresh() + local currentTime = GetTime() + local historyCount = self.Settings.HistoryCount + local historyTime = self.Settings.HistoryTime + local ttdCache = self.Cache + local iterableUnits = self:IterableUnits() + local units = self.Units + local existingUnits = self.ExistingUnits + + wipe(existingUnits) + + local thisUnit + ---@param unit Unit + iterableUnits:each(function(unit) + thisUnit = unit + if thisUnit:Exists() then + local unitGUID = thisUnit:GetGUID() + -- Check if we didn't already scanned this unit. + if unitGUID and not existingUnits[unitGUID] then + existingUnits[unitGUID] = true + local healthPercentage = thisUnit:GetHealthPercent() + -- Check if it's a valid unit + if Player:CanAttack(thisUnit) and healthPercentage < 100 then + local unitTable = units[unitGUID] + -- Check if we have seen one time this unit, if we don't then initialize it. + if not unitTable or healthPercentage > unitTable[1][1][2] then + unitTable = { {}, currentTime } + units[unitGUID] = unitTable + end + local values = unitTable[1] + local time = currentTime - unitTable[2] + -- Check if the % HP changed since the last check (or if there were none) + if not values or healthPercentage ~= values[2] then + local value + local lastIndex = #ttdCache + -- Check if we can re-use a table from the cache + if lastIndex == 0 then + value = { time, healthPercentage } + else + value = ttdCache[lastIndex] + ttdCache[lastIndex] = nil + value[1] = time + value[2] = healthPercentage + end + tableinsert(values, 1, value) + local n = #values + -- Delete values that are no longer valid + while (n > historyCount) or (time - values[n][1] > historyTime) do + ttdCache[#Cache + 1] = values[n] + values[n] = nil + n = n - 1 + end + end + end + end + end + return false + end) + + -- Not sure if it's even worth it to do this here + -- Ideally this should be event driven or done at least once a second if not less + for key in pairs(units) do + if not existingUnits[key] then + units[key] = nil + end + end +end + +TimeToDie.specialTTDPercentageData = { + --- Dragonflight + ----- Dungeons ----- + --- Brackenhide Hollow + -- Decatriarch Wratheye + [186121] = 5, + + --- Shadowlands + ----- Dungeons ----- + --- De Other Side + -- Mueh'zala leaves the fight at 10%. + [166608] = 10, + --- Mists of Tirna Scithe + -- Tirnenns leaves the fight at 20%. + [164929] = 20, -- Tirnenn Villager + [164804] = 20, -- Droman Oulfarran + --- Sanguine Depths + -- General Kaal leaves the fight at 50%. + [162099] = 50, + ----- Castle Nathria ----- + --- Stone Legion Generals + -- General Kaal leaves the fight at 50% if General Grashaal has not fight yet. We take 49% as check value since it get -95% dmg reduction at 50% until intermission is over. + ---@param self Unit + [168112] = function(self) return (not self:CheckHPFromBossList(168113, 99) and 49) or 0 end, + --- Sun King's Salvation + -- Shade of Kael'thas fight is 60% -> 45% and then 10% -> 0%. + ---@param self Unit + [165805] = function(self) return (self:GetHealthPercent() > 20 and 45) or 0 end, + ----- Sanctum of Domination ----- + --- Eye of the Jailer leaves at 66% and 33% + ---@param self Unit + [180018] = function(self) + return (self:GetHealthPercent() > 66 and 66) or + (self:GetHealthPercent() <= 66 and self:GetHealthPercent() > 33 and 33) or 0 + end, + --- Painsmith Raznal leaves at 70% and 40% + ---@param self Unit + [176523] = function(self) + return (self:GetHealthPercent() > 70 and 70) or + (self:GetHealthPercent() <= 70 and self:GetHealthPercent() > 40 and 40) or 0 + end, + --- Fatescribe Roh-Kalo phases at 70% and 40% + ---@param self Unit + [179390] = function(self) + return (self:GetHealthPercent() > 70 and 70) or + (self:GetHealthPercent() <= 70 and self:GetHealthPercent() > 40 and 40) or 0 + end, + --- Sylvanas Windrunner intermission at 83% and "dies" at 50% (45% in MM) + ---@param self Unit + [180828] = function(self) + local _, _, difficultyId = GetInstanceInfo() + return (self:GetHealthPercent() > 83 and 83) or + ((difficultyId == 16 and 45) or 50) + end, + + --- Legion + ----- Open World ----- + --- Stormheim Invasion + -- Lord Commander Alexius + [118566] = 85, + ----- Dungeons ----- + --- Halls of Valor + -- Hymdall leaves the fight at 10%. + [94960] = 10, + -- Fenryr leaves the fight at 60%. We take 50% as check value since it doesn't get immune at 60%. + ---@param self Unit + [95674] = function(self) return (self:GetHealthPercent() > 50 and 60) or 0 end, + -- Odyn leaves the fight at 80%. + [95676] = 80, + --- Maw of Souls + -- Helya leaves the fight at 70%. + [96759] = 70, + ----- Trial of Valor ----- + --- Odyn + -- Hyrja & Hymdall leaves the fight at 25% during first stage and 85%/90% during second stage (HM/MM). + ---@param self Unit + [114360] = function(self) + local _, _, difficultyId = GetInstanceInfo() + return (not self:CheckHPFromBossList(114263, 99) and 25) or + (difficultyId == 16 and 85) or 90 + end, + ---@param self Unit + [114361] = function(self) + return (not self:CheckHPFromBossList(114263, 99) and 25) or + (difficultyId == 16 and 85) or 90 + end, + -- Odyn leaves the fight at 10%. + [114263] = 10, + ----- Nighthold ----- + --- Elisande leaves the fight two times at 10% then normally dies. She looses 50% power per stage (100 -> 50 -> 0). + ---@param self Unit + [106643] = function(self) return (self:GetPower() > 0 and 10) or 0 end, + + --- Warlord of Draenor (WoD) + ----- Dungeons ----- + --- Shadowmoon Burial Grounds + -- Carrion Worm doesn't die but leave the area at 10%. + [88769] = 10, + [76057] = 10, + ----- HellFire Citadel ----- + --- Hellfire Assault + -- Mar'Tak doesn't die and leave fight at 50% (blocked at 1hp anyway). + [93023] = 50, + + --- Classic + ----- Dungeons ----- + --- Uldaman + -- Dwarves + [184580] = 5, + [184581] = 5, + [184582] = 5, +} + +-- Returns the max fight length of boss units, or the current selected target if no boss units +---@param enemies? List +---@param bossOnly? boolean +function TimeToDie.FightRemains(enemies, bossOnly) + local bossExists, maxTimeToDie + for i = 1, 4 do + local bossUnit = Bastion.UnitManager:Get(string.format("boss%d", i)) + if bossUnit:Exists() then + bossExists = true + if not bossUnit:TimeToDieIsNotValid() then + maxTimeToDie = math.max(maxTimeToDie or 0, bossUnit:TimeToDie2()) + end + end + end + + if bossExists or bossOnly then + -- If we have a boss list but no valid boss time, return invalid + return maxTimeToDie or 11111 + end + + -- If we specify an AoE range, iterate through all the targets in the specified range + if enemies then + ---@param enemy Unit + enemies:each(function(enemy) + if (enemy:InCombatOdds() > 80 or enemy:IsDummy()) and enemy:TimeToDieIsNotValid() then + maxTimeToDie = math.max(maxTimeToDie or 0, enemy:TimeToDie2()) + end + return false + end) + if maxTimeToDie then + return maxTimeToDie + end + end + + return Target:TimeToDie2() +end + +-- Returns the max fight length of boss units, 11111 if not a boss fight +function TimeToDie.BossFightRemains() + return TimeToDie.FightRemains(nil, true) +end + +-- Get if the Time To Die is Valid for a boss fight remains +function TimeToDie.BossFightRemainsIsNotValid() + return TimeToDie.BossFightRemains() >= 7777 +end + +-- Returns if the current fight length meets the requirements. +---@param enemies? List +---@param operator CompareThisTable +---@param value number +---@param checkIfValid boolean +---@param bossOnly boolean +function TimeToDie.FilteredFightRemains(enemies, operator, value, checkIfValid, bossOnly) + local fightRemains = TimeToDie.FightRemains(enemies, bossOnly) + if checkIfValid and fightRemains >= 7777 then + return false + end + + return Utils.CompareThis(operator, fightRemains, value) or false +end + +-- Returns if the current boss fight length meets the requirements, 11111 if not a boss fight. +---@param operator CompareThisTable +---@param value number +---@param checkIfValid boolean +function TimeToDie.BossFilteredFightRemains(operator, value, checkIfValid) + return TimeToDie.FilteredFightRemains(nil, operator, value, checkIfValid, true) +end + +return TimeToDie diff --git a/src/Timer/Timer.lua b/src/Timer/Timer.lua index 06771bd..f4888b8 100644 --- a/src/Timer/Timer.lua +++ b/src/Timer/Timer.lua @@ -1,4 +1,4 @@ ----@types Tinkr, Bastion +---@type Tinkr, Bastion local Tinkr, Bastion = ... -- Create a new Timer class diff --git a/src/Unit/Unit.lua b/src/Unit/Unit.lua index eb3511e..69cf442 100644 --- a/src/Unit/Unit.lua +++ b/src/Unit/Unit.lua @@ -89,9 +89,10 @@ function Unit:Token() end -- Get the units name ----@return string +---@return string? function Unit:GetName() - return select(1, UnitName(self:GetOMToken())) + local unitName, realm = UnitName(self:GetOMToken()) + return unitName end -- Get the units GUID @@ -503,7 +504,8 @@ local attachmentOverride = { function Unit:GetHitSphere() --local srcX, srcY, srcZ = GetUnitAttachmentPosition(self:GetOMToken(), attachmentOverride[self:GetID()] or AttachmentPoisitions.PlayerName) --local srcHeight = UnitIsMounted(self:GetOMToken()) and 3.081099 or 2.43808 - return Bastion.Vector3:New(GetUnitAttachmentPosition(self:GetOMToken(), attachmentOverride[self:GetID()] or AttachmentPoisitions.PlayerName)) + return Bastion.Vector3:New(GetUnitAttachmentPosition(self:GetOMToken(), + attachmentOverride[self:GetID()] or AttachmentPoisitions.PlayerName)) end function Unit:GetLOSSourcePosition() @@ -513,47 +515,61 @@ function Unit:GetLOSSourcePosition() end local losFlag = bit.bor(0x10) - -- Check if the unit can see another unit ----@param unit Unit +---@param targetUnit Unit ---@return boolean -function Unit:CanSee(unit) - -- mechagon smoke cloud - -- local mechagonID = 2097 - -- local smokecloud = 298602 - - -- local name, instanceType, difficultyID, difficultyName, maxPlayers, dynamicDifficulty, isDynamic, instanceID, instanceGroupSize, LfgDungeonID = - -- GetInstanceInfo() - - -- otherUnit = otherUnit and otherUnit or "player" - -- if instanceID == 2097 then - -- if (self:debuff(smokecloud, unit) and not self:debuff(smokecloud, otherUnit)) - -- or (self:debuff(smokecloud, otherUnit) and not self:debuff(smokecloud, unit)) - -- then - -- return false - -- end - -- end - local ax, ay, az = ObjectPosition(self:GetOMToken()) - local ah = ObjectHeight(self:GetOMToken()) - local attx, atty, attz = GetUnitAttachmentPosition(unit:GetOMToken(), 34) - - if not attx or not ax then - return false +function Unit:CanSee2(targetUnit) + local npcId = targetUnit:GetID() + if npcId and losBlacklist[npcId] then + return true end - if not ah then - return false - end + local src = self:GetLOSSourcePosition() + local dst = targetUnit:GetHitSphere() - if (ax == 0 and ay == 0 and az == 0) or (attx == 0 and atty == 0 and attz == 0) then + if (src.x == 0 and src.y == 0 and src.z == 0) or (dst.x == 0 and dst.y == 0 and dst.z == 0) then return true end - if not attx or not ax then + local contactPoint = src + + (dst - src):directionOrZero() * math.min(targetUnit:GetDistance(self), self:GetCombatReach()) + + local x, y, z = TraceLine(src.x, src.y, src.z, contactPoint.x, contactPoint.y, contactPoint.z, losFlag) + if x ~= 0 or y ~= 0 or z ~= 0 then return false + else + return true + end +end + +---@param destination Unit +---@return Vector3 +function Unit:GetHitSpherePointFor(destination) + local vThis = Bastion.Vector3:New(self:GetPosition().x, self:GetPosition().y, + self:GetPosition().z + ObjectHeight(self:GetOMToken())) + local vObj = Bastion.Vector3:New(destination:GetPosition().x, destination:GetPosition().y, + destination:GetPosition().z) + local contactPoint = vThis + + (vObj - vThis):directionOrZero() * math.min(destination:GetDistance(self), self:GetCombatReach()) + return contactPoint +end + +---@param destinationUnit Unit +function Unit:CanSee(destinationUnit) + local src = self:GetPosition() + local dst = destinationUnit:GetPosition() + if ObjectType(self:GetOMToken()) == 6 then + src.z = src.z + ObjectHeight(self:GetOMToken()) + else + dst = self:GetHitSpherePointFor(destinationUnit) + end + + if (src.x == 0 and src.y == 0 and src.z == 0) or (dst.x == 0 and dst.y == 0 and dst.z == 0) then + return true end - local x, y, z = TraceLine(ax, ay, az + ah, attx, atty, attz, losFlag) + local x, y, z = TraceLine(src.x, src.y, src.z, dst.x, dst.y, dst.z, losFlag) + if x ~= 0 or y ~= 0 or z ~= 0 then return false else @@ -568,10 +584,12 @@ function Unit:IsCasting() end function Unit:GetTimeCastIsAt(percent) - local name, text, texture, startTimeMS, endTimeMS, isTradeSkill, castID, notInterruptible, spellId = UnitCastingInfo(self:GetOMToken()) + local name, text, texture, startTimeMS, endTimeMS, isTradeSkill, castID, notInterruptible, spellId = UnitCastingInfo( + self:GetOMToken()) if not name then - name, text, texture, startTimeMS, endTimeMS, isTradeSkill, notInterruptible, spellId = UnitChannelInfo(self:GetOMToken()) + name, text, texture, startTimeMS, endTimeMS, isTradeSkill, notInterruptible, spellId = UnitChannelInfo(self + :GetOMToken()) end if name and startTimeMS and endTimeMS then @@ -588,10 +606,12 @@ end -- Get Casting or channeling spell ---@return Spell | nil function Unit:GetCastingOrChannelingSpell() - local name, text, texture, startTimeMS, endTimeMS, isTradeSkill, castID, notInterruptible, spellId = UnitCastingInfo(self:GetOMToken()) + local name, text, texture, startTimeMS, endTimeMS, isTradeSkill, castID, notInterruptible, spellId = UnitCastingInfo( + self:GetOMToken()) if not name then - name, text, texture, startTimeMS, endTimeMS, isTradeSkill, notInterruptible, spellId = UnitChannelInfo(self:GetOMToken()) + name, text, texture, startTimeMS, endTimeMS, isTradeSkill, notInterruptible, spellId = UnitChannelInfo(self + :GetOMToken()) end if name then @@ -604,10 +624,12 @@ end -- Get the end time of the cast or channel ---@return number function Unit:GetCastingOrChannelingEndTime() - local name, text, texture, startTimeMS, endTimeMS, isTradeSkill, castID, notInterruptible, spellId = UnitCastingInfo(self:GetOMToken()) + local name, text, texture, startTimeMS, endTimeMS, isTradeSkill, castID, notInterruptible, spellId = UnitCastingInfo( + self:GetOMToken()) if not name then - name, text, texture, startTimeMS, endTimeMS, isTradeSkill, notInterruptible, spellId = UnitChannelInfo(self:GetOMToken()) + name, text, texture, startTimeMS, endTimeMS, isTradeSkill, notInterruptible, spellId = UnitChannelInfo(self + :GetOMToken()) end if name then @@ -651,10 +673,12 @@ end ---@return number function Unit:GetChannelOrCastPercentComplete() - local name, text, texture, startTimeMS, endTimeMS, isTradeSkill, castID, notInterruptible, spellId = UnitCastingInfo(self:GetOMToken()) + local name, text, texture, startTimeMS, endTimeMS, isTradeSkill, castID, notInterruptible, spellId = UnitCastingInfo( + self:GetOMToken()) if not name then - name, text, texture, startTimeMS, endTimeMS, isTradeSkill, notInterruptible, spellId = UnitChannelInfo(self:GetOMToken()) + name, text, texture, startTimeMS, endTimeMS, isTradeSkill, notInterruptible, spellId = UnitChannelInfo(self + :GetOMToken()) end if name and startTimeMS and endTimeMS then @@ -670,10 +694,12 @@ end -- Check if unit is interruptible ---@return boolean function Unit:IsInterruptible() - local name, text, texture, startTimeMS, endTimeMS, isTradeSkill, castID, notInterruptible, spellId = UnitCastingInfo(self:GetOMToken()) + local name, text, texture, startTimeMS, endTimeMS, isTradeSkill, castID, notInterruptible, spellId = UnitCastingInfo( + self:GetOMToken()) if not name then - name, text, texture, startTimeMS, endTimeMS, isTradeSkill, notInterruptible, spellId = UnitChannelInfo(self:GetOMToken()) + name, text, texture, startTimeMS, endTimeMS, isTradeSkill, notInterruptible, spellId = UnitChannelInfo(self + :GetOMToken()) end if name then @@ -819,7 +845,8 @@ end ---@param unit Unit ---@return boolean function Unit:IsTanking(unit) - local isTanking, status, threatpct, rawthreatpct, threatvalue = UnitDetailedThreatSituation(self:GetOMToken(), unit:GetOMToken()) + local isTanking, status, threatpct, rawthreatpct, threatvalue = UnitDetailedThreatSituation(self:GetOMToken(), + unit:GetOMToken()) return isTanking end @@ -1064,7 +1091,8 @@ function Unit:TimeToDie() -- if the unit has more than 5 million health but there's not enough data to make a prediction we can assume there's roughly 250000 damage per second and estimate the time to die if #self.regression_history < 5 and self:GetMaxHealth() > 5000000 then - return self:GetMaxHealth() / 250000 -- 250000 is an estimate of the average damage per second a well geared group will average + return self:GetMaxHealth() / + 250000 -- 250000 is an estimate of the average damage per second a well geared group will average end if self.ttd ~= self.ttd or self.ttd < 0 or self.ttd == math.huge then @@ -1212,7 +1240,7 @@ end ---@return boolean function Unit:IsMounted() local mountedFormId = { - [3] = true, -- Mount / Travel Form + [3] = true, -- Mount / Travel Form [27] = true, -- Swift Flight Form [29] = true, -- Flight Form } @@ -1374,17 +1402,294 @@ end function Unit:HasIncomingRessurection() return self:IsDead() and UnitHasIncomingResurrection(self:GetOMToken()) end + function Unit:LootTarget() return ObjectLootTarget(self:GetOMToken()) end + function Unit:CanLoot() return ObjectLootable(self:GetOMToken()) end + function Unit:HasTarget() return ObjectTarget(self:GetOMToken()) ~= false end + function Unit:Target() - return self:HasTarget() and Bastion.UnitManager:Get(ObjectTarget(self:GetOMToken()):unit()) or Bastion.UnitManager:Get("none") + return self:HasTarget() and Bastion.UnitManager:Get(ObjectTarget(self:GetOMToken()):unit()) or + Bastion.UnitManager:Get("none") +end + +local dummyUnits = { + -- City (SW, Orgri, ...) + [31146] = true, -- Raider's Training Dummy + [31144] = true, -- Training Dummy + [32666] = true, -- Training Dummy + [32667] = true, -- Training Dummy + [46647] = true, -- Training Dummy + [114832] = true, -- PvP Training Dummy + [153285] = true, -- Training Dummy + [153292] = true, -- Training Dummy + -- MoP Shrine of Two Moons + [67127] = true, -- Training Dummy + -- WoD Alliance Garrison + [87317] = true, -- Mage Tower Damage Training Dummy + [87318] = true, -- Mage Tower Damage Dungeoneer's Training Dummy (& Garrison) + [87320] = true, -- Mage Tower Damage Raider's Training Dummy + [88314] = true, -- Tanking Dungeoneer's Training Dummy + [88316] = true, -- Healing Training Dummy ----> FRIENDLY + -- WoD Horde Garrison + [87760] = true, -- Mage Tower Damage Training Dummy + [87761] = true, -- Mage Tower Damage Dungeoneer's Training Dummy (& Garrison) + [87762] = true, -- Mage Tower Damage Raider's Training Dummy + [88288] = true, -- Tanking Dungeoneer's Training Dummy + [88289] = true, -- Healing Training Dummy ----> FRIENDLY + -- Legion Druid Class Order Hall + [113964] = true, -- Raider's Training Dummy + [113966] = true, -- Dungeoneer's Training Dummy + -- Legion Mage Class Order Hall + [103397] = true, -- Greater Bullwark Construct + [103404] = true, -- Bullwark Construct + [103402] = true, -- Lesser Bullwark Construct + -- Legion Priest Class Order Hall + [107555] = true, -- Bound void Wraith + [107556] = true, -- Bound void Walker + -- Legion Rogue Class Order Hall + [92164] = true, -- Training Dummy + [92165] = true, -- Dungeoneer's Training Dummy + [92166] = true, -- Raider's Training Dummy + -- Legion Warlock Class Order Hall + [101956] = true, -- Rebellious Fel Lord + [102045] = true, -- Rebellious WrathGuard + [102048] = true, -- Rebellious Felguard + [102052] = true, -- Rebellious imp + -- BfA Dazar'Alor + [144081] = true, -- Training Dummy + [144082] = true, -- Training Dummy + [144085] = true, -- Training Dummy + [144086] = true, -- Raider's Training Dummy + -- BfA Boralus + [126781] = true, -- Training Dummy + [131983] = true, -- Raider's Training Dummy + [131989] = true, -- Training Dummy + [131992] = true, -- Dungeoneer's Training Dummy + -- Shadowlands Kyrian + [154564] = true, -- Valiant's Humility + [154567] = true, -- Purity's Cleaning + [154580] = true, -- Reinforced Guardian + [154583] = true, -- Starlwart Guardian + [154585] = true, -- Valiant's Resolve + [154586] = true, -- Stalwart Phalanx + [160325] = true, -- Humility's Obedience + -- Shadowlands Venthyr + [173942] = true, -- Training Dummy + [175449] = true, -- Raider's Training Dummy + [175450] = true, -- Dungeoneer's Training Dummy + [175451] = true, -- Dungeoneer's Tanking Dummy + [175452] = true, -- Raider's Tanking Dummy + [175455] = true, -- Cleave Training Dummy + [175456] = true, -- Swarm Training Dummy + [175462] = true, -- Sinfall Fiend + -- Shadowlands Night Fae + [174565] = true, -- Dungeoneer's Tanking Dummy + [174566] = true, -- Raider's Tanking Dummy + [174567] = true, -- Raider's Training Dummy + [174568] = true, -- Dungeoneer's Training Dummy + [174569] = true, -- Training Dummy + [174570] = true, -- Swarm Training Dummy + [174571] = true, -- Cleave Training Dummy + -- Shadowlands Necrolord + [174484] = true, -- Dungeoneer's Training Dummy + [174487] = true, -- Training Dummy + [174488] = true, -- Raider's Training Dummy + [174491] = true, -- Tanking Dummy + -- DargonFlight Valdrakken + [198594] = true, -- Cleave Training Dummy + [194648] = true, -- Training Dummy + [189632] = true, -- Animated Duelist + [194643] = true, -- Dungeoneer's Training Dummy + [194644] = true, -- Dungeoneer's Training Dummy + [197833] = true, -- PvP Training Dummy + [189617] = true, -- Boulderfist + [194649] = true, -- Normal Tank Dummy + -- DargonFlight Iskaara + [193563] = true, -- Training Dummy + -- Other + [65310] = true, -- Turnip Punching Bag (toy) + [66374] = true, -- Anatomical Dummy (toy) + [196394] = true, -- Tuskarr Training Dummy (toy) + [196406] = true, -- Rubbery Fish Head (toy) + [199057] = true, -- Black Dragon's Challenge Dummy (toy) +} + +function Unit:IsDummy() + local npcId = self:GetID() + return npcId >= 0 and dummyUnits[npcId] == true +end + +---@param npcId? number +function Unit:IsInBossList(npcId) + npcId = npcId or self:GetID() + for i = 1, 4 do + local thisUnit = Bastion.UnitManager:Get(string.format("boss%d", i)) + if thisUnit:Exists() and thisUnit:GetID() == npcId then + return true + end + end +end + +---@param npcId number +function Unit:SpecialTTDPercentage(npcId) + local specialTTDPercentage = Bastion.TimeToDie.specialTTDPercentageData[npcId] + if not specialTTDPercentage then return 0 end + + if type(specialTTDPercentage) == "number" then + return specialTTDPercentage + end + + return specialTTDPercentage(self) +end + +---@param npcId? number +---@param hp? number +function Unit:CheckHPFromBossList(npcId, hp) + npcId = npcId or self:GetID() + local thisHP = hp or 100 + for i = 1, 4 do + local thisUnit = Bastion.UnitManager:Get(string.format("boss%d", i)) + if thisUnit:Exists() and thisUnit:GetID() == npcId and thisUnit:GetHealthPercent() <= thisHP then + return true + end + end + + return false +end + +function Unit:TimeToX(percentage, minSamples) + if self:IsDummy() then return 6666 end + if self:IsPlayer() and Player:CanAttack(self) then return 25 end + local seconds = 8888 + local unitGuid = self:GetGUID() + if not unitGuid then + return seconds + end + local unitTable = Bastion.TimeToDie.Units[unitGuid] + -- Simple linear regression + -- ( E(x^2) E(x) ) ( a ) ( E(xy) ) + -- ( E(x) n ) ( b ) = ( E(y) ) + -- Format of the above: ( 2x2 Matrix ) * ( 2x1 Vector ) = ( 2x1 Vector ) + -- Solve to find a and b, satisfying y = a + bx + -- Matrix arithmetic has been expanded and solved to make the following operation as fast as possible + if unitTable then + minSamples = minSamples or 3 + local values = unitTable[1] + local n = #values + if n > minSamples then + local a, b = 0, 0 + local ex2, ex, exy, ey = 0, 0, 0, 0 + + for i = 1, n do + local value = values[i] + local x, y = value[1], value[2] + + ex2 = ex2 + x * x + ex = ex + x + exy = exy + x * y + ey = ey + y + end + -- Invariant to find matrix inverse + local invariant = 1 / (ex2 * n - ex * ex) + -- Solve for a and b + a = (-ex * exy * invariant) + (ex2 * ey * invariant) + b = (n * exy * invariant) - (ex * ey * invariant) + if b ~= 0 then + -- Use best fit line to calculate estimated time to reach target health + seconds = (percentage - a) / b + -- Subtract current time to obtain "time remaining" + seconds = math.min(7777, seconds - (GetTime() - unitTable[2])) + if seconds < 0 then seconds = 9999 end + end + end + end + return seconds +end + +---@param minSamples? number +function Unit:TimeToDie2(minSamples) + if not self:Exists() then + return 11111 + end + local unitGuid = self:GetGUID() + if not unitGuid then return 11111 end + + minSamples = minSamples or 3 + ---@type {TTD: {[number]: number}} + local unitInfo = Cache.UnitInfo:IsCached(unitGuid) and Cache.UnitInfo:Get(unitGuid) or {} + + local ttd = unitInfo.TTD + if not ttd then + ttd = {} + unitInfo.TTD = ttd + end + if not ttd[minSamples] then + ttd[minSamples] = self:TimeToX(self:SpecialTTDPercentage(self:GetID()), minSamples) + end + + Bastion.Caches.UnitInfo:Set(unitGuid, unitInfo, .5) + + return ttd[minSamples] +end + +-- Get the boss unit TimeToDie +---@param minSamples? number +function Unit:BossTimeToDie(minSamples) + if self:IsInBossList() or self:IsDummy() then + return self:TimeToDie2(minSamples) + end + + return 11111 +end + +-- Get if the unit meets the TimeToDie requirements. +---@param operator CompareThisTable +---@param value number +---@param offset number +---@param valueThreshold number +---@param minSamples? number +function Unit:FilteredTimeToDie(operator, value, offset, valueThreshold, minSamples) + local TTD = self:TimeToDie2(minSamples) + + return TTD < (valueThreshold or 7777) and Bastion.Utils.CompareThis(operator, TTD + (offset or 0), value) or false +end + +-- Get if the boss unit meets the TimeToDie requirements. +---@param operator CompareThisTable +---@param value number +---@param offset number +---@param valueThreshold number +---@param minSamples? number +function Unit:BossFilteredTimeToDie(operator, value, offset, valueThreshold, minSamples) + if self:IsInBossList() or self:IsDummy() then + return self:FilteredTimeToDie(operator, value, offset, valueThreshold, minSamples) + end + + return false +end + +-- Get if the Time To Die is Valid for an Unit (i.e. not returning a warning code). +---@param minSamples? number +function Unit:TimeToDieIsNotValid(minSamples) + return self:TimeToDie2(minSamples) >= 7777 +end + +-- Get if the Time To Die is Valid for a boss Unit (i.e. not returning a warning code or not being a boss). +---@param minSamples? number +function Unit:BossTimeToDieIsNotValid(minSamples) + if self:IsInBossList() then + return self:TimeToDieIsNotValid(minSamples) + end + + return true end -- local empowering = {} diff --git a/src/UnitManager/UnitManager.lua b/src/UnitManager/UnitManager.lua index 63703d7..e971af0 100644 --- a/src/UnitManager/UnitManager.lua +++ b/src/UnitManager/UnitManager.lua @@ -5,12 +5,12 @@ local ObjectManager = Tinkr.Util.ObjectManager local Unit = Bastion.Unit ----@class CacheableUnit : Cacheable +---@class CacheableUnit : Cacheable -- Create a new UnitManager class ---@class UnitManager : { [UnitId]: Unit } ---@field units table ----@field customUnits table +---@field customUnits table, cb: fun(unit: Unit): Unit }> ---@field objects table ---@field cache Cache local UnitManager = { @@ -121,8 +121,8 @@ end -- Create a custom unit and cache it for .5 seconds ---@param token string ----@param cb fun(): Unit ----@return Cacheable +---@param cb fun(unit: Unit?): Unit +---@return Unit function UnitManager:CreateCustomUnit(token, cb) local unit = cb() local cachedUnit = Bastion.Cacheable:New(unit, cb) @@ -269,6 +269,7 @@ function UnitManager:GetFriendWithMostFriends(radius) end -- Get the enemy with the most enemies within a given radius +---@return Unit|nil, Unit[] function UnitManager:GetEnemiesWithMostEnemies(radius) local unit = nil local count = 0 diff --git a/src/Vector3/Vector3.lua b/src/Vector3/Vector3.lua index c1d920d..f9596c7 100644 --- a/src/Vector3/Vector3.lua +++ b/src/Vector3/Vector3.lua @@ -2,10 +2,11 @@ ---@class Vector3 ---@operator add(Vector3): Vector3 ----@operator sub(Vector3): Vector3 ----@operator mul(Vector3): Vector3 ----@operator div(Vector3): Vector3 +---@operator sub(Vector3|number): Vector3 +---@operator mul(number): Vector3 +---@operator div(number): Vector3 ---@operator unm(): Vector3 +---@operator len(): number local Vector3 = {} Vector3.__index = Vector3 @@ -20,7 +21,7 @@ function Vector3:__add(other) return Vector3:New(self.x + other.x, self.y + other.y, self.z + other.z) end ----@param other Vector3 +---@param other Vector3 | number ---@return Vector3 function Vector3:__sub(other) if type(other) == "number" then diff --git a/src/_bastion.lua b/src/_bastion.lua index cb65b68..2c25b1d 100644 --- a/src/_bastion.lua +++ b/src/_bastion.lua @@ -124,6 +124,33 @@ Bastion.Timer = Bastion.require("Timer") Bastion.CombatTimer = Bastion.Timer:New("combat") Bastion.MythicPlusUtils = Bastion.require("MythicPlusUtils"):New() Bastion.Notifications = Bastion.NotificationsList:New() +Bastion.Config = Bastion.require("Config") +Bastion.TimeToDie = Bastion.require("TimeToDie") + +Bastion.Caches = { + UnitInfo = Bastion.Cache:New() +} + +---@enum (key) CompareThisTable +local compareThisTable = { + [">"] = function(A, B) return A > B end, + ["<"] = function(A, B) return A < B end, + [">="] = function(A, B) return A >= B end, + ["<="] = function(A, B) return A <= B end, + ["=="] = function(A, B) return A == B end, + ["min"] = function(A, B) return A < B end, + ["max"] = function(A, B) return A > B end, +} + +Bastion.Utils = { + ---@generic A + ---@param operator CompareThisTable + ---@param a A + ---@param b A + CompareThis = function(operator, a, b) + return compareThisTable[operator](a, b) + end +} local LIBRARIES = {} ---@type Module[] @@ -202,7 +229,9 @@ Bastion.Globals.EventManager:RegisterWoWEvent("COMBAT_LOG_EVENT_UNFILTERED", fun end end end) - +local Timer = { + TTD = 0 +} Bastion.Ticker = C_Timer.NewTicker(0.1, function() if not Bastion.CombatTimer:IsRunning() and UnitAffectingCombat("player") then Bastion.CombatTimer:Start() @@ -212,6 +241,10 @@ Bastion.Ticker = C_Timer.NewTicker(0.1, function() if Bastion.Enabled then Bastion.ObjectManager:Refresh() + if GetTime() > Timer.TTD then + Timer.TTD = GetTime() + Bastion.TimeToDie.Settings.Refresh + Bastion.TimeToDie:Refresh() + end for i = 1, #MODULES do MODULES[i]:Tick() end @@ -233,6 +266,7 @@ function Bastion:FindModule(name) return nil end + Bastion.PrintEnabled = false function Bastion:Print(...) if not Bastion.PrintEnabled then @@ -294,7 +328,8 @@ Command:Register("dumpspells", "Dump spells to a file", function() if spellID then spellName = spellName:gsub("[%W%s]", "") - WriteFile("bastion-" .. UnitClass("player") .. "-" .. rand .. ".lua", "local " .. spellName .. " = Bastion.Globals.SpellBook:GetSpell(" .. spellID .. ")\n", true) + WriteFile("bastion-" .. UnitClass("player") .. "-" .. rand .. ".lua", + "local " .. spellName .. " = Bastion.Globals.SpellBook:GetSpell(" .. spellID .. ")\n", true) end i = i + 1 end