---@type Tinkr local Tinkr, ---@class Bastion Bastion = ... ---@class Bastion.Spell.Traits.Cast ---@field moving boolean ---@field dead boolean ---@field global boolean ---@field casting boolean ---@field channeling boolean ---@field override boolean ---@field talent boolean | spellId ---@field power boolean ---@field precast false | fun(self:Bastion.Spell):boolean ---@class Bastion.Spell.Traits.Cost ---@field type Enum.PowerType ---@field cost number ---@field minCost number ---@field requiredAuraID number ---@field costPercent number ---@field costPerSecond number ---@class Bastion.Spell.Traits.Target ---@field exists boolean ---@field player boolean ---@field facing boolean ---@class Bastion.Spell.Traits.Aura ---@field track boolean ---@class Bastion.Spell.Traits ---@field cast Bastion.Spell.Traits.Cast ---@field target Bastion.Spell.Traits.Target ---@field cost Bastion.Spell.Traits.Cost[] ---@field aura Bastion.Spell.Traits.Aura ---@class Bastion.Spell.Traits.Cast.Params : Bastion.Spell.Traits.Cast, { [string]?: boolean } ---@class Bastion.Spell.Traits.Target.Params : Bastion.Spell.Traits.Target, { [string]?: boolean } ---@class Bastion.Spell.Traits.Cost.Params : Bastion.Spell.Traits.Cost[] ---@class Bastion.Spell.Traits.Aura.Params : Bastion.Spell.Traits.Aura ---@class Bastion.Spell.Traits.Params ---@field cast? Bastion.Spell.Traits.Cast.Params ---@field target? Bastion.Spell.Traits.Target.Params ---@field cost? Bastion.Spell.Traits.Cost.Params ---@field aura? Bastion.Spell.Traits.Aura.Params ---@class Bastion.Spell.Aura ---@field spell Bastion.Spell ---@field source? Bastion.Unit ---@field target? Bastion.Unit ---@field lastApplied number -- Create a new Spell class ---@class Bastion.Spell ---@field auras table ---@field CastableIfFunc false | fun(self:Bastion.Spell):boolean ---@field conditions { [string]: { func: fun(self:Bastion.Spell):boolean } } ---@field damage number ---@field damageFormula false | fun(self:Bastion.Spell):number ---@field lastCastAt number | false ---@field lastCastAttempt number | false ---@field OnCastFunc fun(self:Bastion.Spell) | false ---@field overrides { [spellId]: fun(self: Bastion.Spell): Bastion.Spell } ---@field PostCastFunc fun(self:Bastion.Spell) | false ---@field PreCastFunc fun(self:Bastion.Spell) | false ---@field release_at false ---@field spellID number ---@field target Bastion.Unit | false ---@field traits Bastion.Spell.Traits ---@field wasLooking boolean ---@field tickDuration false | number | fun(self: Bastion.Spell): number local Spell = { CastableIfFunc = false, damage = 0, damageFormula = false, lastCastAt = false, lastCastAttempt = false, OnCastFunc = false, PostCastFunc = false, PreCastFunc = false, release_at = 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 -- Equals ---@param other Bastion.Spell function Spell:__eq(other) return self:GetID() == other:GetID() end -- tostring function Spell:__tostring() return "Bastion.__Spell(" .. self:GetID() .. ")" .. " - " .. self:GetName() end -- Constructor ---@param id number function Spell:New(id) ---@class Bastion.Spell local self = setmetatable({}, Spell) local spellInfo = C_Spell.GetSpellInfo(id) local maxRange = spellInfo and spellInfo.maxRange or 0 local minRange = spellInfo and spellInfo.minRange or 0 self.auras = {} self.overrides = {} self.conditions = {} self.spellID = id self.lastCastGUID = false self.castGUID = false self.lastCastSuccess = false ---@type 0 | 1 | 2 0 = SPELL_RANGE_DEFAULT, 1 = SPELL_RANGE_MELEE, 2 = SPELL_RANGE_RANGED self.rangeEntry = 0 self.minRange = minRange self.maxRange = maxRange self.isMelee = minRange == 0 and maxRange == 0 self.traits = { cast = { moving = true, dead = false, global = false, casting = false, channeling = false, override = false, talent = false, power = false, precast = false, }, target = { exists = true, player = false, facing = false, }, cost = {}, aura = { track = false, }, } self.target = false self.wasLooking = false self.tickDuration = false return self end ---@return string function Spell:GetDescription() return C_Spell.GetSpellDescription(self:GetID()) or "" end -- Duplicator function Spell:Fresh() return Spell:New(self:GetID()) end -- Get the spells id ---@param ignoreOverride? boolean ---@return number function Spell:GetID(ignoreOverride) return ignoreOverride and self.spellID or self:IsOverridden() and self:OverrideSpellID() or self.spellID end -- Add post cast func ---@param func fun(self:Bastion.Spell) function Spell:PostCast(func) self.PostCastFunc = func return self end function Spell:GetSpellInfo() return C_Spell.GetSpellInfo(self:GetID()) end -- Get the spells name ---@return string function Spell:GetName() local spellName = Bastion.Globals.SpellName[self:GetID()] if not spellName then local spellInfo = self:GetSpellInfo() spellName = spellInfo and spellInfo.name or "" end return spellName end -- Get the spells icon ---@return number function Spell:GetIcon() local spellInfo = self:GetSpellInfo() return spellInfo and spellInfo.iconID or 0 end ---@param byId? boolean function Spell:GetCooldownInfo(byId) return C_Spell.GetSpellCooldown(byId and self:GetID() or self:GetName()) end -- Get the spells cooldown ---@param byId? boolean ---@return number function Spell:GetCooldown(byId) local cdInfo = self:GetCooldownInfo(byId) if cdInfo then return cdInfo.duration end return 0 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 ---@param byId? boolean ---@return number function Spell:GetCooldownRemaining(byId) local cdInfo = self:GetCooldownInfo(byId) if cdInfo then if cdInfo.startTime == 0 then return 0 end return cdInfo.startTime + cdInfo.duration - GetTime() end return 0 end -- Get the spell count ---@param byId? boolean ---@return number function Spell:GetCount(byId) return C_Spell.GetSpellCastCount((byId ~= nil and byId) and self:GetID() or self:GetName()) end -- On cooldown ---@return boolean function Spell:OnCooldown() return self:GetCooldownRemaining() > 0 end -- Clear castable function ---@return Bastion.Spell function Spell:ClearCastableFunction() self.CastableIfFunc = false return self end ---@param spell Bastion.Spell ---@param func fun(self: Bastion.Spell): boolean function Spell:AddOverrideSpell(spell, func) self.overrides[spell:GetID()] = func end -- Cast the spell ---@param unit? Bastion.Unit ---@param condition? string | fun(self:Bastion.Spell, target?: Bastion.Unit):boolean ---@return boolean function Spell:Cast(unit, condition) if not self:Castable(unit) then return false end if condition then if type(condition) == "string" and not self:EvaluateCondition(condition) then return false elseif type(condition) == "function" and not condition(self, unit) then return false end end -- Call pre cast function if self:GetPreCastFunction() then self:GetPreCastFunction()(self) end -- Check if the mouse was looking self.wasLooking = IsMouselooking() --[[@as boolean]] -- if unit:GetOMToken() contains 'nameplate' then we need to use Object wrapper to cast local target = unit and unit:GetOMToken() or self.traits.target.player and "player" or "none" if type(target) == "string" and string.find(target, "nameplate") then target = Object(target):unit() end -- Cast the spell CastSpellByName(self:GetName(), target) --[[ local tgt = Object(target) local tgtname = target if tgt then tgtname = tgt:name() or target target = UnitTokenFromGUID(tgt:guid() or "") end Tinkr.Util.Tools.Console.print("|cff1ebf3c[Cast]|r|cffFFCC00[" .. self:GetName() .. "]|r->" .. tgtname .. "[" .. target .. "]") ]] SpellCancelQueuedSpell() Bastion.Util:Debug("Casting", self) -- Set the last cast time self.lastCastAttempt = GetTime() -- Call post cast function if self:GetOnCastFunction() then self:GetOnCastFunction()(self) end return true end -- ForceCast the spell ---@param unit Bastion.Unit ---@return boolean function Spell:ForceCast(unit) -- Check if the mouse was looking self.wasLooking = IsMouselooking() --[[@as boolean]] -- if unit:GetOMToken() contains 'nameplate' then we need to use Object wrapper to cast local target = unit and unit:GetOMToken() or self.traits.target.player and "player" or "none" if type(target) == "string" and string.find(target, "nameplate") then target = Object(target):unit() end -- Cast the spell CastSpellByName(self:GetName(), target) SpellCancelQueuedSpell() Bastion.Util:Debug("Force Casting", self) -- Set the last cast time self.lastCastAttempt = GetTime() return true end -- Get post cast func function Spell:GetPostCastFunction() return self.PostCastFunc end function Spell:OverrideSpellID() return C_Spell.GetOverrideSpell(self.spellID) end function Spell:IsOverridden() return self:OverrideSpellID() ~= self.spellID end -- Check if the spell is known ---@param includeOverrides? boolean ---@return boolean function Spell:IsKnown(includeOverrides) local isKnown = (includeOverrides or self.traits.cast.override) and IsSpellKnownOrOverridesKnown(self:GetID()) or IsSpellKnown(self:GetID()) or IsPlayerSpell(self:GetID()) return isKnown end function Spell:GetSpellLossOfControlCooldown() local start, duration = C_Spell.GetSpellLossOfControlCooldown(self:IsOverridden() and self:GetName() or self:GetID()) or 0, 0 return start, duration end -- Check if the spell is on cooldown ---@return boolean function Spell:IsOnCooldown() local spellCooldownInfo = self:GetCooldownInfo(self:IsOverridden() and false) if spellCooldownInfo and spellCooldownInfo.duration then return spellCooldownInfo.duration > 0 or select(2, self:GetSpellLossOfControlCooldown()) > 0 end return false end ---@param byId? boolean ---@return boolean, boolean function Spell:IsUsableSpell(byId) return C_Spell.IsSpellUsable(byId and self:GetID() or self:GetName()) end -- Check if the spell is usable ---@param byId? boolean ---@return boolean 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 or self.traits.cast.override) and not self:IsOnCooldown() and self:IsUsable(override ~= nil and true or false) end ---@param traits Bastion.Spell.Traits.Params function Spell:SetTraits(traits) for _, trait in pairs({ "cast", "target", "aura" }) do if type(traits[trait]) == "table" then for k, _ in pairs(self.traits[trait]) do if traits[trait][k] ~= nil then self.traits[trait][k] = traits[trait][k] end end end end if traits.cost then self.traits.cost = {} for _, cost in ipairs(traits.cost) do table.insert(self.traits.cost, cost) end end return self end ---@param target? Bastion.Unit function Spell:EvaluateTraits(target) local player = Bastion.Globals.UnitManager:Get("player") if self.traits.cast.precast and not self.traits.cast.precast(self) then return false end if self.traits.target.facing and target and not player:IsFacing(target) then return false end if self.traits.cast.power and not self:HasPower() then return false end if not self.traits.cast.global and player:GetGCD() > 0 then return false end if not self.traits.cast.moving and player:IsMoving() then return false end if not self.traits.cast.dead and player:IsDead() then return false end if not self.traits.cast.casting and player:IsCasting() then return false end if not self.traits.cast.channeling and player:IsChanneling() then return false end if self.traits.target.exists then if (self.traits.target.player and not player:Exists()) then return false elseif (target and (target == Bastion.Globals.UnitManager:Get("none") or not target:Exists())) or (not target and not self:TargetExists()) then return false end end if self.traits.cast.talent and not IsPlayerSpell(tonumber(self.traits.cast.talent) or self:GetID()) then return false end return true end -- Check if the spell is castable ---@param target? Bastion.Unit function Spell:Castable(target) return self:IsKnownAndUsable(self.traits.cast.override) and self:EvaluateTraits(target) and (not self:GetCastableFunction() or self:GetCastableFunction()(self)) end -- Set a script to check if the spell is castable ---@param func fun(spell:Bastion.Spell):boolean function Spell:CastableIf(func) self.CastableIfFunc = func return self end -- Set a script to run before the spell has been cast ---@param func fun(spell:Bastion.Spell) function Spell:PreCast(func) self.PreCastFunc = func return self end -- Set a script to run after the spell has been cast ---@param func fun(spell:Bastion.Spell) function Spell:OnCast(func) self.OnCastFunc = func return self end -- Get was looking ---@return boolean function Spell:GetWasLooking() return self.wasLooking end -- Click the spell ---@param x number|Bastion.Vector3 ---@param y? number ---@param z? number ---@return boolean function Spell:Click(x, y, z) if type(x) == "table" then z, y, x = x.z, x.y, x.x end ---@cast x number ---@cast y number ---@cast z number 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 ---@param unit Bastion.Unit function Spell:Call(unit) return self:Cast(unit) end -- Check if the spell is castable and cast it ---@return boolean? function Spell:HasRange() return C_Spell.SpellHasRange(self:GetName()) end -- Get the range of the spell ---@return number ---@return number function Spell:GetRange() --local _, _, _, _, minRange, maxRange, _, _ = GetSpellInfo(self:GetID()) return self.maxRange, self.minRange end ---@param source Bastion.Unit ---@param target Bastion.Unit function Spell:CheckRange(source, target) return self.isMelee and source:InMelee(target) or source:GetCombatDistance(target) <= self.maxRange end ---@param target? Bastion.Unit function Spell:GetMinMaxRange(target) local rangeMod, minRange, maxRange = 0.0, 0.0, 0.0 local unitCaster = Bastion.Globals.UnitManager:Get("player") if self:HasRange() then minRange, maxRange = self:GetRange() -- probably melee if self.isMelee then rangeMod = unitCaster:GetMeleeRange(target or unitCaster) else local meleeRange = 0 --[[ if maxRange > 0 then meleeRange = unitCaster:GetMeleeRange(target or unitCaster) end ]] minRange = (minRange == maxRange and minRange or not target and 0 or minRange) end end maxRange = maxRange + rangeMod return minRange, maxRange end -- Check if the spell is in range of the unit ---@param unit Bastion.Unit ---@return boolean function Spell:IsInRange(unit) if unit:IsUnit(Bastion.Globals.UnitManager:Get("player")) then return true end local hasRange = self:HasRange() if hasRange == false then return true end local inRange = C_Spell.IsSpellInRange(self:GetName(), unit:GetOMToken()) or false if inRange then return true end return Bastion.Globals.UnitManager:Get("player"):InMelee(unit) end -- Get the last cast time ---@return number | false function Spell:GetLastCastTime() return self.lastCastAt end -- Get time since last cast ---@return number function Spell:GetTimeSinceLastCast() if not self:GetLastCastTime() then return math.huge end return GetTime() - self:GetLastCastTime() end -- Get the time since the last cast attempt ---@return number function Spell:GetTimeSinceLastCastAttempt() if not self.lastCastAttempt then return math.huge end return GetTime() - self.lastCastAttempt end function Spell:GetCastGuid() return self.castGUID end ---@param time number ---@param success boolean ---@param castGUID string function Spell:SetLastCast(time, success, castGUID) self.lastCastSuccess = success if success and self.lastCastGUID and self.lastCastGUID == castGUID then self.lastCastAt = self.lastCastAttempt self.castGUID = self.lastCastGUID end end ---@param time number ---@param castGUID string function Spell:SetLastCastAttempt(time, castGUID) self.lastCastSuccess = false self.lastCastAttempt = time self.lastCastGUID = castGUID end function Spell:GetChargeInfo() return C_Spell.GetSpellCharges(self:GetID()) end -- Get the spells charges function Spell:GetCharges() local chargeInfo = self:GetChargeInfo() return chargeInfo and chargeInfo.currentCharges or 0 end function Spell:GetMaxCharges() local chargeInfo = self:GetChargeInfo() return chargeInfo and chargeInfo.maxCharges or 0 end ---@return number function Spell:GetCastLength() local spellInfo = self:GetSpellInfo() return spellInfo and spellInfo.castTime or 0 end -- Get the full cooldown (time until all charges are available) ---@return number function Spell:GetFullRechargeTime() local spellChargeInfo = self:GetChargeInfo() if not spellChargeInfo or spellChargeInfo.currentCharges == spellChargeInfo.maxCharges then return 0 end return (spellChargeInfo.maxCharges - self:GetChargesFractional()) * spellChargeInfo.cooldownDuration end function Spell:Recharge() local spellChargeInfo = self:GetChargeInfo() if not spellChargeInfo or spellChargeInfo.currentCharges == spellChargeInfo.maxCharges then return 0 end local recharge = spellChargeInfo.cooldownStartTime + spellChargeInfo.cooldownDuration - GetTime() return recharge > 0 and recharge or 0 end -- Get the spells charges plus fractional ---@return number function Spell:GetChargesFractional() local spellChargeInfo = self:GetChargeInfo() if not spellChargeInfo or spellChargeInfo.currentCharges == spellChargeInfo.maxCharges then return spellChargeInfo.currentCharges end return spellChargeInfo.currentCharges + ((spellChargeInfo.cooldownDuration - self:Recharge()) / spellChargeInfo.cooldownDuration) end -- Get the spells charges remaining ---@return number function Spell:GetChargesRemaining() local spellChargeInfo = self:GetChargeInfo() return spellChargeInfo and spellChargeInfo.currentCharges or 0 end -- Create a condition for the spell ---@param name string ---@param func fun(self:Bastion.Spell):boolean ---@return Bastion.Spell function Spell:Condition(name, func) self.conditions[name] = { func = func, } return self end -- Get a condition for the spell ---@param name string ---@return { func: fun(self: Bastion.Spell): boolean } | nil function Spell:GetCondition(name) local condition = self.conditions[name] if condition then return condition end return nil end -- Evaluate a condition for the spell ---@param name string ---@return boolean 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 ---@param name string ---@return boolean function Spell:HasCondition(name) local condition = self:GetCondition(name) if condition then return true end return false end -- Set the spells target ---@param unit Bastion.Unit ---@return Bastion.Spell function Spell:SetTarget(unit) self.target = unit return self end -- Get the spells target function Spell:GetTarget() return self.target end function Spell:TargetExists() return self:GetTarget() and self:GetTarget():Exists() end -- IsEnrageDispel ---@return boolean function Spell:IsEnrageDispel() return ({ [2908] = true, -- Soothe [19801] = true, -- Tranq Shot })[self:GetID()] end -- IsMagicDispel ---@return boolean function Spell:IsMagicDispel() return ({ [77130] = true, -- Purify Spirit [115450] = true, -- Detox [4987] = true, -- Cleanse [527] = true, -- Purify [32375] = true, -- Mass Dispel [89808] = true, -- Singe Magic })[self:GetID()] end -- IsCurseDispel ---@return boolean function Spell:IsCurseDispel() return ({ [77130] = true, -- Purify Spirit [2782] = true, [475] = true, -- Remove Curse [51886] = true, -- Cleanse Spirit })[self:GetID()] end -- IsPoisonDispel ---@return boolean function Spell:IsPoisonDispel() return ({ [2782] = true, [115450] = true, -- Detox [4987] = true, -- Cleanse [213644] = true, -- Cleanse Toxins })[self:GetID()] end -- IsDiseaseDispel ---@return boolean function Spell:IsDiseaseDispel() return ({ [115450] = true, -- Detox [4987] = true, -- Cleanse [213644] = true, -- Cleanse Toxins [527] = true, -- Purify [213634] = true, -- Purify Disease })[self:GetID()] end -- IsSpell ---@param spell Bastion.Spell ---@return boolean function Spell:IsSpell(spell) return self:GetID() == spell:GetID() end ---@return SpellPowerCostInfo[]? function Spell:GetCostInfo() return C_Spell.GetSpellPowerCost(self:GetID()) end function Spell:HasPower() local costs = self:GetCostInfo() local checked = {} local hasPower = costs and #costs > 0 and false or true if costs and not hasPower then for _, cost in ipairs(costs) do if not checked[cost.type] then local powerCost = (cost.cost > cost.minCost and cost.minCost or cost.cost) if cost.hasRequiredAura or cost.requiredAuraID == 0 then hasPower = powerCost == 0 or Bastion.Globals.UnitManager:Get("player"):GetPower(cost.type) >= powerCost checked[cost.type] = true if not hasPower then return false end end end end end return hasPower end -- GetCost ---@return number function Spell:GetCost() local cost = self:GetCostInfo() return cost and cost[1] and cost[1].cost or 0 end -- IsFree ---@return boolean function Spell:IsFree() return self:GetCost() == 0 end ---@param damageFormula fun(self:Bastion.Spell): number function Spell:RegisterDamageFormula(damageFormula) self.damageFormula = damageFormula end ---@return number function Spell:Damage() if self.damageFormula then return self:damageFormula() else return self.damage end end ---@param target? Bastion.Unit ---@param source? "any" | Bastion.Unit function Spell:GetAura(target, source) if type(target) == "nil" then target = Bastion.Globals.UnitManager:Get("player") end if type(source) == "nil" then source = Bastion.Globals.UnitManager:Get("player") end return target:GetAuras():FindFrom(self, source) end ---@param spell Bastion.Spell ---@param source? Bastion.Unit ---@param target? Bastion.Unit function Spell:TrackAura(spell, source, target) self.auras[spell:GetID()] = { spell = spell, source = source, target = target, lastApplied = 0, } end ---@param aura Bastion.Spell ---@param source? Bastion.Unit ---@param target? Bastion.Unit function Spell:CheckAuraStatus(aura, source, target) for id, trackedAura in pairs(self.auras) do if aura:GetID() == id then return true end end return false end ---@param spell Bastion.Spell ---@param source? Bastion.Unit ---@param target? Bastion.Unit function Spell:UpdateAura(spell, source, target) if not self.auras[spell:GetID()] then self:TrackAura(spell, source, target) end self.auras[spell:GetID()].lastApplied = GetTime() end function Spell:GetTickDuration() if type(self.tickDuration) == "function" then return self.tickDuration(self) else return self.tickDuration end end ---@param duration (number) | fun(self: Bastion.Spell): (number) function Spell:TickDuration(duration) self.tickDuration = duration end ---@param params { source: Bastion.Unit, target: Bastion.Unit, spellVisualId?: number } function Spell:GetMissiles(params) return Bastion.Globals.MissileManager:GetMissiles({ source = params.source, target = params.target, spellId = self:GetID(), spellVisualId = params.spellVisualId, }) end ---@param params { source: Bastion.Unit, target: Bastion.Unit, spellVisualId?: number} function Spell:InFlight(params) return #self:GetMissiles(params) > 0 end Bastion.Spell = Spell