landsandboat/scripts/globals/dark_ixion.lua

818 lines
32 KiB
Lua

-----------------------------------
-- Area: One of many zones in Shadowreign zones
-- NM: Dark Ixion
-- Info: https://ffxiclopedia.fandom.com/wiki/Dark_Ixion
-- https://www.bg-wiki.com/ffxi/Dark_Ixion
--[[
Find DI by:
- checking the server's zone var: "!checkvar server DarkIxion_ZoneID"
- go to that zone: !zone <zoneID>
- go to DI: !gotoname Dark_Ixion
Outline of the mob's behavior:
Will run away, most of the time to another zone, if:
- Campaign Battle begins
- a player scares it by getting too close
- attacks or attempts to claim with a Stygian Ash but misses
- or if whoever has successfully claimed it wipes.
- His HP is preserved after wiping and him running away to another zone
- If Dark Ixion is due to spawn or is already spawned during maintenance, he will spawn shortly after server comes back online.
- If he was not due to spawn during this time frame, his spawn window will reset to 21 hours after servers come online.
Roaming: runs fast and can be spooked via normal aggro/claim actions. If spooked, it does the same thing as attempting a claim but missing with a Stygian Ash
Missed Claim: pauses for a few, then runs away very fast
Claim: can only be claimed by landing a hit with Stygian Ash
Targetting: Can damage and kill players riding Chocobo with Area of Effect attacks even if player is not in the Alliance fighting him.
Mobskills:
- has a normal set of TP moves, but telegraphs most/all of them
- if he has an aura then his TP moves are doubled up back to back
- has a special move that isn't listed in the combat log, that many have called 'Trample'
- Trample: Charges forward, dealing high damage to,(400-1000) and lowering the MP (10-30%) of _ANYONE_ in his path. No message is displayed in the chat log, other than basic "<target> takes X damage."
- When Dark Ixion's HP is low, he can do up to 3 Tramples in succession.
- Can be avoided easily by moving out of its path.
- May charge in the opposite, or an entirely random, direction from the one he is currently facing.
- Will load a set number of targets in his path before ramming forward.
- Occasionally, a person in his path will not be hit, as well as those wandering in its path after it has begun its charge.
--]]
-----------------------------------
require('scripts/globals/npc_util')
xi = xi or {}
xi.darkixion = xi.darkixion or {}
-- TODOs, notes, and reminders
-- TODO: dmg taken from front/rear (if we can)
local animationSubs =
{
NORMAL = 0, -- can trample
TRAMPLE = 1, -- animation during trample event
HORN_BROKEN = 2, -- broken horn, cannot trample or double-up mobskills
GLOWING = 3, -- glowing: doubles up mobskills and used shortly during horn regrowth
}
xi.darkixion.hpValue = GetServerVariable('DarkIxion_HP')
xi.darkixion.hornState = GetServerVariable('DarkIxion_HornState')
xi.darkixion.hornStates =
{
[1] =
{
animationSub = animationSubs.NORMAL,
hideHP = true,
},
[2] =
{
animationSub = animationSubs.HORN_BROKEN,
hideHP = false,
},
}
local changeHornState = function(mob, state)
xi.darkixion.hornState = state
local hornStateData = xi.darkixion.hornStates[xi.darkixion.hornState]
if not hornStateData then
xi.darkixion.hornState = GetServerVariable('DarkIxion_HornState')
if not xi.darkixion.hornStates[xi.darkixion.hornState] then
xi.darkixion.hornState = 1
end
hornStateData = xi.darkixion.hornStates[xi.darkixion.hornState]
end
SetServerVariable('DarkIxion_HornState', xi.darkixion.hornState)
if hornStateData.animationSub ~= mob:getAnimationSub() then
mob:setAnimationSub(hornStateData.animationSub)
mob:hideHP(hornStateData.hideHP)
end
-- reset phasechange timer
mob:setLocalVar('phaseChange', GetSystemTime() + math.random(60, 240))
end
xi.darkixion.zoneinfo =
{
[xi.zone.JUGNER_FOREST_S] =
{
pathList =
{
{ x = -191.5, y = 000.0, z = 092.0 },
{ x = -139.6, y = 005.0, z = 097.0 },
{ x = -064.0, y = 002.5, z = 093.0 },
{ x = -025.5, y = 004.5, z = 057.0 },
{ x = 0025.0, y = 006.0, z = 040.0 },
{ x = 0087.0, y = 002.0, z = 029.0 },
{ x = 0074.0, y = 000.0, z = -011.0 },
{ x = 0087.0, y = 000.0, z = -083.0 },
{ x = 0118.0, y = 000.0, z = -109.0 },
{ x = 0088.0, y = 000.0, z = -170.0 },
{ x = 0061.0, y = 002.0, z = -156.0 },
{ x = 0030.8, y = 002.0, z = -179.5 },
{ x = -031.8, y = 000.0, z = -196.5 },
{ x = -098.5, y = 000.0, z = -169.0 },
{ x = -177.5, y = 000.0, z = -152.0 },
{ x = -218.0, y = 000.0, z = -234.0 },
{ x = -323.0, y = 002.0, z = -260.7 },
{ x = -225.0, y = 007.0, z = -110.0 },
{ x = -220.7, y = 006.0, z = -107.0 },
{ x = -260.6, y = 006.0, z = -091.0 },
{ x = -259.0, y = 001.0, z = -030.5 },
{ x = -304.0, y = 001.4, z = 004.0 },
{ x = -299.0, y = 002.7, z = 072.5 },
{ x = -291.0, y = 009.0, z = 140.5 },
{ x = -202.7, y = 002.0, z = 139.0 },
}
},
[xi.zone.WEST_SARUTABARUTA_S] =
{
pathList =
{
{ x = 0231.8, y = -025.0, z = 258.5 },
{ x = 0227.5, y = -017.5, z = 209.0 },
{ x = 0134.6, y = -020.5, z = 195.0 },
{ x = 0068.0, y = -012.4, z = 194.8 },
{ x = 0068.0, y = -020.0, z = 254.0 },
{ x = -027.0, y = -012.0, z = 296.4 },
{ x = -019.0, y = -011.7, z = 349.0 },
{ x = 0016.0, y = -018.0, z = 405.6 },
{ x = 0092.0, y = -022.0, z = 391.5 },
{ x = 0143.7, y = -037.5, z = 344.6 },
{ x = 0098.8, y = -027.0, z = 269.0 },
}
},
[xi.zone.ROLANBERRY_FIELDS_S] =
{
pathList =
{
{ x = 0025.0, y = -008.7, z = -346.8 },
{ x = 0016.6, y = -007.7, z = -377.0 },
{ x = -020.0, y = -004.3, z = -328.0 },
{ x = -055.7, y = -001.0, z = -370.7 },
{ x = -051.0, y = 000.0, z = -465.0 },
{ x = -092.8, y = 004.2, z = -540.0 },
{ x = -032.0, y = 004.8, z = -584.5 },
{ x = -019.0, y = 001.0, z = -654.8 },
{ x = -071.0, y = -006.9, z = -631.6 },
{ x = -116.6, y = -007.0, z = -592.8 },
{ x = -109.8, y = -007.0, z = -553.0 },
{ x = -220.0, y = 004.0, z = -478.7 },
{ x = -219.0, y = 004.4, z = -344.0 },
{ x = -181.0, y = 004.4, z = -330.0 },
{ x = -171.0, y = 004.8, z = -303.0 },
{ x = -142.7, y = 004.6, z = -234.7 },
{ x = -185.0, y = 004.8, z = -186.0 },
{ x = -228.8, y = 004.5, z = -141.5 },
{ x = -235.7, y = 000.0, z = -052.0 },
{ x = -229.0, y = -003.6, z = -025.5 },
{ x = -209.6, y = -007.6, z = -037.5 },
{ x = -176.7, y = -007.0, z = -080.5 },
{ x = -141.3, y = -008.0, z = -089.0 },
{ x = -083.9, y = -007.7, z = -139.5 },
{ x = -042.0, y = -007.3, z = -185.0 },
}
},
[xi.zone.GRAUBERG_S] =
{
pathList =
{
{ x = 344.4, y = 036.7, z = -443.0 },
{ x = 310.5, y = 027.0, z = -429.5 },
{ x = 273.5, y = 016.3, z = -404.7 },
{ x = 212.0, y = 010.0, z = -393.6 },
{ x = 158.5, y = -004.8, z = -380.0 },
{ x = 093.0, y = -015.7, z = -333.8 },
{ x = 032.0, y = -019.0, z = -308.8 },
{ x = 000.7, y = -032.0, z = -280.5 },
{ x = 063.5, y = -029.5, z = -373.0 },
{ x = 130.6, y = -039.5, z = -208.0 },
{ x = 170.0, y = -023.5, z = -240.5 },
{ x = 225.0, y = -010.0, z = -290.0 },
{ x = 249.0, y = 008.6, z = -365.0 },
{ x = 302.0, y = 018.0, z = -388.0 },
{ x = 367.8, y = 017.0, z = -364.0 },
{ x = 436.0, y = 017.3, z = -332.6 },
{ x = 506.0, y = 015.0, z = -293.0 },
{ x = 539.5, y = 015.8, z = -296.7 },
{ x = 528.5, y = 025.0, z = -369.8 },
{ x = 506.5, y = 040.0, z = -399.0 },
{ x = 461.8, y = 039.0, z = -412.0 },
{ x = 388.8, y = 031.0, z = -408.7 },
}
},
[xi.zone.BATALLIA_DOWNS_S] =
{
pathList =
{
{ x = -083.0, y = -008.0, z = 035.0 },
{ x = -059.0, y = -008.0, z = 008.0 },
{ x = -090.5, y = -009.5, z = -027.0 },
{ x = -145.5, y = -018.0, z = -024.7 },
{ x = -181.5, y = -018.5, z = -004.0 },
{ x = -243.0, y = -022.2, z = -027.8 },
{ x = -301.0, y = -018.6, z = -017.0 },
{ x = -363.0, y = -024.0, z = -038.5 },
{ x = -459.0, y = -005.7, z = 033.5 },
{ x = -443.7, y = -011.5, z = 056.8 },
{ x = -418.7, y = -013.0, z = 175.0 },
{ x = -387.5, y = -013.8, z = 219.5 },
{ x = -299.0, y = -010.5, z = 220.8 },
{ x = -261.0, y = -005.5, z = 233.7 },
{ x = -254.5, y = -005.0, z = 293.8 },
{ x = -185.5, y = 002.5, z = 294.0 },
{ x = -086.0, y = 000.0, z = 257.5 },
{ x = -099.5, y = 002.0, z = 164.5 },
{ x = -049.5, y = 002.5, z = 103.0 },
{ x = 0000.0, y = 000.0, z = 046.0 },
}
},
[xi.zone.FORT_KARUGO_NARUGO_S] =
{
pathList =
{
{ x = -088.8, y = -068.0, z = -269.5 },
{ x = -025.0, y = -067.0, z = -282.5 },
{ x = 0023.7, y = -068.0, z = -232.0 },
{ x = 0054.7, y = -069.7, z = -178.0 },
{ x = 0085.0, y = -064.0, z = -165.5 },
{ x = 0105.5, y = -065.0, z = -180.0 },
{ x = 0136.0, y = -059.0, z = -215.5 },
{ x = 0180.7, y = -056.0, z = -195.5 },
{ x = 0198.0, y = -057.8, z = -146.7 },
{ x = 0199.0, y = -043.0, z = -062.5 },
{ x = 0200.8, y = -040.0, z = -026.4 },
{ x = 0205.7, y = -028.0, z = 021.3 },
{ x = 0256.6, y = -022.0, z = 025.0 },
{ x = 0281.6, y = -026.5, z = 010.0 },
{ x = 0259.5, y = -029.8, z = -049.5 },
{ x = 0239.0, y = -040.5, z = -100.0 },
{ x = 0205.7, y = -057.0, z = -145.0 },
{ x = 0194.0, y = -054.0, z = -186.0 },
{ x = 0180.7, y = -047.5, z = -227.2 },
{ x = 0135.0, y = -050.8, z = -251.0 },
{ x = 0107.8, y = -061.7, z = -295.5 },
{ x = 0097.0, y = -061.5, z = -307.5 },
{ x = 0041.0, y = -068.0, z = -321.0 },
{ x = -007.0, y = -067.9, z = -301.0 },
{ x = -052.5, y = -067.7, z = -275.0 },
}
},
[xi.zone.EAST_RONFAURE_S] =
{
pathList =
{
{ x = 236.0, y = -020.0, z = -323.0 },
{ x = 245.6, y = -019.5, z = -290.0 },
{ x = 291.0, y = -016.0, z = -257.5 },
{ x = 358.5, y = -016.0, z = -259.0 },
{ x = 384.7, y = -015.0, z = -227.0 },
{ x = 405.0, y = -016.7, z = -220.0 },
{ x = 434.0, y = -016.7, z = -220.0 },
{ x = 459.0, y = -015.7, z = -225.5 },
{ x = 469.0, y = -015.0, z = -256.0 },
{ x = 496.0, y = -015.0, z = -266.6 },
{ x = 498.0, y = -016.0, z = -303.0 },
{ x = 501.0, y = -015.0, z = -327.5 },
{ x = 509.0, y = -005.0, z = -376.8 },
{ x = 537.0, y = -005.5, z = -387.0 },
{ x = 537.0, y = -006.0, z = -437.0 },
{ x = 514.0, y = -009.0, z = -450.5 },
{ x = 480.0, y = -009.6, z = -446.6 },
{ x = 438.0, y = -010.0, z = -440.5 },
{ x = 411.4, y = -010.0, z = -393.0 },
{ x = 368.5, y = -010.0, z = -407.0 },
{ x = 346.0, y = -007.0, z = -429.8 },
{ x = 318.2, y = 000.0, z = -437.6 },
{ x = 271.5, y = 004.3, z = -461.6 },
{ x = 219.7, y = -017.0, z = -334.0 },
},
},
}
xi.darkixion.repop = function(mob)
DespawnMob(mob:getID())
local keys = {}
for k, _ in pairs(xi.darkixion.zoneinfo) do
table.insert(keys, k)
end
local randZoneID = keys[math.random(#keys)]
SetServerVariable('DarkIxion_ZoneID', randZoneID)
-- wiki says 'It can pop there in less than 10 seconds or take the whole 15 minutes'
SetServerVariable('DarkIxion_PopTime', GetSystemTime() + math.random(1, 15 * 60)) -- based on onGameHour function timing
end
-- Adjustments made once to Dark Ixion when he begins roaming
xi.darkixion.roamingMods = function(mob)
if mob:getLocalVar('RunAway') ~= 0 then
mob:setBaseSpeed(70)
else
mob:setBaseSpeed(40)
end
-- don't take damage until the fight officially starts
mob:setMod(xi.mod.UDMGPHYS, -10000)
mob:setMod(xi.mod.UDMGRANGE, 0) -- TODO figure out how to make Ixion only take damage from stygian ash
mob:setMod(xi.mod.UDMGBREATH, -10000)
mob:setMod(xi.mod.UDMGMAGIC, -10000)
mob:setMod(xi.mod.REGAIN, 0)
-- restore hp just in case something caused him to regen while roaming
if xi.darkixion.hpValue == 0 then
xi.darkixion.hpValue = mob:getHP()
SetServerVariable('DarkIxion_HP', xi.darkixion.hpValue)
end
mob:setHP(xi.darkixion.hpValue)
-- restore horn status
changeHornState(mob, xi.darkixion.hornState)
-- ensure he's in initial state for beginning of fight
mob:setMobSkillAttack(39)
mob:setLocalVar('trampleCount', 0)
mob:setBehavior(0)
mob:setAutoAttackEnabled(true)
mob:setMobAbilityEnabled(true)
end
xi.darkixion.zoneOnInit = function(zone)
local ixion = zone:queryEntitiesByName('Dark_Ixion')[1]
if not ixion then
return
end
local ixionZoneID = GetServerVariable('DarkIxion_ZoneID')
-- check this on only one zone to catch when ixion has no zone assignment
if
xi.darkixion.zoneinfo[ixionZoneID] == nil or
(GetServerVariable('DarkIxion_PopTime') < GetSystemTime() and ixionZoneID == zone:getID())
then
-- reset zone ID but let him spawn next game hour
if xi.darkixion.zoneinfo[ixionZoneID] == nil then
xi.darkixion.repop(ixion)
end
-- 'If Dark Ixion is due to spawn or is already spawned during maintenance, he will spawn shortly after server comes back online.'
SetServerVariable('DarkIxion_PopTime', GetSystemTime() + 10)
elseif
GetServerVariable('DarkIxion_PopTime') > GetSystemTime() and
ixionZoneID == zone:getID()
then
-- leave zone alone, push back repop ... since zone was already randomized, implied by PopTime being in the future
-- 'If he was not due to spawn during this time frame (after maintenance), his spawn window will reset to 21 hours after servers come online.'
SetServerVariable('DarkIxion_PopTime', GetSystemTime() + 21 * 60 * 60)
end
end
xi.darkixion.zoneOnGameHour = function(zone)
local ixion = zone:queryEntitiesByName('Dark_Ixion')[1]
if not ixion then
return
end
local ixionZoneID = GetServerVariable('DarkIxion_ZoneID')
local ixionPopTime = GetServerVariable('DarkIxion_PopTime')
if
ixionZoneID == zone:getID() and
ixionPopTime < GetSystemTime() - 24 * 60 * 60
then
-- wander logic in onGameHour so even sleeping zones with no players can hold DI and cycle him out
xi.darkixion.repop(ixion)
elseif
not ixion:isSpawned() and
ixionZoneID == zone:getID() and
ixionPopTime < GetSystemTime() - 45
then
-- if gamehour flip is within 45s, randomly spawn within next twice that
ixion:setRespawnTime(math.random(0, 90))
elseif
ixion:isSpawned() and
ixionZoneID ~= zone:getID()
then
-- really shouldn't be possible, but catch just in case a GM manually spawned him somewhere else
if ixion:isEngaged() then
-- cleanly handle run away mechanic
ixion:disengage()
else
-- just go away
DespawnMob(ixion:getID())
end
end
end
xi.darkixion.onMobDeath = function(mob, player, optParams)
if player then
player:addTitle(xi.title.IXION_HORNBREAKER)
end
if optParams.isKiller or optParams.noKiller then
-- only reset hp after dying (hp and horn status persist through zones when he runs away and despawns)
xi.darkixion.hpValue = 0
SetServerVariable('DarkIxion_HP', xi.darkixion.hpValue)
changeHornState(mob, 1)
end
end
xi.darkixion.onMobDespawn = function(mob)
DisallowRespawn(mob:getID(), true)
if mob:getZoneID() == GetServerVariable('DarkIxion_ZoneID') then
xi.darkixion.repop(mob)
SetServerVariable('DarkIxion_PopTime', GetSystemTime() + math.random(20, 24) * 60 * 60) -- repop 20-24 hours after death
end
end
local checkHornBreak = function(mob, attacker)
local animationSub = mob:getAnimationSub()
if
not xi.combat.behavior.isEntityBusy(mob) and
(animationSub == animationSubs.NORMAL or animationSub == animationSubs.GLOWING) and
(attacker ~= nil and attacker:isInfront(mob)) and
math.random(1, 100) <= 5
then
changeHornState(mob, 2)
end
end
xi.darkixion.onCriticalHit = function(mob, attacker)
checkHornBreak(mob, attacker)
end
xi.darkixion.onWeaponskillHit = function(mob, attacker, weaponskill)
checkHornBreak(mob, attacker)
end
xi.darkixion.onMobWeaponSkill = function(target, mob, skill)
local skillID = skill:getID()
if skillID == xi.mobSkill.DAMSEL_MEMENTO then -- sometimes after healing, fix horn
if
mob:getAnimationSub() == animationSubs.HORN_BROKEN and
math.random(1, 100) <= 25
then
-- If horn is restored by heal, glow and allow animation to finish, then restore horn
skill:setFinalAnimationSub(3)
mob:queue(0, function(mobArg)
mobArg:stun(500)
changeHornState(mobArg, 1)
end)
end
elseif skillID == xi.mobSkill.DI_GLOW then
-- glow TP move telegraphs a damaging TP move, perform it now
local skillList =
{
xi.mobSkill.WRATH_OF_ZEUS,
xi.mobSkill.LIGHTNING_SPEAR,
xi.mobSkill.ACHERON_KICK,
xi.mobSkill.RAMPANT_STANCE,
}
local chosenSkill = utils.randomEntry(skillList)
-- adjust behavior so he doesn't sneak another move in between the sequence
mob:setBehavior(xi.behavior.NO_TURN + xi.behavior.STANDBACK)
mob:setAutoAttackEnabled(false)
mob:useMobAbility(chosenSkill)
if mob:getAnimationSub() == animationSubs.GLOWING then
-- queue a second if in glowing phase
mob:useMobAbility(chosenSkill)
end
elseif
skillID == xi.mobSkill.DI_HORN_ATTACK or
skillID == xi.mobSkill.DI_BITE_ATTACK or
skillID == xi.mobSkill.DI_KICK_ATTACK
then
-- determine if we want to run (trample) soon, do it randomly off autos, more frequent and more runs when low
if mob:getAnimationSub() == animationSubs.NORMAL then
local mobHPP = mob:getHPP()
local trampleCount = mob:getLocalVar('trampleCount')
local random = math.random(1, 100)
if random <= 30 and mobHPP < 33 then
trampleCount = trampleCount + math.random(1, 3)
elseif random <= 20 and mobHPP < 50 then
trampleCount = trampleCount + math.random(1, 2)
elseif random <= 10 then
trampleCount = trampleCount + 1
end
-- this variable will determine if Ixion can trample, but it's also gated by animation sub and a timestamp
mob:setLocalVar('trampleCount', utils.clamp(trampleCount, 0, 3)) -- Safety net for trample count
end
end
-- once TP move sequences are done, reset mob behaviors
mob:queue(0, function(mobArg)
if not xi.combat.behavior.isEntityBusy(mobArg) then
mobArg:setBehavior(0)
mobArg:setAutoAttackEnabled(true)
mobArg:setMobAbilityEnabled(true)
end
end)
end
xi.darkixion.onMobInitialize = function(mob)
mob:addImmunity(xi.immunity.GRAVITY)
mob:addImmunity(xi.immunity.BIND)
mob:addImmunity(xi.immunity.LIGHT_SLEEP)
mob:addImmunity(xi.immunity.DARK_SLEEP)
mob:addImmunity(xi.immunity.PETRIFY)
mob:addImmunity(xi.immunity.STUN)
mob:addImmunity(xi.immunity.TERROR)
mob:setMobMod(xi.mobMod.NO_REST, 10)
mob:setMobMod(xi.mobMod.AOE_HIT_ALL, 1)
end
-- either turn in a random direction, or turn away from skillTarget to use acheron kick
local turnForSkill = function(mob, skillTarget)
local mobPos = mob:getPos()
local lookAtPos = { x = mobPos.x + math.random(-4, 4), y = mobPos.y, z = mobPos.z + math.random(-4, 4) }
if skillTarget then
local targetPos = skillTarget:getPos()
lookAtPos.x = 2 * mobPos.x - targetPos.x
lookAtPos.z = 2 * mobPos.z - targetPos.z
end
mob:lookAt(lookAtPos)
end
-- Dark Ixion CAN turn around to use this move on anyone with hate
local acheronKickPositioning = function(mob)
local skillTarget = mob:getTarget() -- mobskill has TARGET_SELF flag, extract target from battleTarget
local targets = mob:getEnmityList()
if skillTarget then
-- current target may not be on the enmity table, if it is then there's a slightly higher chance to target it
table.insert(targets, { entity = skillTarget })
end
local potentialTargets = {}
for _, entry in ipairs(targets) do
if entry.entity and mob:checkDistance(entry.entity) < 15 then
table.insert(potentialTargets, entry.entity)
end
end
if #potentialTargets == 0 then
return
end
skillTarget = utils.randomEntry(potentialTargets)
turnForSkill(mob, skillTarget)
end
xi.darkixion.onMobSpawn = function(mob)
xi.darkixion.roamingMods(mob)
SetServerVariable('DarkIxion_PopTime', GetSystemTime())
mob:setAggressive(true)
mob:setRoamFlags(bit.bor(mob:getRoamFlags(), xi.roamFlag.SCRIPTED)) -- do not roam around, only path during roam via patrol path
-- pre-mobskill listeners to turn mob as appropriate
mob:addListener('WEAPONSKILL_STATE_ENTER', 'IXION_WS_STATE_ENTER', function(mobArg, skillId)
if skillId == xi.mobSkill.ACHERON_KICK then
acheronKickPositioning(mobArg)
elseif skillId == xi.mobSkill.LIGHTNING_SPEAR then
turnForSkill(mobArg, nil)
end
end)
end
xi.darkixion.onMobRoam = function(mob)
if
mob:getLocalVar('RunAway') ~= 0 and
mob:getLocalVar('RunAway') + 60 < GetSystemTime()
then
-- 60s of running away, time to repop somewhere else
xi.darkixion.repop(mob)
end
if not mob:isFollowingPath() then
local pathList = xi.darkixion.zoneinfo[mob:getZoneID()].pathList
if mob:checkDistance(pathList[1]) > 50 then
-- not patrolling and is very far from first point in list, path back
-- This ensures he always cleanly paths when roaming (doesn't clip through terrain)
mob:pathTo(pathList[1].x, pathList[1].y, pathList[1].z, xi.path.flag.RUN)
else
-- patrol list persists through combat so if a wipe happens, Ixion will resume pathing when doing the runaway mechanic
mob:pathThrough(pathList, bit.bor(xi.path.flag.RUN, xi.path.flag.PATROL))
end
end
end
xi.darkixion.onMobEngage = function(mob, target)
xi.darkixion.roamingMods(mob)
-- if stygian ash missed or aggro via any other means, immediately disengage (even if hearing/sight aggro 'If you get too close, DI runs away')
if mob:getLocalVar('aeFromItemId') == xi.item.PINCH_OF_STYGIAN_ASH then
mob:setLocalVar('RunAway', 0)
else
mob:disengage()
-- TODO figure out how to make Ixion only take damage from stygian ash
-- Note that xi.darkixion.roamingMods resets his hp to the stored value, so this is a display issue only
-- i.e. there's no concern of say hitting him with high dmg WS, letting him run away, and doing it again when you find him in another zone
return
end
mob:setMod(xi.mod.UDMGPHYS, 0)
mob:setMod(xi.mod.UDMGRANGE, 0)
mob:setMod(xi.mod.UDMGBREATH, 0)
mob:setMod(xi.mod.UDMGMAGIC, 0)
mob:setMod(xi.mod.REGAIN, 20) -- 'has tp regen': https://www.bluegartr.com/threads/59044-Ixion-discussion-thread/page8
mob:setLocalVar('phaseChange', GetSystemTime() + math.random(60, 240))
end
xi.darkixion.onMobDisengage = function(mob)
xi.darkixion.hpValue = mob:getHP()
SetServerVariable('DarkIxion_HP', xi.darkixion.hpValue)
xi.darkixion.roamingMods(mob)
if mob:getLocalVar('RunAway') == 0 then
mob:setBaseSpeed(70)
-- disengage, standing still unclaimed before 'Running away'
local waitTime = 15
mob:stun(waitTime * 1000)
mob:setLocalVar('RunAway', GetSystemTime() + waitTime)
else
-- just reset time until despawn
mob:setLocalVar('RunAway', GetSystemTime())
end
-- no chance of him staying in this zone unless an ash is landed before he runs away and despawns
mob:setAggressive(false)
mob:setLocalVar('aeFromItemId', 0)
end
xi.darkixion.onMobFight = function(mob, target)
-- This section deals with him glowing (double TP moves)
local animationSub = mob:getAnimationSub()
if
not xi.combat.behavior.isEntityBusy(mob) and
GetSystemTime() >= mob:getLocalVar('phaseChange') and
(animationSub == animationSubs.NORMAL or
animationSub == animationSubs.GLOWING)
then
mob:setLocalVar('phaseChange', GetSystemTime() + math.random(60, 240))
-- alternate phase between normal (can trample) and glowing (double-up mobskills)
animationSub = animationSub ~= animationSubs.NORMAL and animationSubs.NORMAL or animationSubs.GLOWING
mob:setAnimationSub(animationSub)
mob:stun(500)
end
-- Everything below deals with his charge attack (trample)
if
not xi.combat.behavior.isEntityBusy(mob) and
mob:getLocalVar('trampleCount') >= 1 and
GetSystemTime() >= mob:getLocalVar('nextTrampleTime') and
animationSub == animationSubs.NORMAL -- don't trample if horn is broken or any other sequence is happening
then
mob:setTP(0)
xi.darkixion.beginTramplePath(mob)
end
if animationSub == animationSubs.TRAMPLE then
-- cleanly exit trample when reaching the point (TODO check explicitly for a scripted path?)
-- runPathTime timestamp hard exit in case of navmesh abuse
if
mob:isFollowingPath() and
GetSystemTime() < mob:getLocalVar('tramplePathTime')
then
xi.darkixion.trampleEntitiesInFront(mob)
else
xi.darkixion.endTramplePath(mob)
end
end
end
-- Used by Trample mechanics
local getEnemiesInRange = function(mob, distance)
local targetTypeFlag = xi.targetType.SELF + xi.targetType.ANY_ALLEGIANCE -- self + any_allegiance is the targetfind code for "target myself, and find any enemies in range"
local aoeCenter = xi.aoeRadius.ATTACKER
local enemies = mob:getEntitiesInRange(mob, xi.aoeType.ROUND, aoeCenter, distance, 0, targetTypeFlag)
return enemies
end
xi.darkixion.beginTramplePath = function(mob)
-- global table to track current trample path
xi.darkixion.hitList = {}
mob:setAnimationSub(animationSubs.TRAMPLE)
mob:setLocalVar('isBusy', 1)
mob:setAutoAttackEnabled(false)
mob:setMobAbilityEnabled(false)
mob:setBaseSpeed(70)
mob:setBehavior(xi.behavior.NO_TURN + xi.behavior.STANDBACK)
-- choose a random ENTITY within 30 yalms and charge towards them
-- can also trample at someone close. Fallback to a random spot nearby
mob:setLocalVar('trampleTargID', 0)
local tramplePos = mob:getPos()
tramplePos.x = tramplePos.x + math.random(-10, 10)
tramplePos.z = tramplePos.z + math.random(-10, 10)
local potentialTargets = getEnemiesInRange(mob, 30)
local trampleTarget = nil
if #potentialTargets > 0 then
for i = 1, 3 do
-- rather than loop the whole list, just try a few times to get an entity other than Ixion
if trampleTarget == nil or trampleTarget == mob then
trampleTarget = utils.randomEntry(potentialTargets)
end
end
end
-- ANY_ALLEGIANCE + SELF returns the actor itself in the list
if trampleTarget and trampleTarget ~= mob then
-- store targid of the trample destination to ensure pathing wonkiness doesn't make them avoid trample
-- this target will be hit by trample if it's ever within 7 yalms during the trample pathing
mob:setLocalVar('trampleTargID', trampleTarget:getTargID())
-- choose point by drawing a line between Ixion and target, then extending it beyond
local overshootDistance = 5
local mobPos = mob:getPos()
local xM = mobPos.x
local zM = mobPos.z
local targetPos = trampleTarget:getPos()
local xT = targetPos.x
local yT = targetPos.y
local zT = targetPos.z
-- deltas
local xD = xT - xM
local zD = zT - zM
-- complete the square
local rss = math.sqrt(xD * xD + zD * zD)
local xU = xD / rss
local zU = zD / rss
tramplePos = { x = xT + (xU * overshootDistance), y = yT, z = zT + (zU * overshootDistance) }
end
mob:clearPath()
mob:lookAt(tramplePos)
xi.darkixion.trampleEntitiesInFront(mob)
mob:pathTo(tramplePos.x, tramplePos.y, tramplePos.z, xi.path.flag.WALLHACK + xi.path.flag.RUN + xi.path.flag.SCRIPT + xi.path.flag.SLIDE)
-- max time to let a single trample path take to avoid navmesh abuse
-- some paths can take upwards of 20+s with certain terrain, but no need to let Ixion path that long
mob:setLocalVar('tramplePathTime', GetSystemTime() + 15)
end
xi.darkixion.endTramplePath = function(mob)
-- variable is unused until the next trample sequence
xi.darkixion.hitList = {}
local trampleCount = mob:getLocalVar('trampleCount') - 1
if trampleCount > 0 then
-- trample again at a random entity in range
xi.darkixion.beginTramplePath(mob)
else
trampleCount = 0
mob:setAnimationSub(animationSubs.NORMAL)
mob:stun(500)
mob:setLocalVar('isBusy', 0)
mob:setAutoAttackEnabled(true)
mob:setMobAbilityEnabled(true)
mob:setBaseSpeed(40)
mob:setBehavior(0)
mob:setLocalVar('nextTrampleTime', GetSystemTime() + math.random(15, 60))
end
mob:setLocalVar('trampleCount', trampleCount)
end
xi.darkixion.trampleEntitiesInFront = function(mob)
local trampleTargID = mob:getLocalVar('trampleTargID')
-- find entities in range
-- check if they're in a conal in front of Ixion
-- skip them if they have already been considered during this path
-- xi.mobSkill.DI_TRAMPLE otherwise
local potentialTargets = getEnemiesInRange(mob, 7)
for _, potentialTarget in ipairs(potentialTargets) do
-- ANY_ALLEGIANCE + SELF returns the actor itself in the list
-- targetfind has no height restrictions, just finding entities within a sphere of the mob
-- TODO should we limit trample consideration to within 5 yalms vertically: math.abs(mob:getPos().y - potentialTarget:getPos().y) <= 5
-- This would make things like the watchtowers in jugner_s somewhat safe, I believe this is a valid strategy in retail
if
potentialTarget and
potentialTarget ~= mob
then
-- ignore if already considered during this trample path
if not xi.darkixion.hitList[potentialTarget:getTargID()] then
xi.darkixion.hitList[potentialTarget:getTargID()] = true
-- trample the found entity if it's in front
-- or if it's the original trample target (to avoid weird pathing issues causing the final target to be seen as not in front)
if
potentialTarget:isInfront(mob, 30) or
potentialTarget:getTargID() == trampleTargID
then
mob:useMobAbility(xi.mobSkill.DI_TRAMPLE, potentialTarget)
end
end
end
end
end