Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
165 changes: 108 additions & 57 deletions scripts/globals/pirates.lua
Original file line number Diff line number Diff line change
Expand Up @@ -2,12 +2,9 @@
-- Ship bound for [Mhaura/Selbina] Pirates helpers
-- NOTE: Careful with queues as they don't resolve until a zone wakes from sleep, potentially having mismatched timing. Timers are fine.
-----------------------------------

xi = xi or {}
xi.pirates = xi.pirates or {}

-- chance for encounter to have a special middle NPC, which indicates a chance for NM to spawn
local vermCloakPirateChance = 10
-----------------------------------

local actions =
{
Expand All @@ -20,92 +17,113 @@ local actions =
DEPARTING = 6,
}

-- times are minutes after midnight for first cycle, cycle is 480 minutes
-- Times are minutes after midnight for first cycle. Cycle is 480 minutes.
local piratesSchedule =
{
{ endTime = utils.timeStringToMinutes('01:10'), action = actions.ARRIVING },
{ endTime = utils.timeStringToMinutes('01:30'), action = actions.ARRIVE },
{ endTime = utils.timeStringToMinutes('01:32'), action = actions.PIRATES_ARRIVE },
{ endTime = utils.timeStringToMinutes('01:35'), action = actions.MOBS_SPAWN },
{ endTime = utils.timeStringToMinutes('01:10'), action = actions.ARRIVING },
{ endTime = utils.timeStringToMinutes('01:30'), action = actions.ARRIVE },
{ endTime = utils.timeStringToMinutes('01:32'), action = actions.PIRATES_ARRIVE },
{ endTime = utils.timeStringToMinutes('01:34'), action = actions.MOBS_SPAWN },
{ endTime = utils.timeStringToMinutes('04:20'), action = actions.PIRATES_RETREAT },
{ endTime = utils.timeStringToMinutes('04:27'), action = actions.DEPART },
{ endTime = utils.timeStringToMinutes('04:48'), action = actions.DEPARTING },
{ endTime = utils.timeStringToMinutes('04:27'), action = actions.DEPART },
{ endTime = utils.timeStringToMinutes('04:48'), action = actions.DEPARTING },
}

local piratesData =
{
-- Pirate ship is on left side of boat
-- Pirate ship is on left side of boat.
[xi.zone.SHIP_BOUND_FOR_SELBINA_PIRATES] =
{
{
startPos = { x = -33.601, y = -7.16, z = 13.37, rotation = 0 },
standingPos = { x = -21.90, y = -7.16, z = 10.46, rotation = 0 },
standingPos = { x = -21.900, y = -7.16, z = 10.46, rotation = 0 },
},
{
startPos = { x = -29.728, y = -7.16, z = 1.30, rotation = 0 },
standingPos = { x = -21.90, y = -7.16, z = 6.59, rotation = 0 },
startPos = { x = -29.728, y = -7.16, z = 1.30, rotation = 0 },
standingPos = { x = -21.900, y = -7.16, z = 6.59, rotation = 0 },
},
{
startPos = { x = -29.602, y = -7.16, z = -2.47, rotation = 0 },
standingPos = { x = -21.90, y = -7.16, z = 2.10, rotation = 0 },
standingPos = { x = -21.900, y = -7.16, z = 2.10, rotation = 0 },
},
},
-- Pirate ship is on right side of boat
-- Pirate ship is on right side of boat.
[xi.zone.SHIP_BOUND_FOR_MHAURA_PIRATES] =
{
{
startPos = { x = 33.601, y = -7.16, z = 13.37, rotation = 128 },
standingPos = { x = 21.90, y = -7.16, z = 10.46, rotation = 128 },
standingPos = { x = 21.900, y = -7.16, z = 10.46, rotation = 128 },
},
{
startPos = { x = 29.728, y = -7.16, z = 1.30, rotation = 128 },
standingPos = { x = 21.90, y = -7.16, z = 6.59, rotation = 128 },
startPos = { x = 29.728, y = -7.16, z = 1.30, rotation = 128 },
standingPos = { x = 21.900, y = -7.16, z = 6.59, rotation = 128 },
},
{
startPos = { x = 29.602, y = -7.16, z = -2.47, rotation = 128 },
standingPos = { x = 21.90, y = -7.16, z = 2.10, rotation = 128 },
standingPos = { x = 21.900, y = -7.16, z = 2.10, rotation = 128 },
},
},
}

xi.pirates.setupPirateNPCSchedule = function(npc)
npc:initNpcAi()
-- This ride's NM: Blackbeard sails the Selbina route, Silverhook the Mhaura route.
local function getNMId(zoneId)
if zoneId == xi.zone.SHIP_BOUND_FOR_SELBINA_PIRATES then
return zones[zoneId].mob.BLACKBEARD
end

