forked from Bastion/Bastion
You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
1621 lines
44 KiB
1621 lines
44 KiB
---@type Tinkr
|
|
local Tinkr,
|
|
---@class Bastion
|
|
Bastion = ...
|
|
|
|
-- Create a new Unit class
|
|
---@class Bastion.Unit
|
|
---@field id boolean | number
|
|
---@field ttd_ticker false | cbObject
|
|
---@field unit? TinkrObjectReference
|
|
---@field aura_table Bastion.AuraTable | nil
|
|
local Unit = {
|
|
---@type Bastion.Cache
|
|
cache = nil,
|
|
aura_table = nil,
|
|
---@type UnitId | WowGameObject
|
|
unit = nil,
|
|
ttd_ticker = false,
|
|
ttd = 0,
|
|
id = false, --[[ @asnumber ]]
|
|
watching_for_swings = false,
|
|
health = {}
|
|
}
|
|
|
|
function Unit:UpdateHealth()
|
|
if #self.health > 60 then
|
|
table.remove(self.health, 1)
|
|
end
|
|
table.insert(self.health, {
|
|
time = GetTime(),
|
|
percent = self:GetHP(),
|
|
health = self:GetHealth(),
|
|
maxHealth =
|
|
self:GetMaxHealth()
|
|
})
|
|
end
|
|
|
|
function Unit:__index(k)
|
|
local response = Bastion.ClassMagic:Resolve(Unit, k)
|
|
|
|
if k == "unit" then
|
|
return rawget(self, k)
|
|
end
|
|
|
|
if response == nil then
|
|
response = rawget(self, k)
|
|
end
|
|
|
|
if response == nil then
|
|
error("Unit:__index: " .. k .. " does not exist")
|
|
end
|
|
|
|
return response
|
|
end
|
|
|
|
-- Equals
|
|
---@param other Bastion.Unit
|
|
---@return boolean
|
|
function Unit:__eq(other)
|
|
return UnitIsUnit(self:GetOMToken(), other:GetOMToken())
|
|
end
|
|
|
|
-- tostring
|
|
---ToString
|
|
---
|
|
---```lua
|
|
---print(Unit:New('player'))
|
|
---```
|
|
---@return string
|
|
function Unit:__tostring()
|
|
return "Bastion.__Unit(" .. tostring(self:GetOMToken()) .. ")" .. " - " .. (self:GetName() or "")
|
|
end
|
|
|
|
-- Constructor
|
|
---@param unit? TinkrObjectReference
|
|
---@return Bastion.Unit
|
|
function Unit:New(unit)
|
|
---@class Bastion.Unit
|
|
local self = setmetatable({}, Unit)
|
|
self.unit = unit
|
|
self.last_shadow_techniques = 0
|
|
self.swings_since_sht = 0
|
|
self.last_off_attack = 0
|
|
self.last_main_attack = 0
|
|
self.last_combat_time = 0
|
|
self.cache = Bastion.Cache:New()
|
|
self.aura_table = Bastion.AuraTable:New(self)
|
|
self.regression_history = {}
|
|
self.health = {}
|
|
return self
|
|
end
|
|
|
|
-- Check if the unit is valid
|
|
---@return boolean
|
|
function Unit:IsValid()
|
|
return self:GetOMToken() ~= nil and self:Exists()
|
|
end
|
|
|
|
-- Check if the unit exists
|
|
---@return boolean
|
|
function Unit:Exists()
|
|
return Object(self:GetOMToken()) ~= false
|
|
end
|
|
|
|
-- Get the units token
|
|
function Unit:Token()
|
|
return self:GetOMToken()
|
|
end
|
|
|
|
-- Get the units name
|
|
function Unit:GetName()
|
|
local unitName, realm = UnitName(self:GetOMToken())
|
|
return unitName
|
|
end
|
|
|
|
-- Get the units GUID
|
|
function Unit:GetGUID()
|
|
return ObjectGUID(self:GetOMToken())
|
|
end
|
|
|
|
-- Get the units health
|
|
---@return number
|
|
function Unit:GetHealth()
|
|
return UnitHealth(self:GetOMToken())
|
|
end
|
|
|
|
-- Get the units max health
|
|
---@return number
|
|
function Unit:GetMaxHealth()
|
|
return UnitHealthMax(self:GetOMToken())
|
|
end
|
|
|
|
-- Return Health Percent as an integer (0-100)
|
|
---@return number
|
|
function Unit:GetHP()
|
|
return self:GetHealth() / self:GetMaxHealth() * 100
|
|
end
|
|
|
|
-- Get realized health
|
|
---@return number
|
|
function Unit:GetRealizedHealth()
|
|
return self:GetHealth() - self:GetHealAbsorbedHealth()
|
|
end
|
|
|
|
-- get realized health percentage
|
|
---@return number
|
|
function Unit:GetRealizedHP()
|
|
return self:GetRealizedHealth() / self:GetMaxHealth() * 100
|
|
end
|
|
|
|
-- Get the abosorbed unit health
|
|
---@return number
|
|
function Unit:GetHealAbsorbedHealth()
|
|
return UnitGetTotalHealAbsorbs(self:GetOMToken())
|
|
end
|
|
|
|
-- Return Health Percent as an integer (0-100)
|
|
---@return number
|
|
function Unit:GetHealthPercent()
|
|
return self:GetHP()
|
|
end
|
|
|
|
-- Get the units power type
|
|
---@return Enum.PowerType
|
|
function Unit:GetPowerType()
|
|
return select(1, UnitPowerType(self:GetOMToken()))
|
|
end
|
|
|
|
-- Get the units power
|
|
---@param powerType? number
|
|
---@return number
|
|
function Unit:GetPower(powerType)
|
|
local powerType = powerType or self:GetPowerType()
|
|
return UnitPower(self:GetOMToken(), powerType)
|
|
end
|
|
|
|
-- Get the units max power
|
|
---@param powerType? number
|
|
---@return number
|
|
function Unit:GetMaxPower(powerType)
|
|
local powerType = powerType or self:GetPowerType()
|
|
return UnitPowerMax(self:GetOMToken(), powerType)
|
|
end
|
|
|
|
-- Get the units power percentage
|
|
---@param powerType number | nil
|
|
---@return number
|
|
function Unit:GetPP(powerType)
|
|
local powerType = powerType or self:GetPowerType()
|
|
return self:GetPower(powerType) / self:GetMaxPower(powerType) * 100
|
|
end
|
|
|
|
-- Get the units power deficit
|
|
---@param powerType number | nil
|
|
---@return number
|
|
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:GetOMToken())
|
|
return Bastion.Vector3:New(x, y, z)
|
|
end
|
|
|
|
-- Get the units distance from another unit
|
|
---@param unit Bastion.Unit
|
|
---@return number
|
|
function Unit:GetDistance(unit)
|
|
local pself = self:GetPosition()
|
|
local punit = unit:GetPosition()
|
|
|
|
return pself:Distance(punit)
|
|
end
|
|
|
|
-- Is the unit dead
|
|
---@return boolean
|
|
function Unit:IsDead()
|
|
return UnitIsDeadOrGhost(self:GetOMToken())
|
|
end
|
|
|
|
-- Is the unit alive
|
|
---@return boolean
|
|
function Unit:IsAlive()
|
|
return not UnitIsDeadOrGhost(self:GetOMToken())
|
|
end
|
|
|
|
-- Is the unit a pet
|
|
---@return boolean
|
|
function Unit:IsPet()
|
|
return UnitIsUnit(self:GetOMToken(), "pet")
|
|
end
|
|
|
|
function Unit:IsOtherPet()
|
|
local petName = self:GetName()
|
|
local ownerName = ""
|
|
ownerName = (
|
|
string.match(petName, string.gsub(UNITNAME_TITLE_PET, "%%s", "(%.*)"))
|
|
or string.match(petName, string.gsub(UNITNAME_TITLE_MINION, "%%s", "(%.*)"))
|
|
or string.match(petName, string.gsub(UNITNAME_TITLE_GUARDIAN, "%%s", "(%.*)"))
|
|
)
|
|
return ownerName ~= nil
|
|
end
|
|
|
|
-- Is the unit a friendly unit
|
|
---@return boolean
|
|
function Unit:IsFriendly()
|
|
return UnitIsFriend("player", self:GetOMToken())
|
|
end
|
|
|
|
-- IsEnemy
|
|
---@return boolean
|
|
function Unit:IsEnemy()
|
|
return UnitCanAttack("player", self:GetOMToken())
|
|
end
|
|
|
|
-- Is the unit a hostile unit
|
|
---@return boolean
|
|
function Unit:IsHostile()
|
|
return UnitCanAttack(self:GetOMToken(), "player")
|
|
end
|
|
|
|
---@param unit Bastion.Unit
|
|
function Unit:GetReaction(unit)
|
|
return UnitReaction(unit:GetOMToken(), self:GetOMToken())
|
|
end
|
|
|
|
-- Is the unit a boss
|
|
---@return boolean
|
|
function Unit:IsBoss()
|
|
if UnitClassification(self:GetOMToken()) == "worldboss" then
|
|
return true
|
|
end
|
|
|
|
for i = 1, 5 do
|
|
local bossGUID = UnitGUID("boss" .. i)
|
|
|
|
if self:GetGUID() == bossGUID then
|
|
return true
|
|
end
|
|
end
|
|
|
|
return false
|
|
end
|
|
|
|
---@return UnitIds
|
|
function Unit:GetOMToken()
|
|
if not self.unit then
|
|
return "none"
|
|
end
|
|
return self.unit:unit()
|
|
end
|
|
|
|
-- Is the unit a target
|
|
---@return boolean
|
|
function Unit:IsTarget()
|
|
return UnitIsUnit(self:GetOMToken(), "target")
|
|
end
|
|
|
|
-- Is the unit a focus
|
|
---@return boolean
|
|
function Unit:IsFocus()
|
|
return UnitIsUnit(self:GetOMToken(), "focus")
|
|
end
|
|
|
|
-- Is the unit a mouseover
|
|
---@return boolean
|
|
function Unit:IsMouseover()
|
|
return UnitIsUnit(self:GetOMToken(), "mouseover")
|
|
end
|
|
|
|
-- Is the unit a tank
|
|
---@return boolean
|
|
function Unit:IsTank()
|
|
return UnitGroupRolesAssigned(self:GetOMToken()) == "TANK"
|
|
end
|
|
|
|
-- Is the unit a healer
|
|
---@return boolean
|
|
function Unit:IsHealer()
|
|
return UnitGroupRolesAssigned(self:GetOMToken()) == "HEALER"
|
|
end
|
|
|
|
-- Is the unit a damage dealer
|
|
---@return boolean
|
|
function Unit:IsDamage()
|
|
return UnitGroupRolesAssigned(self:GetOMToken()) == "DAMAGER"
|
|
end
|
|
|
|
-- Get the units role
|
|
---@return "TANK" | "HEALER" | "DAMAGER" | "NONE"
|
|
function Unit:GetRole()
|
|
return UnitGroupRolesAssigned(self:GetOMToken())
|
|
end
|
|
|
|
---@return number | boolean
|
|
function Unit:GetSpecializationID()
|
|
if CanInspect(self:GetOMToken(), false) then
|
|
return ObjectSpecializationID(self:GetOMToken())
|
|
end
|
|
return false
|
|
end
|
|
|
|
---@param fallback? boolean
|
|
---@return "TANK" | "HEALER" | "DAMAGER" | "NONE" | false
|
|
function Unit:GetSpecializationRole(fallback)
|
|
local specID = self:GetSpecializationID()
|
|
if type(specID) == "number" then
|
|
return GetSpecializationRoleByID(specID)
|
|
end
|
|
return fallback and self:GetRole() or false
|
|
end
|
|
|
|
-- Is the unit a player
|
|
---@return boolean
|
|
function Unit:IsPlayer()
|
|
return UnitIsPlayer(self:GetOMToken())
|
|
end
|
|
|
|
-- Is the unit a player controlled unit
|
|
---@return boolean
|
|
function Unit:IsPCU()
|
|
return UnitPlayerControlled(self:GetOMToken())
|
|
end
|
|
|
|
-- Get if the unit is affecting combat
|
|
---@return boolean
|
|
function Unit:IsAffectingCombat()
|
|
return UnitAffectingCombat(self:GetOMToken())
|
|
end
|
|
|
|
-- Get the units class id
|
|
---@return Bastion.Class
|
|
function Unit:GetClass()
|
|
local locale, class, classID = UnitClass(self:GetOMToken())
|
|
return Bastion.Class:New(locale, class, classID)
|
|
end
|
|
|
|
-- Get the units auras
|
|
---@return Bastion.AuraTable
|
|
function Unit:GetAuras()
|
|
return self.aura_table
|
|
end
|
|
|
|
-- Get the raw unit
|
|
---@return string
|
|
function Unit:GetRawUnit()
|
|
return self:GetOMToken()
|
|
end
|
|
|
|
local isClassicWow = select(4, GetBuildInfo()) < 40000
|
|
|
|
-- Check if two units are in melee
|
|
-- function Unit:InMelee(unit)
|
|
-- return UnitInMelee(self:GetOMToken(), unit:GetOMToken())
|
|
-- end
|
|
|
|
local losFlag = bit.bor(0x1, 0x10)
|
|
|
|
-- Check if the unit can see another unit
|
|
---@param unit Bastion.Unit
|
|
---@return boolean
|
|
function Unit:CanSee(unit)
|
|
-- mechagon smoke cloud
|
|
-- local mechagonID = 2097
|
|
-- local smokecloud = 298602
|
|
|
|
-- local name, instanceType, difficultyID, difficultyName, maxPlayers, dynamicDifficulty, isDynamic, instanceID, instanceGroupSize, LfgDungeonID =
|
|
-- GetInstanceInfo()
|
|
|
|
-- otherUnit = otherUnit and otherUnit or "player"
|
|
-- if instanceID == 2097 then
|
|
-- if (self:debuff(smokecloud, unit) and not self:debuff(smokecloud, otherUnit))
|
|
-- or (self:debuff(smokecloud, otherUnit) and not self:debuff(smokecloud, unit))
|
|
-- then
|
|
-- return false
|
|
-- end
|
|
-- end
|
|
local ignoreLoS = {
|
|
[98696] = true -- Illysanna Ravencrest (BRH)
|
|
}
|
|
if not unit:IsPlayer() then
|
|
local id = unit:GetID()
|
|
if id and ignoreLoS[id] then
|
|
return true
|
|
end
|
|
end
|
|
local ax, ay, az = ObjectPosition(self:GetOMToken())
|
|
local ah = ObjectHeight(self:GetOMToken())
|
|
local attx, atty, attz = GetUnitAttachmentPosition(unit:GetOMToken(), 34)
|
|
|
|
if not attx or not ax then
|
|
return false
|
|
end
|
|
|
|
if not ah then
|
|
return false
|
|
end
|
|
|
|
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 false
|
|
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
|
|
---@return boolean
|
|
function Unit:IsCasting()
|
|
return UnitCastingInfo(self:GetOMToken()) ~= nil
|
|
end
|
|
|
|
---@param percent number
|
|
---@return number
|
|
function Unit:GetTimeCastIsAt(percent)
|
|
local name, text, texture, startTimeMS, endTimeMS, isTradeSkill, castID, notInterruptible, spellId = UnitCastingInfo(
|
|
self:GetOMToken())
|
|
|
|
if not name then
|
|
name, text, texture, startTimeMS, endTimeMS, isTradeSkill, notInterruptible, spellId = UnitChannelInfo(self
|
|
:GetOMToken())
|
|
end
|
|
|
|
if name and startTimeMS and endTimeMS then
|
|
local castLength = endTimeMS - startTimeMS
|
|
local startTime = startTimeMS / 1000
|
|
local timeUntil = (castLength / 1000) * (percent / 100)
|
|
|
|
return startTime + timeUntil
|
|
end
|
|
|
|
return 0
|
|
end
|
|
|
|
-- Get Casting or channeling spell
|
|
---@return Bastion.Spell | boolean
|
|
function Unit:GetCastingOrChannelingSpell()
|
|
local name, text, texture, startTimeMS, endTimeMS, isTradeSkill, castID, notInterruptible, spellId = UnitCastingInfo(
|
|
self:GetOMToken())
|
|
|
|
if not name then
|
|
name, text, texture, startTimeMS, endTimeMS, isTradeSkill, notInterruptible, spellId = UnitChannelInfo(self
|
|
:GetOMToken())
|
|
end
|
|
|
|
if name then
|
|
return Bastion.Globals.SpellBook:GetSpell(spellId)
|
|
end
|
|
return false
|
|
end
|
|
|
|
-- Get the end time of the cast or channel
|
|
---@return number
|
|
function Unit:GetCastingOrChannelingEndTime()
|
|
local name, text, texture, startTimeMS, endTimeMS, isTradeSkill, castID, notInterruptible, spellId = UnitCastingInfo(
|
|
self:GetOMToken())
|
|
|
|
if not name then
|
|
name, text, texture, startTimeMS, endTimeMS, isTradeSkill, notInterruptible, spellId = UnitChannelInfo(self
|
|
:GetOMToken())
|
|
end
|
|
|
|
if name then
|
|
return endTimeMS / 1000
|
|
end
|
|
|
|
return 0
|
|
end
|
|
|
|
-- Check if the unit is channeling a spell
|
|
---@return boolean
|
|
function Unit:IsChanneling()
|
|
return UnitChannelInfo(self:GetOMToken()) ~= nil
|
|
end
|
|
|
|
-- Check if the unit is casting or channeling a spell
|
|
---@return boolean
|
|
function Unit:IsCastingOrChanneling()
|
|
return self:IsCasting() or self:IsChanneling()
|
|
end
|
|
|
|
---@return Bastion.Unit
|
|
function Unit:CastTarget()
|
|
---@diagnostic disable-next-line: param-type-mismatch
|
|
return self:IsCastingOrChanneling() and Bastion.UnitManager:Get(ObjectCastingTarget(self:GetOMToken())) or
|
|
Bastion.UnitManager:Get("none")
|
|
end
|
|
|
|
---@param unit Bastion.Unit
|
|
---@return boolean
|
|
function Unit:CastTargetIsUnit(unit)
|
|
return self:IsCastingOrChanneling() and self:CastTarget():IsUnit(unit)
|
|
end
|
|
|
|
---@return boolean
|
|
function Unit:IsImmobilized()
|
|
return bit.band(self:GetMovementFlag(), 0x400) > 0
|
|
end
|
|
|
|
-- Check if the unit can attack the target
|
|
---@param unit Bastion.Unit
|
|
---@return boolean
|
|
function Unit:CanAttack(unit)
|
|
return UnitCanAttack(self:GetOMToken(), unit:GetOMToken())
|
|
end
|
|
|
|
---@return number
|
|
function Unit:GetChannelOrCastPercentComplete()
|
|
local name, text, texture, startTimeMS, endTimeMS, isTradeSkill, castID, notInterruptible, spellId = UnitCastingInfo(
|
|
self:GetOMToken())
|
|
|
|
if not name then
|
|
name, text, texture, startTimeMS, endTimeMS, isTradeSkill, notInterruptible, spellId = UnitChannelInfo(self
|
|
:GetOMToken())
|
|
end
|
|
|
|
if name and startTimeMS and endTimeMS then
|
|
local start = startTimeMS / 1000
|
|
local finish = endTimeMS / 1000
|
|
local current = GetTime()
|
|
|
|
return ((current - start) / (finish - start)) * 100
|
|
end
|
|
return 0
|
|
end
|
|
|
|
-- Check if unit is interruptible
|
|
---@return boolean
|
|
function Unit:IsInterruptible()
|
|
local name, text, texture, startTimeMS, endTimeMS, isTradeSkill, castID, notInterruptible, spellId = UnitCastingInfo(
|
|
self:GetOMToken())
|
|
|
|
if not name then
|
|
name, text, texture, startTimeMS, endTimeMS, isTradeSkill, notInterruptible, spellId = UnitChannelInfo(self
|
|
:GetOMToken())
|
|
end
|
|
|
|
if name then
|
|
return not notInterruptible
|
|
end
|
|
|
|
return false
|
|
end
|
|
|
|
-- Check if unit is interruptible
|
|
---@param percent? number
|
|
---@param ignoreInterruptible? boolean
|
|
---@return boolean
|
|
function Unit:IsInterruptibleAt(percent, ignoreInterruptible)
|
|
if not ignoreInterruptible and not self:IsInterruptible() then
|
|
return false
|
|
end
|
|
|
|
local percent = percent or math.random(2, 20)
|
|
|
|
local castPercent = self:GetChannelOrCastPercentComplete()
|
|
if castPercent >= percent then
|
|
return true
|
|
end
|
|
|
|
return false
|
|
end
|
|
|
|
-- Get the number of enemies in a given range of the unit and cache the result for .5 seconds
|
|
---@param range number
|
|
---@return number
|
|
function Unit:GetEnemies(range)
|
|
local enemies = self.cache:Get("enemies_" .. range)
|
|
if enemies then
|
|
return enemies
|
|
end
|
|
|
|
local count = 0
|
|
|
|
Bastion.UnitManager:EnumEnemies(function(unit)
|
|
if not self:IsUnit(unit) and self:IsWithinCombatDistance(unit, range) and unit:IsAlive() and self:CanSee(unit) and unit:IsEnemy() then
|
|
count = count + 1
|
|
end
|
|
return false
|
|
end)
|
|
|
|
self.cache:Set("enemies_" .. range, count, 0.5)
|
|
return count
|
|
end
|
|
|
|
-- Get the number of melee attackers
|
|
---@param facing? boolean
|
|
---@return number
|
|
function Unit:GetMeleeAttackers(facing)
|
|
local enemies = self.cache:Get("melee_attackers")
|
|
if enemies then
|
|
return enemies
|
|
end
|
|
|
|
local count = 0
|
|
|
|
Bastion.UnitManager:EnumEnemies(function(unit)
|
|
if not self:IsUnit(unit) and unit:IsAlive() and self:CanSee(unit) and self:InMelee(unit) and unit:IsEnemy() and (not facing or self:IsFacing(unit)) then
|
|
count = count + 1
|
|
end
|
|
return false
|
|
end)
|
|
|
|
self.cache:Set("melee_attackers", count, 0.5)
|
|
return count
|
|
end
|
|
|
|
---@param distance number
|
|
---@param percent number
|
|
---@return number
|
|
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
|
|
return false
|
|
end)
|
|
|
|
return count
|
|
end
|
|
|
|
-- Is moving
|
|
---@return boolean
|
|
function Unit:IsMoving()
|
|
return GetUnitSpeed(self:GetOMToken()) > 0
|
|
end
|
|
|
|
---@return TinkrMovementFlags
|
|
function Unit:GetMovementFlag()
|
|
return ObjectMovementFlag(self:GetOMToken())
|
|
end
|
|
|
|
-- Is moving at all
|
|
---@return boolean
|
|
function Unit:IsMovingAtAll()
|
|
return ObjectMovementFlag(self:GetOMToken()) ~= 0
|
|
end
|
|
|
|
---@param unit? Bastion.Unit
|
|
---@return number
|
|
function Unit:GetComboPoints(unit)
|
|
if Tinkr.classic or Tinkr.era then
|
|
if not unit then
|
|
return 0
|
|
end
|
|
return GetComboPoints(self:GetOMToken(), unit:GetOMToken())
|
|
end
|
|
return UnitPower(self:GetOMToken(), Enum.PowerType.ComboPoints)
|
|
end
|
|
|
|
---@return number
|
|
function Unit:GetComboPointsMax()
|
|
if Tinkr.classic or Tinkr.era then
|
|
return 5
|
|
end
|
|
return UnitPowerMax(self:GetOMToken(), 4)
|
|
end
|
|
|
|
-- Get combopoints deficit
|
|
---@param unit Bastion.Unit | nil
|
|
---@return number
|
|
function Unit:GetComboPointsDeficit(unit)
|
|
if Tinkr.classic or Tinkr.era then
|
|
return self:GetComboPointsMax() - self:GetComboPoints(unit)
|
|
end
|
|
return self:GetComboPointsMax() - self:GetComboPoints()
|
|
end
|
|
|
|
-- IsUnit
|
|
---@param unit Bastion.Unit
|
|
---@return boolean
|
|
function Unit:IsUnit(unit)
|
|
return UnitIsUnit(self:GetOMToken(), unit and unit:GetOMToken() or "none")
|
|
end
|
|
|
|
-- IsTanking
|
|
---@param unit Bastion.Unit
|
|
---@return boolean
|
|
function Unit:IsTanking(unit)
|
|
local isTanking, status, threatpct, rawthreatpct, threatvalue = UnitDetailedThreatSituation(self:GetOMToken(),
|
|
unit:GetOMToken())
|
|
return isTanking
|
|
end
|
|
|
|
-- IsFacing
|
|
---@param unit Bastion.Unit
|
|
---@return boolean
|
|
function Unit:IsFacing(unit)
|
|
local rot = ObjectRotation(self:GetOMToken())
|
|
local x, y, z = ObjectPosition(self:GetOMToken())
|
|
local x2, y2, z2 = ObjectPosition(unit:GetOMToken())
|
|
|
|
if not x or not x2 or not rot then
|
|
return false
|
|
end
|
|
|
|
---@diagnostic disable-next-line: deprecated
|
|
local angle = math.atan2(y2 - y, x2 - x) - rot
|
|
angle = math.deg(angle)
|
|
angle = angle % 360
|
|
if angle > 180 then
|
|
angle = angle - 360
|
|
end
|
|
|
|
return math.abs(angle) < 90
|
|
end
|
|
|
|
-- IsBehind
|
|
---@param unit Bastion.Unit
|
|
---@return boolean
|
|
function Unit:IsBehind(unit)
|
|
local rot = ObjectRotation(unit:GetOMToken())
|
|
local x, y, z = ObjectPosition(unit:GetOMToken())
|
|
local x2, y2, z2 = ObjectPosition(self:GetOMToken())
|
|
|
|
if not x or not x2 then
|
|
return false
|
|
end
|
|
---@diagnostic disable-next-line: deprecated
|
|
local angle = math.atan2(y2 - y, x2 - x) - rot
|
|
angle = math.deg(angle)
|
|
angle = angle % 360
|
|
if angle > 180 then
|
|
angle = angle - 360
|
|
end
|
|
|
|
return math.abs(angle) > 90
|
|
end
|
|
|
|
-- IsInfront
|
|
---@param unit Bastion.Unit
|
|
---@return boolean
|
|
function Unit:IsInfront(unit)
|
|
return not self:IsBehind(unit)
|
|
end
|
|
|
|
---@return number
|
|
function Unit:GetMeleeBoost()
|
|
local meleeBoost = 0
|
|
if IsPlayerSpell(197524) then
|
|
local astralInfluenceNode = C_Traits.GetNodeInfo(C_ClassTalents.GetActiveConfigID() or 0, 82210)
|
|
local currentSpec = select(1, GetSpecializationInfo(GetSpecialization()))
|
|
if astralInfluenceNode then
|
|
local currentRank = astralInfluenceNode.activeRank
|
|
if currentRank > 0 then
|
|
meleeBoost = ((currentSpec == 103 or currentSpec == 104) and 1 or 3) + (currentRank == 2 and 2 or 0)
|
|
end
|
|
end
|
|
elseif IsPlayerSpell(196924) then
|
|
meleeBoost = 3
|
|
end
|
|
return meleeBoost
|
|
end
|
|
|
|
function Unit:GetModelId()
|
|
return ObjectModelId(self:GetOMToken())
|
|
end
|
|
|
|
-- Melee calculation
|
|
-- float fMaxDist = fmaxf((float)(*(float*)((uintptr_t)this + 0x1BF8) + 1.3333) + *(float*)((uintptr_t)target + 0x1BF8), 5.0);
|
|
-- fMaxDist = fMaxDist + 1.0;
|
|
-- Vector3 myPos = ((WoWGameObject*)this)->GetPosition();
|
|
-- Vector3 targetPos = ((WoWGameObject*)target)->GetPosition();
|
|
-- return ((myPos.x - targetPos.x) * (myPos.x - targetPos.x)) + ((myPos.y - targetPos.y) * (myPos.y - targetPos.y)) + ((myPos.z - targetPos.z) * (myPos.z - targetPos.z)) <= (float)(fMaxDist * fMaxDist);
|
|
|
|
-- InMelee
|
|
---@param unit Bastion.Unit
|
|
---@return boolean
|
|
function Unit:InMelee(unit)
|
|
local x, y, z = ObjectPosition(self:GetOMToken())
|
|
local x2, y2, z2 = ObjectPosition(unit:GetOMToken())
|
|
|
|
if not x or not x2 then
|
|
return false
|
|
end
|
|
|
|
local scr = ObjectCombatReach(self:GetOMToken())
|
|
local ucr = ObjectCombatReach(unit:GetOMToken())
|
|
|
|
if not scr or not ucr then
|
|
return false
|
|
end
|
|
|
|
local dist = math.sqrt((x - x2) ^ 2 + (y - y2) ^ 2 + (z - z2) ^ 2)
|
|
local maxDist = math.max((scr + 1.3333) + ucr, 5.0)
|
|
maxDist = maxDist + 1.0 + self:GetMeleeBoost()
|
|
|
|
return dist <= maxDist
|
|
end
|
|
|
|
-- Get object id
|
|
---@return number
|
|
function Unit:GetID()
|
|
if self.id ~= false then
|
|
---@type number
|
|
return self.id
|
|
end
|
|
self.id = ObjectID(self:GetOMToken())
|
|
---@type number
|
|
return self.id or 0
|
|
end
|
|
|
|
-- In party
|
|
---@return boolean
|
|
function Unit:IsInParty()
|
|
return UnitInParty(self:GetOMToken())
|
|
end
|
|
|
|
-- In party
|
|
---@return boolean
|
|
function Unit:IsInRaid()
|
|
return UnitInRaid(self:GetOMToken()) ~= nil
|
|
end
|
|
|
|
---@return boolean
|
|
function Unit:IsInPartyOrRaid()
|
|
return self:IsInParty() or self:IsInRaid()
|
|
end
|
|
|
|
-- Linear regression between time and percent to something
|
|
---@param time table
|
|
---@param percent table
|
|
---@return number, number
|
|
function Unit:LinearRegression(time, percent)
|
|
local x = time
|
|
local y = percent
|
|
|
|
local n = #x
|
|
local sum_x = 0
|
|
local sum_y = 0
|
|
local sum_xy = 0
|
|
local sum_xx = 0
|
|
local sum_yy = 0
|
|
|
|
for i = 1, n do
|
|
sum_x = sum_x + x[i]
|
|
sum_y = sum_y + y[i]
|
|
sum_xy = sum_xy + x[i] * y[i]
|
|
sum_xx = sum_xx + x[i] * x[i]
|
|
sum_yy = sum_yy + y[i] * y[i]
|
|
end
|
|
|
|
local slope = (n * sum_xy - sum_x * sum_y) / (n * sum_xx - sum_x * sum_x)
|
|
local intercept = (sum_y - slope * sum_x) / n
|
|
|
|
return slope, intercept
|
|
end
|
|
|
|
-- Use linear regression to get the health percent at a given time in the future
|
|
---@param time number
|
|
---@return number
|
|
function Unit:PredictHealth(time)
|
|
local x = {}
|
|
local y = {}
|
|
|
|
if #self.regression_history > 60 then
|
|
table.remove(self.regression_history, 1)
|
|
end
|
|
|
|
table.insert(self.regression_history, { time = GetTime(), percent = self:GetHP() })
|
|
|
|
for i = 1, #self.regression_history do
|
|
local entry = self.regression_history[i]
|
|
table.insert(x, entry.time)
|
|
table.insert(y, entry.percent)
|
|
end
|
|
|
|
local slope, intercept = self:LinearRegression(x, y)
|
|
return slope * (GetTime() + time) + intercept
|
|
end
|
|
|
|
-- Use linear regression to guess the time until a given health percent
|
|
---@param percent number
|
|
---@return number
|
|
function Unit:PredictTime(percent)
|
|
local x = {}
|
|
local y = {}
|
|
|
|
if #self.regression_history > 60 then
|
|
table.remove(self.regression_history, 1)
|
|
end
|
|
|
|
table.insert(self.regression_history, { time = GetTime(), percent = self:GetHP() })
|
|
|
|
for i = 1, #self.regression_history do
|
|
local entry = self.regression_history[i]
|
|
table.insert(x, entry.time)
|
|
table.insert(y, entry.percent)
|
|
end
|
|
|
|
local slope, intercept = self:LinearRegression(x, y)
|
|
return (percent - intercept) / slope
|
|
end
|
|
|
|
-- Start time to die ticker
|
|
function Unit:StartTTDTicker()
|
|
if self.ttd_ticker then
|
|
return
|
|
end
|
|
|
|
self.ttd_ticker = C_Timer.NewTicker(0.5, function()
|
|
local timeto = self:PredictTime(0) - GetTime()
|
|
self.ttd = timeto
|
|
end)
|
|
end
|
|
|
|
-- Time until death
|
|
---@return number
|
|
function Unit:TimeToDie()
|
|
if self:IsDead() then
|
|
self.regression_history = {}
|
|
if self.ttd_ticker then
|
|
self.ttd_ticker:Cancel()
|
|
self.ttd_ticker = false
|
|
end
|
|
return 0
|
|
end
|
|
|
|
if not self.ttd_ticker then
|
|
self:StartTTDTicker()
|
|
end
|
|
|
|
-- If there's not enough data to make a prediction return 0 unless the unit has more than 5 million health
|
|
if #self.regression_history < 5 and self:GetMaxHealth() < 5000000 then
|
|
return 0
|
|
end
|
|
|
|
-- if the unit has more than 5 million health but there's not enough data to make a prediction we can assume there's roughly 250000 damage per second and estimate the time to die
|
|
if #self.regression_history < 5 and self:GetMaxHealth() > 5000000 then
|
|
return self:GetMaxHealth() /
|
|
250000 -- 250000 is an estimate of the average damage per second a well geared group will average
|
|
end
|
|
|
|
if self.ttd ~= self.ttd or self.ttd < 0 or self.ttd == math.huge then
|
|
return 0
|
|
end
|
|
|
|
return self.ttd
|
|
end
|
|
|
|
---@param lower number
|
|
---@param upper? number
|
|
---@return boolean
|
|
function Unit:TTD(lower, upper)
|
|
upper = type(upper) == "nil" and lower or upper
|
|
local ttd = self:TimeToDie()
|
|
return ttd >= lower and ttd <= upper
|
|
end
|
|
|
|
-- Set combat time if affecting combat and return the difference between now and the last time
|
|
---@return number
|
|
function Unit:GetCombatTime()
|
|
return GetTime() - self.last_combat_time
|
|
end
|
|
|
|
-- Set last combat time
|
|
---@param time number
|
|
function Unit:SetLastCombatTime(time)
|
|
self.last_combat_time = time
|
|
end
|
|
|
|
-- Get combat odds (if the last combat time is less than 1 minute ago return 1 / time, else return 0)
|
|
-- the closer to 0 the more likely the unit is to be in combat (0 = 100%) 60 = 0%
|
|
---@return number
|
|
function Unit:InCombatOdds()
|
|
local time = self:GetCombatTime()
|
|
|
|
local percent = 1 - (time / 60)
|
|
|
|
return percent * 100
|
|
end
|
|
|
|
-- Get units gcd time
|
|
---@return number
|
|
function Unit:GetGCD()
|
|
local start, duration = GetSpellCooldown(61304)
|
|
|
|
return start == 0 and 0 or duration - (GetTime() - start)
|
|
end
|
|
|
|
-- Get units max gcd time
|
|
--[[
|
|
The GCD without Haste is 1.5 seconds
|
|
With 50% Haste the GCD is 1 second
|
|
With 100% Haste the GCD is 0.5 seconds
|
|
The GCD won't drop below 1 second
|
|
More than 50% Haste will drop a spell below 1 second
|
|
|
|
]]
|
|
---@return number
|
|
function Unit:GetMaxGCD()
|
|
local haste = UnitSpellHaste(self:GetOMToken())
|
|
if haste > 50 then
|
|
haste = 50
|
|
end
|
|
|
|
-- if the unit uses focus their gcd is 1.0 seconds not 1.5
|
|
local base = 1.5
|
|
if self:GetPowerType() == 3 then
|
|
base = 1.0
|
|
end
|
|
return base / (1 + haste / 100)
|
|
end
|
|
|
|
-- IsStealthed
|
|
---@return boolean
|
|
function Unit:IsStealthed()
|
|
local Stealth = Bastion.Globals.SpellBook:GetSpell(1784)
|
|
local Vanish = Bastion.Globals.SpellBook:GetSpell(1856)
|
|
local ShadowDance = Bastion.Globals.SpellBook:GetSpell(185422)
|
|
local Subterfuge = Bastion.Globals.SpellBook:GetSpell(115192)
|
|
local Shadowmeld = Bastion.Globals.SpellBook:GetSpell(58984)
|
|
local Sepsis = Bastion.Globals.SpellBook:GetSpell(328305)
|
|
|
|
local stealthList = Bastion.List:New({
|
|
Stealth,
|
|
Vanish,
|
|
ShadowDance,
|
|
Subterfuge,
|
|
Shadowmeld,
|
|
Sepsis,
|
|
})
|
|
|
|
return self:GetAuras():FindAnyOf(stealthList):IsUp()
|
|
end
|
|
|
|
-- Get unit swing timers
|
|
---@return number, number
|
|
function Unit:GetSwingTimers()
|
|
local main_speed, off_speed = UnitAttackSpeed(self:GetOMToken())
|
|
local main_speed = main_speed or 2
|
|
local off_speed = off_speed or 2
|
|
|
|
local main_speed_remains = main_speed - (GetTime() - self.last_main_attack)
|
|
local off_speed_remains = off_speed - (GetTime() - self.last_off_attack)
|
|
|
|
if main_speed_remains < 0 then
|
|
main_speed_remains = 0
|
|
end
|
|
|
|
if off_speed_remains < 0 then
|
|
off_speed_remains = 0
|
|
end
|
|
|
|
return main_speed_remains, off_speed_remains
|
|
end
|
|
|
|
function Unit:WatchForSwings()
|
|
if not self.watching_for_swings then
|
|
Bastion.Globals.EventManager:RegisterWoWEvent("COMBAT_LOG_EVENT_UNFILTERED", function()
|
|
local _, subtype, _, sourceGUID, sourceName, _, _, destGUID, destName, destFlags, _, spellID, spellName, _, amount, interrupt, a, b, c, d, offhand, multistrike =
|
|
CombatLogGetCurrentEventInfo()
|
|
|
|
if sourceGUID == self:GetGUID() and subtype then
|
|
if subtype == "SPELL_ENERGIZE" and spellID == 196911 then
|
|
self.last_shadow_techniques = GetTime()
|
|
self.swings_since_sht = 0
|
|
end
|
|
|
|
if subtype:sub(1, 5) == "SWING" and not multistrike then
|
|
if subtype == "SWING_MISSED" then
|
|
offhand = spellName
|
|
end
|
|
|
|
local now = GetTime()
|
|
|
|
if now > self.last_shadow_techniques + 3 then
|
|
self.swings_since_sht = self.swings_since_sht + 1
|
|
end
|
|
|
|
if offhand then
|
|
self.last_off_attack = GetTime()
|
|
else
|
|
self.last_main_attack = GetTime()
|
|
end
|
|
end
|
|
end
|
|
end)
|
|
self.watching_for_swings = true
|
|
end
|
|
end
|
|
|
|
-- ismounted
|
|
---@return boolean
|
|
function Unit:IsMounted()
|
|
local mountedFormId = {
|
|
[3] = true, -- Mount / Travel Form
|
|
[27] = true, -- Swift Flight Form
|
|
[29] = true, -- Flight Form
|
|
}
|
|
return UnitIsMounted(self.unit) or (GetShapeshiftFormID() and mountedFormId[GetShapeshiftFormID()])
|
|
end
|
|
|
|
-- isindoors
|
|
---@return boolean
|
|
function Unit:IsOutdoors()
|
|
return ObjectIsOutdoors(self.unit)
|
|
end
|
|
|
|
-- IsIndoors
|
|
---@return boolean
|
|
function Unit:IsIndoors()
|
|
return not ObjectIsOutdoors(self.unit)
|
|
end
|
|
|
|
-- IsSubmerged
|
|
---@return boolean
|
|
function Unit:IsSubmerged()
|
|
return ObjectIsSubmerged(self.unit)
|
|
end
|
|
|
|
-- IsDry
|
|
---@return boolean
|
|
function Unit:IsDry()
|
|
return not ObjectIsSubmerged(self.unit)
|
|
end
|
|
|
|
-- The unit stagger amount
|
|
---@return number
|
|
function Unit:GetStagger()
|
|
return UnitStagger(self:GetOMToken())
|
|
end
|
|
|
|
-- The percent of health the unit is currently staggering
|
|
---@return number
|
|
function Unit:GetStaggerPercent()
|
|
local stagger = self:GetStagger()
|
|
local max_health = self:GetMaxHealth()
|
|
|
|
return (stagger / max_health) * 100
|
|
end
|
|
|
|
-- Get the units power regen rate
|
|
---@return number, number
|
|
function Unit:GetPowerRegen()
|
|
---@diagnostic disable-next-line: redundant-parameter
|
|
return GetPowerRegen(self:GetOMToken())
|
|
end
|
|
|
|
-- Get the units staggered health relation
|
|
---@return number
|
|
function Unit:GetStaggeredHealth()
|
|
local stagger = self:GetStagger()
|
|
local max_health = self:GetMaxHealth()
|
|
|
|
return (stagger / max_health) * 100
|
|
end
|
|
|
|
-- get the units combat reach
|
|
---@return number
|
|
function Unit:GetCombatReach()
|
|
return ObjectCombatReach(self:GetOMToken()) or 0
|
|
end
|
|
|
|
-- Get the units combat distance (distance - combat reach (realized distance))
|
|
---@return number
|
|
function Unit:GetCombatDistance(Target)
|
|
return self:GetDistance(Target) - Target:GetCombatReach()
|
|
end
|
|
|
|
-- Is the unit within distance of the target (combat reach + distance)
|
|
--- If the target is within 8 combat yards (8 + combat reach) of the unit
|
|
---@param Target Bastion.Unit
|
|
---@param Distance number
|
|
---@return boolean
|
|
function Unit:IsWithinCombatDistance(Target, Distance)
|
|
if not Target:Exists() then
|
|
return false
|
|
end
|
|
return self:GetDistance(Target) <= Distance + Target:GetCombatReach()
|
|
end
|
|
|
|
-- Check if the unit is within X yards (consider combat reach)
|
|
---@param Target Bastion.Unit
|
|
---@param Distance number
|
|
---@return boolean
|
|
function Unit:IsWithinDistance(Target, Distance)
|
|
return self:GetDistance(Target) <= Distance
|
|
end
|
|
|
|
-- Get the angle between the unit and the target in raidans
|
|
---@param Target Bastion.Unit
|
|
---@return number
|
|
function Unit:GetAngle(Target)
|
|
if not Target:Exists() then
|
|
return 0
|
|
end
|
|
|
|
local sp = self:GetPosition()
|
|
local tp = Target:GetPosition()
|
|
|
|
local an = Tinkr.Common.GetAnglesBetweenPositions(sp.x, sp.y, sp.z, tp.x, tp.y, tp.z)
|
|
|
|
return an
|
|
end
|
|
|
|
function Unit:GetFacing()
|
|
return ObjectRotation(self:GetOMToken()) or 0
|
|
end
|
|
|
|
-- Check if target is within a arc around the unit (angle, distance) accounting for a rotation of self
|
|
---@param Target Bastion.Unit
|
|
---@param Angle number
|
|
---@param Distance number
|
|
---@param rotation? number
|
|
---@return boolean
|
|
function Unit:IsWithinCone(Target, Angle, Distance, rotation)
|
|
if not Target:Exists() then
|
|
return false
|
|
end
|
|
|
|
local angle = self:GetAngle(Target)
|
|
rotation = rotation or self:GetFacing()
|
|
|
|
local diff = math.abs(angle - rotation)
|
|
|
|
if diff > math.pi then
|
|
diff = math.abs(diff - math.pi * 2)
|
|
end
|
|
|
|
return diff <= Angle and self:GetDistance(Target) <= Distance
|
|
end
|
|
|
|
---@return number
|
|
function Unit:GetEmpoweredStage()
|
|
local stage = 0
|
|
local _, _, _, startTime, _, _, _, spellID, _, numStages = UnitChannelInfo(self:GetOMToken())
|
|
|
|
if numStages and numStages > 0 then
|
|
startTime = startTime / 1000
|
|
local currentTime = GetTime()
|
|
local stageDuration = 0
|
|
for i = 1, numStages do
|
|
stageDuration = stageDuration + GetUnitEmpowerStageDuration((self:GetOMToken()), i - 1) / 1000
|
|
if startTime + stageDuration > currentTime then
|
|
break
|
|
end
|
|
stage = i
|
|
end
|
|
end
|
|
return stage
|
|
end
|
|
|
|
---@return boolean
|
|
function Unit:IsConnected()
|
|
return UnitIsConnected(self:GetOMToken())
|
|
end
|
|
|
|
---@return boolean
|
|
function Unit:HasIncomingRessurection()
|
|
---@diagnostic disable-next-line: return-type-mismatch
|
|
return self:IsDead() and UnitHasIncomingResurrection(self:GetOMToken())
|
|
end
|
|
|
|
---@return WowGameObject | false
|
|
function Unit:LootTarget()
|
|
return ObjectLootTarget(self:GetOMToken())
|
|
end
|
|
|
|
---@return boolean
|
|
function Unit:CanLoot()
|
|
return ObjectLootable(self:GetOMToken())
|
|
end
|
|
|
|
---@return boolean
|
|
function Unit:HasTarget()
|
|
return ObjectTarget(self:GetOMToken()) ~= false
|
|
end
|
|
|
|
---@return Bastion.Unit
|
|
function Unit:Target()
|
|
return self:HasTarget() and Bastion.UnitManager:Get(ObjectTarget(self:GetOMToken()):unit()) or
|
|
Bastion.UnitManager:Get("none")
|
|
end
|
|
|
|
local dummyUnits = {
|
|
-- City (SW, Orgri, ...)
|
|
[31146] = true, -- Raider's Training Dummy
|
|
[31144] = true, -- Training Dummy
|
|
[32666] = true, -- Training Dummy
|
|
[32667] = true, -- Training Dummy
|
|
[46647] = true, -- Training Dummy
|
|
[114832] = true, -- PvP Training Dummy
|
|
[153285] = true, -- Training Dummy
|
|
[153292] = true, -- Training Dummy
|
|
-- MoP Shrine of Two Moons
|
|
[67127] = true, -- Training Dummy
|
|
-- WoD Alliance Garrison
|
|
[87317] = true, -- Mage Tower Damage Training Dummy
|
|
[87318] = true, -- Mage Tower Damage Dungeoneer's Training Dummy (& Garrison)
|
|
[87320] = true, -- Mage Tower Damage Raider's Training Dummy
|
|
[88314] = true, -- Tanking Dungeoneer's Training Dummy
|
|
[88316] = true, -- Healing Training Dummy ----> FRIENDLY
|
|
-- WoD Horde Garrison
|
|
[87760] = true, -- Mage Tower Damage Training Dummy
|
|
[87761] = true, -- Mage Tower Damage Dungeoneer's Training Dummy (& Garrison)
|
|
[87762] = true, -- Mage Tower Damage Raider's Training Dummy
|
|
[88288] = true, -- Tanking Dungeoneer's Training Dummy
|
|
[88289] = true, -- Healing Training Dummy ----> FRIENDLY
|
|
-- Legion Druid Class Order Hall
|
|
[113964] = true, -- Raider's Training Dummy
|
|
[113966] = true, -- Dungeoneer's Training Dummy
|
|
-- Legion Mage Class Order Hall
|
|
[103397] = true, -- Greater Bullwark Construct
|
|
[103404] = true, -- Bullwark Construct
|
|
[103402] = true, -- Lesser Bullwark Construct
|
|
-- Legion Priest Class Order Hall
|
|
[107555] = true, -- Bound void Wraith
|
|
[107556] = true, -- Bound void Walker
|
|
-- Legion Rogue Class Order Hall
|
|
[92164] = true, -- Training Dummy
|
|
[92165] = true, -- Dungeoneer's Training Dummy
|
|
[92166] = true, -- Raider's Training Dummy
|
|
-- Legion Warlock Class Order Hall
|
|
[101956] = true, -- Rebellious Fel Lord
|
|
[102045] = true, -- Rebellious WrathGuard
|
|
[102048] = true, -- Rebellious Felguard
|
|
[102052] = true, -- Rebellious imp
|
|
-- BfA Dazar'Alor
|
|
[144081] = true, -- Training Dummy
|
|
[144082] = true, -- Training Dummy
|
|
[144085] = true, -- Training Dummy
|
|
[144086] = true, -- Raider's Training Dummy
|
|
-- BfA Boralus
|
|
[126781] = true, -- Training Dummy
|
|
[131983] = true, -- Raider's Training Dummy
|
|
[131989] = true, -- Training Dummy
|
|
[131992] = true, -- Dungeoneer's Training Dummy
|
|
-- Shadowlands Kyrian
|
|
[154564] = true, -- Valiant's Humility
|
|
[154567] = true, -- Purity's Cleaning
|
|
[154580] = true, -- Reinforced Guardian
|
|
[154583] = true, -- Starlwart Guardian
|
|
[154585] = true, -- Valiant's Resolve
|
|
[154586] = true, -- Stalwart Phalanx
|
|
[160325] = true, -- Humility's Obedience
|
|
-- Shadowlands Venthyr
|
|
[173942] = true, -- Training Dummy
|
|
[175449] = true, -- Raider's Training Dummy
|
|
[175450] = true, -- Dungeoneer's Training Dummy
|
|
[175451] = true, -- Dungeoneer's Tanking Dummy
|
|
[175452] = true, -- Raider's Tanking Dummy
|
|
[175455] = true, -- Cleave Training Dummy
|
|
[175456] = true, -- Swarm Training Dummy
|
|
[175462] = true, -- Sinfall Fiend
|
|
-- Shadowlands Night Fae
|
|
[174565] = true, -- Dungeoneer's Tanking Dummy
|
|
[174566] = true, -- Raider's Tanking Dummy
|
|
[174567] = true, -- Raider's Training Dummy
|
|
[174568] = true, -- Dungeoneer's Training Dummy
|
|
[174569] = true, -- Training Dummy
|
|
[174570] = true, -- Swarm Training Dummy
|
|
[174571] = true, -- Cleave Training Dummy
|
|
-- Shadowlands Necrolord
|
|
[174484] = true, -- Dungeoneer's Training Dummy
|
|
[174487] = true, -- Training Dummy
|
|
[174488] = true, -- Raider's Training Dummy
|
|
[174491] = true, -- Tanking Dummy
|
|
-- DargonFlight Valdrakken
|
|
[198594] = true, -- Cleave Training Dummy
|
|
[194648] = false, -- Training Dummy
|
|
[189632] = true, -- Animated Duelist
|
|
[194643] = true, -- Dungeoneer's Training Dummy
|
|
[194644] = true, -- Dungeoneer's Training Dummy
|
|
[197833] = true, -- PvP Training Dummy
|
|
[189617] = true, -- Boulderfist
|
|
[194649] = true, -- Normal Tank Dummy
|
|
-- DargonFlight Iskaara
|
|
[193563] = true, -- Training Dummy
|
|
-- Other
|
|
[65310] = true, -- Turnip Punching Bag (toy)
|
|
[66374] = true, -- Anatomical Dummy (toy)
|
|
[196394] = true, -- Tuskarr Training Dummy (toy)
|
|
[196406] = true, -- Rubbery Fish Head (toy)
|
|
[199057] = true, -- Black Dragon's Challenge Dummy (toy)
|
|
}
|
|
|
|
---@return boolean
|
|
function Unit:IsDummy()
|
|
local npcId = self:GetID()
|
|
return npcId and npcId >= 0 and dummyUnits[npcId] == true or false
|
|
end
|
|
|
|
---@param npcId? number
|
|
---@return boolean
|
|
function Unit:IsInBossList(npcId)
|
|
npcId = npcId or self:GetID()
|
|
for i = 1, 4 do
|
|
local thisUnit = Bastion.UnitManager:Get(string.format("boss%d", i))
|
|
if thisUnit:Exists() and thisUnit:GetID() == npcId then
|
|
return true
|
|
end
|
|
end
|
|
return false
|
|
end
|
|
|
|
---@param npcId number
|
|
---@return number
|
|
function Unit:SpecialTTDPercentage(npcId)
|
|
local specialTTDPercentage = Bastion.TimeToDie.specialTTDPercentageData[npcId]
|
|
if not specialTTDPercentage then return 0 end
|
|
|
|
if type(specialTTDPercentage) == "number" then
|
|
return specialTTDPercentage
|
|
end
|
|
|
|
return specialTTDPercentage(self)
|
|
end
|
|
|
|
---@param npcId? number
|
|
---@param hp? number
|
|
---@return boolean
|
|
function Unit:CheckHPFromBossList(npcId, hp)
|
|
npcId = npcId or self:GetID()
|
|
local thisHP = hp or 100
|
|
for i = 1, 4 do
|
|
local thisUnit = Bastion.UnitManager:Get(string.format("boss%d", i))
|
|
if thisUnit:Exists() and thisUnit:GetID() == npcId and thisUnit:GetHealthPercent() <= thisHP then
|
|
return true
|
|
end
|
|
end
|
|
|
|
return false
|
|
end
|
|
|
|
---@param percentage number
|
|
---@param minSamples? number
|
|
---@return Bastion.TimeToDie.Enums | integer
|
|
function Unit:TimeToX(percentage, minSamples)
|
|
--if self:IsDummy() then return 6666 end
|
|
if self:IsPlayer() and Bastion.UnitManager:Get("player"):CanAttack(self) then return Bastion.TimeToDie.Enums.PLAYER end
|
|
local seconds = 0
|
|
local unitGuid = self:GetGUID()
|
|
if not unitGuid then
|
|
return Bastion.TimeToDie.Enums.NO_GUID
|
|
end
|
|
local unitTable = Bastion.TimeToDie.Units[unitGuid]
|
|
-- Simple linear regression
|
|
-- ( E(x^2) E(x) ) ( a ) ( E(xy) )
|
|
-- ( E(x) n ) ( b ) = ( E(y) )
|
|
-- Format of the above: ( 2x2 Matrix ) * ( 2x1 Vector ) = ( 2x1 Vector )
|
|
-- Solve to find a and b, satisfying y = a + bx
|
|
-- Matrix arithmetic has been expanded and solved to make the following operation as fast as possible
|
|
if unitTable then
|
|
minSamples = minSamples or 3
|
|
local history = unitTable.history
|
|
local n = #history
|
|
if n > minSamples then
|
|
local a, b = 0, 0
|
|
local ex2, ex, exy, ey = 0, 0, 0, 0
|
|
|
|
for i = 1, n do
|
|
local value = history[i]
|
|
local x, y = value.time, value.percentage
|
|
|
|
ex2 = ex2 + x * x
|
|
ex = ex + x
|
|
exy = exy + x * y
|
|
ey = ey + y
|
|
end
|
|
-- Invariant to find matrix inverse
|
|
local invariant = 1 / (ex2 * n - ex * ex)
|
|
-- Solve for a and b
|
|
a = (-ex * exy * invariant) + (ex2 * ey * invariant)
|
|
b = (n * exy * invariant) - (ex * ey * invariant)
|
|
if b ~= 0 then
|
|
-- Use best fit line to calculate estimated time to reach target health
|
|
seconds = (percentage - a) / b
|
|
-- Subtract current time to obtain "time remaining"
|
|
seconds = seconds - (GetTime() - unitTable.time)
|
|
if seconds < 0 then seconds = Bastion.TimeToDie.Enums.NEGATIVE_TTD end
|
|
end
|
|
end
|
|
end
|
|
return seconds
|
|
end
|
|
|
|
---@param minSamples? number
|
|
---@return Bastion.TimeToDie.Enums | integer
|
|
function Unit:TimeToDie2(minSamples)
|
|
if not self:Exists() then
|
|
return Bastion.TimeToDie.Enums.DOES_NOT_EXIST
|
|
end
|
|
local unitGuid = self:GetGUID()
|
|
if not unitGuid then
|
|
return Bastion.TimeToDie.Enums.NO_GUID
|
|
end
|
|
|
|
minSamples = minSamples or 3
|
|
---@type {TTD: {[number]: number}}
|
|
local unitInfo = Bastion.Globals.UnitInfo:IsCached(unitGuid) and
|
|
Bastion.Globals.UnitInfo:Get(unitGuid) or {}
|
|
|
|
local ttd = unitInfo.TTD
|
|
if not ttd then
|
|
ttd = {}
|
|
unitInfo.TTD = ttd
|
|
end
|
|
local v
|
|
if not ttd[minSamples] then
|
|
v = self:TimeToX(self:SpecialTTDPercentage(self:GetID()), minSamples)
|
|
if v >= 0 then
|
|
ttd[minSamples] = v
|
|
Bastion.Globals.UnitInfo:Set(unitGuid, unitInfo, .5)
|
|
end
|
|
end
|
|
|
|
|
|
return ttd[minSamples] or v
|
|
end
|
|
|
|
-- Get the boss unit TimeToDie
|
|
---@param minSamples? number
|
|
---@return Bastion.TimeToDie.Enums | integer
|
|
function Unit:BossTimeToDie(minSamples)
|
|
if self:IsInBossList() or self:IsDummy() then
|
|
return self:TimeToDie2(minSamples)
|
|
end
|
|
|
|
return Bastion.TimeToDie.Enums.DOES_NOT_EXIST
|
|
end
|
|
|
|
-- Get if the unit meets the TimeToDie requirements.
|
|
---@param operator CompareThisTable
|
|
---@param value number
|
|
---@param offset number
|
|
---@param valueThreshold number
|
|
---@param minSamples? number
|
|
---@return boolean
|
|
function Unit:FilteredTimeToDie(operator, value, offset, valueThreshold, minSamples)
|
|
local TTD = self:TimeToDie2(minSamples)
|
|
|
|
return TTD > -1 and TTD < valueThreshold and Bastion.Util:CompareThis(operator, TTD + (offset or 0), value) or false
|
|
end
|
|
|
|
-- Get if the boss unit meets the TimeToDie requirements.
|
|
---@param operator CompareThisTable
|
|
---@param value number
|
|
---@param offset number
|
|
---@param valueThreshold number
|
|
---@param minSamples? number
|
|
---@return boolean
|
|
function Unit:BossFilteredTimeToDie(operator, value, offset, valueThreshold, minSamples)
|
|
if self:IsInBossList() or self:IsDummy() then
|
|
return self:FilteredTimeToDie(operator, value, offset, valueThreshold, minSamples)
|
|
end
|
|
|
|
return false
|
|
end
|
|
|
|
-- Get if the Time To Die is Valid for an Unit (i.e. not returning a warning code).
|
|
---@param minSamples? number
|
|
---@return boolean
|
|
function Unit:TimeToDieIsNotValid(minSamples)
|
|
return self:TimeToDie2(minSamples) > -1
|
|
end
|
|
|
|
-- Get if the Time To Die is Valid for a boss Unit (i.e. not returning a warning code or not being a boss).
|
|
---@param minSamples? number
|
|
---@return boolean
|
|
function Unit:BossTimeToDieIsNotValid(minSamples)
|
|
if self:IsInBossList() then
|
|
return self:TimeToDieIsNotValid(minSamples)
|
|
end
|
|
|
|
return true
|
|
end
|
|
|
|
Bastion.Unit = Unit
|
|
|