landsandboat/scripts/globals/voidwalker.lua
2026-03-20 15:21:30 +01:00

600 lines
17 KiB
Lua

-----------------------------------
-- The Voidwalker NM System
-----------------------------------
require('scripts/globals/mobs')
require('scripts/globals/voidwalkerpos')
-----------------------------------
xi = xi or {}
xi.voidwalker = xi.voidwalker or {}
local abyssiteKeyitems =
{
[9] = xi.keyItem.BLACK_ABYSSITE,
[8] = xi.keyItem.GREY_ABYSSITE,
[7] = xi.keyItem.PURPLE_ABYSSITE,
[6] = xi.keyItem.YELLOW_ABYSSITE,
[5] = xi.keyItem.BROWN_ABYSSITE,
[4] = xi.keyItem.ORANGE_ABYSSITE,
[3] = xi.keyItem.BLUE_ABYSSITE,
[2] = xi.keyItem.COLORFUL_ABYSSITE,
[1] = xi.keyItem.CLEAR_ABYSSITE,
}
local abyssiteMessage =
{
[xi.keyItem.CLEAR_ABYSSITE] = 0,
[xi.keyItem.COLORFUL_ABYSSITE] = 1,
[xi.keyItem.BLUE_ABYSSITE] = 2,
[xi.keyItem.ORANGE_ABYSSITE] = 2,
[xi.keyItem.BROWN_ABYSSITE] = 2,
[xi.keyItem.YELLOW_ABYSSITE] = 2,
[xi.keyItem.GREY_ABYSSITE] = 2,
[xi.keyItem.BLACK_ABYSSITE] = 3
}
local function getCurrentKIsBitsFromPlayer(player)
local results = 0
for i, keyitem in ipairs(abyssiteKeyitems) do
local currentBit = 0
if player:hasKeyItem(keyitem) then
currentBit = 1
end
results = results + bit.lshift(currentBit, i - 1)
end
return results
end
local function getCurrentKIsFromPlayer(player)
local results = {}
for i, keyitem in ipairs(abyssiteKeyitems) do
if player:hasKeyItem(keyitem) then
table.insert(results, keyitem)
end
end
return results
end
local function getMobsFromAbyssites(zoneId, abyssites)
local results = {}
for i, keyitem in ipairs(abyssites) do
if
zones[zoneId] and
zones[zoneId].mob and
zones[zoneId].mob.VOIDWALKER[keyitem]
then
for _, mobId in ipairs(zones[zoneId].mob.VOIDWALKER[keyitem]) do
local mob = GetMobByID(mobId)
if mob and mob:isAlive() and mob:getLocalVar('[VoidWalker]PopedBy') == 0 then
table.insert(results, { mobId = mobId, keyItem = keyitem })
end
end
end
end
return results
end
local function removeMobIdFromPos(zoneId, mobId)
for i, pos in ipairs(xi.voidwalker.pos[zoneId]) do
if pos.mobId == mobId then
xi.voidwalker.pos[zoneId][i].mobId = nil
end
end
end
local function searchEmptyPos(zoneId)
local maxPos = #xi.voidwalker.pos[zoneId]
local pos = math.random(1, maxPos)
local currentPos = xi.voidwalker.pos[zoneId][pos]
if currentPos.mobId == nil then
return pos
else
return searchEmptyPos(zoneId)
end
end
local function setRandomPos(zoneId, mobId)
local mob = GetMobByID(mobId)
if
not mob or
not xi.voidwalker.pos[zoneId]
then
return
end
local pos = searchEmptyPos(zoneId)
xi.voidwalker.pos[zoneId][pos].mobId = mobId
local vPos = xi.voidwalker.pos[zoneId][pos].pos
mob:setSpawn(vPos[1], vPos[2], vPos[3])
mob:setPos(vPos[1], vPos[2], vPos[3])
end
local getNearestMob = function(player, mobs)
local results = {}
for _, v in ipairs(mobs) do
local mob = GetMobByID(v.mobId)
local distance = player:checkDistance(mob)
table.insert(results, { mobId = v.mobId, keyItem = v.keyItem, distance = distance })
end
table.sort(results, function(a, b)
return a.distance < b.distance
end)
if #results > 0 then
return results[1]
else
return nil
end
end
local getDirection = function(player, mob, distance)
local posPlayer = player:getPos()
local posMob = mob:getPos()
local diffx = posMob.x - posPlayer.x
local diffz = posMob.z - posPlayer.z
local tan = math.atan(diffz / diffx)
local degree = math.deg(tan)
if degree < 0 then
degree = degree * -1
end
local minDegree = 20
local maxDegree = 70
-- Degree >= 70
if degree >= maxDegree then
if diffz >= 0 then
return 6
else
return 2
end
-- Degree <= 20
elseif degree <= minDegree then
if diffx >= 0 then
return 0
else
return 4
end
-- Degree between 20 and 70
else
if diffz >= 0 then
if diffx >= 0 then
return 7
else
return 5
end
else
if diffx >= 0 then
return 1
else
return 3
end
end
end
end
-----------------------------------
-- check keyitem upgrade
-----------------------------------
local function checkUpgrade(player, mob, nextKeyItem)
if
player and
mob:getZoneID() == player:getZoneID()
then
local zoneTextTable = zones[mob:getZoneID()].text
local currentKeyItem = mob:getLocalVar('[VoidWalker]PopedWith')
local rand = math.random(1, 10)
if rand == 5 then
if player:hasKeyItem(currentKeyItem) then
player:delKeyItem(currentKeyItem)
end
if nextKeyItem then
player:addKeyItem(nextKeyItem)
if currentKeyItem == xi.keyItem.CLEAR_ABYSSITE then
player:messageSpecial(zoneTextTable.VOIDWALKER_UPGRADE_KI_1, currentKeyItem, nextKeyItem)
elseif currentKeyItem == xi.keyItem.COLORFUL_ABYSSITE then
player:messageSpecial(zoneTextTable.VOIDWALKER_UPGRADE_KI_2, currentKeyItem, nextKeyItem)
elseif nextKeyItem == xi.keyItem.BLACK_ABYSSITE then
player:messageSpecial(zoneTextTable.VOIDWALKER_OBTAIN_KI, nextKeyItem)
end
end
end
end
end
-----------------------------------
-- NPC Assai Nybaem
-----------------------------------
xi.voidwalker.npcOnTrigger = function(player, npc)
if xi.settings.main.ENABLE_VOIDWALKER ~= 1 then
return
end
local currentKIS = getCurrentKIsBitsFromPlayer(player)
player:startEvent(10120, currentKIS)
end
xi.voidwalker.npcOnEventUpdate = function(player, csid, option, npc)
local opt = bit.band(option, 0xF)
if
csid == 10120 and
opt == 3
then
local hasGil = player:getGil() >= 1000
local hasKi = player:hasKeyItem(xi.keyItem.CLEAR_ABYSSITE)
if not hasGil then
player:updateEvent(3)
elseif hasKi then
player:updateEvent(2)
else
player:updateEvent(1)
end
end
end
xi.voidwalker.npcOnEventFinish = function(player, csid, option, npc)
local opt = bit.band(option, 0xF)
if csid == 10120 then
if opt == 1 then
local msg = zones[xi.zone.RULUDE_GARDENS]
local ki = abyssiteKeyitems[1]
player:delGil(1000)
player:addKeyItem(ki)
player:messageSpecial(msg.text.KEYITEM_OBTAINED, ki)
elseif opt == 2 then
local numAbyssite = bit.rshift(option, 4)
player:delKeyItem(abyssiteKeyitems[numAbyssite])
end
end
end
-----------------------------------
-- Zone On Init
-----------------------------------
xi.voidwalker.zoneOnInit = function(zone)
local zoneId = zone:getID()
local voidwalkerMobs = zones[zoneId].mob.VOIDWALKER
for ki, mobs in pairs(voidwalkerMobs) do
for _, mob in pairs(mobs) do
setRandomPos(zoneId, mob)
end
end
end
local function doMobSkillEveryHPP(mob, every, start, mobskill, condition)
local mobhpp = mob:getHPP()
if
mobhpp <= start and
condition
then
local mobHppModulo = mobhpp % every
local startHppModulo = start % every
local isSame = startHppModulo == mobHppModulo
if
isSame and
mob:getLocalVar('MOB_SKILL_' .. mobhpp) == 0
then
mob:useMobAbility(mobskill)
mob:setLocalVar('MOB_SKILL_' .. mobhpp, 1)
end
end
end
local function randomly(mob, chance, between, effect, skill)
if
math.random(0, 100) <= chance and
not mob:hasStatusEffect(effect) and
GetSystemTime() > (mob:getLocalVar('MOBSKILL_TIME') + between)
then
mob:setLocalVar('MOBSKILL_USE', 1)
mob:setLocalVar('MOBSKILL_TIME', GetSystemTime())
mob:useMobAbility(skill)
end
end
local function DespawnPet(mob)
local zoneId = mob:getZoneID()
local mobId = mob:getID()
if zones[zoneId].pet and zones[zoneId].pet[mobId] then
local petIds = zones[zoneId].pet[mobId]
for i, petId in ipairs(petIds) do
local pet = GetMobByID(petId)
if pet then
DespawnMob(petId)
pet:setSpawn(mob:getXPos(), mob:getYPos(), mob:getZPos())
pet:setPos(mob:getXPos(), mob:getYPos(), mob:getZPos())
end
end
end
end
local modByMobName =
{
['Krabkatoa'] = function(mob)
mob:addStatusEffect(xi.effect.REGAIN, { power = 10, origin = mob })
mob:addMod(xi.mod.DOUBLE_ATTACK, 10)
end,
['Tammuz'] = function(mob)
mob:addStatusEffect(xi.effect.MIGHTY_STRIKES, { power = 1, origin = mob })
end,
['Erebus'] = function(mob)
mob:addImmunity(xi.immunity.GRAVITY)
mob:addImmunity(xi.immunity.BIND)
end,
['Raker_Bee'] = function(mob)
mob:addImmunity(xi.immunity.GRAVITY)
mob:addImmunity(xi.immunity.BIND)
end,
['Gjenganger'] = function(mob)
mob:addImmunity(xi.immunity.STUN)
end,
}
local mixinByMobName =
{
['Capricornus'] = function(mob)
doMobSkillEveryHPP(mob, 20, 80, xi.mobSkill.MIGHTY_STRIKES_1, not mob:hasStatusEffect(xi.effect.MIGHTY_STRIKES))
if
mob:hasStatusEffect(xi.effect.MIGHTY_STRIKES) and
not xi.combat.behavior.isEntityBusy(mob)
then
mob:useMobAbility(xi.mobSkill.RECOIL_DIVE_1)
end
end,
['Yacumama'] = function(mob)
doMobSkillEveryHPP(mob, 20, 80, xi.mobSkill.HUNDRED_FISTS_1, not mob:hasStatusEffect(xi.effect.HUNDRED_FISTS))
end,
['Lamprey_Lord'] = function(mob)
randomly(mob, 10, 60, xi.effect.BLOOD_WEAPON, xi.mobSkill.BLOOD_WEAPON_1)
end,
['Shoggoth'] = function(mob)
doMobSkillEveryHPP(mob, 20, 80, xi.mobSkill.CHAINSPELL_1, not mob:hasStatusEffect(xi.effect.CHAINSPELL))
end,
['Jyeshtha'] = function(mob)
randomly(mob, 30, 60, xi.mobSkill.MIGHTY_STRIKES_1, xi.mobSkill.MIGHTY_STRIKES_1)
if
mob:getLocalVar('MOBSKILL_USE') == 1 and
not mob:hasStatusEffect(xi.effect.MIGHTY_STRIKES)
then
mob:setLocalVar('MOBSKILL_USE', 0)
end
end,
['Blobdingnag'] = function(mob)
doMobSkillEveryHPP(mob, 20, 82, xi.mobSkill.CYTOKINESIS, true)
end,
['Farruca_Fly'] = function(mob)
doMobSkillEveryHPP(mob, 20, 80, xi.mobSkill.PERFECT_DODGE_1, not mob:hasStatusEffect(xi.effect.PERFECT_DODGE))
end,
['Skuld'] = function(mob)
doMobSkillEveryHPP(mob, 20, 80, xi.mobSkill.CHAINSPELL_1, not mob:hasStatusEffect(xi.effect.CHAINSPELL))
end,
['Erebus'] = function(mob)
randomly(mob, 30, 60, xi.effect.BLOOD_WEAPON, xi.mobSkill.BLOOD_WEAPON_1)
if
mob:hasStatusEffect(xi.effect.BLOOD_WEAPON) and
not mob:hasStatusEffect(xi.effect.HUNDRED_FISTS)
then
mob:addStatusEffect(xi.effect.HUNDRED_FISTS, { power = 1, duration = 30, origin = mob })
end
end,
['Feuerunke'] = function(mob)
randomly(mob, 30, 60, xi.effect.HUNDRED_FISTS, xi.mobSkill.HUNDRED_FISTS_1)
end,
['Dawon'] = function(mob)
doMobSkillEveryHPP(mob, 20, 80, xi.mobSkill.PERFECT_DODGE_1, not mob:hasStatusEffect(xi.effect.PERFECT_DODGE))
end
}
-----------------------------------
-- Mob On Init
-----------------------------------
xi.voidwalker.onMobInitialize = function(mob)
end
xi.voidwalker.onMobSpawn = function(mob)
local mobName = mob:getName()
mob:setStatus(xi.status.INVISIBLE)
mob:hideHP(true)
mob:hideName(true)
mob:setUntargetable(true)
local mods = modByMobName[mobName]
if mods then
mods(mob)
end
end
xi.voidwalker.onMobFight = function(mob, target)
local mobName = mob:getName()
local mixin = mixinByMobName[mobName]
if mixin then
mixin(mob)
end
local poptime = mob:getLocalVar('[VoidWalker]PopedAt')
local now = GetSystemTime()
if
mob:isSpawned() and
(
now > (poptime + 7200) or
mob:checkDistance(target) > 25
)
then
local zoneTextTable = zones[mob:getZoneID()].text
target:messageSpecial(zoneTextTable.VOIDWALKER_DESPAWN)
DespawnMob(mob:getID())
end
end
xi.voidwalker.onMobDisengage = function(mob)
mob:setLocalVar('[VoidWalker]PopedBy', 0)
mob:setLocalVar('[VoidWalker]checkPopedBy', 0)
mob:setLocalVar('[VoidWalker]PopedWith', 0)
mob:setLocalVar('[VoidWalker]PopedAt', 0)
mob:setLocalVar('MOBSKILL_USE', 0)
mob:setLocalVar('MOBSKILL_TIME', 0)
DespawnPet(mob)
mob:setStatus(xi.status.INVISIBLE)
mob:hideHP(true)
mob:hideName(true)
mob:setUntargetable(true)
end
xi.voidwalker.onMobDespawn = function(mob)
local zoneId = mob:getZoneID()
local mobId = mob:getID()
removeMobIdFromPos(zoneId, mobId)
setRandomPos(zoneId, mobId)
mob:setLocalVar('[VoidWalker]PopedBy', 0)
mob:setLocalVar('[VoidWalker]checkPopedBy', 0)
mob:setLocalVar('[VoidWalker]PopedWith', 0)
mob:setLocalVar('[VoidWalker]PopedAt', 0)
mob:setLocalVar('MOBSKILL_USE', 0)
mob:setLocalVar('MOBSKILL_TIME', 0)
DespawnPet(mob)
end
xi.voidwalker.onMobDeath = function(mob, player, optParams, keyItem)
if player then
local popkeyitem = mob:getLocalVar('[VoidWalker]PopedWith')
if optParams.isKiller then
local playerpoped = GetPlayerByID(mob:getLocalVar('[VoidWalker]PopedBy'))
local alliance = player:getAlliance()
local outOfParty = true
for _, member in pairs(alliance) do
if
playerpoped and
member:getID() == playerpoped:getID()
then
outOfParty = false
break
end
end
if
outOfParty and
playerpoped and
not playerpoped:hasKeyItem(keyItem)
then
checkUpgrade(playerpoped, mob, keyItem)
end
end
if
player:hasKeyItem(popkeyitem) and
not player:hasKeyItem(keyItem)
then
checkUpgrade(player, mob, keyItem)
end
end
end
-----------------------------------
-- onHealing : trigg when player /heal
-----------------------------------
xi.voidwalker.onHealing = function(player)
if xi.settings.main.ENABLE_VOIDWALKER ~= 1 then
return
end
local zoneId = player:getZoneID()
local zoneTextTable = zones[zoneId].text
local abyssites = getCurrentKIsFromPlayer(player)
if
#abyssites == 0 or
not zones[zoneId].mob or
not zones[zoneId].mob.VOIDWALKER
then
return
end
local mobs = getMobsFromAbyssites(zoneId, abyssites)
local mobNearest = getNearestMob(player, mobs)
if not mobNearest then
player:messageSpecial(zoneTextTable.VOIDWALKER_NO_MOB, abyssites[1])
elseif mobNearest.distance <= 4 then
local mob = GetMobByID(mobNearest.mobId)
if not mob then
return
end
mob:setLocalVar('[VoidWalker]PopedBy', player:getID())
mob:setLocalVar('[VoidWalker]PopedWith', mobNearest.keyItem)
mob:setLocalVar('[VoidWalker]PopedAt', GetSystemTime())
if
mobNearest.keyItem ~= xi.keyItem.CLEAR_ABYSSITE and
mobNearest.keyItem ~= xi.keyItem.COLORFUL_ABYSSITE
then
player:delKeyItem(mobNearest.keyItem)
player:messageSpecial(zoneTextTable.VOIDWALKER_BREAK_KI, mobNearest.keyItem)
else
player:messageSpecial(zoneTextTable.VOIDWALKER_SPAWN_MOB)
mob:hideHP(false)
end
mob:hideName(false)
mob:setUntargetable(false)
mob:setStatus(xi.status.UPDATE)
mob:updateClaim(player)
elseif mobNearest.distance >= 300 then
player:messageSpecial(zoneTextTable.VOIDWALKER_MOB_TOO_FAR, mobNearest.keyItem)
else
local mob = GetMobByID(mobNearest.mobId)
local direction = getDirection(player, mob, mobNearest.distance)
player:messageSpecial(zoneTextTable.VOIDWALKER_MOB_HINT, abyssiteMessage[mobNearest.keyItem], direction, mobNearest.distance, mobNearest.keyItem)
end
end