landsandboat/scripts/globals/mobs.lua

897 lines
30 KiB
Lua

-----------------------------------
-- Global version of onMobDeath
-----------------------------------
require('scripts/globals/magic')
require('scripts/globals/missions')
require('scripts/globals/quests')
-----------------------------------
xi = xi or {}
xi.mob = xi.mob or {}
-- onMobDeathEx is called from the core
xi.mob.onMobDeathEx = function(mob, player, isKiller, isWeaponSkillKill)
end
-----------------------------------
-- placeholder / lottery NMs
-----------------------------------
-- is a lottery NM in the table already spawned or primed to pop?
local function lotteryPrimed(phList)
local nm = nil
for k, v in pairs(phList) do
-- if `v` is a table, then it's a table of numbers: { id, id2 }
if type(v) == 'table' then
for _, innerId in pairs(v) do
nm = GetMobByID(innerId)
if nm ~= nil and (nm:isSpawned() or nm:getRespawnTime() ~= 0) then
return true
end
end
else -- `v` is a number
nm = GetMobByID(v)
if nm ~= nil and (nm:isSpawned() or nm:getRespawnTime() ~= 0) then
return true
end
end
end
return false
end
local function getMobLuaPathObject(mob)
if not mob then
return nil
end
return xi.zones[mob:getZoneName()].mobs[mob:getName()]
end
-- - mobParam can either be a mobid or a mob entity object
-- it either accepts a table of spawn points to randomize, or looks to the mob's cached lua object for a spawnPoints entry
---@param mobParam number|CBaseEntity?
---@param spawnPointsOverride table?
xi.mob.updateNMSpawnPoint = function(mobParam, spawnPointsOverride)
local origMobParam = mobParam
-- sometimes we call from Zone.lua files and only have the mob id
if type(mobParam) == 'number' then
mobParam = GetMobByID(mobParam)
end
if mobParam == nil then
print('[updateNMSpawnPoint] Invalid mob parameter:')
print(origMobParam)
return
end
-- if no spawnPoints table was sent, extract from mob lua object
local spawnPoints = spawnPointsOverride
if spawnPoints == nil then
local mobObject = getMobLuaPathObject(mobParam)
if mobObject and mobObject.spawnPoints then
spawnPoints = mobObject.spawnPoints
end
end
-- Special check for NMs with the same name but multiple IDs
if spawnPoints and spawnPoints[mobParam:getID()] then
spawnPoints = spawnPoints[mobParam:getID()]
end
if
spawnPoints ~= nil and
type(spawnPoints) == 'table' and
#spawnPoints > 0
then
local chosenSpawn = utils.randomEntry(spawnPoints)
local randomRotation = math.random(0, 255) -- rotation does not matter
-- Updates the mob's spawn point
mobParam:setSpawn(chosenSpawn.x, chosenSpawn.y, chosenSpawn.z, randomRotation)
end
end
local function getMobEntityObj(phNmId)
local mobEntityObj = nil
if type(phNmId) == 'number' then
mobEntityObj = getMobLuaPathObject(GetMobByID(phNmId))
elseif type(phNmId) == 'table' then
mobEntityObj = getMobLuaPathObject(GetMobByID(utils.randomEntry(phNmId)))
end
return mobEntityObj
end
local function getNmId(phList, phId)
local nmId = nil
if phList and phList[phId] then
if type(phList[phId]) == 'number' then
nmId = phList[phId]
elseif type(phList[phId]) == 'table' then
nmId = utils.randomEntry(phList[phId])
end
end
return nmId
end
-- potential lottery placeholder was killed
---@param ph CBaseEntity
---@param phNmId integer|table
---@param chance integer
---@param cooldown integer
---@param params table?
xi.mob.phOnDespawn = function(ph, phNmId, chance, cooldown, params)
params = params or {}
--[[
params.immediate = true pop NM without waiting for next PH pop time
params.dayOnly = true spawn NM only at day time
params.nightOnly = true spawn NM only at night time
params.noPosUpdate = true do not run xi.mob.updateNMSpawnPoint()
params.spawnPoints = { {x = , y = , z = } } table of spawn points to choose from, overrides NM's lua-defined table
params.doNotEnablePhSpawn = true Don't enable ph respawns after NM is killed (for chained ph systems like steelfleece)
]]
local phId = ph:getID()
local nmId = nil
local nm = nil
local phList = nil
local mobEntityObj = getMobEntityObj(phNmId)
if mobEntityObj then
phList = mobEntityObj.phList
nmId = getNmId(phList, phId)
nm = nmId and GetMobByID(nmId)
end
-- This was not a PH for the NM
if
type(nmId) ~= 'number' or
nm == nil or
phList == nil
then
return false
end
-- ensure certain boolean params exist
local paramKeys =
{
'immediate',
'dayOnly',
'nightOnly',
'noPosUpdate',
'doNotEnablePhSpawn',
}
for _, pKey in ipairs(paramKeys) do
if type(params[pKey]) ~= 'boolean' then
params[pKey] = false
end
end
if xi.settings.main.NM_LOTTERY_CHANCE then
chance = xi.settings.main.NM_LOTTERY_CHANCE >= 0 and (chance * xi.settings.main.NM_LOTTERY_CHANCE) or 100
end
if xi.settings.main.NM_LOTTERY_COOLDOWN then
cooldown = xi.settings.main.NM_LOTTERY_COOLDOWN >= 0 and (cooldown * xi.settings.main.NM_LOTTERY_COOLDOWN) or cooldown
end
local pop = nm:getLocalVar('pop')
chance = math.ceil(chance * 10) -- chance / 1000.
if
GetSystemTime() <= pop or
lotteryPrimed(phList) or
math.random(1, 1000) > chance
then
return false
end
local nextRepopTime = VanadielTime() + GetMobRespawnTime(phId)
local nextRepopHour = math.floor((nextRepopTime % xi.vanaTime.DAY) / xi.vanaTime.HOUR)
-- If the NM is day only and spawn would happen during the night, bail out
if
params.dayOnly and
nextRepopHour < 4 and
nextRepopHour >= 20
then
return false
-- If the NM is night only and spawn would happen during the day, bail out
elseif
params.nightOnly and
nextRepopHour >= 4 and
nextRepopHour < 20
then
return false
end
-- on PH death, replace PH repop with NM repop
-- TODO, fetch phId's spawn slot and disable respawn for all mobs in that spawn slot
DisallowRespawn(phId, true)
DisallowRespawn(nmId, false)
-- Update mob's spawn position, if available
if not params.noPosUpdate then
xi.mob.updateNMSpawnPoint(nm, params.spawnPoints or nil)
end
-- if params.immediate is true, spawn the nm params.immediately (1ms) else use placeholder's timer
nm:setRespawnTime(params.immediate and 1 or GetMobRespawnTime(phId))
nm:addListener('DESPAWN', 'DESPAWN_' .. nmId, function(m)
-- on NM death, replace NM repop with PH repop
DisallowRespawn(nmId, true)
if not params.doNotEnablePhSpawn then
DisallowRespawn(phId, false)
local phMob = GetMobByID(phId)
if phMob then
phMob:setRespawnTime(GetMobRespawnTime(phId))
end
end
if m:getLocalVar('doNotInvokeCooldown') == 0 then
m:setLocalVar('pop', GetSystemTime() + cooldown)
end
m:removeListener('DESPAWN_' .. nmId)
end)
return true
end
-----------------------------------
-- mob additional melee effects
-----------------------------------
xi.mob.ae =
{
HP_DRAIN = 11,
MP_DRAIN = 12,
PETRIFY = 14,
PLAGUE = 15,
POISON = 16,
SILENCE = 17,
SLOW = 18,
STUN = 19,
TERROR = 20,
TP_DRAIN = 21,
WEIGHT = 22,
ENAMNESIA = 23,
BIND = 25,
SLEEP = 26,
DEFENSE_DOWN = 27,
ATTACK_DOWN = 28,
}
local additionalEffects =
{
[xi.mob.ae.HP_DRAIN] =
{
chance = 10,
ele = xi.element.DARK,
sub = xi.subEffect.HP_DRAIN,
msg = xi.msg.basic.ADD_EFFECT_HP_DRAIN,
mod = xi.mod.INT,
bonusAbilityParams = { bonusmab = 0, includemab = false },
code = function(mob, target, power)
mob:addHP(power)
end,
},
[xi.mob.ae.MP_DRAIN] =
{
chance = 10,
ele = xi.element.DARK,
sub = xi.subEffect.MP_DRAIN,
msg = xi.msg.basic.ADD_EFFECT_MP_DRAIN,
mod = xi.mod.INT,
bonusAbilityParams = { bonusmab = 0, includemab = false },
code = function(mob, target, power)
local mp = math.min(power, target:getMP())
target:delMP(mp)
mob:addMP(mp)
end,
},
[xi.mob.ae.PETRIFY] =
{
chance = 20,
ele = xi.element.EARTH,
sub = xi.subEffect.PETRIFY,
msg = xi.msg.basic.ADD_EFFECT_STATUS,
applyEffect = true,
eff = xi.effect.PETRIFICATION,
power = 1,
duration = 30,
minDuration = 1,
maxDuration = 45,
},
[xi.mob.ae.PLAGUE] =
{
chance = 25,
ele = xi.element.WATER,
sub = xi.subEffect.PLAGUE,
msg = xi.msg.basic.ADD_EFFECT_STATUS,
applyEffect = true,
eff = xi.effect.PLAGUE,
power = 1,
duration = 60,
minDuration = 1,
maxDuration = 60,
},
[xi.mob.ae.POISON] =
{
chance = 25,
ele = xi.element.WATER,
sub = xi.subEffect.POISON,
msg = xi.msg.basic.ADD_EFFECT_STATUS,
applyEffect = true,
eff = xi.effect.POISON,
power = 1,
duration = 30,
minDuration = 1,
maxDuration = 30,
tick = 3,
},
[xi.mob.ae.SILENCE] =
{
chance = 25,
ele = xi.element.WIND,
sub = xi.subEffect.SILENCE,
msg = xi.msg.basic.ADD_EFFECT_STATUS,
applyEffect = true,
eff = xi.effect.SILENCE,
power = 1,
duration = 30,
minDuration = 1,
maxDuration = 30,
},
[xi.mob.ae.ENAMNESIA] =
{
chance = 25,
ele = xi.element.FIRE,
sub = xi.subEffect.AMNESIA,
msg = xi.msg.basic.ADD_EFFECT_STATUS,
applyEffect = true,
eff = xi.effect.AMNESIA,
power = 1,
duration = 30,
minDuration = 1,
maxDuration = 30,
},
[xi.mob.ae.SLEEP] =
{
chance = 25,
ele = xi.element.DARK,
sub = xi.subEffect.SLEEP,
msg = xi.msg.basic.ADD_EFFECT_STATUS,
applyEffect = true,
eff = xi.effect.SLEEP_I,
power = 20,
duration = 30,
minDuration = 1,
maxDuration = 45,
},
[xi.mob.ae.SLOW] =
{
chance = 25,
ele = xi.element.EARTH,
sub = xi.subEffect.DEFENSE_DOWN,
msg = xi.msg.basic.ADD_EFFECT_STATUS,
applyEffect = true,
eff = xi.effect.SLOW,
power = 1000,
duration = 30,
minDuration = 1,
maxDuration = 45,
},
[xi.mob.ae.STUN] =
{
chance = 20,
ele = xi.element.THUNDER,
sub = xi.subEffect.STUN,
msg = xi.msg.basic.ADD_EFFECT_STATUS,
applyEffect = true,
eff = xi.effect.STUN,
duration = 5,
},
[xi.mob.ae.TERROR] =
{
chance = 20,
sub = xi.subEffect.PARALYSIS,
msg = xi.msg.basic.ADD_EFFECT_STATUS,
applyEffect = true,
eff = xi.effect.TERROR,
duration = 5,
code = function(mob, target, power)
mob:resetEnmity(target)
end,
},
[xi.mob.ae.TP_DRAIN] =
{
chance = 25,
ele = xi.element.DARK,
sub = xi.subEffect.TP_DRAIN,
msg = xi.msg.basic.ADD_EFFECT_TP_DRAIN,
mod = xi.mod.INT,
bonusAbilityParams = { bonusmab = 0, includemab = false },
code = function(mob, target, power)
local tp = math.min(power, target:getTP())
target:delTP(tp)
mob:addTP(tp)
end,
},
[xi.mob.ae.WEIGHT] =
{
chance = 25,
ele = xi.element.WIND,
sub = xi.subEffect.ATTACK_DOWN,
msg = xi.msg.basic.ADD_EFFECT_STATUS,
applyEffect = true,
eff = xi.effect.WEIGHT,
power = 1,
duration = 30,
minDuration = 1,
maxDuration = 45,
},
[xi.mob.ae.BIND] =
{
chance = 10,
ele = xi.element.ICE,
sub = xi.subEffect.DARKNESS_DAMAGE,
msg = xi.msg.basic.ADD_EFFECT_STATUS,
applyEffect = true,
eff = xi.effect.BIND,
power = 1,
duration = 30,
minDuration = 1,
maxDuration = 90,
},
[xi.mob.ae.DEFENSE_DOWN] =
{
chance = 20,
ele = xi.element.WIND,
sub = xi.subEffect.DEFENSE_DOWN,
msg = xi.msg.basic.ADD_EFFECT_STATUS,
applyEffect = true,
eff = xi.effect.DEFENSE_DOWN,
power = 25,
duration = 30,
minDuration = 1,
maxDuration = 60,
},
[xi.mob.ae.ATTACK_DOWN] =
{
chance = 20,
ele = xi.element.WATER,
sub = xi.subEffect.ATTACK_DOWN,
msg = xi.msg.basic.ADD_EFFECT_STATUS,
applyEffect = true,
eff = xi.effect.ATTACK_DOWN,
power = 25,
duration = 30,
minDuration = 1,
maxDuration = 60,
},
}
--[[
Helper function for xi.mob.onAddEffect that applies a status effect.
--]]
local addEffectStatus = function(mob, target, ae, params)
local resist = 1
if ae.ele then
resist = applyResistanceAddEffect(mob, target, ae.ele, ae.eff)
end
if resist > 0.5 and not target:hasStatusEffect(ae.eff) then
local power = params.power or ae.power or 0
local tick = ae.tick or 0
local duration = params.duration or ae.duration
duration = utils.clamp(duration, ae.minDuration, ae.maxDuration) * resist
target:addStatusEffect(ae.eff, { power = power, duration = duration, origin = mob, tick = tick })
if params.code then
params.code(mob, target, power)
elseif ae.code then
ae.code(mob, target, power)
end
return ae.sub, ae.msg, ae.eff
end
return 0, 0, 0
end
--[[
Helper function for xi.mob.onAddEffect that applies damage.
--]]
local addEffectImmediate = function(mob, target, damage, ae, params)
local power = 0
if params.power then
power = params.power
elseif ae.mod then
local dMod = mob:getStat(ae.mod) - target:getStat(ae.mod)
if dMod > 20 then
dMod = 20 + (dMod - 20) / 2
end
-- This is a bad assumption, but it prevents some negative damage (healing) when there otherwise shouldn't be
-- TODO: better understand damage add effects from mobs
if dMod < 0 then
dMod = 0
end
power = dMod + target:getMainLvl() - mob:getMainLvl() + damage / 2
end
-- target:printToPlayer(string.format('Initial Power: %f', power)) -- DEBUG
power = addBonusesAbility(mob, ae.ele, target, power, ae.bonusAbilityParams)
power = power * applyResistanceAddEffect(mob, target, ae.ele, 0)
power = power * xi.spells.damage.calculateAbsorption(target, ae.ele, true)
power = power * xi.spells.damage.calculateNullification(target, ae.ele, true, false)
if ae.sub ~= xi.subEffect.TP_DRAIN and ae.sub ~= xi.subEffect.MP_DRAIN then
power = finalMagicNonSpellAdjustments(mob, target, ae.ele, power)
end
-- target:printToPlayer(string.format('Adjusted Power: %f', power)) -- DEBUG
local message = ae.msg
if power < 0 then
if ae.negMsg then
message = ae.negMsg
power = power * -1 -- outgoing action packets only support unsigned integers. The "negative message" will also handle healing automagically deep inside core somewhere.
else
power = 0
end
end
if power ~= 0 then
if params.code then
params.code(mob, target, power)
elseif ae.code then
ae.code(mob, target, power)
end
return ae.sub, message, power
end
return 0, 0, 0
end
--[[
mob, target, and damage are passed from core into mob script's onAdditionalEffect
effect should be of type xi.mob.additionalEffect (see above)
params is a table that can contain any of:
chance: percent chance that effect procs on hit (default 20)
power: power of effect
duration: duration of effect, in seconds
code: additional code that will run when effect procs, of form function(mob, target, power)
params will override effect's default settings
--]]
xi.mob.onAddEffect = function(mob, target, damage, effect, params)
if type(params) ~= 'table' then
params = {}
end
local ae = additionalEffects[effect]
if ae then
local chance = params.chance or ae.chance or 100
local dLevel = target:getMainLvl() - mob:getMainLvl()
if dLevel > 0 then
chance = chance - 5 * dLevel
chance = utils.clamp(chance, 5, 95)
end
-- target:printToPlayer(string.format('Chance: %i', chance)) -- DEBUG
if math.random(1, 100) <= chance then
-- STATUS EFFECT
if ae.applyEffect then
return addEffectStatus(mob, target, ae, params)
-- IMMEDIATE EFFECT
else
return addEffectImmediate(mob, target, damage, ae, params)
end
end
else
printf('invalid additional effect for mobId %i', mob:getID())
end
return 0, 0, 0
end
-----------------------------------
-- Centralized function for calling one or more mob "pets"
-- It may be helpful to think of mobs with multiple as having "helpers" rather than explicitly pets
-- Since this is a looser definition than an explicit `->PMaster` and `->PPet` relationship that exists:
-- - these "pets" can have real pets of their own
-- - mobs can have multiple "pets"
-- - if the petId maps to the mob's actual pet (or petIds is nil and the mob has a pet mapped),
-- then no ROAM listener is installed on the pet, but the animations can still be consistently managed in one place
-----------------------------------
xi.mob.callPets = function(mob, petIds, params)
params = params or {}
-- params table:
-- params.dieWithOwner: will kill pets immediately if owner dies
-- params.persistOnDeath: pets persist when owner dies/disengages (default: false)
-- params.superLink: mob will assist pet (pet will always assist mob)
-- params.maxSpawns: stop if this many pets get spawned
-- params.ignoreBusy: allow pets to get summoned even if owner is busy, interupting any action it was performing
-- params.noAnimation: no animation packet from owner when calling pet
-- params.inactiveTime: how long for the call pet to take (owner will be inactive during period)
-- params.ignoreInactive: summoner does not become inactive while summoning a pet
-- this implies using summoner start/stop entity animation packet (which most mobs use when calling either pets or additional helpers)
-- if inactiveTime is zero, the following will determine an action packet to signal the mob is calling a pet
-- params.callPetJob will map to a particular mobskill action packet
-- if not, the function will use a generic 2-hour action packet
-- optionally you can override particular action packet params with params.action.X (see that code below)
-- NOTE these are not arbitrary choices, but multiple options to emulate retail behavior for any particular owner of pets/helpers
if xi.combat.behavior.isEntityBusy(mob) and not params.ignoreBusy then
return false
end
-- make sure at least one pet is available to summon
if type(petIds) == 'number' then
petIds = { petIds }
elseif petIds == nil then
-- ensure petIds is always a table so ipairs doesn't fail below
petIds = mob:getPet() and { mob:getPet():getID() } or {}
end
local canSummonPets = false
for _, petId in ipairs(petIds) do
local petToSummon = GetMobByID(petId)
if
petToSummon and
not petToSummon:isSpawned()
then
canSummonPets = true
end
end
if not canSummonPets then
return false
end
-- don't allow times so short the animations will bug out
if params.inactiveTime == nil or params.inactiveTime < 1000 then
params.inactiveTime = 0
end
local actionParams = nil
if params.inactiveTime == 0 and not params.action then
-- job based action packet
switch (params.callPetJob) : caseof
{
[xi.job.BST] = function(x)
-- inject "<mob> uses Call Beast"
actionParams =
{
finishCategory = xi.action.category.MOBABILITY_FINISH,
animationID = 718,
actionID = xi.mobSkill.CALL_BEAST,
messageID = xi.msg.basic.USES,
param = 0,
}
end,
[xi.job.DRG] = function(x)
-- inject "<mob> uses Call Wyvern"
actionParams =
{
finishCategory = xi.action.category.MOBABILITY_FINISH,
animationID = 438,
actionID = xi.mobSkill.CALL_WYVERN_1,
messageID = xi.msg.basic.USES,
param = 0,
}
end,
[xi.job.PUP] = function(x)
-- inject "<mob> uses Activate"
-- The mobskill has no action message, so we use the job ability
actionParams =
{
finishCategory = xi.action.category.JOBABILITY_FINISH,
animationID = 83,
actionID = xi.jobAbility.ACTIVATE,
messageID = xi.msg.basic.USES_JA,
param = 0,
}
end,
}
end
params.action = params.action or {}
if not actionParams then
-- Generic 2-hour animation with no message or options from params table
actionParams =
{
finishCategory = params.action.finishCategory or 11,
actionID = params.action.messageID or 307,
animationID = params.action.animationID or 439,
messageID = params.action.messageID or 0,
param = params.action.param or 0,
}
end
-- function to execute when pets are actually called (there may be an inactiveTime)
local callPetFinish = function(mobArg)
if mobArg:isDead() then
return
end
-- inject action packet to indicate mob is summoning a pet
if not params.noAnimation then
if params.inactiveTime > 0 then
mobArg:entityAnimationPacket(xi.animationString.CAST_SUMMONER_STOP)
else
if actionParams then
-- Generic 2-hour animation with no message
mobArg:injectActionPacket(mobArg:getID(), actionParams.finishCategory, actionParams.animationID, 0, 0x18, actionParams.messageID, actionParams.actionID, actionParams.param)
end
end
end
local spawnPos = mobArg:getSpawnPos()
local pos = mobArg:getPos()
params.maxSpawns = params.maxSpawns or #petIds
local spawnedCount = 0
for _, petId in ipairs(petIds) do
local petToSummon = GetMobByID(petId)
if
spawnedCount < params.maxSpawns and
petToSummon and
not petToSummon:isSpawned()
then
spawnedCount = spawnedCount + 1
-- spawn pet around owner
petToSummon:setSpawn(pos.x + math.random(-2, 2), pos.y, pos.z + math.random(-2, 2), pos.rot)
petToSummon:spawn()
-- set home to be the owner's home position
petToSummon:setSpawn(spawnPos.x, spawnPos.y, spawnPos.z, spawnPos.rot)
local ownerRoamListenerName = fmt('OWNER_ASSIST_{}', petId)
if params.superLink then
mobArg:addListener('ROAM_TICK', ownerRoamListenerName, function(owner)
local petToAssist = GetMobByID(petId)
local assistTarg = petToAssist and petToAssist:getTarget() or nil
if assistTarg then
owner:updateEnmity(assistTarg)
end
end)
end
-- so they die at the same time
-- even without this parameter, if the owner is dead and the pet is roaming, it will die
if params.dieWithOwner then
local listenerName = fmt('OWNER_DEATH_{}', petId)
mobArg:addListener('DEATH', listenerName, function(owner)
local petToKill = GetMobByID(petId)
if petToKill and petToKill:isSpawned() then
petToKill:setHP(0)
end
owner:removeListener(listenerName)
end)
end
-- make pet assist with a slight delay to allow spawn to complete so animations don't get bugged
local ownerID = mobArg:getID()
petToSummon:stun(500)
if petToSummon ~= mobArg:getPet() then
local persistOnDeath = params.persistOnDeath or false
if persistOnDeath then
petToSummon:addListener('ROAM_TICK', 'ASSIST_OWNER', function(petArg)
local owner = GetMobByID(ownerID)
if not owner then
return
end
local newTarget = owner:getTarget() or nil
if newTarget then
petArg:updateEnmity(newTarget)
return
end
if owner:isAlive() and not petArg:hasFollowTarget() then
petArg:follow(owner, xi.followType.ROAM)
return
end
end)
else
petToSummon:addListener('ROAM_TICK', 'ASSIST_OWNER', function(petArg)
local owner = GetMobByID(ownerID)
if not owner then
return
end
local newTarget = owner:getTarget() or nil
if newTarget then
petArg:updateEnmity(newTarget)
return
end
if owner:isDead() then
petArg:setHP(0)
return
end
if not petArg:hasFollowTarget() then
petArg:follow(owner, xi.followType.ROAM)
return
end
end)
end
-- so we don't wait for the next roam tick (pet assists as soon as :stun is complete)
petToSummon:queue(0, function(petArg)
petArg:triggerListener('ROAM_TICK', petArg)
end)
end
-- cleanup any listeners related to this pet when it dies
-- (:removeListener quietly exits if the listener doesn't exist)
petToSummon:addListener('DESPAWN', 'PET_LISTENER_CLEANUP', function(petArg)
local owner = GetMobByID(ownerID)
if owner then
owner:removeListener(ownerRoamListenerName)
end
petArg:removeListener('ASSIST_OWNER')
petArg:removeListener('PET_LISTENER_CLEANUP')
end)
end
end
end
if params.inactiveTime > 0 then
if not params.ignoreInactive then
-- put owner into inactive state until the timer fires
mob:stun(params.inactiveTime)
end
-- start call pet animation
if not params.noAnimation then
mob:entityAnimationPacket(xi.animationString.CAST_SUMMONER_START)
end
end
-- regardless, call the anonymous function from above in params.inactiveTime ms (possibly zero)
-- note that timers cause xi.combat.behavior.isEntityBusy to return true, and so does mob:stun(X)
mob:timer(params.inactiveTime, callPetFinish)
return true
end