-- create triggers for every stage of the encounter on each Pirate NPC
for _, eventData in ipairs(piratesSchedule) do
npc:addPeriodicTrigger(eventData.action, 480, eventData.endTime)
return zones[zoneId].mob.SILVERHOOK
end

-- Clear the deck of pirate mobs: disable respawns, despawn idle ones, and leave any still in combat to be finished off (they won't respawn).
-- Used at retreat and before a fresh ride spawns
local function clearPirates(zoneId)
local ID = zones[zoneId]
local mobIdTable = { ID.mob.SHIP_WIGHT, getNMId(zoneId) }
for _, mobId in ipairs(ID.mob.CROSSBONES) do
table.insert(mobIdTable, mobId)
end

for _, mobId in ipairs(mobIdTable) do
local mob = GetMobByID(mobId)
if mob then
mob:setRespawnTime(0) -- Stop the waves / Cancel any pending respawn.
if mob:isSpawned() and not mob:isEngaged() then
DespawnMob(mobId) -- Engaged mobs stay until killed, then won't return.
end
end
end
end

-- calls itself via timer until the npc is hidden
-- Calls itself via timer until the npc is hidden.
local function summonAnimations(npc, rotation, offset)
if npc:getStatus() == xi.status.DISAPPEAR then
return
end

if not npc:isFollowingPath() then
local pos = npc:getPos()
local pos = npc:getPos()
local currentTime = GetSystemTime()

if npc:getLocalVar('initialNpcState') == 1 then
npc:setLocalVar('initialNpcState', 0)
-- rotate to face the player boat
npc:setPos(pos.x, pos.y, pos.z, rotation)
-- first summoning rotation happens in order of NPC ID
npc:setLocalVar('summonStartTime', GetSystemTime() + (offset - 1) * 2)
npc:setLocalVar('summonStartTime', currentTime + (offset - 1) * 2)
end

local summonStartTime = npc:getLocalVar('summonStartTime')
if summonStartTime ~= 0 and summonStartTime <= GetSystemTime() then
if summonStartTime ~= 0 and summonStartTime <= currentTime then
npc:setLocalVar('summonStartTime', 0)
npc:setLocalVar('summonEndTime', GetSystemTime() + math.random(1, 2))
npc:setLocalVar('summonEndTime', currentTime + math.random(1, 2))

npc:entityAnimationPacket(xi.animationString.CAST_SUMMONER_START)
end

local summonEndTime = npc:getLocalVar('summonEndTime')
if summonEndTime ~= 0 and summonEndTime <= GetSystemTime() then
if summonEndTime ~= 0 and summonEndTime <= currentTime then
npc:setLocalVar('summonStartTime', currentTime + math.random(4 + offset, 10))
npc:setLocalVar('summonEndTime', 0)
-- npcs seem to wait anywhere from 5 to 10s to do another summoning animation
npc:setLocalVar('summonStartTime', GetSystemTime() + math.random(4 + offset, 10))

npc:entityAnimationPacket(xi.animationString.CAST_SUMMONER_STOP)
end
Expand All @@ -126,15 +144,25 @@ local function summonAnimations(npc, rotation, offset)
end)
end

-- called on every NPC periodic trigger, which is mapped 1-1 to the schedule table, with triggerId == action
xi.pirates.setupPirateNPCSchedule = function(npc)
npc:initNpcAi()

-- Create triggers for every stage of the encounter on each Pirate NPC.
for _, eventData in ipairs(piratesSchedule) do
npc:addPeriodicTrigger(eventData.action, 480, eventData.endTime)
end
end

-- Called on every NPC periodic trigger, which is mapped 1-1 to the schedule table, with triggerId == action
xi.pirates.pirateNPCTimeTrigger = function(npc, triggerId, zoneKey)
local pirateZone = npc:getZone()
if not pirateZone then
return
end

local pirateNPCs = zones[pirateZone:getID()].npc.PIRATES
local pirateIdx = 0
local pirateIdx = 0

for i, npcId in ipairs(pirateNPCs) do
if npcId == npc:getID() then
pirateIdx = i
Expand All @@ -147,19 +175,13 @@ xi.pirates.pirateNPCTimeTrigger = function(npc, triggerId, zoneKey)
return
end

-- Pirates appear and run to position
if triggerId == actions.PIRATES_ARRIVE then
-- Pirates appear and run to position
if pirateIdx == 2 then
-- middle pirate has chance to wear a verm cloak, which then means the pirate encounter _might_ have the NM spawn
local bodyModel = 8195
local nmCanSpawn = 0
if math.random(1, 100) <= vermCloakPirateChance then
bodyModel = 47
nmCanSpawn = 1
end

