----------------------------------- -- 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