landsandboat/scripts/globals/confrontation.lua
2026-02-20 18:42:33 -07:00

279 lines
8.4 KiB
Lua

-----------------------------------
-- Confrontation
--
-- Confrontation is a status effect that isolates a player or group of players
-- until it wears off or is removed. Affected characters suffer no personal ill effects,
-- but cannot be aided by anyone who is not participating in the same battle, nor can
-- they interact with players or mobs outside of the battle.
--
-- How to remove the effect
-- - Finishing the battle by completing all objectives will remove this effect.
-- - Failing to complete the battle will remove the effect.
--
-- How the effect is inflicted/gained
-- - Spawning an NM at a Field Parchment as part of an elite training regime for Fields of Valor.
-- - Initiating ANNM battles. (?)
-- - Certain battles fought as part of A Crystalline Prophecy or A Moogle Kupo d'Etat.
-- - Spawning a Zone Boss in Dynamis.
--
-- Other Notes
-- - Any summoned pets and trusts (excluding pet wyverns) are dismissed when placed under this status effect.
-- - Often, but not always, accompanied by a Level Restriction.
-----------------------------------
require('scripts/globals/npc_util')
-----------------------------------
xi = xi or {}
xi.confrontation = xi.confrontation or {}
xi.confrontation.lookup = {}
---@param mobs table
---@return nil
xi.confrontation.despawnMobs = function(mobs)
for _, mob in ipairs(mobs) do
if mob:isSpawned() then
DespawnMob(mob:getID())
end
end
end
local msgIfExists = function(player, msgID, offset)
if
not player or
not msgID
then
return
end
player:messageSpecial(msgID + (offset or 0))
end
-- will early exit with false return if confrontation doesn't define a distanceLimit
-- will default to warn 5 times before failing confrontation
-- messages are in the zone's IDs.lua text table offset from CONFRONTATION_DISENGAGED:
-- -1: warning you have left the area
-- +1: returned to area
-- 0: confrontation has ended
local confrontationDistCheck = function(player, npc, distanceLimit, distanceWarnLimit)
if
not (player and
npc and
distanceLimit and
distanceLimit > 0)
then
return false
end
local endMsg = zones[player:getZoneID()].text.CONFRONTATION_DISENGAGED
local prevOutOfRange = player:getLocalVar('Confrontation_Dist')
if player:checkDistance(npc) < distanceLimit then
player:setLocalVar('Confrontation_Dist', 0)
if prevOutOfRange > 0 then
-- you have returned to the area
msgIfExists(player, endMsg, 1)
end
else
-- running out of range
player:setLocalVar('Confrontation_Dist', prevOutOfRange + 1)
if prevOutOfRange >= (distanceWarnLimit or 5) then
-- the confrontation has ended
msgIfExists(player, endMsg)
return true
else
-- you have ventured too far
msgIfExists(player, endMsg, -1)
end
end
return false
end
---@param lookupKey integer
---@param setupTimer boolean
---@return nil
xi.confrontation.check = function(lookupKey, setupTimer)
-- Get the confrontation information
local lookup = xi.confrontation.lookup[lookupKey]
if not lookup then
return
end
local didWin = false
local didLose = false
local players = {}
for _, id in ipairs(lookup.registeredPlayerIds) do
local player = GetPlayerByID(id)
if player then
table.insert(players, player)
end
end
local mobs = {}
for _, id in ipairs(lookup.mobIds) do
table.insert(mobs, GetMobByID(id))
end
-- Check to see if the players are still valid
local validPlayerCount = 0
for _, member in ipairs(players) do
if
member:isAlive() and
member:getZoneID() == lookup.npc:getZoneID() and
member:hasStatusEffect(xi.effect.CONFRONTATION) and
member:getStatusEffect(xi.effect.CONFRONTATION):getPower() == lookupKey
then
validPlayerCount = validPlayerCount + 1
-- send out-of-range messages multiple times in a row then fail the confrontation if any member stays out of range long enough
if confrontationDistCheck(member, lookup.npc, lookup.distanceLimit, lookup.distanceWarnLimit) then
didLose = true
end
end
end
if validPlayerCount == 0 then
didLose = true
end
if lookup.timeLimit then
if GetSystemTime() > lookup.timeLimit then
didLose = true
end
end
-- Check to see if the mobs are still valid
local validMobCount = 0
for _, mob in pairs(mobs) do
if
mob:isAlive() and
mob:getZoneID() == lookup.npc:getZoneID() and
mob:hasStatusEffect(xi.effect.CONFRONTATION) and
mob:getStatusEffect(xi.effect.CONFRONTATION):getPower() == lookupKey
then
validMobCount = validMobCount + 1
end
end
if validMobCount == 0 then
didWin = true
end
if didWin or didLose then
for _, member in ipairs(players) do
-- Clear effect
member:delStatusEffect(xi.effect.CONFRONTATION)
-- Fire callbacks
if didWin and type(lookup.onWin) == 'function' then
lookup.onWin(member)
elseif didLose and type(lookup.onLose) == 'function' then
lookup.onLose(member)
end
end
-- Despawn mobs if lost, otherwise let them despawn naturally
if didLose then
xi.confrontation.despawnMobs(mobs)
end
-- Reset mobs/npcs/variables that may not be handled by win/lose
if lookup.cleanUp then
lookup.cleanUp()
end
xi.confrontation.lookup[lookupKey] = nil
else -- Check again soon
if setupTimer then
lookup.npc:timer(2400, function(npcArg)
xi.confrontation.check(bit.rshift(npcArg:getID(), 16), true)
end)
end
end
end
---@param player CBaseEntity
---@param npc CBaseEntity
---@param mobIds table|integer
---@param params table
---@return nil
xi.confrontation.start = function(player, npc, mobIds, params)
-- Generate lookup ID from spawn npc data
local lookupKey = bit.rshift(npc:getID(), 16)
params = params or {}
-- Extract mobIds
local mobs = {}
if type(mobIds) == 'number' then
table.insert(mobs, mobIds)
elseif type(mobIds) == 'table' then
for _, v in pairs(mobIds) do
if type(v) == 'number' then
table.insert(mobs, v)
end
end
end
mobIds = mobs
-- Tag alliance members with the confrontation effect
local alliance = {}
if type(params.playerList) == 'table' then
alliance = params.playerList
else
alliance = player:getAlliance()
end
local registeredPlayerIds = {}
for _, member in ipairs(alliance) do
-- Using the pop npc's ID as the 'key'
member:addStatusEffect(xi.effect.CONFRONTATION, { power = lookupKey, origin = member })
table.insert(registeredPlayerIds, member:getID())
end
-- Tag mobs with the confrontation effect
for _, mobId in pairs(mobs) do
local mob = GetMobByID(mobId)
if mob then
mob:addStatusEffect(xi.effect.CONFRONTATION, { power = lookupKey, origin = mob })
mob:addListener('DEATH', 'CONFRONTATION_DEATH', function(mobArg)
mobArg:removeListener('CONFRONTATION_DEATH')
xi.confrontation.check(lookupKey, false)
end)
end
end
-- Cache the lists into the global lookup
local lookup = {}
lookup.npc = npc
lookup.registeredPlayerIds = registeredPlayerIds
lookup.mobIds = mobIds
lookup.onWin = params.onWin
lookup.onLose = params.onLose
lookup.cleanUp = params.cleanUp
lookup.distanceLimit = params.distanceLimit
lookup.distanceWarnLimit = params.distanceWarnLimit
if params.timeLimit then
lookup.timeLimit = GetSystemTime() + params.timeLimit
end
xi.confrontation.lookup[lookupKey] = lookup
-- Pop!
if params.allRegPlayerEnmity then
npcUtil.popFromQM(player, npc, mobIds, { look = true, claim = true, hide = 1, enmityPlayerList = alliance })
else
npcUtil.popFromQM(player, npc, mobIds, { look = true, claim = true, hide = 1 })
end
-- Set up timed checks
xi.confrontation.check(lookupKey, true)
end