npc:setModelId(bodyModel, xi.slot.BODY)
pirateZone:setLocalVar('nmCanSpawn', nmCanSpawn)
local hasVermCloak = math.random(1, 100) <= 10
npc:setModelId(hasVermCloak and 47 or 8195, xi.slot.BODY) -- 47 = verm cloak body, 8195 = default body
pirateZone:setLocalVar('nmCanSpawn', hasVermCloak and 1 or 0) -- 1 = NM still eligible; cleared to 0 once it spawns
end

npc:setPos(pirateData.startPos)
Expand All @@ -170,8 +192,9 @@ xi.pirates.pirateNPCTimeTrigger = function(npc, triggerId, zoneKey)
-- Indicates we need to rotate NPC after pathing completes
npc:setLocalVar('initialNpcState', 1)
summonAnimations(npc, pirateData.standingPos.rotation, pirateIdx)

-- Retreat.
elseif triggerId == actions.PIRATES_RETREAT then
-- retreat
local summonEndTime = npc:getLocalVar('summonEndTime')
-- No more animations will happen and recursive function self destructs
npc:setLocalVar('summonStartTime', 0)
Expand All @@ -181,8 +204,9 @@ xi.pirates.pirateNPCTimeTrigger = function(npc, triggerId, zoneKey)
end

npc:pathTo(pirateData.startPos.x, pirateData.startPos.y, pirateData.startPos.z, xi.path.flag.RUN + xi.path.flag.WALLHACK)

-- Just in case summonAnimations didn't set status
elseif triggerId == actions.DEPART then
-- Just in case summonAnimations didn't set status
npc:clearPath()
npc:setStatus(xi.status.DISAPPEAR)
end
Expand All @@ -192,15 +216,42 @@ end

xi.pirates.zoneStateChange = function(zone, action)
-- change the zone's state once per action cycle (this function is called by each NPC)
if zone:getLocalVar('currPiratesAction') ~= action then
zone:setLocalVar('currPiratesAction', action)

if action == actions.MOBS_SPAWN then
-- TODO enable mob spawns (and NM spawns if nmCanSpawn is set to 1)
-- set them to setRespawn(1s), then set normal respawnTime in onMobSpawn
elseif action == actions.PIRATES_RETREAT then
-- TODO disable all spawns and despawn any not in combat
-- mobs in combat do not despawn when the ship leaves
if zone:getLocalVar('currPiratesAction') == action then
return
end

zone:setLocalVar('currPiratesAction', action)

local zoneId = zone:getID()
local ID = zones[zoneId]

if action == actions.MOBS_SPAWN then
-- clear any mobs lingering from a previous ride before summoning fresh ones
clearPirates(zoneId)

-- the skeletons the pirate NPCs are "summoning" onto the deck
for _, mobId in ipairs(ID.mob.CROSSBONES) do
local crossbones = GetMobByID(mobId)
if crossbones then
crossbones:setRespawnTime(1)
end
end

if zone:getLocalVar('nmCanSpawn') == 1 and math.random(1, 100) <= 75 then
-- HQ ride, 75%: NM appears from the start
local nm = GetMobByID(getNMId(zoneId))
if nm then
nm:setRespawnTime(1)
zone:setLocalVar('nmCanSpawn', 0) -- NM is up; no longer eligible to spawn
end
else
-- normal ride, or the 25% placeholder Wight on an HQ ride
local wight = GetMobByID(ID.mob.SHIP_WIGHT)
if wight then
wight:setRespawnTime(1)
end
end
elseif action == actions.PIRATES_RETREAT then
clearPirates(zoneId)
end
end
3 changes: 2 additions & 1 deletion scripts/zones/Ship_bound_for_Mhaura_Pirates/IDs.lua
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,8 @@ zones[xi.zone.SHIP_BOUND_FOR_MHAURA_PIRATES] =
},
mob =
{
WIGHT = GetFirstID('Ship_Wight'),
CROSSBONES = GetTableOfIDs('Crossbones'),
SHIP_WIGHT = GetFirstID('Ship_Wight'),
SILVERHOOK = GetFirstID('Silverhook'),
},
npc =
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,4 +9,8 @@ entity.onMobInitialize = function(mob)
mob:setMobMod(xi.mobMod.NO_STANDBACK, 1)
end

entity.onMobSpawn = function(mob)
mob:setRespawnTime(60) -- respawns every 60s while the pirate ship is alongside
end

