From 764655ea51e9e28d7eb071cd61534cf1d9628d76 Mon Sep 17 00:00:00 2001 From: 4n0n <4n0n@tinkr.site> Date: Fri, 30 Dec 2022 04:58:45 -0500 Subject: [PATCH] Init --- .gitignore | 3 + README.md | 2 + scripts/restodruid.lua | 436 ++++++++++++++++++++++++++++++ src/APL/APL.lua | 76 ++++++ src/Aura/Aura.lua | 245 +++++++++++++++++ src/AuraTable/AuraTable.lua | 229 ++++++++++++++++ src/Cache/Cache.lua | 46 ++++ src/Cacheable/Cacheable.lua | 68 +++++ src/Class/Class.lua | 51 ++++ src/ClassMagic/ClassMagic.lua | 49 ++++ src/Command/Command.lua | 60 ++++ src/EventManager/EventManager.lua | 60 ++++ src/Item/Item.lua | 343 +++++++++++++++++++++++ src/ItemBook/ItemBook.lua | 23 ++ src/Module/Module.lua | 65 +++++ src/Spell/Spell.lua | 340 +++++++++++++++++++++++ src/SpellBook/SpellBook.lua | 23 ++ src/Timer/Timer.lua | 41 +++ src/Unit/Unit.lua | 368 +++++++++++++++++++++++++ src/UnitManager/UnitManager.lua | 275 +++++++++++++++++++ src/Vector3/Vector3.lua | 240 ++++++++++++++++ src/_bastion.lua | 152 +++++++++++ 22 files changed, 3195 insertions(+) create mode 100644 .gitignore create mode 100644 README.md create mode 100644 scripts/restodruid.lua create mode 100644 src/APL/APL.lua create mode 100644 src/Aura/Aura.lua create mode 100644 src/AuraTable/AuraTable.lua create mode 100644 src/Cache/Cache.lua create mode 100644 src/Cacheable/Cacheable.lua create mode 100644 src/Class/Class.lua create mode 100644 src/ClassMagic/ClassMagic.lua create mode 100644 src/Command/Command.lua create mode 100644 src/EventManager/EventManager.lua create mode 100644 src/Item/Item.lua create mode 100644 src/ItemBook/ItemBook.lua create mode 100644 src/Module/Module.lua create mode 100644 src/Spell/Spell.lua create mode 100644 src/SpellBook/SpellBook.lua create mode 100644 src/Timer/Timer.lua create mode 100644 src/Unit/Unit.lua create mode 100644 src/UnitManager/UnitManager.lua create mode 100644 src/Vector3/Vector3.lua create mode 100644 src/_bastion.lua diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..7bb1f9d --- /dev/null +++ b/.gitignore @@ -0,0 +1,3 @@ +## Ignore mac files +.DS_Store +DS_Store diff --git a/README.md b/README.md new file mode 100644 index 0000000..20da9a5 --- /dev/null +++ b/README.md @@ -0,0 +1,2 @@ +# bastion +Bastion is a heavily cached World of Warcraft scripting platform diff --git a/scripts/restodruid.lua b/scripts/restodruid.lua new file mode 100644 index 0000000..5006f5a --- /dev/null +++ b/scripts/restodruid.lua @@ -0,0 +1,436 @@ +local Tinkr, Bastion = ... + +local RestoModule = Bastion.Module:New('resto_druid') + +local Player = Bastion.UnitManager:Get('player') +local None = Bastion.UnitManager:Get('none') +local Target = Bastion.UnitManager:Get('target') + +local AnomalyDetectionMarkI = Bastion.SpellBook:GetSpell(382499) +local AutoAttack = Bastion.SpellBook:GetSpell(6603) +local MechanismBypass = Bastion.SpellBook:GetSpell(382501) +local OverloadElementalDeposit = Bastion.SpellBook:GetSpell(388213) +local ReviveBattlePets = Bastion.SpellBook:GetSpell(125439) +local WarStomp = Bastion.SpellBook:GetSpell(20549) +local ArmorSkills = Bastion.SpellBook:GetSpell(76275) +local Brawn = Bastion.SpellBook:GetSpell(154743) +local Cultivation = Bastion.SpellBook:GetSpell(20552) +local Endurance = Bastion.SpellBook:GetSpell(20550) +local Languages = Bastion.SpellBook:GetSpell(79746) +local MasterRiding = Bastion.SpellBook:GetSpell(90265) +local NatureResistance = Bastion.SpellBook:GetSpell(20551) +local WeaponSkills = Bastion.SpellBook:GetSpell(76300) +local ActivateEmpowerment = Bastion.SpellBook:GetSpell(357857) +local BlessingofOhnara = Bastion.SpellBook:GetSpell(384522) +local BronzeTimelock = Bastion.SpellBook:GetSpell(374990) +local ChampionAbility = Bastion.SpellBook:GetSpell(356550) +local CenarionWard = Bastion.SpellBook:GetSpell(102351) +local CombatAlly = Bastion.SpellBook:GetSpell(211390) +local ConstructAbility = Bastion.SpellBook:GetSpell(347013) +local CovenantAbility = Bastion.SpellBook:GetSpell(313347) +local GarrisonAbility = Bastion.SpellBook:GetSpell(161691) +local HeartEssence = Bastion.SpellBook:GetSpell(296208) +local HuntingCompanion = Bastion.SpellBook:GetSpell(376280) +local SanityRestorationOrb = Bastion.SpellBook:GetSpell(314955) +local SignatureAbility = Bastion.SpellBook:GetSpell(326526) +local SkywardAscent = Bastion.SpellBook:GetSpell(372610) +local SummonPocopoc = Bastion.SpellBook:GetSpell(360078) +local SurgeForward = Bastion.SpellBook:GetSpell(372608) +local Throw = Bastion.SpellBook:GetSpell(385265) +local VenthyrAbility = Bastion.SpellBook:GetSpell(315594) +local WartimeAbility = Bastion.SpellBook:GetSpell(264739) +local WhirlingSurge = Bastion.SpellBook:GetSpell(361584) +local PocopocZoneAbilitySkill = Bastion.SpellBook:GetSpell(363942) +local DragonridingBasics = Bastion.SpellBook:GetSpell(376777) +local LiftOff = Bastion.SpellBook:GetSpell(383363) +local ThrilloftheSkies = Bastion.SpellBook:GetSpell(383366) +local Vigor = Bastion.SpellBook:GetSpell(383359) +local WindsoftheIsles = Bastion.SpellBook:GetSpell(373586) +local Barkskin = Bastion.SpellBook:GetSpell(22812) +local BearForm = Bastion.SpellBook:GetSpell(5487) +local CatForm = Bastion.SpellBook:GetSpell(768) +local Cyclone = Bastion.SpellBook:GetSpell(33786) +local EntanglingRoots = Bastion.SpellBook:GetSpell(339) +local FerociousBite = Bastion.SpellBook:GetSpell(22568) +local FrenziedRegeneration = Bastion.SpellBook:GetSpell(22842) +local Growl = Bastion.SpellBook:GetSpell(6795) +local Innervate = Bastion.SpellBook:GetSpell(29166) +local Mangle = Bastion.SpellBook:GetSpell(33917) +local MarkoftheWild = Bastion.SpellBook:GetSpell(1126) +local Moonfire = Bastion.SpellBook:GetSpell(8921) +local MoonfireAura = Bastion.SpellBook:GetSpell(164812) +local Prowl = Bastion.SpellBook:GetSpell(5215) +local Rebirth = Bastion.SpellBook:GetSpell(20484) +local Regrowth = Bastion.SpellBook:GetSpell(8936) +local Rejuvenation = Bastion.SpellBook:GetSpell(774) +local RejuvenationAura = Bastion.SpellBook:GetSpell(25299) +local Revive = Bastion.SpellBook:GetSpell(50769) +local Rip = Bastion.SpellBook:GetSpell(1079) +local Shred = Bastion.SpellBook:GetSpell(5221) +local Soothe = Bastion.SpellBook:GetSpell(2908) +local StampedingRoar = Bastion.SpellBook:GetSpell(106898) +local Sunfire = Bastion.SpellBook:GetSpell(93402) +local SunfireAura = Bastion.SpellBook:GetSpell(164815) +local Swiftmend = Bastion.SpellBook:GetSpell(18562) +local TeleportMoonglade = Bastion.SpellBook:GetSpell(18960) +local Thrash = Bastion.SpellBook:GetSpell(106832) +local TigerDash = Bastion.SpellBook:GetSpell(252216) +local TravelForm = Bastion.SpellBook:GetSpell(783) +local UrsolsVortex = Bastion.SpellBook:GetSpell(102793) +local WildGrowth = Bastion.SpellBook:GetSpell(48438) +local Wrath = Bastion.SpellBook:GetSpell(5176) +local AquaticForm = Bastion.SpellBook:GetSpell(276012) +local FlightForm = Bastion.SpellBook:GetSpell(276029) +local TigerDash = Bastion.SpellBook:GetSpell(252216) +local Efflorescence = Bastion.SpellBook:GetSpell(145205) +local IncarnationTreeofLife = Bastion.SpellBook:GetSpell(33891) +local Ironbark = Bastion.SpellBook:GetSpell(102342) +local Lifebloom = Bastion.SpellBook:GetSpell(33763) +local LifebloomAura = Bastion.SpellBook:GetSpell(188550) +local NaturesCure = Bastion.SpellBook:GetSpell(88423) +local NaturesSwiftness = Bastion.SpellBook:GetSpell(132158) +local Revitalize = Bastion.SpellBook:GetSpell(212040) +local Tranquility = Bastion.SpellBook:GetSpell(740) +local MasteryHarmony = Bastion.SpellBook:GetSpell(77495) +local Moonfire = Bastion.SpellBook:GetSpell(8921) +local Wrath = Bastion.SpellBook:GetSpell(5176) +local BearForm = Bastion.SpellBook:GetSpell(5487) +local AdaptiveSwarm = Bastion.SpellBook:GetSpell(391888) +local AdaptiveSwarmBuff = Bastion.SpellBook:GetSpell(391891) +local ClearCasting = Bastion.SpellBook:GetSpell(16870) +local ConvokeTheSpirits = Bastion.SpellBook:GetSpell(391528) +local Flourish = Bastion.SpellBook:GetSpell(197721) +local SoulOfTheForest = Bastion.SpellBook:GetSpell(114108) +local Bursting = Bastion.SpellBook:GetSpell(240443) +local Rake = Bastion.SpellBook:GetSpell(1822) +local RakeAura = Bastion.SpellBook:GetSpell(155722) +local Starsurge = Bastion.SpellBook:GetSpell(197626) +local NaturesVigil = Bastion.SpellBook:GetSpell(124974) +local SpringBlossoms = Bastion.SpellBook:GetSpell(207386) + +local Lowest = Bastion.UnitManager:CreateCustomUnit('lowest', function(unit) + local lowest = nil + local lowestHP = math.huge + + Bastion.UnitManager:EnumFriends(function(unit) + if unit:IsDead() then + return false + end + + if Player:GetDistance(unit) > 40 then + return false + end + + if not Player:CanSee(unit) then + return false + end + + local hp = unit:GetHP() + if hp < lowestHP then + lowest = unit + lowestHP = hp + end + end) + + if not lowest then + lowest = Player + end + + return lowest +end) + +local Tank = Bastion.UnitManager:CreateCustomUnit('tank', function(unit) + local tank = nil + + Bastion.UnitManager:EnumFriends(function(unit) + if Player:GetDistance(unit) > 40 then + return false + end + + if not Player:CanSee(unit) then + return false + end + + if unit:IsDead() then + return false + end + + if unit:IsTank() then + tank = unit + return true + end + + return false + end) + + if tank == nil then + tank = Player + end + + return tank +end) + +local RejuvUnit = Bastion.UnitManager:CreateCustomUnit('rejuv', function(unit) + local lowest = nil + local lowestHP = math.huge + + Bastion.UnitManager:EnumFriends(function(unit) + if unit:IsDead() then + return false + end + + if not Player:CanSee(unit) then + return false + end + + if Player:GetDistance(unit) > 40 then + return false + end + + if not unit:IsDead() and Player:CanSee(unit) and not unit:GetAuras():FindMy(Rejuvenation):IsUp() then + + local hp = unit:GetHP() + if hp < lowestHP then + lowest = unit + lowestHP = hp + end + end + + end) + + + if lowest == nil then + lowest = Player + end + + return lowest +end) + +local SwiftmendUnit = Bastion.UnitManager:CreateCustomUnit('swiftmend', function(unit) + local lowest = nil + local lowestHP = math.huge + + Bastion.UnitManager:EnumFriends(function(unit) + if unit:IsDead() then + return false + end + + if not Player:CanSee(unit) then + return false + end + + if Player:GetDistance(unit) > 40 then + return false + end + + if ( + Player:CanSee(unit) and ( + (unit:GetAuras():FindMy(Regrowth):IsUp()) + or + ( + unit:GetAuras():FindMy(Rejuvenation):IsUp() and + not unit:GetAuras():FindMy(WildGrowth):IsUp()) + ) + ) then + + local hp = unit:GetHP() + if hp < lowestHP then + lowest = unit + lowestHP = hp + end + end + + end) + + + if lowest == nil then + lowest = Player + end + + return lowest +end) + +local RestoCommands = Bastion.Command:New('resto') + +local PLACE_EFFLO = false + +RestoCommands:Register('efflo', 'Request the engine to place an Efflorescence', function() + PLACE_EFFLO = true + Bastion:Print('Efflorescence will be placed on next cast') +end) + +local DefaultAPL = Bastion.APL:New('default') +local DamageAPL = Bastion.APL:New('damage') + +DefaultAPL:AddSpell( + Efflorescence:CastableIf(function(self) + return PLACE_EFFLO and Player:Exists() and self:IsKnownAndUsable() and not Player:IsCastingOrChanneling() + end):SetTarget(None):OnCast(function(self) + local loc = Bastion.UnitManager:FindFriendsCentroid(10, 40) + PLACE_EFFLO = false + self:Click(loc) + end) +) + +DefaultAPL:AddAction( + 'cat_form_shift', + function() + if IsShiftKeyDown() and not Player:GetAuras():FindMy(CatForm):IsUp() and not Player:IsCastingOrChanneling() then + CatForm:Cast(Player) + elseif not IsShiftKeyDown() and Player:GetAuras():FindMy(CatForm):IsUp() then + CancelShapeshiftForm() + end + end +) + +DefaultAPL:AddSpell( + NaturesSwiftness:CastableIf(function(self) + return Lowest:Exists() and self:IsKnownAndUsable() and not Player:IsCastingOrChanneling() + and Player:CanSee(Lowest) and + (Lowest:GetHP() < 75 or (Player:GetPartyHPAround(40, 65) >= 2 or Player:GetPartyHPAround(40, 70)) + ) + end):SetTarget(Lowest) +) + +DefaultAPL:AddSpell( + ConvokeTheSpirits:CastableIf(function(self) + return Player:Exists() and self:IsKnownAndUsable() and not Player:IsCastingOrChanneling() and + self:IsInRange(Player) and (Player:GetPartyHPAround(40, 65) >= 2 or Player:GetPartyHPAround(40, 70) >= 3) + end):SetTarget(Player) +) + +DefaultAPL:AddSpell( + Flourish:CastableIf(function(self) + return Player:Exists() and self:IsKnownAndUsable() and not Player:IsCastingOrChanneling() and + self:IsInRange(Player) and (Player:GetPartyHPAround(40, 65) >= 2 or Player:GetPartyHPAround(40, 70) >= 3) and + (not ConvokeTheSpirits:IsKnownAndUsable() and ConvokeTheSpirits:GetTimeSinceLastCast() > 7) and + WildGrowth:GetTimeSinceLastCast() <= 4 + end):SetTarget(Player) +) + +DefaultAPL:AddSpell( + AdaptiveSwarm:CastableIf(function(self) + return Lowest:Exists() and self:IsKnownAndUsable() and not Player:IsCastingOrChanneling() + and Player:CanSee(Lowest) + end):SetTarget(Lowest) +) + +DefaultAPL:AddSpell( + Swiftmend:CastableIf(function(self) + return SwiftmendUnit:Exists() and self:IsKnownAndUsable() and not Player:IsCastingOrChanneling() + and Player:CanSee(SwiftmendUnit) and + ( + SwiftmendUnit:GetHP() <= 85 or + ( + Lowest:GetPartyHPAround(30, 90) >= 3 or Lowest:GetPartyHPAround(30, 85) >= 2 + ) + ) + end):SetTarget(SwiftmendUnit) +) + +DefaultAPL:AddSpell( + WildGrowth:CastableIf(function(self) + return Lowest:Exists() and self:IsKnownAndUsable() and not Player:IsCastingOrChanneling() + and Player:CanSee(Lowest) and + ( + ( + Player:GetAuras():FindMy(SoulOfTheForest):IsUp() and + ( + Player:GetAuras():FindMy(SoulOfTheForest):GetRemainingTime() <= 5 or + Lowest:GetPartyHPAround(30, 90) >= 2)) or + (Lowest:GetPartyHPAround(30, 90) >= 3 or Lowest:GetPartyHPAround(30, 85) >= 2)) and not Player:IsMoving() + end):SetTarget(Lowest) +) + +DefaultAPL:AddSpell( + Regrowth:CastableIf(function(self) + return Lowest:Exists() and self:IsKnownAndUsable() and not Player:IsCastingOrChanneling() + and Player:CanSee(Lowest) and Lowest:GetHP() < 75 and + ( + NaturesSwiftness:GetTimeSinceLastCast() < 2 or Player:GetAuras():FindMy(NaturesSwiftness):IsUp() or + NaturesSwiftness:IsKnownAndUsable()) and not Player:IsMoving() + end):SetTarget(Lowest) +) + +DefaultAPL:AddSpell( + Regrowth:CastableIf(function(self) + return Lowest:Exists() and self:IsKnownAndUsable() and not Player:IsCastingOrChanneling() + and Player:CanSee(Lowest) and + (Lowest:GetHP() < 70 or (Lowest:GetHP() <= 85 and Player:GetAuras():FindMy(ClearCasting):IsUp())) and + not Player:GetAuras():FindMy(Regrowth):IsUp() and not Player:GetAuras():FindMy(SoulOfTheForest):IsUp() and + not Player:IsMoving() + end):SetTarget(Lowest) +) + +DefaultAPL:AddSpell( + CenarionWard:CastableIf(function(self) + return Lowest:Exists() and self:IsKnownAndUsable() and not Player:IsCastingOrChanneling() + and Player:CanSee(Lowest) and Lowest:GetHP() <= 90 + end):SetTarget(Lowest) +) + +DefaultAPL:AddSpell( + Ironbark:CastableIf(function(self) + return Lowest:Exists() and self:IsKnownAndUsable() and not Player:IsCastingOrChanneling() + and Player:CanSee(Lowest) and Lowest:GetHP() <= 70 and not Lowest:GetAuras():FindMy(CenarionWard):IsUp() + end):SetTarget(Lowest) +) + +DefaultAPL:AddSpell( + Lifebloom:CastableIf(function(self) + return Player:Exists() and self:IsKnownAndUsable() and not Player:IsCastingOrChanneling() + and not Player:GetAuras():FindMy(LifebloomAura):IsUp() and Player:IsAffectingCombat() + end):SetTarget(Player) +) + +DefaultAPL:AddSpell( + Lifebloom:CastableIf(function(self) + return Tank:Exists() and self:IsKnownAndUsable() and not Player:IsCastingOrChanneling() + and not Tank:GetAuras():FindMy(LifebloomAura):IsUp() and Tank:IsAffectingCombat() + end):SetTarget(Tank) +) + +DefaultAPL:AddSpell( + Rejuvenation:CastableIf(function(self) + return RejuvUnit:Exists() and self:IsKnownAndUsable() and not Player:IsCastingOrChanneling() + and Player:CanSee(RejuvUnit) and RejuvUnit:GetHP() <= 94 and + not Player:GetAuras():FindMy(SoulOfTheForest):IsUp() + end):SetTarget(RejuvUnit) +) + +DefaultAPL:AddSpell( + Sunfire:CastableIf(function(self) + return Target:Exists() and self:IsKnownAndUsable() and not Player:IsCastingOrChanneling() + and Player:CanSee(Target) and not Target:GetAuras():FindMy(SunfireAura):IsUp() + end):SetTarget(Target) +) + +DefaultAPL:AddSpell( + Moonfire:CastableIf(function(self) + return Target:Exists() and self:IsKnownAndUsable() and not Player:IsCastingOrChanneling() + and Player:CanSee(Target) and not Target:GetAuras():FindMy(MoonfireAura):IsUp() + end):SetTarget(Target) +) + +DefaultAPL:AddSpell( + Starsurge:CastableIf(function(self) + return Target:Exists() and self:IsKnownAndUsable() and not Player:IsCastingOrChanneling() + and Player:CanSee(Target) + end):SetTarget(Target) +) + +DefaultAPL:AddSpell( + Wrath:CastableIf(function(self) + return Target:Exists() and self:IsKnownAndUsable() and not Player:IsCastingOrChanneling() + and Player:CanSee(Target) and not Player:IsMoving() + end):SetTarget(Target) +) + +RestoModule:Sync(function() + if IsShiftKeyDown() and Player:GetAuras():FindMy(CatForm):IsUp() then + return DamageAPL:Execute() + end + DefaultAPL:Execute() +end) + +Bastion:Register(RestoModule) diff --git a/src/APL/APL.lua b/src/APL/APL.lua new file mode 100644 index 0000000..697afd4 --- /dev/null +++ b/src/APL/APL.lua @@ -0,0 +1,76 @@ +-- APL (Attack priority list) class + +local APL = {} +APL.__index = APL + +-- Constructor +function APL:New(name) + local self = setmetatable({}, APL) + + self.apl = {} + self.variables = {} + self.name = name + + return self +end + +-- Add a variable to the APL +function APL:SetVariable(name, value) + self.variables[name] = value +end + +-- Get and evaluate a variable +function APL:GetVariable(name) + return self.variables[name] +end + +-- Add a manual action to the APL +function APL:AddAction(action, cb) + table.insert(self.apl, { action = action, cb = cb }) +end + +-- Add a spell to the APL +function APL:AddSpell(spell, condition) + local castableFunc = spell.CastableIfFunc + local target = spell:GetTarget() + + table.insert(self.apl, { spell = spell, condition = condition, castableFunc = castableFunc, target = target }) +end + +-- Add an APL to the APL (for sub APLs) +function APL:AddAPL(apl, condition) + table.insert(self.apl, { apl = apl, condition = condition }) +end + +-- Execute the APL +function APL:Execute() + for _, actor in ipairs(self.apl) do + if actor.apl then + if actor.condition() then + -- print("Bastion: APL:Execute: Executing sub APL " .. actor.apl.name) + actor.apl:Execute() + end + end + if actor.spell then + if actor.condition then + -- print("Bastion: APL:Execute: Condition for spell " .. actor.spell:GetName()) + actor.spell:CastableIf(actor.castableFunc):Cast(actor.target, + actor.condition) + end + + -- print("Bastion: APL:Execute: No condition for spell " .. actor.spell:GetName()) + actor.spell:CastableIf(actor.castableFunc):Cast(actor.target) + end + if actor.action then + -- print("Bastion: APL:Execute: Executing action " .. actor.action) + actor.cb(self) + end + end +end + +-- tostring +function APL:__tostring() + return "Bastion.__APL(" .. self.name .. ")" +end + +return APL diff --git a/src/Aura/Aura.lua b/src/Aura/Aura.lua new file mode 100644 index 0000000..d81a4ea --- /dev/null +++ b/src/Aura/Aura.lua @@ -0,0 +1,245 @@ +local Tinkr, Bastion = ... + +-- Create a new Aura class +local Aura = {} + +function Aura:__index(k) + local response = Bastion.ClassMagic:Resolve(Aura, k) + + if response == nil then + response = rawget(self, k) + end + + if response == nil then + error("Aura:__index: " .. k .. " does not exist") + end + + return response +end + +function Aura:__tostring() + return "Bastion.__Aura(" .. self:GetSpell():GetID() .. ")" .. " - " .. (self:GetName() or "''") +end + +-- Constructor +function Aura:New(unit, index, type) + if unit == nil then + local self = setmetatable({}, Aura) + self.aura = { + name = nil, + icon = nil, + count = 0, + dispelType = nil, + duration = 0, + expirationTime = 0, + source = nil, + isStealable = false, + nameplateShowPersonal = false, + spellId = 0, + canApplyAura = false, + isBossDebuff = false, + castByPlayer = false, + nameplateShowAll = false, + timeMod = 0, + + index = nil, + type = nil, + + } + return self + end + + local name, icon, count, dispelType, duration, expirationTime, source, isStealable, nameplateShowPersonal, + spellId, canApplyAura, isBossDebuff, castByPlayer, nameplateShowAll, timeMod = UnitAura(unit.unit, index, type) + + local self = setmetatable({}, Aura) + self.aura = { + name = name, + icon = icon, + count = count, + dispelType = dispelType, + duration = duration, + expirationTime = expirationTime, + source = source, + isStealable = isStealable, + nameplateShowPersonal = nameplateShowPersonal, + spellId = spellId, + canApplyAura = canApplyAura, + isBossDebuff = isBossDebuff, + castByPlayer = castByPlayer, + nameplateShowAll = nameplateShowAll, + timeMod = timeMod, + + index = index, + type = type, + } + return self +end + +function Aura:CreateFromUnitAuraInfo(unitAuraInfo) + local self = setmetatable({}, Aura) + self.aura = { + name = unitAuraInfo.name, + icon = unitAuraInfo.icon, + count = unitAuraInfo.applications, + dispelType = unitAuraInfo.dispelName, + duration = unitAuraInfo.duration, + expirationTime = unitAuraInfo.expirationTime, + source = unitAuraInfo.sourceUnit, + isStealable = unitAuraInfo.isStealable, + nameplateShowPersonal = unitAuraInfo.nameplateShowPersonal, + spellId = unitAuraInfo.spellId, + canApplyAura = unitAuraInfo.canApplyAura, + isBossDebuff = unitAuraInfo.isBossAura, + castByPlayer = unitAuraInfo.isFromPlayerOrPlayerPet, + nameplateShowAll = unitAuraInfo.nameplateShowAll, + timeMod = unitAuraInfo.timeMod, + + index = nil, + type = unitAuraInfo.isHarmful and "HARMFUL" or "HELPFUL", + } + return self +end + +-- Check if the aura is valid +function Aura:IsValid() + return self.aura.name ~= nil +end + +-- Check if the aura is up +function Aura:IsUp() + return self:IsValid() and (self:GetDuration() == 0 or self:GetRemainingTime() > 0) +end + +-- Get the auras index +function Aura:GetIndex() + return self.aura.index +end + +-- Get the auras type +function Aura:GetType() + return self.aura.type +end + +-- Get the auras name +function Aura:GetName() + return self.aura.name +end + +-- Get the auras icon +function Aura:GetIcon() + return self.aura.icon +end + +-- Get the auras count +function Aura:GetCount() + return self.aura.count +end + +-- Get the auras dispel type +function Aura:GetDispelType() + return self.aura.dispelType +end + +-- Get the auras duration +function Aura:GetDuration() + return self.aura.duration +end + +-- Get the auras remaining time +function Aura:GetRemainingTime() + local remainingTime = self.aura.expirationTime - GetTime() + + if remainingTime < 0 then + remainingTime = 0 + end + + return remainingTime +end + +-- Get the auras expiration time +function Aura:GetExpirationTime() + return self.aura.expirationTime +end + +-- Get the auras source +function Aura:GetSource() + return Bastion.UnitManager[self.aura.source] +end + +-- Get the auras stealable status +function Aura:GetIsStealable() + return self.aura.isStealable +end + +-- Get the auras nameplate show personal status +function Aura:GetNameplateShowPersonal() + return self.aura.nameplateShowPersonal +end + +-- Get the auras spell id +function Aura:GetSpell() + return Bastion.Spell:New(self.aura.spellId) +end + +-- Get the auras can apply aura status +function Aura:GetCanApplyAura() + return self.aura.canApplyAura +end + +-- Get the auras is boss debuff status +function Aura:GetIsBossDebuff() + return self.aura.isBossDebuff +end + +-- Get the auras cast by player status +function Aura:GetCastByPlayer() + return self.aura.castByPlayer +end + +-- Get the auras nameplate show all status +function Aura:GetNameplateShowAll() + return self.aura.nameplateShowAll +end + +-- Get the auras time mod +function Aura:GetTimeMod() + return self.aura.timeMod +end + +-- Check if the aura is a buff +function Aura:IsBuff() + return self.aura.type == "HELPFUL" +end + +-- Check if the aura is a debuff +function Aura:IsDebuff() + return self.aura.type == "HARMFUL" +end + +-- Check if the aura is dispelable by a spell +function Aura:IsDispelableBySpell(spell) + if self:GetDispelType() == nil then + return false + end + + if self:GetDispelType() == 'Magic' and spell:IsMagicDispel() then + return true + end + + if self:GetDispelType() == 'Curse' and spell:IsCurseDispel() then + return true + end + + if self:GetDispelType() == 'Poison' and spell:IsPoisonDispel() then + return true + end + + if self:GetDispelType() == 'Disease' and spell:IsDiseaseDispel() then + return true + end + + return false +end + +return Aura diff --git a/src/AuraTable/AuraTable.lua b/src/AuraTable/AuraTable.lua new file mode 100644 index 0000000..09ac386 --- /dev/null +++ b/src/AuraTable/AuraTable.lua @@ -0,0 +1,229 @@ +local Tinkr, Bastion = ... + +-- Create a new AuraTable class +local AuraTable = {} +AuraTable.__index = AuraTable + +-- Constructor +function AuraTable:New(unit) + local self = setmetatable({}, AuraTable) + + self.unit = unit + self.buffs = {} + self.debuffs = {} + self.auras = {} + + -- Our player is usually the most important unit, so we cache the auras for it + self.playerAppliedBuffs = {} + self.playerAppliedDebuffs = {} + self.playerAuras = {} + self.guid = unit:GetGUID() + + Bastion.EventManager:RegisterWoWEvent('UNIT_AURA', function(unit, auras) + local u = Bastion.UnitManager[unit] + + if not self.unit:IsUnit(unit) then + return + end + + local addedAuras = auras.addedAuras + + if #addedAuras > 0 then + for i = 1, #addedAuras do + local aura = Bastion.Aura:CreateFromUnitAuraInfo(addedAuras[i]) + if aura:IsBuff() then + if aura:GetSource():Exists() and aura:GetSource():IsUnit(Bastion.UnitManager['player']) then + if not self.playerAppliedBuffs[aura:GetSpell():GetID()] then + self.playerAppliedBuffs[aura:GetSpell():GetID()] = {} + end + table.insert(self.playerAppliedBuffs[aura:GetSpell():GetID()], aura) + else + if not self.buffs[aura:GetSpell():GetID()] then + self.buffs[aura:GetSpell():GetID()] = {} + end + table.insert(self.buffs[aura:GetSpell():GetID()], aura) + end + else + if aura:GetSource():Exists() and aura:GetSource():IsUnit(Bastion.UnitManager['player']) then + if not self.playerAppliedDebuffs[aura:GetSpell():GetID()] then + self.playerAppliedDebuffs[aura:GetSpell():GetID()] = {} + end + table.insert(self.playerAppliedDebuffs[aura:GetSpell():GetID()], aura) + else + if not self.debuffs[aura:GetSpell():GetID()] then + self.debuffs[aura:GetSpell():GetID()] = {} + end + table.insert(self.debuffs[aura:GetSpell():GetID()], aura) + end + end + end + end + end) + + return self +end + +-- Get a units buffs +function AuraTable:GetUnitBuffs() + for i = 1, 40 do + local aura = Bastion.Aura:New(self.unit, i, 'HELPFUL') + if aura:IsValid() then + if aura:GetSource():Exists() and aura:GetSource():IsUnit(Bastion.UnitManager['player']) then + if not self.playerAppliedBuffs[aura:GetSpell():GetID()] then + self.playerAppliedBuffs[aura:GetSpell():GetID()] = {} + end + table.insert(self.playerAppliedBuffs[aura:GetSpell():GetID()], aura) + else + if not self.buffs[aura:GetSpell():GetID()] then + self.buffs[aura:GetSpell():GetID()] = {} + end + table.insert(self.buffs[aura:GetSpell():GetID()], aura) + end + end + end +end + +-- Get a units debuffs +function AuraTable:GetUnitDebuffs() + for i = 1, 40 do + local aura = Bastion.Aura:New(self.unit, i, 'HARMFUL') + if aura:IsValid() then + if aura:GetSource():Exists() and aura:GetSource():IsUnit(Bastion.UnitManager['player']) then + if not self.playerAppliedDebuffs[aura:GetSpell():GetID()] then + self.playerAppliedDebuffs[aura:GetSpell():GetID()] = {} + end + table.insert(self.playerAppliedDebuffs[aura:GetSpell():GetID()], aura) + else + if not self.debuffs[aura:GetSpell():GetID()] then + self.debuffs[aura:GetSpell():GetID()] = {} + end + table.insert(self.debuffs[aura:GetSpell():GetID()], aura) + end + end + end +end + +local function merge(t1, t2) + for k, v in pairs(t2) do + if type(v) == "table" then + if type(t1[k] or false) == "table" then + merge(t1[k] or {}, t2[k] or {}) + else + t1[k] = v + end + else + t1[k] = v + end + end + return t1 +end + +-- Update auras +function AuraTable:Update() + self:Clear() + self.lastUpdate = GetTime() + + self:GetUnitBuffs() + self:GetUnitDebuffs() + self.auras = merge(self.buffs, self.debuffs) + self.playerAuras = merge(self.playerAppliedBuffs, self.playerAppliedDebuffs) +end + +-- Get a units auras +function AuraTable:GetUnitAuras() + -- For token units, we need to check if the GUID has changed + if self.unit:GetGUID() ~= self.guid then + self.guid = self.unit:GetGUID() + self:Update() + return self.auras + end + + -- Cache the auras for the unit so we don't have to query the API every time we want to check if the unit has a specific aura or not + -- If it's less than .4 seconds since the last time we queried the API, return the cached auras + if self.lastUpdate and GetTime() - self.lastUpdate < 0.5 then + return self.auras + end + + self:Update() + return self.auras +end + +-- Get a units auras +function AuraTable:GetMyUnitAuras() + -- For token units, we need to check if the GUID has changed + if self.unit:GetGUID() ~= self.guid then + self.guid = self.unit:GetGUID() + self:Update() + return self.playerAuras + end + + -- Cache the auras for the unit so we don't have to query the API every time we want to check if the unit has a specific aura or not + -- If it's less than .4 seconds since the last time we queried the API, return the cached auras + if self.lastUpdate and GetTime() - self.lastUpdate < 0.5 then + return self.playerAuras + end + + self:Update() + return self.playerAuras +end + +-- Clear the aura table +function AuraTable:Clear() + self.buffs = {} + self.debuffs = {} + self.auras = {} + self.playerAppliedBuffs = {} + self.playerAppliedDebuffs = {} + self.playerAuras = {} +end + +-- Check if the unit has a specific aura +function AuraTable:Find(spell) + local auras = self:GetUnitAuras() + local aura = auras[spell:GetID()] + + if aura then + return aura[1] + end + + return Bastion.Aura:New() +end + +function AuraTable:FindMy(spell) + local auras = self:GetMyUnitAuras() + local aura = auras[spell:GetID()] + + if aura then + return aura[1] + end + + return Bastion.Aura:New() +end + +-- Has any stealable aura +function AuraTable:HasAnyStealableAura() + for _, auras in pairs(self:GetUnitAuras()) do + for _, aura in pairs(auras) do + if aura:GetIsStealable() then + return true + end + end + end + + return false +end + +-- Has any dispelable aura +function AuraTable:HasAnyDispelableAura(spell) + for _, auras in pairs(self:GetUnitAuras()) do + for _, aura in pairs(auras) do + if aura:IsDebuff() and aura:IsDispelableBySpell(spell) then + return true + end + end + end + + return false +end + +return AuraTable diff --git a/src/Cache/Cache.lua b/src/Cache/Cache.lua new file mode 100644 index 0000000..a88f0b0 --- /dev/null +++ b/src/Cache/Cache.lua @@ -0,0 +1,46 @@ +local Cache = {} +Cache.__index = Cache + +-- Constructor +function Cache:New() + local self = setmetatable({}, Cache) + self.cache = {} + return self +end + +function Cache:Set(key, value, duration) + self.cache = self.cache or {} + self.cache[key] = { + value = value, + duration = duration, + time = GetTime() + } +end + +function Cache:Get(key) + self.cache = self.cache or {} + local cache = self.cache[key] + if cache and cache.time + cache.duration > GetTime() then + return cache.value + end + + -- It's old or doesn't exist, so remove it + self.cache[key] = nil + + return nil +end + +function Cache:IsCached(key) + self.cache = self.cache or {} + local cache = self.cache[key] + if cache and cache.time + cache.duration > GetTime() then + return true + end + + -- It's old or doesn't exist, so remove it + self.cache[key] = nil + + return false +end + +return Cache diff --git a/src/Cacheable/Cacheable.lua b/src/Cacheable/Cacheable.lua new file mode 100644 index 0000000..b344814 --- /dev/null +++ b/src/Cacheable/Cacheable.lua @@ -0,0 +1,68 @@ +local Tinkr, Bastion = ... + +-- Define a Cacheable class +local Cacheable = { + cache = nil, + callback = nil, + value = nil +} + +-- On index check the cache to be valid and return the value or reconstruct the value and return it +function Cacheable:__index(k) + if Cacheable[k] then + return Cacheable[k] + end + + if self.cache == nil then + error("Cacheable:__index: " .. k .. " does not exist") + end + + if not self.cache:IsCached('self') then + self.value = self.callback() + self.cache:Set('self', self.value, 0.5) + end + + return self.value[k] +end + +-- When the object is accessed return the value +function Cacheable:__tostring() + return "Bastion.__Cacheable(" .. tostring(self.value) .. ")" +end + +-- Create +function Cacheable:New(value, cb) + local self = setmetatable({}, Cacheable) + + self.cache = Bastion.Cache:New() + self.value = value + self.callback = cb + + self.cache:Set('self', self.value, 0.5) + + return self +end + +-- Try to update the value +function Cacheable:TryUpdate() + if self.cache:IsCached("value") then + self.value = self.callback() + end +end + +-- Update the value +function Cacheable:Update() + self.value = self.callback() +end + +-- Set a new value +function Cacheable:Set(value) + self.value = value +end + +-- Set a new callback +function Cacheable:SetCallback(cb) + self.callback = cb +end + +return Cacheable diff --git a/src/Class/Class.lua b/src/Class/Class.lua new file mode 100644 index 0000000..d9c4a97 --- /dev/null +++ b/src/Class/Class.lua @@ -0,0 +1,51 @@ +local Tinkr, Bastion = ... + +-- Create a new Class class +local Class = {} + +function Class:__index(k) + local response = Bastion.ClassMagic:Resolve(Class, k) + + if response == nil then + response = rawget(self, k) + end + + if response == nil then + error("Class:__index: " .. k .. " does not exist") + end + + return response +end + +-- Constructor +function Class:New(locale, name, id) + local self = setmetatable({}, Class) + self.class = { + locale = locale, + name = name, + id = id + } + return self +end + +-- Get the classes locale +function Class:GetLocale() + return self.class.locale +end + +-- Get the classes name +function Class:GetName() + return self.class.name +end + +-- Get the classes id +function Class:GetID() + return self.class.id +end + +-- Return the classes color +function Class:GetColor() + return C_ClassColor.GetClassColor(self.class.name) +end + +return Class diff --git a/src/ClassMagic/ClassMagic.lua b/src/ClassMagic/ClassMagic.lua new file mode 100644 index 0000000..b4bef57 --- /dev/null +++ b/src/ClassMagic/ClassMagic.lua @@ -0,0 +1,49 @@ +local ClassMagic = {} +ClassMagic.__index = ClassMagic + +function ClassMagic:Resolve(Class, key) + if Class[key] or Class[key] == false then + return Class[key] + end + + if Class['Get' .. key:sub(1, 1):upper() .. key:sub(2)] then + local func = Class['Get' .. key:sub(1, 1):upper() .. key:sub(2)] + + -- Call the function and return the result if there's more than one return value return it as a table + local result = { func(self) } + if #result > 1 then + return result + end + + return result[1] + end + + + if Class['Get' .. key:upper()] then + local func = Class['Get' .. key:upper()] + + -- Call the function and return the result if there's more than one return value return it as a table + local result = { func(self) } + if #result > 1 then + return result + end + + return result[1] + end + + if Class['Is' .. key:upper()] then + local func = Class['Is' .. key:upper()] + + -- Call the function and return the result if there's more than one return value return it as a table + local result = { func(self) } + if #result > 1 then + return result + end + + return result[1] + end + + return Class[key] +end + +return ClassMagic diff --git a/src/Command/Command.lua b/src/Command/Command.lua new file mode 100644 index 0000000..1341ed9 --- /dev/null +++ b/src/Command/Command.lua @@ -0,0 +1,60 @@ +-- Create a wow command handler class + +local Command = {} +Command.__index = Command + +function Command:__tostring() + return "Command(" .. self.command .. ")" +end + +function Command:New(prefix) + local self = setmetatable({}, Command) + + self.prefix = prefix + self.commands = {} + + _G['SLASH_' .. prefix:upper() .. '1'] = "/" .. prefix + SlashCmdList[prefix:upper()] = function(msg, editbox) + self:OnCommand(msg) + end + + return self +end + +function Command:Register(command, helpmsg, cb) + self.commands[command] = { + helpmsg = helpmsg, + cb = cb + } +end + +function Command:Parse(msg) + local args = {} + for arg in msg:gmatch("%S+") do + table.insert(args, arg) + end + + return args +end + +function Command:OnCommand(msg) + local args = self:Parse(msg) + + if #args == 0 then + self:PrintHelp() + return + end + + local runner = self.commands[args[1]] + if runner then + runner.cb(args) + end +end + +function Command:PrintHelp() + for k, v in pairs(self.commands) do + print('/' .. self.prefix .. ' ' .. k .. " - " .. v.helpmsg) + end +end + +return Command diff --git a/src/EventManager/EventManager.lua b/src/EventManager/EventManager.lua new file mode 100644 index 0000000..eade68e --- /dev/null +++ b/src/EventManager/EventManager.lua @@ -0,0 +1,60 @@ +-- Create an EventManager class + +local EventManager = { + events = {}, + eventHandlers = {}, + wowEventHandlers = {}, + frame = nil +} + +EventManager.__index = EventManager + +-- Constructor +function EventManager:New() + local self = setmetatable({}, EventManager) + self.events = {} + self.eventHandlers = {} + self.wowEventHandlers = {} + + -- Frame for wow events + self.frame = CreateFrame("Frame") + + self.frame:SetScript('OnEvent', function(f, event, ...) + if self.wowEventHandlers[event] then + for _, callback in ipairs(self.wowEventHandlers[event]) do + callback(...) + end + end + end) + + + return self +end + +-- Register an event +function EventManager:RegisterEvent(event, handler) + if not self.events[event] then + self.events[event] = {} + end + table.insert(self.events[event], handler) +end + +-- Register a wow event +function EventManager:RegisterWoWEvent(event, handler) + if not self.wowEventHandlers[event] then + self.wowEventHandlers[event] = {} + self.frame:RegisterEvent(event) + end + table.insert(self.wowEventHandlers[event], handler) +end + +-- Trigger an event +function EventManager:TriggerEvent(event, ...) + if self.events[event] then + for _, handler in pairs(self.events[event]) do + handler(...) + end + end +end + +return EventManager diff --git a/src/Item/Item.lua b/src/Item/Item.lua new file mode 100644 index 0000000..19e511b --- /dev/null +++ b/src/Item/Item.lua @@ -0,0 +1,343 @@ +local Tinkr, Bastion = ... + +-- Create a new Item class +local Item = { + UsableIfFunc = false, + PreUseFunc = false, + OnUseFunc = false, + wasLooking = false, + lastUseAt = 0, + conditions = {}, + target = false, +} + +local usableExcludes = { + [18562] = true, +} + +function Item:__index(k) + local response = Bastion.ClassMagic:Resolve(Item, k) + + if response == nil then + response = rawget(self, k) + end + + if response == nil then + error("Item:__index: " .. k .. " does not exist") + end + + return response +end + +-- tostring +function Item:__tostring() + return "Bastion.__Item(" .. self:GetID() .. ")" .. " - " .. self:GetName() +end + +-- Constructor +function Item:New(id) + local self = setmetatable({}, Item) + + self.ItemID = id + + return self +end + +-- Get the Items id +function Item:GetID() + return self.ItemID +end + +-- Get the Items name +function Item:GetName() + return GetItemInfo(self:GetID()) +end + +-- Get the Items icon +function Item:GetIcon() + return select(3, GetItemInfo(self:GetID())) +end + +-- Get the Items cooldown +function Item:GetCooldown() + return select(2, GetItemCooldown(self:GetID())) +end + +-- Return the Usable function +function Item:GetUsableFunction() + return self.UsableIfFunc +end + +-- Return the preUse function +function Item:GetPreUseFunction() + return self.PreUseFunc +end + +-- Get the on Use func +function Item:GetOnUseFunction() + return self.OnUseFunc +end + +-- Get the Items cooldown remaining +function Item:GetCooldownRemaining() + local start, duration = GetItemCooldown(self:GetID()) + return start + duration - GetTime() +end + +-- Use the Item +function Item:Use(unit, condition) + if condition and not self:EvaluateCondition(condition) then + return false + end + + if not self:Usable() then + return false + end + + -- Call pre Use function + if self:GetPreUseFunction() then + self:GetPreUseFunction()(self) + end + + -- Check if the mouse was looking + self.wasLooking = IsMouselooking() + + -- Use the Item + UseItemByName(self:GetName(), unit.unit) + + Bastion:Debug("Using", self) + + -- Set the last Use time + self.lastUseAt = GetTime() + + -- Call post Use function + if self:GetOnUseFunction() then + self:GetOnUseFunction()(self) + end +end + +-- Check if the Item is known +function Item:IsEquipped() + return IsEquippedItem(self:GetID()) +end + +-- Check if the Item is on cooldown +function Item:IsOnCooldown() + return select(2, GetItemCooldown(self:GetID())) > 0 +end + +-- Check if the Item is usable +function Item:IsUsable() + local usable, noMana = IsUsableItem(self:GetID()) + return usable or usableExcludes[self:GetID()] +end + +-- Check if the Item is Usable +function Item:IsEquippedAndUsable() + return (self:IsEquippable() and self:IsEquipped()) or (not self:IsEquippable() and self:IsUsable()) +end + +-- Is equippable +function Item:IsEquippable() + return IsEquippableItem(self:GetID()) +end + +-- Check if the Item is Usable +function Item:Usable() + if self:GetUsableFunction() then + return self:GetUsableFunction()(self) + end + + return self:IsKnownAndUsable() +end + +-- Set a script to check if the Item is Usable +function Item:UsableIf(func) + self.UsableIfFunc = func + return self +end + +-- Set a script to run before the Item has been Use +function Item:PreUse(func) + self.PreUseFunc = func + return self +end + +-- Set a script to run after the Item has been Use +function Item:OnUse(func) + self.OnUseFunc = func + return self +end + +-- Get was looking +function Item:GetWasLooking() + return self.wasLooking +end + +-- Click the Item +function Item:Click(x, y, z) + if type(x) == 'table' then + x, y, z = x.x, x.y, x.z + end + if IsItemPending() == 64 then + MouselookStop() + Click(x, y, z) + if self:GetWasLooking() then + MouselookStart() + end + return true + end + return false +end + +-- Check if the Item is Usable and Use it +function Item:Call(unit) + if self:Usable() then + self:Use(unit) + return true + end + return false +end + +-- Check if the Item is in range of the unit +function Item:IsInRange(unit) + local name, rank, icon, UseTime, Itemmin, Itemmax, ItemID = GetItemInfo(self:GetID()) + + local them = Object(unit.unit) + + local tx, ty, tz = ObjectPosition(unit.unit) + local px, py, pz = ObjectPosition('player') + + if not them then + return false + end + + if tx == 0 and ty == 0 and tz == 0 then + return true + end + + local combatReach = ObjectCombatReach("player") + local themCombatReach = ObjectCombatReach(unit.unit) + + if Bastion.UnitManager['player']:InMelee(unit) and Itemmin == 0 then + return true + end + + local distance = FastDistance(px, py, pz, tx, ty, tz) + + if Itemmax + and distance >= Itemmin + and distance <= combatReach + themCombatReach + Itemmax + then + return true + end + + return false +end + +-- Get the last use time +function Item:GetLastUseTime() + return self.lastUseAt +end + +-- Get time since last use +function Item:GetTimeSinceLastUse() + if not self:GetLastUseTime() then + return math.huge + end + return GetTime() - self:GetLastUseTime() +end + +-- Get the Items charges +function Item:GetCharges() + return GetItemCharges(self:GetID()) +end + +-- Get the Items charges remaining +function Item:GetChargesRemaining() + local charges, maxCharges, start, duration = GetItemCharges(self:GetID()) + return charges +end + +-- Create a condition for the Item +function Item:Condition(name, func) + self.conditions[name] = { + func = func + } + return self +end + +-- Get a condition for the Item +function Item:GetCondition(name) + local condition = self.conditions[name] + if condition then + return condition + end + + return nil +end + +-- Evaluate a condition for the Item +function Item:EvaluateCondition(name) + local condition = self:GetCondition(name) + if condition then + return condition.func(self) + end + + return false +end + +-- Check if the Item has a condition +function Item:HasCondition(name) + local condition = self:GetCondition(name) + if condition then + return true + end + + return false +end + +-- Set the Items target +function Item:SetTarget(unit) + self.target = unit + return self +end + +-- Get the Items target +function Item:GetTarget() + return self.target +end + +-- IsMagicDispel +function Item:IsMagicDispel() + return ({ + [88423] = true + })[self:GetID()] +end + +-- IsCurseDispel +function Item:IsCurseDispel() + return ({ + [88423] = true + })[self:GetID()] +end + +-- IsPoisonDispel +function Item:IsPoisonDispel() + return ({ + [88423] = true + })[self:GetID()] +end + +-- IsDiseaseDispel +function Item:IsDiseaseDispel() + return ({ + + })[self:GetID()] +end + +function Item:IsItem(Item) + return self:GetID() == Item:GetID() +end + +return Item diff --git a/src/ItemBook/ItemBook.lua b/src/ItemBook/ItemBook.lua new file mode 100644 index 0000000..9b3eebb --- /dev/null +++ b/src/ItemBook/ItemBook.lua @@ -0,0 +1,23 @@ +local Tinkr, Bastion = ... + +-- Create a new ItemBook class +local ItemBook = {} +ItemBook.__index = ItemBook + +-- Constructor +function ItemBook:New() + local self = setmetatable({}, ItemBook) + self.items = {} + return self +end + +-- Get a spell from the ItemBook +function ItemBook:GetItem(id) + if self.items[id] == nil then + self.items[id] = Bastion.Item:New(id) + end + + return self.items[id] +end + +return ItemBook diff --git a/src/Module/Module.lua b/src/Module/Module.lua new file mode 100644 index 0000000..6c19d98 --- /dev/null +++ b/src/Module/Module.lua @@ -0,0 +1,65 @@ +-- Create a module class for a bastion module + +local Module = {} +Module.__index = Module + +-- __tostring +function Module:__tostring() + return "Bastion.__Module(" .. self.name .. ")" +end + +function Module:New(name) + local module = {} + setmetatable(module, Module) + + module.name = name + module.enabled = false + module.synced = {} + + return module +end + +-- Enable the module +function Module:Enable() + self.enabled = true +end + +-- Disable the module +function Module:Disable() + self.enabled = false +end + +-- Toggle the module +function Module:Toggle() + if self.enabled then + self:Disable() + else + self:Enable() + end +end + +-- Add a function to the sync list +function Module:Sync(func) + table.insert(self.synced, func) +end + +-- Remove a function from the sync list +function Module:Unsync(func) + for i = 1, #self.synced do + if self.synced[i] == func then + table.remove(self.synced, i) + return + end + end +end + +-- Sync +function Module:Tick() + if self.enabled then + for i = 1, #self.synced do + self.synced[i]() + end + end +end + +return Module diff --git a/src/Spell/Spell.lua b/src/Spell/Spell.lua new file mode 100644 index 0000000..63d85df --- /dev/null +++ b/src/Spell/Spell.lua @@ -0,0 +1,340 @@ +local Tinkr, Bastion = ... + +-- Create a new Spell class +local Spell = { + CastableIfFunc = false, + PreCastFunc = false, + OnCastFunc = false, + wasLooking = false, + lastCastAt = 0, + conditions = {}, + target = false, +} + +local usableExcludes = { + [18562] = true, +} + +function Spell:__index(k) + local response = Bastion.ClassMagic:Resolve(Spell, k) + + if response == nil then + response = rawget(self, k) + end + + if response == nil then + error("Spell:__index: " .. k .. " does not exist") + end + + return response +end + +-- tostring +function Spell:__tostring() + return "Bastion.__Spell(" .. self:GetID() .. ")" .. " - " .. self:GetName() +end + +-- Constructor +function Spell:New(id) + local self = setmetatable({}, Spell) + + self.spellID = id + + return self +end + +-- Get the spells id +function Spell:GetID() + return self.spellID +end + +-- Get the spells name +function Spell:GetName() + return GetSpellInfo(self:GetID()) +end + +-- Get the spells icon +function Spell:GetIcon() + return select(3, GetSpellInfo(self:GetID())) +end + +-- Get the spells cooldown +function Spell:GetCooldown() + return select(2, GetSpellCooldown(self:GetID())) +end + +-- Return the castable function +function Spell:GetCastableFunction() + return self.CastableIfFunc +end + +-- Return the precast function +function Spell:GetPreCastFunction() + return self.PreCastFunc +end + +-- Get the on cast func +function Spell:GetOnCastFunction() + return self.OnCastFunc +end + +-- Get the spells cooldown remaining +function Spell:GetCooldownRemaining() + local start, duration = GetSpellCooldown(self:GetID()) + return start + duration - GetTime() +end + +-- Cast the spell +function Spell:Cast(unit, condition) + if condition and not self:EvaluateCondition(condition) then + return false + end + + if not self:Castable() then + return false + end + + -- Call pre cast function + if self:GetPreCastFunction() then + self:GetPreCastFunction()(self) + end + + -- Check if the mouse was looking + self.wasLooking = IsMouselooking() + + -- Cast the spell + CastSpellByName(self:GetName(), unit.unit) + + Bastion:Debug("Casting", self) + + -- Set the last cast time + self.lastCastAt = GetTime() + + -- Call post cast function + if self:GetOnCastFunction() then + self:GetOnCastFunction()(self) + end +end + +-- Check if the spell is known +function Spell:IsKnown() + local isKnown = IsSpellKnown(self:GetID()) + local isPlayerSpell = IsPlayerSpell(self:GetID()) + return isKnown or isPlayerSpell +end + +-- Check if the spell is on cooldown +function Spell:IsOnCooldown() + return select(2, GetSpellCooldown(self:GetID())) > 0 +end + +-- Check if the spell is usable +function Spell:IsUsable() + local usable, noMana = IsUsableSpell(self:GetID()) + return usable or usableExcludes[self:GetID()] +end + +-- Check if the spell is castable +function Spell:IsKnownAndUsable() + return self:IsKnown() and not self:IsOnCooldown() and self:IsUsable() +end + +-- Check if the spell is castable +function Spell:Castable() + if self:GetCastableFunction() then + return self:GetCastableFunction()(self) + end + + return self:IsKnownAndUsable() +end + +-- Set a script to check if the spell is castable +function Spell:CastableIf(func) + self.CastableIfFunc = func + return self +end + +-- Set a script to run before the spell has been cast +function Spell:PreCast(func) + self.PreCastFunc = func + return self +end + +-- Set a script to run after the spell has been cast +function Spell:OnCast(func) + self.OnCastFunc = func + return self +end + +-- Get was looking +function Spell:GetWasLooking() + return self.wasLooking +end + +-- Click the spell +function Spell:Click(x, y, z) + if type(x) == 'table' then + x, y, z = x.x, x.y, x.z + end + if IsSpellPending() == 64 then + MouselookStop() + Click(x, y, z) + if self:GetWasLooking() then + MouselookStart() + end + return true + end + return false +end + +-- Check if the spell is castable and cast it +function Spell:Call(unit) + if self:Castable() then + self:Cast(unit) + return true + end + return false +end + +-- Check if the spell is in range of the unit +function Spell:IsInRange(unit) + local name, rank, icon, castTime, spellmin, spellmax, spellID = GetSpellInfo(self:GetID()) + + local them = Object(unit.unit) + + local tx, ty, tz = ObjectPosition(unit.unit) + local px, py, pz = ObjectPosition('player') + + if not them then + return false + end + + if tx == 0 and ty == 0 and tz == 0 then + return true + end + + local combatReach = ObjectCombatReach("player") + local themCombatReach = ObjectCombatReach(unit.unit) + + if Bastion.UnitManager['player']:InMelee(unit) and spellmin == 0 then + return true + end + + local distance = FastDistance(px, py, pz, tx, ty, tz) + + if spellmax + and distance >= spellmin + and distance <= combatReach + themCombatReach + spellmax + then + return true + end + + return false +end + +-- Get the last cast time +function Spell:GetLastCastTime() + return self.lastCastAt +end + +-- Get time since last cast +function Spell:GetTimeSinceLastCast() + if not self:GetLastCastTime() then + return math.huge + end + return GetTime() - self:GetLastCastTime() +end + +-- Get the spells charges +function Spell:GetCharges() + return GetSpellCharges(self:GetID()) +end + +-- Get the spells charges remaining +function Spell:GetChargesRemaining() + local charges, maxCharges, start, duration = GetSpellCharges(self:GetID()) + return charges +end + +-- Create a condition for the spell +function Spell:Condition(name, func) + self.conditions[name] = { + func = func + } + return self +end + +-- Get a condition for the spell +function Spell:GetCondition(name) + local condition = self.conditions[name] + if condition then + return condition + end + + return nil +end + +-- Evaluate a condition for the spell +function Spell:EvaluateCondition(name) + local condition = self:GetCondition(name) + if condition then + return condition.func(self) + end + + return false +end + +-- Check if the spell has a condition +function Spell:HasCondition(name) + local condition = self:GetCondition(name) + if condition then + return true + end + + return false +end + +-- Set the spells target +function Spell:SetTarget(unit) + self.target = unit + return self +end + +-- Get the spells target +function Spell:GetTarget() + return self.target +end + +-- IsMagicDispel +function Spell:IsMagicDispel() + return ({ + [88423] = true + })[self:GetID()] +end + +-- IsCurseDispel +function Spell:IsCurseDispel() + return ({ + [88423] = true + })[self:GetID()] +end + +-- IsPoisonDispel +function Spell:IsPoisonDispel() + return ({ + [88423] = true + })[self:GetID()] +end + +-- IsDiseaseDispel +function Spell:IsDiseaseDispel() + return ({ + + })[self:GetID()] +end + +function Spell:IsSpell(spell) + return self:GetID() == spell:GetID() +end + +return Spell diff --git a/src/SpellBook/SpellBook.lua b/src/SpellBook/SpellBook.lua new file mode 100644 index 0000000..9519e0a --- /dev/null +++ b/src/SpellBook/SpellBook.lua @@ -0,0 +1,23 @@ +local Tinkr, Bastion = ... + +-- Create a new SpellBook class +local SpellBook = {} +SpellBook.__index = SpellBook + +-- Constructor +function SpellBook:New() + local self = setmetatable({}, SpellBook) + self.spells = {} + return self +end + +-- Get a spell from the spellbook +function SpellBook:GetSpell(id) + if self.spells[id] == nil then + self.spells[id] = Bastion.Spell:New(id) + end + + return self.spells[id] +end + +return SpellBook diff --git a/src/Timer/Timer.lua b/src/Timer/Timer.lua new file mode 100644 index 0000000..14ec866 --- /dev/null +++ b/src/Timer/Timer.lua @@ -0,0 +1,41 @@ +local Tinkr, Bastion = ... + +-- Create a new Timer class +local Timer = { + startTime = nil, + resetAfterCombat = false, +} +Timer.__index = Timer + +-- Constructor +function Timer:New(type) + local self = setmetatable({}, Timer) + self.startTime = nil + self.type = type + return self +end + +-- Start the timer +function Timer:Start() + self.startTime = GetTime() +end + +-- Get the time since the timer was started +function Timer:GetTime() + if not self:IsRunning() then + return 0 + end + return GetTime() - self.startTime +end + +-- Check if the timer is running +function Timer:IsRunning() + return self.startTime ~= nil +end + +-- Reset the timer +function Timer:Reset() + self.startTime = nil +end + +return Timer diff --git a/src/Unit/Unit.lua b/src/Unit/Unit.lua new file mode 100644 index 0000000..79a9a0f --- /dev/null +++ b/src/Unit/Unit.lua @@ -0,0 +1,368 @@ +local Tinkr, Bastion = ... + +-- Create a new Unit class +local Unit = { + cache = nil, + aura_table = nil, + unit = nil +} + +function Unit:__index(k) + local response = Bastion.ClassMagic:Resolve(Unit, k) + + if response == nil then + response = rawget(self, k) + end + + if response == nil then + error("Unit:__index: " .. k .. " does not exist") + end + + return response +end + +-- tostring +function Unit:__tostring() + return "Bastion.__Unit(" .. self.unit .. ")" .. " - " .. (self:GetName() or '') +end + +-- Constructor +function Unit:New(unit) + local self = setmetatable({}, Unit) + self.unit = unit + self.cache = Bastion.Cache:New() + self.aura_table = Bastion.AuraTable:New(self) + return self +end + +-- Check if the unit is valid +function Unit:IsValid() + return self.unit ~= nil and self:Exists() +end + +-- Check if the unit exists +function Unit:Exists() + return UnitExists(self.unit) +end + +-- Get the units token +function Unit:Token() + return self.unit +end + +-- Get the units name +function Unit:GetName() + return UnitName(self.unit) +end + +-- Get the units GUID +function Unit:GetGUID() + return ObjectGUID(self.unit) +end + +-- Get the units health +function Unit:GetHealth() + return UnitHealth(self.unit) +end + +-- Get the units max health +function Unit:GetMaxHealth() + return UnitHealthMax(self.unit) +end + +-- Get the units health percentage +function Unit:GetHP() + return self:GetHealth() / self:GetMaxHealth() * 100 +end + +function Unit:GetHealthPercent() + return self:GetHP() +end + +-- Get the units power type +function Unit:GetPowerType() + return UnitPowerType(self.unit) +end + +-- Get the units power +function Unit:GetPower(powerType) + local powerType = powerType or self:GetPowerType() + return UnitPower(self.unit, powerType) +end + +-- Get the units max power +function Unit:GetMaxPower(powerType) + local powerType = powerType or self:GetPowerType() + return UnitPowerMax(self.unit, powerType) +end + +-- Get the units power percentage +function Unit:GetPP(powerType) + local powerType = powerType or self:GetPowerType() + return self:GetPower(powerType) / self:GetMaxPower(powerType) * 100 +end + +-- Get the units power deficit +function Unit:GetPowerDeficit(powerType) + local powerType = powerType or self:GetPowerType() + return self:GetMaxPower(powerType) - self:GetPower(powerType) +end + +-- Get the units position +function Unit:GetPosition() + local x, y, z = ObjectPosition(self.unit) + return Bastion.Vector3:New(x, y, z) +end + +-- Get the units distance from another unit +function Unit:GetDistance(unit) + local pself = self:GetPosition() + local punit = unit:GetPosition() + + return pself:Distance(punit) +end + +-- Is the unit dead +function Unit:IsDead() + return UnitIsDeadOrGhost(self.unit) +end + +-- Is the unit alive +function Unit:IsAlive() + return not UnitIsDeadOrGhost(self.unit) +end + +-- Is the unit a pet +function Unit:IsPet() + return UnitIsUnit(self.unit, "pet") +end + +-- Is the unit a friendly unit +function Unit:IsFriendly() + return UnitIsFriend("player", self.unit) +end + +-- Is the unit a hostile unit +function Unit:IsHostile() + return UnitIsEnemy("player", self.unit) +end + +-- Is the unit a boss +function Unit:IsBoss() + return UnitClassification(self.unit) == "worldboss" +end + +-- Is the unit a target +function Unit:IsTarget() + return UnitIsUnit(self.unit, "target") +end + +-- Is the unit a focus +function Unit:IsFocus() + return UnitIsUnit(self.unit, "focus") +end + +-- Is the unit a mouseover +function Unit:IsMouseover() + return UnitIsUnit(self.unit, "mouseover") +end + +-- Is the unit a tank +function Unit:IsTank() + return UnitGroupRolesAssigned(self.unit) == "TANK" +end + +-- Is the unit a healer +function Unit:IsHealer() + return UnitGroupRolesAssigned(self.unit) == "HEALER" +end + +-- Is the unit a damage dealer +function Unit:IsDamage() + return UnitGroupRolesAssigned(self.unit) == "DAMAGER" +end + +-- Is the unit a player +function Unit:IsPlayer() + return UnitIsPlayer(self.unit) +end + +-- Is the unit a player controlled unit +function Unit:IsPCU() + return UnitPlayerControlled(self.unit) +end + +-- Get if the unit is affecting combat +function Unit:IsAffectingCombat() + return UnitAffectingCombat(self.unit) +end + +-- Get the units class id +function Unit:GetClass() + local locale, class, classID = UnitClass(self.unit) + return Bastion.Class:New(locale, class, classID) +end + +-- Get the units auras +function Unit:GetAuras() + return self.aura_table +end + +-- Get the raw unit +function Unit:GetRawUnit() + return self.unit +end + +local isClassicWow = select(4, GetBuildInfo()) < 40000 + +-- Check if two units are in melee +function Unit:InMelee(unit) + local combatReach = ObjectCombatReach(self.unit) + local themCombatReach = ObjectCombatReach(unit.unit) + + if not combatReach or not themCombatReach then + return false + end + + return self:GetDistance(unit) + < math.max(combatReach + 1.3333 + themCombatReach, 5) +end + +local losFlag = bit.bor(0x1, 0x10, 0x100000) + +-- Check if the unit can see another unit +function Unit:CanSee(unit) + -- mechagon smoke cloud + -- local mechagonID = 2097 + -- local smokecloud = 298602 + + -- local name, instanceType, difficultyID, difficultyName, maxPlayers, dynamicDifficulty, isDynamic, instanceID, instanceGroupSize, LfgDungeonID = + -- GetInstanceInfo() + + -- otherUnit = otherUnit and otherUnit or "player" + -- if instanceID == 2097 then + -- if (self:debuff(smokecloud, unit) and not self:debuff(smokecloud, otherUnit)) + -- or (self:debuff(smokecloud, otherUnit) and not self:debuff(smokecloud, unit)) + -- then + -- return false + -- end + -- end + local ax, ay, az = ObjectPosition(self.unit) + local ah = ObjectHeight(self.unit) + local attx, atty, attz = GetUnitAttachmentPosition(unit.unit, 34) + + if (ax == 0 and ay == 0 and az == 0) or (attx == 0 and atty == 0 and attz == 0) then + return true + end + + if not attx or not ax then + return true + end + + 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 + return true + end +end + +-- Check if the unit is casting a spell +function Unit:IsCasting() + return UnitCastingInfo(self.unit) ~= nil +end + +-- Check if the unit is channeling a spell +function Unit:IsChanneling() + return UnitChannelInfo(self.unit) ~= nil +end + +-- Check if the unit is casting or channeling a spell +function Unit:IsCastingOrChanneling() + return self:IsCasting() or self:IsChanneling() +end + +-- Check if the unit can attack the target +function Unit:CanAttack(unit) + return UnitCanAttack(self.unit, unit.unit) +end + +-- Check if unit is interruptible +function Unit:IsInterruptible(percent) + local percent = percent or math.random(2, 5) + + local name, text, texture, startTimeMS, endTimeMS, isTradeSkill, castID, notInterruptible, spellId = UnitCastingInfo(self + .unit) + + if not name then + name, text, texture, startTimeMS, endTimeMS, isTradeSkill, notInterruptible, spellId = UnitChannelInfo(self.unit) + end + + if name and startTimeMS and endTimeMS and not notInterruptible then + local castTimeRemaining = endTimeMS / 1000 - GetTime() + local castTimeTotal = (endTimeMS - startTimeMS) / 1000 + if castTimeTotal > 0 and castTimeRemaining / castTimeTotal * 100 >= percent then + return true + end + end + return false +end + +-- Get the number of enemies in a given range of the unit and cache the result for .5 seconds +function Unit:GetEnemies(range) + + local enemies = self.cache:Get("enemies_" .. range) + if enemies then + return enemies + end + + local count = 0 + local objs = Objects() + local numobjs = #objs + + for i = 1, numobjs do + local object = objs[i] + -- unit types 5,6,7 + if ObjectType(object) == 5 or ObjectType(object) == 6 then + local unit = Unit:New(object) + if Bastion.UnitManager['player']:CanAttack(unit) and unit:IsAffectingCombat() and unit:IsAlive() and + unit:CanSee(self) + and + self:GetDistance(unit) <= range then + count = count + 1 + end + end + end + + self.cache:Set("enemies_" .. range, count, .5) + return count +end + +function Unit:GetPartyHPAround(distance, percent) + local count = 0 + + Bastion.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 + end) + + return count +end + +-- Is moving +function Unit:IsMoving() + return GetUnitSpeed(self.unit) > 0 +end + +function Unit:GetComboPoints(unit) + return GetComboPoints(self.unit, unit.unit) +end + +-- IsUnit +function Unit:IsUnit(unit) + return UnitIsUnit(self.unit, unit.unit) +end + +return Unit diff --git a/src/UnitManager/UnitManager.lua b/src/UnitManager/UnitManager.lua new file mode 100644 index 0000000..11d9fb7 --- /dev/null +++ b/src/UnitManager/UnitManager.lua @@ -0,0 +1,275 @@ +local Tinkr, Bastion = ... + +local Unit = Bastion.Unit + +local prefixes = { + '^player', + '^pet', + '^vehicle', + '^target', + '^focus', + '^mouseover', + '^none', + '^npc', + '^party[1-4]', + '^raid[1-4]?[0-9]', + '^boss[1-5]', + '^arena[1-5]' +} + +-- Validate a unit is a valid token +local function Validate(token) + local start, index + local length, offset = string.len(token), 0 + for i = 1, #prefixes do + start, index = string.find(token, prefixes[i]) + if start then + offset = index + 1 + if offset > length then + return true + else + while true do + start, index = string.find(token, 'target', offset, true) + if start then + offset = index + 1 + if offset > length then + return true + end + else + return false + end + end + end + end + end + return false +end + +-- Create a new UnitManager class +local UnitManager = { + units = {}, + customUnits = {}, + cache = {} +} + +function UnitManager:__index(k) + if UnitManager[k] then + return UnitManager[k] + end + + local k = 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) + end + + + return self.customUnits[k].unit + end + + -- if not Validate(k) then + -- error("UnitManager:Get - Invalid token: " .. k) + -- end + + if self.units[k] == nil then + self.units[k] = Unit:New(k) + end + + return self.units[k] +end + +-- Constructor +function UnitManager:New() + local self = setmetatable({}, UnitManager) + self.units = {} + self.customUnits = {} + self.cache = Bastion.Cache:New() + return self +end + +function UnitManager:Validate(token) + return Validate(token) +end + +-- Get or create a unit +function UnitManager:Get(token) + -- if not Validate(token) then + -- error("UnitManager:Get - Invalid token: " .. token) + -- end + + if self.units[token] == nil then + self.units[token] = Unit:New(token) + end + + return self.units[token] +end + +-- Create a custom unit and cache it for .5 seconds +function UnitManager:CreateCustomUnit(token, cb) + local unit = cb() + local cachedUnit = Bastion.Cacheable:New(unit, cb) + + if unit == nil then + error("UnitManager:CreateCustomUnit - Invalid unit: " .. token) + end + + if self.customUnits[token] == nil then + self.customUnits[token] = { + unit = cachedUnit, + cb = cb + } + end + + self.cache:Set(token, cachedUnit, 0.5) + + return cachedUnit +end + +-- Enum Friends (party/raid members) +function UnitManager:EnumFriends(cb) + local isRaid = IsInRaid() + local n = GetNumGroupMembers() + + if cb(self:Get('player')) then + return + end + + if isRaid then + for i = 1, n do + local unit = self:Get('raid' .. i) + if unit:IsValid() then + if cb(unit) then + break + end + end + end + else + for i = 1, n do + local unit = self:Get('party' .. i) + if unit:IsValid() then + if cb(unit) then + break + end + end + end + end +end + +-- Enum Enemies (object manager) +function UnitManager:EnumEnemies(cb) + local objs = Objects() + for i = 1, #objs do + local obj = objs[i] + if ObjectType(obj) == 5 or ObjectType(obj) == 6 then + local unit = Unit:New(obj) + if unit:IsHostile() and unit:IsAffectingCombat() and unit:IsAlive() and unit:CanSee(self) + then + cb(unit) + end + end + end +end + +-- Enum Units (object manager) +function UnitManager:EnumUnits(cb) + local objs = Objects() + for i = 1, #objs do + local obj = objs[i] + if ObjectType(obj) == 5 or ObjectType(obj) == 6 then + local unit = Unit:New(obj) + cb(unit) + end + end +end + +-- Get the number of friends with a buff (party/raid members) +function UnitManager:GetNumFriendsWithBuff(spell) + local count = 0 + self:EnumFriends(function(unit) + if unit:GetAuras():FindMy(spell):IsUp() then + count = count + 1 + end + end) + return count +end + +-- Get the number of friends alive (party/raid members) +function UnitManager:GetNumFriendsAlive() + local count = 0 + self:EnumFriends(function(unit) + if unit:IsAlive() then + count = count + 1 + end + end) + return count +end + +-- Get the friend with the most friends within a given radius (party/raid members) +-- Return unit, friends +function UnitManager:GetFriendWithMostFriends(radius) + local unit = nil + local count = 0 + local friends = {} + self:EnumFriends(function(u) + if u:IsAlive() then + local c = 0 + self:EnumFriends(function(other) + if other:IsAlive() and u:GetDistance(other) <= radius then + c = c + 1 + end + end) + if c > count then + unit = u + count = c + friends = {} + self:EnumFriends(function(other) + if other:IsAlive() and u:GetDistance(other) <= radius then + table.insert(friends, other) + end + end) + end + end + end) + return unit, friends +end + +-- Find the centroid of the most dense area of friends (party/raid members) of a given radius within a given range +function UnitManager:FindFriendsCentroid(radius, range) + local unit, friends = self:GetFriendWithMostFriends(radius) + if unit == nil then + return nil + end + + local centroid = Bastion.Vector3:New(0, 0, 0) + local zstart = -math.huge + for i = 1, #friends do + local p = friends[i]:GetPosition() + centroid = centroid + p + zstart = p.z > zstart and p.z or zstart + end + + centroid = centroid / #friends + + if unit:GetPosition():Distance(centroid) > range then + return unit:GetPosition() + end + + local _, _, z = TraceLine( + centroid.x, + centroid.y, + centroid.z + 5, + centroid.x, + centroid.y, + centroid.z - 5, + 0x100151 + ) + + centroid.z = z + 0.01 + + return centroid +end + +return UnitManager diff --git a/src/Vector3/Vector3.lua b/src/Vector3/Vector3.lua new file mode 100644 index 0000000..406d9be --- /dev/null +++ b/src/Vector3/Vector3.lua @@ -0,0 +1,240 @@ +-- Create a Vector3 class + +local Vector3 = {} +Vector3.__index = Vector3 + +function Vector3:__tostring() + return "Vector3(" .. self.x .. ", " .. self.y .. ", " .. self.z .. ")" +end + +function Vector3:__add(other) + return Vector3:New(self.x + other.x, self.y + other.y, self.z + other.z) +end + +function Vector3:__sub(other) + if type(other) == "number" then + return Vector3:New(self.x - other, self.y - other, self.z - other) + end + return Vector3:New(self.x - other.x, self.y - other.y, self.z - other.z) +end + +function Vector3:__mul(other) + return Vector3:New(self.x * other, self.y * other, self.z * other) +end + +function Vector3:__div(other) + return Vector3:New(self.x / other, self.y / other, self.z / other) +end + +function Vector3:__eq(other) + return self.x == other.x and self.y == other.y and self.z == other.z +end + +function Vector3:__lt(other) + return self.x < other.x and self.y < other.y and self.z < other.z +end + +function Vector3:__le(other) + return self.x <= other.x and self.y <= other.y and self.z <= other.z +end + +function Vector3:__unm() + return Vector3:New(-self.x, -self.y, -self.z) +end + +function Vector3:__len() + return math.sqrt(self.x * self.x + self.y * self.y + self.z * self.z) +end + +function Vector3:__index(k) + if Vector3[k] then + return Vector3[k] + end + + if k == "length" then + return math.sqrt(self.x * self.x + self.y * self.y + self.z * self.z) + end + + if k == "normalized" then + local length = math.sqrt(self.x * self.x + self.y * self.y + self.z * self.z) + return Vector3:New(self.x / length, self.y / length, self.z / length) + end + + if k == "magnitude" then + return math.sqrt(self.x * self.x + self.y * self.y + self.z * self.z) + end + + if k == "sqrMagnitude" then + return self.x * self.x + self.y * self.y + self.z * self.z + end + + if k == "zero" then + return Vector3:New(0, 0, 0) + end + + if k == "one" then + return Vector3:New(1, 1, 1) + end + + if k == "up" then + return Vector3:New(0, 1, 0) + end + + if k == "down" then + return Vector3:New(0, -1, 0) + end + + if k == "left" then + return Vector3:New(-1, 0, 0) + end + + if k == "right" then + return Vector3:New(1, 0, 0) + end + + if k == "forward" then + return Vector3:New(0, 0, 1) + end + + if k == "back" then + return Vector3:New(0, 0, -1) + end + + if k == "positiveInfinity" then + return Vector3:New(math.huge, math.huge, math.huge) + end + + if k == "negativeInfinity" then + return Vector3:New(-math.huge, -math.huge, -math.huge) + end + + if k == "nan" then + return Vector3:New(0 / 0, 0 / 0, 0 / 0) + end + + if k == "epsilon" then + return 1.401298E-45 + end + + if k == "maxValue" then + return 3.402823E+38 + end + + if k == "minValue" then + return -3.402823E+38 + end + + if k == "x" then + return self[1] + end + + if k == "y" then + return self[2] + end + + if k == "z" then + return self[3] + end + + return nil +end + +function Vector3:__newindex(k, v) + if k == "x" then + self[1] = v + elseif k == "y" then + self[2] = v + elseif k == "z" then + self[3] = v + else + rawset(self, k, v) + end +end + +function Vector3:New(x, y, z) + if x == false then + return Vector3:New(0, 0, 0) + end + + local self = setmetatable({ x, y, z }, Vector3) + return self +end + +function Vector3:Dot(rhs) + return self.x * rhs.x + self.y * rhs.y + self.z * rhs.z +end + +function Vector3:Cross(rhs) + return Vector3:New(self.y * rhs.z - self.z * rhs.y, self.z * rhs.x - self.x * rhs.z, self.x * rhs.y - self.y * rhs.x) +end + +function Vector3:Distance(b) + return FastDistance(self.x, self.y, self.z, b.x, b.y, b.z) +end + +function Vector3:Angle(to) + return math.acos(self:Dot(to) / + ( + math.sqrt(self.x * self.x + self.y * self.y + self.z * self.z) * + math.sqrt(to.x * to.x + to.y * to.y + to.z * to.z))) +end + +function Vector3:ClampMagnitude(maxLength) + if self:Dot(self) > maxLength * maxLength then + return self.normalized * maxLength + end + + return self +end + +-- Implement a clamp function +local function clamp(x, min, max) + return x < min and min or (x > max and max or x) +end + +function Vector3:Lerp(b, t) + t = clamp(t, 0, 1) + return Vector3:New(self.x + (b.x - self.x) * t, self.y + (b.y - self.y) * t, self.z + (b.z - self.z) * t) +end + +function Vector3:MoveTowards(target, maxDistanceDelta) + local toVector = target - self + local distance = toVector.magnitude + if distance <= maxDistanceDelta or distance == 0 then + return target + end + + return self + toVector / distance * maxDistanceDelta +end + +function Vector3:Scale(b) + return Vector3:New(self.x * b.x, self.y * b.y, self.z * b.z) +end + +function Vector3:Project(onNormal) + local num = onNormal:Dot(onNormal) + if num < 1.401298E-45 then + return Vector3:New(0, 0, 0) + end + + return onNormal * self:Dot(onNormal) / num +end + +function Vector3:ProjectOnPlane(planeNormal) + return self - self:Project(planeNormal) +end + +function Vector3:Reflect(inNormal) + return -2 * inNormal:Dot(self) * inNormal + self +end + +function Vector3:Normalize() + local num = self:Dot(self) + if num > 1E-05 then + return self / math.sqrt(num) + end + + return Vector3:New(0, 0, 0) +end + +return Vector3 diff --git a/src/_bastion.lua b/src/_bastion.lua new file mode 100644 index 0000000..e7d8d5f --- /dev/null +++ b/src/_bastion.lua @@ -0,0 +1,152 @@ +local Tinkr = ... + +local Bastion = { + DebugMode = true +} +Bastion.__index = Bastion + +function Bastion.require(class) + if Bastion[class] then + return Bastion[class] + end + + Bastion[class] = Tinkr:require("scripts/bastion/src/" .. class .. "/" .. class, Bastion) + return Bastion[class] +end + +Bastion.ClassMagic = Bastion.require("ClassMagic") +Bastion.Vector3 = Bastion.require("Vector3") +Bastion.Commmand = Bastion.require("Command") +Bastion.Cache = Bastion.require("Cache") +Bastion.Cacheable = Bastion.require("Cacheable") +Bastion.Unit = Bastion.require("Unit") +Bastion.Aura = Bastion.require("Aura") +Bastion.APL = Bastion.require("APL") +Bastion.Module = Bastion.require("Module") +Bastion.UnitManager = Bastion.require("UnitManager"):New() +Bastion.EventManager = Bastion.require("EventManager"):New() +Bastion.Spell = Bastion.require("Spell") +Bastion.Item = Bastion.require("Item") +Bastion.SpellBook = Bastion.require("SpellBook"):New() +Bastion.ItemBook = Bastion.require("ItemBook"):New() +Bastion.AuraTable = Bastion.require("AuraTable") +Bastion.Class = Bastion.require("Class") +Bastion.Timer = Bastion.require("Timer") +Bastion.CombatTimer = Bastion.Timer:New('combat') + +Bastion.modules = {} +Bastion.Enabled = false + +Bastion.Ticker = C_Timer.NewTicker(0.1, function() + if not Bastion.CombatTimer:IsRunning() and UnitAffectingCombat("player") then + Bastion.CombatTimer:Start() + elseif Bastion.CombatTimer:IsRunning() and not UnitAffectingCombat("player") then + Bastion.CombatTimer:Reset() + end + + if Bastion.Enabled then + for i = 1, #Bastion.modules do + Bastion.modules[i]:Tick() + end + end +end) + +function Bastion:Register(module) + table.insert(Bastion.modules, module) + Bastion:Print("Registered", module) +end + +-- Find a module by name +function Bastion:FindModule(name) + for i = 1, #Bastion.modules do + if Bastion.modules[i].name == name then + return Bastion.modules[i] + end + end + + return nil +end + +function Bastion:Print(...) + local args = { ... } + local str = "|cFFDF362D[Bastion]|r |cFFFFFFFF" + for i = 1, #args do + str = str .. tostring(args[i]) .. " " + end + print(str) +end + +function Bastion:Debug(...) + if not Bastion.DebugMode then + return + end + local args = { ... } + local str = "|cFFDF6520[Bastion]|r |cFFFFFFFF" + for i = 1, #args do + str = str .. tostring(args[i]) .. " " + end + print(str) +end + +local Command = Bastion.Commmand:New('bastion') + +Command:Register('toggle', 'Toggle bastion on/off', function() + Bastion.Enabled = not Bastion.Enabled + if Bastion.Enabled then + Bastion:Print("Enabled") + else + Bastion:Print("Disabled") + end +end) + +Command:Register('debug', 'Toggle debug mode on/off', function() + Bastion.DebugMode = not Bastion.DebugMode + if Bastion.DebugMode then + Bastion:Print("Debug mode enabled") + else + Bastion:Print("Debug mode disabled") + end +end) + +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) + if not spellName then + do break end + end + + -- use spellName and spellSubName here + local spellID = select(7, GetSpellInfo(spellName)) + + if spellID then + WriteFile('bastion-' .. UnitClass('player') .. '-' .. rand .. '.lua', + "local " .. spellName .. " = Bastion.SpellBook:GetSpell(" .. spellID .. ")", true) + end + i = i + 1 + end +end) + +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:Print("Enabled", module.name) + else + Bastion:Print("Disabled", module.name) + end + else + Bastion:Print("Module not found") + end +end) + +local files = ListFiles("scripts/bastion/scripts") + +for i = 1, #files do + local file = files[i] + if file:sub(-4) == ".lua" or file:sub(-5) == '.luac' then + Tinkr:require("scripts/bastion/scripts/" .. file:sub(1, -5), Bastion) + end +end