return entity
29 changes: 29 additions & 0 deletions scripts/zones/Ship_bound_for_Mhaura_Pirates/mobs/Ship_Wight.lua
Original file line number Diff line number Diff line change
Expand Up @@ -9,4 +9,33 @@ entity.onMobInitialize = function(mob)
mob:setMobMod(xi.mobMod.NO_STANDBACK, 1)
end

entity.onMobSpawn = function(mob)
mob:setRespawnTime(60) -- Respawns every 60s while the pirate ship is alongside.
end

entity.onMobDespawn = function(mob)
local zone = mob:getZone()
if not zone then
return
end

-- On an HQ ride, the Ship Wight is Silverhook's placeholder (90%) until he appears.
if zone:getLocalVar('nmCanSpawn') == 0 then
return
end

if math.random(1, 100) > 90 then
return
end

local silverhook = GetMobByID(zones[xi.zone.SHIP_BOUND_FOR_MHAURA_PIRATES].mob.SILVERHOOK)
if not silverhook then
return
end

mob:setRespawnTime(0) -- This Wight stays down; Silverhook takes its slot.
silverhook:setRespawnTime(1)
zone:setLocalVar('nmCanSpawn', 0)
end

return entity
Original file line number Diff line number Diff line change
@@ -1,7 +1,8 @@
-----------------------------------
-- Area: Ship Bound for Mhaura (Pirates)
-- NM: Silverhook
-- To do: This NM does not use an SP ability like JP wiki claims but it does have a dust cloud animation that triggers as soon as it is pushed below 50% HP. It is unclear what this does, if anything, after multiple retail captures.
-- To do: This NM does not use an SP ability like JP wiki claims but it does have a dust cloud animation that triggers as soon as it is pushed below 50% HP.
-- It is unclear what this does, if anything, after multiple retail captures.
-----------------------------------
---@type TMobEntity
local entity = {}
Expand All @@ -19,6 +20,7 @@ entity.onMobSpawn = function(mob)
mob:setMod(xi.mod.ICE_RES_RANK, 10)
mob:setMod(xi.mod.POWER_MULTIPLIER_SPELL, 55)
mob:setMobMod(xi.mobMod.NO_STANDBACK, 1)
mob:setRespawnTime(0) -- one-and-done per ride; the 1s spawn timer must not respawn him
end

entity.onMobSpellChoose = function(mob, target, spellId)
Expand Down
1 change: 1 addition & 0 deletions scripts/zones/Ship_bound_for_Selbina_Pirates/IDs.lua
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ zones[xi.zone.SHIP_BOUND_FOR_SELBINA_PIRATES] =
mob =
{
BLACKBEARD = GetFirstID('Blackbeard'),
CROSSBONES = GetTableOfIDs('Crossbones'),
ENAGAKURE = GetFirstID('Enagakure'),
SHIP_WIGHT = GetFirstID('Ship_Wight'),
},
Expand Down
Original file line number Diff line number Diff line change
@@ -1,7 +1,8 @@
-----------------------------------
-- Area: Ship Bound for Selbina (Pirates)
-- NM: Blackbeard
-- To do: This NM does not use an SP ability like JP wiki claims but it does have a dust cloud animation that triggers as soon as it is pushed below 50% HP. It is unclear what this does, if anything, after multiple retail captures.
-- To do: This NM does not use an SP ability like JP wiki claims but it does have a dust cloud animation that triggers as soon as it is pushed below 50% HP.
-- It is unclear what this does, if anything, after multiple retail captures.
-----------------------------------
---@type TMobEntity
local entity = {}
Expand All @@ -19,6 +20,7 @@ entity.onMobSpawn = function(mob)
mob:setMod(xi.mod.ICE_RES_RANK, 10)
mob:setMod(xi.mod.POWER_MULTIPLIER_SPELL, 55)
mob:setMobMod(xi.mobMod.NO_STANDBACK, 1)
mob:setRespawnTime(0) -- one-and-done per ride; the 1s spawn timer must not respawn him
end

entity.onMobSpellChoose = function(mob, target, spellId)
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
-----------------------------------
-- Area: Ship bound for Selbina Pirates
-- Mob: Ship Wight
-- Mob: Crossbones
-----------------------------------
---@type TMobEntity
local entity = {}
Expand All @@ -9,4 +9,8 @@ entity.onMobInitialize = function(mob)
mob:setMobMod(xi.mobMod.NO_STANDBACK, 1)
end

entity.onMobSpawn = function(mob)
mob:setRespawnTime(60) -- respawns every 60s while the pirate ship is alongside
end

return entity
Loading
Loading