1563 lines
62 KiB
Lua
1563 lines
62 KiB
Lua
-----------------------------------
|
|
-- Battlefield Functions
|
|
-----------------------------------
|
|
require('scripts/globals/pathfind')
|
|
-----------------------------------
|
|
xi = xi or {}
|
|
|
|
local maxAreas =
|
|
{
|
|
{
|
|
Max = 8,
|
|
Zones =
|
|
{
|
|
xi.zone.TEMENOS,
|
|
},
|
|
},
|
|
|
|
{
|
|
Max = 6,
|
|
Zones =
|
|
{
|
|
xi.zone.APOLLYON,
|
|
},
|
|
},
|
|
|
|
{
|
|
Max = 1,
|
|
Zones =
|
|
{
|
|
xi.zone.DYNAMIS_VALKURM,
|
|
xi.zone.DYNAMIS_BUBURIMU,
|
|
xi.zone.DYNAMIS_QUFIM,
|
|
xi.zone.DYNAMIS_TAVNAZIA,
|
|
xi.zone.DYNAMIS_BEAUCEDINE,
|
|
xi.zone.DYNAMIS_XARCABARD,
|
|
xi.zone.DYNAMIS_SAN_DORIA,
|
|
xi.zone.DYNAMIS_BASTOK,
|
|
xi.zone.DYNAMIS_WINDURST,
|
|
xi.zone.DYNAMIS_JEUNO,
|
|
xi.zone.RIVERNE_SITE_B01,
|
|
xi.zone.GHELSBA_OUTPOST,
|
|
xi.zone.THE_GARDEN_OF_RUHMET
|
|
},
|
|
},
|
|
}
|
|
|
|
function onBattlefieldHandlerInitialize(zone)
|
|
local id = zone:getID()
|
|
local default = 3
|
|
|
|
for _, battlefield in pairs(maxAreas) do
|
|
for _, zoneid in pairs(battlefield.Zones) do
|
|
if id == zoneid then
|
|
return battlefield.Max
|
|
end
|
|
end
|
|
end
|
|
|
|
return default
|
|
end
|
|
|
|
xi.loot = xi.loot or {}
|
|
|
|
xi.loot.weight =
|
|
{
|
|
EXTREMELY_LOW = 2,
|
|
VERY_LOW = 10,
|
|
LOW = 30,
|
|
NORMAL = 50,
|
|
HIGH = 70,
|
|
VERY_HIGH = 100,
|
|
EXTREMELY_HIGH = 140,
|
|
}
|
|
|
|
xi.battlefield = xi.battlefield or {}
|
|
xi.battlefield.contents = xi.battlefield.contents or {}
|
|
xi.battlefield.contentsByZone = xi.battlefield.contentsByZone or {}
|
|
|
|
xi.battlefield.status =
|
|
{
|
|
OPEN = 0,
|
|
LOCKED = 1,
|
|
WON = 2,
|
|
LOST = 3,
|
|
}
|
|
|
|
xi.battlefield.returnCode =
|
|
{
|
|
WAIT = 1,
|
|
CUTSCENE = 2,
|
|
INCREMENT_REQUEST = 3,
|
|
LOCKED = 4,
|
|
REQS_NOT_MET = 5,
|
|
BATTLEFIELD_FULL = 6,
|
|
PARTY_ENGAGED = 9, -- Used as 2nd parameter to LOCKED
|
|
}
|
|
|
|
xi.battlefield.leaveCode =
|
|
{
|
|
EXIT = 1,
|
|
WON = 2,
|
|
WARPDC = 3,
|
|
LOST = 4
|
|
}
|
|
|
|
xi.battlefield.id =
|
|
{
|
|
RANK_2_MISSION_1 = 0, -- Converted
|
|
TAILS_OF_WOE = 1, -- Converted
|
|
DISMEMBERMENT_BRIGADE = 2, -- Converted
|
|
THE_SECRET_WEAPON = 3, -- Converted
|
|
HOSTILE_HERBIVORES = 4, -- Converted
|
|
SHATTERING_STARS_WAR = 5, -- Converted
|
|
SHATTERING_STARS_BLM = 6, -- Converted
|
|
SHATTERING_STARS_RNG = 7, -- Converted
|
|
CARAPACE_COMBATANTS = 8, -- Converted
|
|
SHOOTING_FISH = 9, -- Experimental
|
|
DROPPING_LIKE_FLIES = 10, -- Converted
|
|
HORNS_OF_WAR = 11, -- Experimental
|
|
UNDER_OBSERVATION = 12, -- Converted
|
|
EYE_OF_THE_TIGER = 13, -- Converted
|
|
SHOTS_IN_THE_DARK = 14, -- Experimental
|
|
DOUBLE_DRAGONIAN = 15, -- Converted
|
|
TODAYS_HOROSCOPE = 16, -- Experimental
|
|
CONTAMINATED_COLOSSEUM = 17, -- Converted
|
|
KINDERGARTEN_CAP = 18,
|
|
LAST_ORC_SHUNNED_HERO = 19,
|
|
BEYOND_INFINITY_HORLAIS_PEAK = 20, -- Converted
|
|
AMAN_TROVE_MARS_HORLAIS_PEAK = 24, -- Incomplete
|
|
AMAN_TROVE_VENUS_HORLAIS_PEAK = 25, -- Incomplete
|
|
SAVE_THE_CHILDREN = 32, -- Converted
|
|
HOLY_CREST = 33, -- Converted
|
|
WINGS_OF_FURY = 34, -- Converted
|
|
PETRIFYING_PAIR = 35, -- Converted
|
|
TOADAL_RECALL = 36, -- Converted
|
|
MIRROR_MIRROR = 37,
|
|
RANK_2_MISSION_2 = 64, -- Converted
|
|
WORMS_TURN = 65, -- Converted
|
|
GRIMSHELL_SHOCKTROOPERS = 66, -- Converted
|
|
ON_MY_WAY = 67, -- Converted
|
|
THIEF_IN_NORG = 68, -- Converted
|
|
THREE_TWO_ONE = 69, -- Converted
|
|
SHATTERING_STARS_RDM = 70, -- Converted
|
|
SHATTERING_STARS_THF = 71, -- Converted
|
|
SHATTERING_STARS_BST = 72, -- Converted
|
|
BIRDS_OF_A_FEATHER = 73, -- Converted
|
|
CRUSTACEAN_CONUNDRUM = 74, -- Experimental
|
|
GROVE_GUARDIANS = 75, -- Converted
|
|
HILLS_ARE_ALIVE = 76, -- Experimental
|
|
ROYAL_JELLY = 77, -- Converted
|
|
FINAL_BOUT = 78, -- Experimental
|
|
UP_IN_ARMS = 79, -- Converted
|
|
COPYCAT = 80,
|
|
OPERATION_DESERT_SWARM = 81, -- Experimental
|
|
PREHISTORIC_PIGEONS = 82, -- Experimental
|
|
PALBOROUGH_PROJECT = 83,
|
|
SHELL_SHOCKED = 84,
|
|
BEYOND_INFINITY_WAUGHROON_SHRINE = 85, -- Converted
|
|
RANK_2_MISSION = 96, -- Converted
|
|
STEAMED_SPROUTS = 97, -- Converted
|
|
DIVINE_PUNISHERS = 98, -- Converted
|
|
SAINTLY_INVITATION = 99, -- Converted
|
|
TREASURE_AND_TRIBULATIONS = 100, -- Converted
|
|
SHATTERING_STARS_MNK = 101, -- Converted
|
|
SHATTERING_STARS_WHM = 102, -- Converted
|
|
SHATTERING_STARS_SMN = 103, -- Converted
|
|
CREEPING_DOOM = 104, -- Converted
|
|
CHARMING_TRIO = 105, -- Converted
|
|
HAREM_SCAREM = 106, -- Converted
|
|
EARLY_BIRD_CATCHES_THE_WYRM = 107, -- Experimental
|
|
ROYAL_SUCCESSION = 108, -- Converted
|
|
RAPID_RAPTORS = 109, -- Converted
|
|
WILD_WILD_WHISKERS = 110, -- Converted
|
|
SEASONS_GREETINGS = 111, -- Experimental
|
|
ROYALE_RAMBLE = 112,
|
|
MOA_CONSTRICTORS = 113,
|
|
V_FORMATION = 114,
|
|
AVIAN_APOSTATES = 115,
|
|
BEYOND_INFINITY_BALGAS_DAIS = 116, -- Converted
|
|
TEMPLE_OF_UGGALEPIH = 128, -- Converted
|
|
JUNGLE_BOOGYMEN = 129, -- Converted
|
|
AMPHIBIAN_ASSAULT = 130, -- Converted
|
|
PROJECT_SHANTOTTOFICATION = 131,
|
|
WHOM_WILT_THOU_CALL = 132,
|
|
SHADOW_LORD_BATTLE = 160,
|
|
WHERE_TWO_PATHS_CONVERGE = 161,
|
|
KINDRED_SPIRITS = 162, -- Experimental
|
|
SURVIVAL_OF_THE_WISEST = 163,
|
|
SMASH_A_MALEVOLENT_MENACE = 164, -- Experimental
|
|
THROUGH_THE_QUICKSAND_CAVES = 192, -- Converted
|
|
LEGION_XI_COMITATENSIS = 193, -- Converted
|
|
SHATTERING_STARS_SAM = 194, -- Converted
|
|
SHATTERING_STARS_NIN = 195, -- Converted
|
|
SHATTERING_STARS_DRG = 196, -- Converted
|
|
CACTUAR_SUAVE = 197,
|
|
EYE_OF_THE_STORM = 198, -- Converted
|
|
SCARLET_KING = 199,
|
|
CAT_BURGLAR_BARES_FANGS = 200, -- Experimental
|
|
DRAGON_SCALES = 201,
|
|
MOONLIT_PATH = 224, -- Converted
|
|
MOON_READING = 225, -- Converted
|
|
WAKING_THE_BEAST_FULLMOON = 226,
|
|
BATTARU_ROYALE = 227,
|
|
RETURN_TO_DELKFUTTS_TOWER = 256,
|
|
INDOMITABLE_TRIUMVIRATE_VS3 = 257,
|
|
DAUNTLESS_DUO_VS2 = 258,
|
|
SOLITARY_DEMOLISHER_VS1 = 259,
|
|
HEROINES_COMBAT = 260,
|
|
MERCENARY_CAMP = 261,
|
|
ODE_OF_LIFE_BESTOWING = 262,
|
|
ARK_ANGELS_1 = 288, -- Converted
|
|
ARK_ANGELS_2 = 289, -- Converted
|
|
ARK_ANGELS_3 = 290, -- Converted
|
|
ARK_ANGELS_4 = 291, -- Converted
|
|
ARK_ANGELS_5 = 292, -- Converted
|
|
DIVINE_MIGHT = 293, -- Converted
|
|
CELESTIAL_NEXUS = 320, -- Converted
|
|
FIAT_LUX = 352,
|
|
DARKNESS_DESCENDS = 353,
|
|
BONDS_OF_MYTHRIL = 354,
|
|
MAIDEN_OF_THE_DUSK = 385,
|
|
TRIAL_BY_WIND = 416, -- Converted
|
|
CARBUNCLE_DEBACLE_CLOISTER_OF_GALES = 417, -- Converted
|
|
TRIAL_SIZE_TRIAL_BY_WIND = 418, -- Converted
|
|
WAKING_THE_BEAST_CLOISTER_OF_GALES = 419,
|
|
SUGAR_COATED_DIRECTIVE_CLOISTER_OF_GALES = 420, -- Converted
|
|
TRIAL_BY_LIGHTNING = 448, -- Converted
|
|
CARBUNCLE_DEBACLE_CLOISTER_OF_STORMS = 449, -- Converted
|
|
TRIAL_SIZE_TRIAL_BY_LIGHTNING = 450, -- Converted
|
|
WAKING_THE_BEAST_CLOISTER_OF_STORMS = 451,
|
|
SUGAR_COATED_DIRECTIVE_CLOISTER_OF_STORMS = 452, -- Converted
|
|
TRIAL_BY_ICE = 480, -- Converted
|
|
CLASS_REUNION = 481, -- Converted
|
|
TRIAL_SIZE_TRIAL_BY_ICE = 482, -- Converted
|
|
WAKING_THE_BEAST_CLOISTER_OF_FROST = 483,
|
|
SUGAR_COATED_DIRECTIVE_CLOISTER_OF_FROST = 484, -- Converted
|
|
RANK_5_MISSION = 512, -- Converted
|
|
COME_INTO_MY_PARLOR = 513,
|
|
E_VASE_IVE_ACTION = 514,
|
|
INFERNAL_SWARM = 515,
|
|
HEIR_TO_THE_LIGHT = 516, -- Converted
|
|
SHATTERING_STARS_PLD = 517, -- Converted
|
|
SHATTERING_STARS_DRK = 518, -- Converted
|
|
SHATTERING_STARS_BRD = 519, -- Converted
|
|
DEMOLITION_SQUAD = 520, -- Converted
|
|
DIE_BY_THE_SWORD = 521, -- Converted
|
|
LET_SLEEPING_DOGS_DIE = 522, -- Converted
|
|
BROTHERS_D_AURPHE = 523, -- Converted
|
|
UNDYING_PROMISE = 524, -- Converted
|
|
FACTORY_REJECTS = 525, -- Converted
|
|
IDOL_THOUGHTS = 526, -- Converted
|
|
AWFUL_AUTOPSY = 527, -- Converted
|
|
CELERY = 528, -- Experimental
|
|
MIRROR_IMAGES = 529,
|
|
FURIOUS_FINALE = 530,
|
|
CLASH_OF_THE_COMRADES = 531,
|
|
THOSE_WHO_LURK_IN_SHADOWS = 532, -- Experimental
|
|
BEYOND_INFINITY = 533, -- Converted
|
|
TRIAL_BY_FIRE = 544, -- Converted
|
|
TRIAL_SIZE_TRIAL_BY_FIRE = 545, -- Converted
|
|
WAKING_THE_BEAST_CLOISTER_OF_FLAMES = 546,
|
|
SUGAR_COATED_DIRECTIVE_CLOISTER_OF_FLAMES = 547, -- Converted
|
|
TRIAL_BY_EARTH = 576, -- Converted
|
|
PUPPET_MASTER = 577, -- Converted
|
|
TRIAL_SIZE_TRIAL_BY_EARTH = 578, -- Converted
|
|
WAKING_THE_BEAST_CLOISTER_OF_TREMORS = 579,
|
|
SUGAR_COATED_DIRECTIVE_CLOISTER_OF_TREMORS = 580, -- Converted
|
|
TRIAL_BY_WATER = 608, -- Converted
|
|
TRIAL_SIZE_TRIAL_BY_WATER = 609, -- Converted
|
|
WAKING_THE_BEAST_CLOISTER_OF_TIDES = 610,
|
|
SUGAR_COATED_DIRECTIVE_CLOISTER_OF_TIDES = 611, -- Converted
|
|
FLAMES_FOR_THE_DEAD = 640, -- Converted
|
|
FOLLOW_THE_WHITE_RABBIT = 641,
|
|
WHEN_HELL_FREEZES_OVER = 642,
|
|
BROTHERS = 643, -- Converted
|
|
HOLY_COW = 644, -- Converted
|
|
HEAD_WIND = 672, -- Converted
|
|
LIKE_THE_WIND = 673, -- Experimental
|
|
SHEEP_IN_ANTLIONS_CLOTHING = 674, -- Converted
|
|
SHELL_WE_DANCE = 675, -- Experimental
|
|
TOTENTANZ = 676,
|
|
TANGO_WITH_A_TRACKER = 677,
|
|
REQUIEM_OF_SIN = 678,
|
|
ANTAGONISTIC_AMBUSCADE = 679,
|
|
DARKNESS_NAMED = 704, -- Converted
|
|
TEST_YOUR_MITE = 705,
|
|
WAKING_DREAMS = 706, -- Converted
|
|
CENTURY_OF_HARDSHIP = 736, -- Converted
|
|
RETURN_TO_THE_DEPTHS = 737,
|
|
BIONIC_BUG = 738,
|
|
PULLING_THE_STRINGS = 739,
|
|
AUTOMATON_ASSAULT = 740,
|
|
MOBLINE_COMEDY = 741,
|
|
ANCIENT_FLAMES_BECKON_SPIRE_OF_HOLLA = 768, -- Converted
|
|
SIMULANT = 769, -- Experimental
|
|
EMPTY_HOPES = 770,
|
|
ANCIENT_FLAMES_BECKON_SPIRE_OF_DEM = 800, -- Converted
|
|
YOU_ARE_WHAT_YOU_EAT = 801, -- Experimental
|
|
EMPTY_DREAMS = 802,
|
|
ANCIENT_FLAMES_BECKON_SPIRE_OF_MEA = 832, -- Converted
|
|
PLAYING_HOST = 833, -- Experimental
|
|
EMPTY_DESIRES = 834,
|
|
DESIRES_OF_EMPTINESS = 864, -- Converted
|
|
PULLING_THE_PLUG = 865,
|
|
EMPTY_ASPIRATIONS = 866,
|
|
STORMS_OF_FATE = 896, -- Converted
|
|
WYRMKING_DESCENDS = 897,
|
|
OURYU_COMETH = 928,
|
|
ANCIENT_VOWS = 960, -- Converted
|
|
SAVAGE = 961,
|
|
FIRE_IN_THE_SKY = 962, -- Experimental
|
|
BAD_SEED = 963,
|
|
BUGARD_IN_THE_CLOUDS = 964,
|
|
BELOVED_OF_THE_ATLANTES = 965,
|
|
UNINVITED_GUESTS = 966, -- Experimental
|
|
NEST_OF_NIGHTMARES = 967,
|
|
ONE_TO_BE_FEARED = 992, -- Converted
|
|
WARRIORS_PATH = 993, -- Converted
|
|
WHEN_ANGELS_FALL = 1024, -- Converted
|
|
DAWN = 1056, -- Converted
|
|
APOCALYPSE_NIGH = 1057, -- Converted
|
|
CALL_TO_ARMS = 1088,
|
|
COMPLIMENTS_TO_THE_CHEF = 1089,
|
|
PUPPETMASTER_BLUES = 1090,
|
|
BREAKING_THE_BONDS_OF_FATE = 1091,
|
|
LEGACY_OF_THE_LOST = 1092, -- Converted
|
|
TOUGH_NUT_TO_CRACK = 1120,
|
|
HAPPY_CASTER = 1121,
|
|
OMENS = 1122, -- Converted
|
|
ACHIEVING_TRUE_POWER = 1123,
|
|
SHIELD_OF_DIPLOMACY = 1124, -- Converted
|
|
MAKING_A_MOCKERY = 1152,
|
|
SHADOWS_OF_THE_MIND = 1153,
|
|
BEAST_WITHIN = 1154,
|
|
MOMENT_OF_TRUTH = 1155,
|
|
PUPPET_IN_PERIL = 1156, -- Converted
|
|
RIDER_COMETH = 1184,
|
|
NW_APOLLYON = 1290, -- Converted
|
|
SW_APOLLYON = 1291, -- Converted
|
|
NE_APOLLYON = 1292, -- Converted
|
|
SE_APOLLYON = 1293, -- Converted
|
|
CS_APOLLYON = 1294, -- Converted
|
|
CS_APOLLYON_II = 1295, -- Converted
|
|
CENTRAL_APOLLYON = 1296, -- Converted
|
|
CENTRAL_APOLLYON_II = 1297, -- Converted
|
|
TEMENOS_WESTERN_TOWER = 1298, -- Converted
|
|
TEMENOS_NORTHERN_TOWER = 1299, -- Converted
|
|
TEMENOS_EASTERN_TOWER = 1300, -- Converted
|
|
CENTRAL_TEMENOS_BASEMENT = 1301, -- Converted
|
|
CENTRAL_TEMENOS_BASEMENT_II = 1302, -- Converted
|
|
CENTRAL_TEMENOS_1ST_FLOOR = 1303, -- Converted
|
|
CENTRAL_TEMENOS_2ND_FLOOR = 1304, -- Converted
|
|
CENTRAL_TEMENOS_3RD_FLOOR = 1305, -- Converted
|
|
CENTRAL_TEMENOS_4TH_FLOOR = 1306, -- Converted
|
|
CENTRAL_TEMENOS_4TH_FLOOR_II = 1307, -- Converted
|
|
PURPLE_THE_NEW_BLACK = 2721, -- Converted
|
|
}
|
|
|
|
xi.battlefield.itemUses =
|
|
{
|
|
[xi.item.WARRIORS_TESTIMONY] = 3,
|
|
[xi.item.MONKS_TESTIMONY] = 3,
|
|
[xi.item.WHITE_MAGES_TESTIMONY] = 3,
|
|
[xi.item.BLACK_MAGES_TESTIMONY] = 3,
|
|
[xi.item.RED_MAGES_TESTIMONY] = 3,
|
|
[xi.item.THIEFS_TESTIMONY] = 3,
|
|
[xi.item.PALADINS_TESTIMONY] = 3,
|
|
[xi.item.DARK_KNIGHTS_TESTIMONY] = 3,
|
|
[xi.item.BEASTMASTERS_TESTIMONY] = 3,
|
|
[xi.item.BARDS_TESTIMONY] = 3,
|
|
[xi.item.RANGERS_TESTIMONY] = 3,
|
|
[xi.item.SAMURAIS_TESTIMONY] = 3,
|
|
[xi.item.NINJAS_TESTIMONY] = 3,
|
|
[xi.item.DRAGOONS_TESTIMONY] = 3,
|
|
[xi.item.SUMMONERS_TESTIMONY] = 3,
|
|
[xi.item.BLUE_MAGES_TESTIMONY] = 3,
|
|
[xi.item.CORSAIRS_TESTIMONY] = 3,
|
|
[xi.item.PUPPETMASTERS_TESTIMONY] = 3,
|
|
}
|
|
|
|
Battlefield = setmetatable({}, { __index = Container })
|
|
Battlefield.__index = Battlefield
|
|
|
|
---@diagnostic disable-next-line: duplicate-set-field
|
|
Battlefield.__eq = function(m1, m2)
|
|
return m1.id == m2.id
|
|
end
|
|
|
|
-- Creates a new Battlefield interaction
|
|
-- Data takes the following keys:
|
|
-- - zoneId: Which zone this battlefield exists within (required)
|
|
-- - battlefieldId: Battlefield ID used in the database (required)
|
|
-- - maxPlayers: Maximum number of players allowed to enter (required)
|
|
-- - timeLimit: Time in seconds alotted to complete the battlefield before being ejected. (required)
|
|
-- - index: The index of the battlefield within the zone. This is used to communicate with the client on which menu item this battlefield is. (required)
|
|
-- - levelCap: Level cap imposed upon the battlefield. Defaults to 0 - no level cap. (optional)
|
|
-- - area: Some battlefields has multiple areas (Burning Circles) while others have fixed areas (Apollyon). Set to have a fixed area. (optional)
|
|
-- - entryNpc: The name of the NPC used for entering (required)
|
|
-- - exitNpc: The name of the NPC used for exiting (optional)
|
|
-- - exitNpcs: The names of the NPC used for exiting (optional)
|
|
-- - allowSubjob: Determines if character subjobs are enabled or disabled upon entry. Defaults to true. (optional)
|
|
-- - hasWipeGrace: Grants players a 3 minute grace period on a full wipe before ejecting them. Defaults to true. (optional)
|
|
-- - canLoseExp: Determines if a character loses experience points upon death while inside the battlefield. Defaults to true. (optional)
|
|
-- - showTimer: Show the time remaining in the battlefield in the UI for the player. Defaults to true. (optional)
|
|
-- - delayToExit: Amount of time to wait before exiting the battlefield. Defaults to 5 seconds. (optional)
|
|
-- - requiredItems: Items required to be traded to enter the battlefield.
|
|
-- Needs to be in the format of { itemid, quantity, useMessage = ID.text.*, wearMessage = ID.text.*, wornMessage = ID.text.* }. (optional)
|
|
-- - requiredKeyItems: Key items required to be able to enter the battlefield - these are removed upon entry unless 'keep = true' (optional)
|
|
-- - title: Title given to players upon victory (optional)
|
|
-- - grantXP: Amount of XP to grant upon victory (optional)
|
|
-- - grantXPLockout: If true, players can only receive the grantXP once per day, resetting at JST midnight. (optional)
|
|
-- - lossEventParams: Parameters given to the loss event (32002). Defaults to none. (optional)
|
|
---@diagnostic disable-next-line: duplicate-set-field
|
|
function Battlefield:new(data)
|
|
local obj = Container:new(Battlefield.getVarPrefix(data.battlefieldId))
|
|
|
|
setmetatable(obj, self)
|
|
obj.zoneId = data.zoneId
|
|
obj.battlefieldId = data.battlefieldId
|
|
obj.maxPlayers = data.maxPlayers
|
|
obj.timeLimit = data.timeLimit
|
|
obj.index = data.index
|
|
|
|
if data.entryNpcs then
|
|
obj.entryNpcs = data.entryNpcs
|
|
elseif data.entryNpc then
|
|
obj.entryNpcs = { data.entryNpc }
|
|
end
|
|
|
|
obj.area = data.area
|
|
|
|
if data.exitNpcs then
|
|
obj.exitNpcs = data.exitNpcs
|
|
elseif data.exitNpc then
|
|
obj.exitNpcs = { data.exitNpc }
|
|
end
|
|
|
|
obj.title = data.title
|
|
obj.grantXP = data.grantXP
|
|
obj.grantXPLockout = data.grantXPLockout
|
|
obj.levelCap = data.levelCap or 0
|
|
obj.allowSubjob = (data.allowSubjob == nil or data.allowSubjob) or false
|
|
obj.allowTrusts = data.allowTrusts and data.allowTrusts or false
|
|
obj.hasWipeGrace = (data.hasWipeGrace == nil or data.hasWipeGrace) or false
|
|
obj.isMission = data.isMission and data.isMission or false
|
|
obj.canLoseExp = (data.canLoseExp == nil or data.canLoseExp) or false
|
|
obj.showTimer = (data.showTimer == nil or data.showTimer) or false
|
|
obj.delayToExit = data.delayToExit or 5
|
|
obj.requiredItems = data.requiredItems or {}
|
|
obj.requiredKeyItems = data.requiredKeyItems or {}
|
|
obj.lossEventParams = data.lossEventParams or {}
|
|
obj.armouryCrates = data.armouryCrates or false
|
|
obj.experimental = data.experimental or false
|
|
obj.allowedAreas = data.allowedAreas
|
|
obj.csParam7 = data.csParam7 and data.csParam7 or 0
|
|
obj.csParam8 = data.csParam8 and data.csParam8 or 0
|
|
|
|
obj.sections = obj.sections or { { [obj.zoneId] = {} } }
|
|
obj.groups = {}
|
|
obj.paths = {}
|
|
obj.loot = {}
|
|
|
|
-- Determine which items need to be traded as the requiredItems format is incompatable with npcUtil.tradeHasExactly
|
|
obj.tradeItems = {}
|
|
|
|
for index, value in ipairs(obj.requiredItems) do
|
|
obj.tradeItems[index] = value
|
|
end
|
|
|
|
return obj
|
|
end
|
|
|
|
function Battlefield.getVarPrefix(battlefieldID)
|
|
return string.format('Battlefield[%d]', battlefieldID)
|
|
end
|
|
|
|
function Battlefield:register()
|
|
-- Only hookup the entry and exit listeners if there aren't any other battlefields already registered for that entrance
|
|
local setupEvents = true
|
|
local setupEntryNpcs = true
|
|
|
|
if utils.hasKey(self.zoneId, xi.battlefield.contentsByZone) then
|
|
local contents = xi.battlefield.contentsByZone[self.zoneId]
|
|
|
|
for _, content in ipairs(contents) do
|
|
-- Always setup listeners if we're reloading a battlefield
|
|
if self.battlefieldId == content.battlefieldId and content.hasListeners then
|
|
setupEvents = true
|
|
setupEntryNpcs = true
|
|
|
|
break
|
|
end
|
|
|
|
-- Do not setup events if there are any other battlefields
|
|
setupEvents = false
|
|
|
|
-- Do not setup npcs if there is another battlefield using the same entry npc
|
|
if self.entryNpcs then
|
|
for _, entryNpc in ipairs(self.entryNpcs) do
|
|
if utils.contains(entryNpc, content.entryNpcs) then
|
|
setupEntryNpcs = false
|
|
|
|
break
|
|
end
|
|
end
|
|
end
|
|
end
|
|
end
|
|
|
|
local zoneSection = self.sections[1][self.zoneId]
|
|
if setupEvents then
|
|
utils.append(zoneSection, {
|
|
onEventUpdate =
|
|
{
|
|
[32000] = Battlefield.redirectEventUpdate,
|
|
[32003] = utils.bind(Battlefield.redirectEventCall, 'onExitEventUpdate'),
|
|
},
|
|
|
|
onEventFinish =
|
|
{
|
|
[32000] = utils.bind(Battlefield.redirectEventCall, 'onEventFinishEnter'),
|
|
[32001] = utils.bind(Battlefield.redirectEventCall, 'onEventFinishWin'),
|
|
[32002] = utils.bind(Battlefield.redirectEventCall, 'onEventFinishLeave'),
|
|
[32003] = utils.bind(Battlefield.redirectEventCall, 'onEventFinishExit'),
|
|
[32004] = utils.bind(Battlefield.redirectEventCall, 'onEventFinishBattlefield'),
|
|
}
|
|
})
|
|
self.hasListeners = true
|
|
end
|
|
|
|
if setupEntryNpcs and self.entryNpcs then
|
|
for _, entryNpc in ipairs(self.entryNpcs) do
|
|
utils.append(zoneSection, {
|
|
[entryNpc] =
|
|
{
|
|
onTrade = Battlefield.onEntryTrade,
|
|
onTrigger = Battlefield.onEntryTrigger,
|
|
}
|
|
})
|
|
end
|
|
end
|
|
|
|
if self.exitNpcs then
|
|
local exitTrigger = self.onExitTrigger and self.onExitTrigger or Battlefield.onExitTrigger
|
|
for _, exitNpc in ipairs(self.exitNpcs) do
|
|
utils.append(zoneSection, {
|
|
[exitNpc] =
|
|
{
|
|
onTrigger = exitTrigger,
|
|
}
|
|
})
|
|
end
|
|
end
|
|
|
|
xi.battlefield.contents[self.battlefieldId] = self
|
|
|
|
if utils.hasKey(self.zoneId, xi.battlefield.contentsByZone) then
|
|
table.insert(xi.battlefield.contentsByZone[self.zoneId], self)
|
|
else
|
|
xi.battlefield.contentsByZone[self.zoneId] = { self }
|
|
end
|
|
|
|
return self
|
|
end
|
|
|
|
function Battlefield:isValidEntry(player, npc)
|
|
return utils.contains(npc:getName(), self.entryNpcs)
|
|
end
|
|
|
|
-- Allow for Battlefield scripts to easily add additional requirements for entry by
|
|
-- redefining this function
|
|
function Battlefield:entryRequirement(player, npc, isRegistrant, trade)
|
|
return true
|
|
end
|
|
|
|
function Battlefield:checkRequirements(player, npc, isRegistrant, trade)
|
|
if not self:isValidEntry(player, npc) then
|
|
return false
|
|
end
|
|
|
|
-- Do not show battlefields to registrant when either they don't require items and player is trading or
|
|
-- that do require items and but player is not trading
|
|
if isRegistrant and (trade == nil) ~= (#self.tradeItems == 0) then
|
|
return false
|
|
end
|
|
|
|
for _, keyItem in ipairs(self.requiredKeyItems) do
|
|
if type(keyItem) == 'table' then
|
|
|
|
-- Only need one from the group
|
|
local hasAny = false
|
|
|
|
for _, subitem in ipairs(keyItem) do
|
|
if player:hasKeyItem(subitem) then
|
|
hasAny = true
|
|
|
|
break
|
|
end
|
|
end
|
|
|
|
if not hasAny then
|
|
return false
|
|
end
|
|
|
|
else
|
|
if not player:hasKeyItem(keyItem) then
|
|
return false
|
|
end
|
|
end
|
|
end
|
|
|
|
if trade and #self.tradeItems > 0 then
|
|
if not npcUtil.tradeHasExactly(trade, self.tradeItems) then
|
|
return false
|
|
end
|
|
end
|
|
|
|
-- Additional Requirements that may be necessary for battlefield entry
|
|
-- contained within the script itself, defaults to True
|
|
return self:entryRequirement(player, npc, isRegistrant, trade)
|
|
end
|
|
|
|
function Battlefield:checkSkipCutscene(player)
|
|
return false
|
|
end
|
|
|
|
function Battlefield.onEntryTrade(player, npc, trade, onUpdate)
|
|
-- Check if player's party has level sync
|
|
if xi.battlefield.rejectLevelSyncedParty(player, npc) then
|
|
return
|
|
end
|
|
|
|
-- Validate trade
|
|
if not trade then
|
|
return
|
|
end
|
|
|
|
-- Validate battlefield status
|
|
if player:hasStatusEffect(xi.effect.BATTLEFIELD) and not onUpdate then
|
|
player:messageBasic(xi.msg.basic.WAIT_LONGER, 0, 0)
|
|
|
|
return
|
|
end
|
|
|
|
-- Check if another party member has battlefield status effect. If so, don't allow trade.
|
|
local alliance = player:getAlliance()
|
|
|
|
for _, member in pairs(alliance) do
|
|
if member:hasStatusEffect(xi.effect.BATTLEFIELD) then
|
|
player:messageBasic(xi.msg.basic.WAIT_LONGER, 0, 0)
|
|
|
|
return
|
|
end
|
|
end
|
|
|
|
local zoneId = player:getZoneID()
|
|
|
|
-- Determine which battlefields are available given the traded items
|
|
local options = xi.battlefield.getBattlefieldOptions(player, npc, trade)
|
|
|
|
if options == 0 then
|
|
local noEntryMessage = zones[zoneId].text.NO_BATTLEFIELD_ENTRY
|
|
|
|
if noEntryMessage then
|
|
player:messageSpecial(noEntryMessage)
|
|
end
|
|
|
|
return
|
|
end
|
|
|
|
-- Ensure that the traded item(s) are not worn out
|
|
local contents = xi.battlefield.contentsByZone[zoneId]
|
|
|
|
for _, content in ipairs(contents) do
|
|
if
|
|
#content.requiredItems > 0 and
|
|
content.requiredItems.wornMessage and
|
|
npcUtil.tradeHas(trade, content.tradeItems)
|
|
then
|
|
local itemId = content.requiredItems[1]
|
|
-- Gets the total number of item uses for the given item. Default to one since that is the majority of them.
|
|
local totalUses = xi.battlefield.itemUses[itemId] or 1
|
|
|
|
if player:getWornUses(itemId) >= totalUses then
|
|
if type(content.requiredItems.wornMessage) == 'table' then
|
|
player:messageSpecial(unpack(content.requiredItems.wornMessage))
|
|
elseif totalUses > 1 then
|
|
player:messageSpecial(content.requiredItems.wornMessage, itemId)
|
|
else
|
|
player:messageSpecial(content.requiredItems.wornMessage, 0, 0, 0, itemId)
|
|
end
|
|
|
|
return
|
|
end
|
|
end
|
|
end
|
|
|
|
if not onUpdate then
|
|
-- Open menu of valid battlefields
|
|
return Battlefield:event(32000, 0, 0, 0, options, 0, 0, 0, 0)
|
|
end
|
|
end
|
|
|
|
function Battlefield.onEntryTrigger(player, npc)
|
|
-- Cannot enter if anyone in party is level/master sync'd
|
|
if xi.battlefield.rejectLevelSyncedParty(player, npc) then
|
|
return
|
|
end
|
|
|
|
-- Player has battlefield status effect. That means a battlefield is open OR the player is inside a battlefield.
|
|
if player:hasStatusEffect(xi.effect.BATTLEFIELD) then
|
|
-- Player is outside battlefield. Attempting to enter.
|
|
local status = player:getStatusEffect(xi.effect.BATTLEFIELD)
|
|
local id = status:getPower()
|
|
local content = xi.battlefield.contents[id]
|
|
if not content then
|
|
return
|
|
end
|
|
|
|
if not content:checkRequirements(player, npc, false) then
|
|
return
|
|
end
|
|
|
|
local options = utils.mask.setBit(0, content.index, true)
|
|
player:setLocalVar('[BCNM]EnterExisting', 1)
|
|
return Battlefield:event(32000, 0, 0, 0, options, 0, 0, 0, 0)
|
|
end
|
|
|
|
-- Player doesn't have battlefield status effect. That means player wants to register a new battlefield OR is attempting to enter a closed one.
|
|
-- Check if another party member has battlefield status effect. If so, that means the player is trying to enter a closed battlefield.
|
|
local alliance = player:getAlliance()
|
|
|
|
for _, member in pairs(alliance) do
|
|
if member:hasStatusEffect(xi.effect.BATTLEFIELD) then
|
|
player:messageSpecial(zones[player:getZoneID()].text.PARTY_MEMBERS_ARE_ENGAGED)
|
|
return
|
|
end
|
|
end
|
|
|
|
-- No one in party/alliance has battlefield status effect. We want to register a new battlefield.
|
|
local options = xi.battlefield.getBattlefieldOptions(player, npc)
|
|
|
|
-- GMs get access to all BCNMs with visible GM
|
|
if player:getGMLevel() > 0 and player:getVisibleGMLevel() >= 3 then
|
|
options = 268435455
|
|
end
|
|
|
|
-- options = 268435455 -- uncomment to open menu with all possible battlefields
|
|
if options == 0 then
|
|
local noEntryMessage = zones[player:getZoneID()].text.NO_BATTLEFIELD_ENTRY
|
|
|
|
if noEntryMessage then
|
|
player:messageSpecial(noEntryMessage)
|
|
end
|
|
|
|
return
|
|
end
|
|
|
|
return Battlefield:event(32000, 0, 0, 0, options, 0, 0, 0, 0)
|
|
end
|
|
|
|
-- Static function to lookup the correct battlefield to handle this event update
|
|
function Battlefield.redirectEventUpdate(player, csid, option, npc)
|
|
if option == 0 or option == 255 then
|
|
return false
|
|
end
|
|
|
|
local contents = xi.battlefield.contentsByZone[player:getZoneID()]
|
|
local value = bit.rshift(option, 4)
|
|
|
|
for _, content in pairs(contents) do
|
|
if value == content.index then
|
|
content:onEntryEventUpdate(player, csid, option, npc)
|
|
|
|
break
|
|
end
|
|
end
|
|
end
|
|
|
|
-- NOTE: Return values from this function impact if the server will honor update position
|
|
-- requests or not. The client will request each area, and so long as we return 0, it
|
|
-- will still send the appropriate position packet, but not change the values for the player.
|
|
|
|
function Battlefield:onEntryEventUpdate(player, csid, option, npc)
|
|
-- Can't enter if party locked the battlefield
|
|
local isEnteringExisting = player:getLocalVar('[BCNM]EnterExisting') == 1
|
|
if isEnteringExisting and not player:hasStatusEffect(xi.effect.BATTLEFIELD) then
|
|
player:setLocalVar('[BCNM]EnterExisting', 0)
|
|
player:setLocalVar('[battlefield]area', 0)
|
|
player:updateEvent(xi.battlefield.returnCode.LOCKED, xi.battlefield.returnCode.PARTY_ENGAGED)
|
|
player:setLocalVar('noPosUpdate', 1)
|
|
return 0
|
|
end
|
|
|
|
local clearTime = 1
|
|
local name = 'Meme'
|
|
local partySize = 1
|
|
local area = self.area or (player:getLocalVar('[battlefield]area') + 1)
|
|
|
|
-- NOTE: 'area' is used when there are not multiple arenas present, and cannot be used
|
|
-- if there are indeed multiple areas, but the implementation is partial. allowedAreas
|
|
-- is used for this purpose, and additional logic on increment request below. Setting area
|
|
-- for this purpose will cause issues with mob spawning if the implementation has placeholder
|
|
-- areas! This should be used sparingly and removed upon proper captures for BCNM areas, as
|
|
-- this bypasses all registration logic if the battlefield area is flagged as disabled.
|
|
|
|
if
|
|
self.allowedAreas and
|
|
not self.allowedAreas[area]
|
|
then
|
|
if area < 3 then
|
|
player:setLocalVar('[battlefield]area', area)
|
|
else
|
|
player:updateEvent(xi.battlefield.returnCode.WAIT)
|
|
end
|
|
|
|
-- TODO: Remove the localVar when issue with function return is resolved
|
|
player:setLocalVar('noPosUpdate', 1)
|
|
return 0
|
|
end
|
|
|
|
if self.area then
|
|
area = self.area
|
|
end
|
|
|
|
local result = player:registerBattlefield(self.battlefieldId, area, player:getID(), self)
|
|
local status = xi.battlefield.status.OPEN
|
|
|
|
if result ~= xi.battlefield.returnCode.CUTSCENE then
|
|
if result == xi.battlefield.returnCode.INCREMENT_REQUEST then
|
|
if area < 3 then
|
|
player:setLocalVar('[battlefield]area', area)
|
|
else
|
|
player:updateEvent(xi.battlefield.returnCode.WAIT)
|
|
end
|
|
elseif result == xi.battlefield.returnCode.REQS_NOT_MET then
|
|
-- Ensure Battlefield Effect is removed
|
|
-- If you cant enter now, you cant enter by trying again right now...
|
|
player:delStatusEffect(xi.effect.BATTLEFIELD)
|
|
player:updateEvent(xi.battlefield.returnCode.REQS_NOT_MET)
|
|
end
|
|
|
|
-- TODO: Remove the localVar when issue with function return is resolved
|
|
player:setLocalVar('noPosUpdate', 1)
|
|
return 0
|
|
end
|
|
|
|
-- Only allow entrance if battlefield is open and player has battlefield effect, witch can be lost mid battlefield selection.
|
|
if
|
|
not player:getBattlefield() and
|
|
player:hasStatusEffect(xi.effect.BATTLEFIELD)
|
|
-- and id:getStatus() == xi.battlefield.status.OPEN -- TODO: Uncomment only once that can-of-worms is dealt with.
|
|
then
|
|
player:enterBattlefield()
|
|
end
|
|
|
|
-- Handle record
|
|
local initiatorId = 0
|
|
local battlefield = player:getBattlefield()
|
|
|
|
if battlefield then
|
|
name, clearTime, partySize = battlefield:getRecord()
|
|
initiatorId, _ = battlefield:getInitiator()
|
|
end
|
|
|
|
-- Register party members
|
|
if initiatorId == player:getID() then
|
|
local effect = player:getStatusEffect(xi.effect.BATTLEFIELD)
|
|
local zone = player:getZoneID()
|
|
|
|
-- Handle traded items if not wearing them
|
|
if
|
|
self.requiredItems.wearMessage == nil and
|
|
#self.tradeItems > 0
|
|
then
|
|
if not self.requiredItems.keep then
|
|
player:tradeComplete()
|
|
end
|
|
end
|
|
|
|
-- Handle party/alliance members
|
|
local alliance = player:getAlliance()
|
|
for _, member in pairs(alliance) do
|
|
if
|
|
member:getZoneID() == zone and
|
|
member:getStatus() ~= xi.status.DISAPPEAR and
|
|
not member:hasStatusEffect(xi.effect.BATTLEFIELD) and
|
|
not member:getBattlefield()
|
|
then
|
|
member:copyStatusEffect(effect)
|
|
member:registerBattlefield(self.battlefieldId, area, player:getID(), self)
|
|
end
|
|
end
|
|
end
|
|
|
|
local autoSkipCS = self:getLocalVar(player, 'CS') == 1 and 100 or 0
|
|
player:updateEvent(result, self.index, autoSkipCS, clearTime, partySize, self:checkSkipCutscene(player), self.csParam7, self.csParam8)
|
|
player:updateEventString(name)
|
|
|
|
return (status < xi.battlefield.status.LOCKED and result < xi.battlefield.returnCode.LOCKED) and 1 or 0
|
|
end
|
|
|
|
function Battlefield.redirectEventCall(eventName, player, csid, option)
|
|
local battlefieldID = 0
|
|
local battlefield = player:getBattlefield()
|
|
|
|
if battlefield then
|
|
battlefieldID = battlefield:getID()
|
|
else
|
|
battlefieldID = player:getLocalVar('battlefieldID')
|
|
end
|
|
|
|
if battlefieldID == 0 then
|
|
return
|
|
end
|
|
|
|
local content = xi.battlefield.contents[battlefieldID]
|
|
content[eventName](content, player, csid, option)
|
|
end
|
|
|
|
function Battlefield:onEventFinishEnter(player, csid, option, npc)
|
|
player:setEnteredBattlefield(true)
|
|
player:setLocalVar('[battlefield]area', 0)
|
|
self:setLocalVar(player, 'CS', 1)
|
|
end
|
|
|
|
function Battlefield:onEventFinishWin(player, csid, option, npc)
|
|
if self.title then
|
|
player:addTitle(self.title)
|
|
end
|
|
|
|
if self.grantXP then
|
|
if self.grantXPLockout then
|
|
if self:getVar(player, 'XP') > GetSystemTime() then
|
|
return
|
|
end
|
|
|
|
self:setVar(player, 'XP', JstMidnight())
|
|
end
|
|
|
|
player:addExp(self.grantXP)
|
|
end
|
|
end
|
|
|
|
function Battlefield.onExitTrigger(player, npc)
|
|
if player:getBattlefield() then
|
|
return Battlefield:progressOptionalCutscene(32003, { cs_option = 3, canSkip = true })
|
|
end
|
|
end
|
|
|
|
function Battlefield:onExitEventUpdate(player, csid, option, npc)
|
|
if option == 2 then
|
|
player:updateEvent(3)
|
|
elseif option == 3 then
|
|
player:updateEvent(0)
|
|
end
|
|
end
|
|
|
|
function Battlefield:onEventFinishLeave(player, csid, option, npc)
|
|
player:leaveBattlefield(1)
|
|
end
|
|
|
|
function Battlefield:onEventFinishExit(player, csid, option, npc)
|
|
if option == 4 and player:getBattlefield() then
|
|
player:leaveBattlefield(1)
|
|
end
|
|
end
|
|
|
|
function Battlefield:onEventFinishBattlefield(player, csid, option, npc)
|
|
end
|
|
|
|
-- Override this function if necessary to perform additional steps in battlefield
|
|
-- Initialize.
|
|
function Battlefield:setupBattlefield(battlefield)
|
|
end
|
|
|
|
function Battlefield:onBattlefieldInitialize(battlefield)
|
|
local hasMultipleAreas = not self.area
|
|
battlefield:addGroups(self.groups, hasMultipleAreas)
|
|
|
|
-- NOTE: Experimental battlefields are at most partially implemented. Increase mob levels
|
|
-- for temporary tuning.
|
|
if self.experimental then
|
|
local battlefieldMobs = battlefield:getMobs(true, true)
|
|
for _, mobObj in ipairs(battlefieldMobs) do
|
|
mobObj:setMobLevel(math.min(mobObj:getMainLvl() * 2, 255))
|
|
end
|
|
end
|
|
|
|
for mobId, path in pairs(self.paths) do
|
|
local mEntity = GetMobByID(mobId)
|
|
if mEntity then
|
|
mEntity:pathThrough(path, xi.path.flag.PATROL)
|
|
end
|
|
end
|
|
|
|
self:setupBattlefield(battlefield)
|
|
end
|
|
|
|
function Battlefield:onBattlefieldTick(battlefield, tick)
|
|
local status = battlefield:getStatus()
|
|
local cutsceneTimer = battlefield:getLocalVar('cutsceneTimer')
|
|
local isExiting = status == xi.battlefield.status.LOST or status == xi.battlefield.status.WON
|
|
|
|
if isExiting then
|
|
battlefield:setLocalVar('cutsceneTimer', cutsceneTimer - 1)
|
|
end
|
|
|
|
-- Check that players haven't all died or that their dead time is over.
|
|
local players = battlefield:getPlayers()
|
|
self:handleWipe(battlefield, players)
|
|
|
|
local hasTimeRemaining = xi.battlefield.SendTimePrompts(battlefield, players)
|
|
|
|
if not hasTimeRemaining then
|
|
for _, player in pairs(players) do
|
|
player:messageSpecial(zones[player:getZoneID()].text.TIME_IN_THE_BATTLEFIELD_IS_UP)
|
|
end
|
|
|
|
battlefield:setStatus(xi.battlefield.status.LOST)
|
|
isExiting = true
|
|
end
|
|
|
|
if isExiting then
|
|
-- Remaining in battlefield for an extended time
|
|
if cutsceneTimer > 0 then
|
|
return
|
|
end
|
|
|
|
if status == xi.battlefield.status.LOST then
|
|
for _, player in pairs(players) do
|
|
player:messageSpecial(zones[player:getZoneID()].text.PARTY_MEMBERS_HAVE_FALLEN)
|
|
end
|
|
end
|
|
|
|
battlefield:cleanup(true)
|
|
end
|
|
end
|
|
|
|
function Battlefield:onBattlefieldRegister(player, battlefield)
|
|
end
|
|
|
|
function Battlefield:onBattlefieldStatusChange(battlefield, status)
|
|
-- Remove battlefield effect for players in alliance not inside battlefield once the battlefield gets locked. Do this only once.
|
|
if
|
|
status == xi.battlefield.status.LOCKED and
|
|
battlefield:getLocalVar('statusRemoval') == 0
|
|
then
|
|
battlefield:setLocalVar('statusRemoval', 1)
|
|
local players = battlefield:getPlayers()
|
|
|
|
for _, player in pairs(players) do
|
|
local alliance = player:getAlliance()
|
|
|
|
for _, member in pairs(alliance) do
|
|
if
|
|
member:hasStatusEffect(xi.effect.BATTLEFIELD) and
|
|
not member:getBattlefield()
|
|
then
|
|
member:delStatusEffect(xi.effect.BATTLEFIELD)
|
|
end
|
|
end
|
|
end
|
|
end
|
|
end
|
|
|
|
---@diagnostic disable-next-line: duplicate-set-field
|
|
function Battlefield:battlefieldEntry(player, battlefield)
|
|
end
|
|
|
|
---@diagnostic disable-next-line: duplicate-set-field
|
|
function Battlefield:onBattlefieldEnter(player, battlefield)
|
|
player:setLocalVar('battlefieldID', battlefield:getID())
|
|
|
|
local initiatorId, _ = battlefield:getInitiator()
|
|
|
|
if
|
|
#self.requiredKeyItems > 0 and
|
|
(not self.requiredKeyItems.onlyInitiator or player:getID() == initiatorId)
|
|
then
|
|
|
|
local items = {}
|
|
|
|
for _, item in ipairs(self.requiredKeyItems) do
|
|
-- If this item is a table then that means we only need one of them so delete the first one we find
|
|
if type(item) == 'table' then
|
|
for _, subitem in ipairs(item) do
|
|
if player:hasKeyItem(subitem) then
|
|
if not self.requiredKeyItems.keep then
|
|
player:delKeyItem(subitem)
|
|
end
|
|
|
|
table.insert(items, subitem)
|
|
end
|
|
end
|
|
else
|
|
if not self.requiredKeyItems.keep then
|
|
player:delKeyItem(item)
|
|
end
|
|
|
|
table.insert(items, item)
|
|
end
|
|
end
|
|
|
|
if type(self.requiredKeyItems.message) == 'table' then
|
|
player:messageSpecial(self.requiredKeyItems.message[1], unpack(self.requiredKeyItems.message[2]))
|
|
elseif self.requiredKeyItems.message then
|
|
player:messageSpecial(self.requiredKeyItems.message, unpack(items))
|
|
end
|
|
|
|
if not self.requiredKeyItems.keep and self.requiredKeyItems.deleteMessage then
|
|
for _, keyItemId in ipairs(items) do
|
|
player:messageSpecial(self.requiredKeyItems.deleteMessage, keyItemId)
|
|
end
|
|
end
|
|
end
|
|
|
|
if
|
|
player:getID() == initiatorId and
|
|
self.requiredItems.wearMessage and
|
|
player:hasItem(self.requiredItems[1])
|
|
then
|
|
local itemId = self.requiredItems[1]
|
|
local uses = player:incrementItemWear(itemId)
|
|
local totalUses = xi.battlefield.itemUses[itemId] or 1 -- Gets number of item uses. (Tests = 3; Else = 1)
|
|
|
|
if totalUses > 1 then
|
|
local remaining = totalUses - uses
|
|
player:messageSpecial(self.requiredItems.wearMessage, itemId, remaining + 1, remaining)
|
|
else
|
|
player:messageSpecial(self.requiredItems.wearMessage, 0, 0, 0, itemId)
|
|
end
|
|
end
|
|
|
|
-- Handle mob initial spell casts (blaze spikes, protect, etc)
|
|
if player:getID() == initiatorId then
|
|
local mobs = battlefield:getMobs(true, true)
|
|
for _, mob in pairs(mobs) do
|
|
if mob:isSpawned() then
|
|
-- wait until initiator is out of cutscene
|
|
mob:addListener('ROAM_TICK', 'FIRST_CAST', function(mobArg)
|
|
local firstPlayer = GetPlayerByID(initiatorId)
|
|
if firstPlayer and not firstPlayer:isInEvent() then
|
|
mobArg:castSpell()
|
|
|
|
mobArg:removeListener('FIRST_CAST')
|
|
end
|
|
end)
|
|
end
|
|
end
|
|
end
|
|
|
|
local ID = zones[self.zoneId]
|
|
player:messageSpecial(ID.text.ENTERING_THE_BATTLEFIELD_FOR, 0, self.index)
|
|
|
|
if self.maxPlayers > 6 then
|
|
-- NOTE: Update tooling does not allow for duplicate messages to be stored in IDs.lua, even if the ID is different.
|
|
-- Apollyon does not differentiate between party and alliance for message, so if the entry is nil, use party message.
|
|
local messageId = ID.text.MEMBERS_OF_YOUR_ALLIANCE and ID.text.MEMBERS_OF_YOUR_ALLIANCE or ID.text.MEMBERS_OF_YOUR_PARTY
|
|
|
|
player:messageSpecial(messageId, 0, 0, 0, self.maxPlayers)
|
|
else
|
|
player:messageSpecial(ID.text.MEMBERS_OF_YOUR_PARTY, 0, 0, 0, self.maxPlayers)
|
|
end
|
|
|
|
player:messageSpecial(ID.text.TIME_LIMIT_FOR_THIS_BATTLE_IS, 0, 0, 0, math.floor(self.timeLimit / 60))
|
|
|
|
if self.experimental then
|
|
player:printToPlayer('This battlefield has been marked as experimental. Enemy levels have increased!', xi.msg.channel.NS_SHOUT)
|
|
end
|
|
|
|
self:battlefieldEntry(player, battlefield)
|
|
end
|
|
|
|
---@diagnostic disable-next-line: duplicate-set-field
|
|
function Battlefield:onBattlefieldDestroy(battlefield)
|
|
end
|
|
|
|
---@diagnostic disable-next-line: duplicate-set-field
|
|
function Battlefield:onBattlefieldLeave(player, battlefield, leavecode)
|
|
if leavecode == xi.battlefield.leaveCode.WON then
|
|
self:onBattlefieldWin(player, battlefield)
|
|
elseif leavecode == xi.battlefield.leaveCode.LOST then
|
|
self:onBattlefieldLoss(player, battlefield)
|
|
end
|
|
end
|
|
|
|
function Battlefield:onBattlefieldWin(player, battlefield)
|
|
local _, clearTime, partySize = battlefield:getRecord()
|
|
|
|
player:setLocalVar('battlefieldWin', battlefield:getID())
|
|
player:startEvent(32001, battlefield:getArea(), clearTime, partySize, battlefield:getTimeInside(), player:getZoneID(), self.index, 0)
|
|
end
|
|
|
|
function Battlefield:onBattlefieldLoss(player, battlefield)
|
|
player:startEvent(32002, self.lossEventParams)
|
|
end
|
|
|
|
function Battlefield:onBattlefieldKick(player)
|
|
player:startEvent(32002, self.lossEventParams)
|
|
end
|
|
|
|
function Battlefield:handleWipe(battlefield, players)
|
|
local wipeTime = battlefield:getWipeTime()
|
|
local elapsed = battlefield:getTimeInside()
|
|
|
|
-- If party has not yet wiped.
|
|
if wipeTime <= 0 then
|
|
-- Return if any players are still alive
|
|
for _, player in pairs(players) do
|
|
if player:isAlive() then
|
|
return
|
|
end
|
|
end
|
|
|
|
self:onBattlefieldWipe(battlefield, players)
|
|
|
|
-- Party has already wiped.
|
|
else
|
|
if (elapsed - wipeTime) > utils.minutes(3) then
|
|
battlefield:setStatus(xi.battlefield.status.LOST)
|
|
else
|
|
-- Cancel the wipe timer if any players are alive once again
|
|
for _, player in pairs(players) do
|
|
if player:isAlive() then
|
|
battlefield:setWipeTime(0)
|
|
break
|
|
end
|
|
end
|
|
end
|
|
end
|
|
end
|
|
|
|
function Battlefield:onBattlefieldWipe(battlefield, players)
|
|
-- Party has wiped. Save and send time remaining before being booted.
|
|
if self.hasWipeGrace then
|
|
for _, player in pairs(players) do
|
|
player:messageSpecial(zones[player:getZoneID()].text.THE_PARTY_WILL_BE_REMOVED, 0, 0, 0, 3)
|
|
end
|
|
|
|
battlefield:setWipeTime(battlefield:getTimeInside())
|
|
else
|
|
battlefield:setStatus(xi.battlefield.status.LOST)
|
|
end
|
|
end
|
|
|
|
function Battlefield:addEssentialMobs(mobNames)
|
|
table.insert(self.groups, {
|
|
mobs = mobNames,
|
|
superlink = true,
|
|
isParty = true,
|
|
allDeath = utils.bind(self.handleAllMonstersDefeated, self),
|
|
})
|
|
end
|
|
|
|
function Battlefield:handleAllMonstersDefeated(battlefield, mob)
|
|
local crateId = battlefield:getArmouryCrate()
|
|
|
|
-- NOTE: Default to core returning us a value, but if that fails, use the fallback
|
|
-- definition if available. This should only be used where mobIds table has to be
|
|
-- used instead of mobs!
|
|
if crateId == 0 and self.armouryCrates then
|
|
crateId = self.armouryCrates[battlefield:getArea()]
|
|
end
|
|
|
|
if crateId ~= 0 then
|
|
local crate = GetNPCByID(crateId)
|
|
|
|
if crate then
|
|
battlefield:insertEntity(crate:getTargID(), false, true)
|
|
npcUtil.showCrate(crate)
|
|
crate:addListener('ON_TRIGGER', 'TRIGGER_CRATE', utils.bind(self.handleOpenArmouryCrate, self))
|
|
end
|
|
else
|
|
battlefield:setLocalVar('cutsceneTimer', self.delayToExit)
|
|
battlefield:setStatus(xi.battlefield.status.WON)
|
|
end
|
|
end
|
|
|
|
function Battlefield:handleOpenArmouryCrate(player, npc)
|
|
npcUtil.openCrate(npc, function()
|
|
local battlefield = player:getBattlefield()
|
|
self:handleLootRolls(battlefield, self.loot, npc, player:getMod(xi.mod.MOGHANCEMENT_GIL_BONUS_P))
|
|
battlefield:setStatus(xi.battlefield.status.WON)
|
|
battlefield:setLocalVar('cutsceneTimer', self.delayToExit)
|
|
|
|
return true
|
|
end)
|
|
end
|
|
|
|
function Battlefield:handleLootRolls(battlefield, lootTable, npc, gilBonusMod)
|
|
local players = battlefield:getPlayers()
|
|
local firstPlayer = players[1]
|
|
|
|
local selectedLoot = utils.selectFromLootGroups(firstPlayer, lootTable)
|
|
for _, entry in ipairs(selectedLoot) do
|
|
if entry.itemId ~= xi.item.GIL then
|
|
firstPlayer:addTreasure(entry.itemId, npc)
|
|
else
|
|
local gilBonusPct = (100 + gilBonusMod) / 100
|
|
local gilPerPlayer = entry.amount * gilBonusPct / #players
|
|
|
|
for _, player in ipairs(players) do
|
|
npcUtil.giveCurrency(player, 'gil', gilPerPlayer, true)
|
|
end
|
|
end
|
|
end
|
|
end
|
|
|
|
function xi.battlefield.getBattlefieldOptions(player, npc, trade)
|
|
local result = 0
|
|
local contents = xi.battlefield.contentsByZone[player:getZoneID()]
|
|
|
|
if contents == nil then
|
|
return result
|
|
end
|
|
|
|
-- TODO: if the battlefield is at capacity, the 32000 event should not start, but instead
|
|
-- a separate message is displayed on retail
|
|
|
|
for _, content in ipairs(contents) do
|
|
if
|
|
content:checkRequirements(player, npc, true, trade) and
|
|
not player:battlefieldAtCapacity(content.battlefieldId) and
|
|
(xi.settings.map.BCNM_ENABLE_EXPERIMENTAL or not content.experimental)
|
|
then
|
|
result = utils.mask.setBit(result, content.index, true)
|
|
end
|
|
end
|
|
|
|
return result
|
|
end
|
|
|
|
function xi.battlefield.rejectLevelSyncedParty(player, npc)
|
|
for _, member in pairs(player:getAlliance()) do
|
|
if member:hasStatusEffect(xi.effect.LEVEL_SYNC) then
|
|
local zoneId = player:getZoneID()
|
|
local ID = zones[zoneId]
|
|
|
|
-- Your party is unable to participate because certain members' levels are restricted
|
|
player:messageText(npc, ID.text.MEMBERS_LEVELS_ARE_RESTRICTED, false)
|
|
|
|
return true
|
|
end
|
|
end
|
|
|
|
return false
|
|
end
|
|
|
|
BattlefieldMission = setmetatable({ }, { __index = Battlefield })
|
|
BattlefieldMission.__index = BattlefieldMission
|
|
|
|
---@diagnostic disable-next-line: duplicate-set-field
|
|
BattlefieldMission.__eq = function(m1, m2)
|
|
return m1.name == m2.name
|
|
end
|
|
|
|
-- Creates a new Limbus Battlefield interaction
|
|
-- Data takes the additional following keys:
|
|
-- - missionArea: The mission area this battlefield is associated with (optional)
|
|
-- - mission: The mission this battlefield is associated with (optional)
|
|
-- - missionStatusArea: The mission area to retrieve the mission status from. Will default to using the player's nation (optional)
|
|
-- - missionStatus: The optional extra status information xi.mission.status (optional)
|
|
-- - requiredMissionStatus: The required mission status to enter
|
|
-- - skipMissionStatus: The required mission status to skip the cutscene. Defaults to the required mission status.
|
|
-- - canLoseExp: Determines if a character loses experience points upon death while inside the battlefield. Defaults to false. (optional)
|
|
---@diagnostic disable-next-line: duplicate-set-field
|
|
function BattlefieldMission:new(data)
|
|
local obj = Battlefield:new(data)
|
|
|
|
setmetatable(obj, self)
|
|
obj.isMission = true
|
|
obj.missionArea = data.missionArea
|
|
obj.mission = data.mission
|
|
obj.missionStatusArea = data.missionStatusArea
|
|
obj.missionStatus = data.missionStatus
|
|
obj.requiredMissionStatus = data.requiredMissionStatus
|
|
obj.skipMissionStatus = data.skipMissionStatus or data.requiredMissionStatus
|
|
obj.requiredVar = data.requiredVar
|
|
obj.requiredValue = data.requiredValue
|
|
obj.canLoseExp = data.canLoseExp or false
|
|
|
|
return obj
|
|
end
|
|
|
|
---@diagnostic disable-next-line: duplicate-set-field
|
|
function BattlefieldMission:checkRequirements(player, npc, isRegistrant, trade)
|
|
if not Battlefield.checkRequirements(self, player, npc, isRegistrant, trade) then
|
|
return false
|
|
end
|
|
|
|
local missionArea = self.missionArea or player:getNation()
|
|
local current = player:getCurrentMission(missionArea)
|
|
|
|
if self.requiredMissionStatus ~= nil then
|
|
local missionStatusArea = self.missionStatusArea or player:getNation()
|
|
local status = player:getMissionStatus(missionStatusArea, self.missionStatus)
|
|
|
|
return (not isRegistrant and (player:hasCompletedMission(missionArea, self.mission) or
|
|
(current == self.mission and status >= self.requiredMissionStatus))) or
|
|
(current == self.mission and status == self.requiredMissionStatus)
|
|
elseif self.requiredVar ~= nil then
|
|
-- COP Missions use a charVar to track since missionStatus is used to define menu settings.
|
|
-- When converting these, allow for the same structure of status, but with defined requiredVar (str)
|
|
-- and requiredValue (uint requirement). These are currently mutually exclusive, and treated
|
|
-- as such.
|
|
|
|
local status = player:getCharVar(self.requiredVar)
|
|
|
|
return (not isRegistrant and (player:hasCompletedMission(missionArea, self.mission) or
|
|
(current == self.mission and status >= self.requiredValue))) or
|
|
(current == self.mission and status == self.requiredValue)
|
|
else
|
|
return (not isRegistrant and player:hasCompletedMission(missionArea, self.mission)) or
|
|
current == self.mission
|
|
end
|
|
end
|
|
|
|
---@diagnostic disable-next-line: duplicate-set-field
|
|
function BattlefieldMission:checkSkipCutscene(player)
|
|
local missionArea = self.missionArea or player:getNation()
|
|
local current = player:getCurrentMission(missionArea)
|
|
local missionStatusArea = self.missionStatusArea or player:getNation()
|
|
local status = player:getMissionStatus(missionStatusArea, self.missionStatus)
|
|
|
|
return player:hasCompletedMission(missionArea, self.mission) or
|
|
(self.requiredMissionStatus and current == self.mission and status > self.skipMissionStatus) or
|
|
(self.requiredValue and current == self.mission and player:getCharVar(self.requiredVar) > self.requiredValue)
|
|
end
|
|
|
|
---@diagnostic disable-next-line: duplicate-set-field
|
|
function BattlefieldMission:onBattlefieldWin(player, battlefield)
|
|
local missionArea = self.missionArea or player:getNation()
|
|
local current = player:getCurrentMission(missionArea)
|
|
local missionStatusArea = self.missionStatusArea or player:getNation()
|
|
|
|
if current == self.mission then
|
|
player:setLocalVar('battlefieldWin', battlefield:getID())
|
|
end
|
|
|
|
local _, clearTime, partySize = battlefield:getRecord()
|
|
local canSkipCS = (player:hasCompletedMission(missionArea, self.mission) or
|
|
(self.requiredMissionStatus and current == self.mission and player:getMissionStatus(missionStatusArea, self.missionStatus) > self.skipMissionStatus) or
|
|
(self.requiredValue and current == self.mission and player:getCharVar(self.requiredVar) > self.requiredValue)) and 1 or 0
|
|
|
|
player:startEvent(32001, battlefield:getArea(), clearTime, partySize, battlefield:getTimeInside(), player:getZoneID(), self.index, canSkipCS)
|
|
end
|
|
|
|
---@diagnostic disable-next-line: duplicate-set-field
|
|
function BattlefieldMission:onEventFinishWin(player, csid, option, npc)
|
|
if self.title then
|
|
player:addTitle(self.title)
|
|
end
|
|
|
|
-- Only grant mission XP once per JP midnight
|
|
if self.grantXP and self:getVar(player, 'XP') <= GetSystemTime() then
|
|
self:setVar(player, 'XP', JstMidnight())
|
|
player:addExp(self.grantXP)
|
|
end
|
|
end
|
|
|
|
BattlefieldQuest = setmetatable({ }, { __index = Battlefield })
|
|
BattlefieldQuest.__index = BattlefieldQuest
|
|
|
|
---@diagnostic disable-next-line: duplicate-set-field
|
|
BattlefieldQuest.__eq = function(m1, m2)
|
|
return m1.name == m2.name
|
|
end
|
|
|
|
BattlefieldQuest.isQuest = true
|
|
|
|
-- Creates a new Limbus Battlefield interaction
|
|
-- Data takes the additional following keys:
|
|
-- - questArea: The quest area this battlefield is associated with (optional)
|
|
-- - quest: The quest this battlefield is associated with (optional)
|
|
-- - canLoseExp: Determines if a character loses experience points upon death while inside the battlefield. Defaults to false. (optional)
|
|
---@diagnostic disable-next-line: duplicate-set-field
|
|
function BattlefieldQuest:new(data)
|
|
local obj = Battlefield:new(data)
|
|
setmetatable(obj, self)
|
|
|
|
obj.questArea = data.questArea
|
|
obj.quest = data.quest
|
|
obj.canLoseExp = data.canLoseExp or false
|
|
obj.requiredVar = data.requiredVar
|
|
obj.requiredValue = data.requiredValue
|
|
|
|
return obj
|
|
end
|
|
|
|
---@diagnostic disable-next-line: duplicate-set-field
|
|
function BattlefieldQuest:checkRequirements(player, npc, isRegistrant, trade)
|
|
if not Battlefield.checkRequirements(self, player, npc, isRegistrant, trade) then
|
|
return false
|
|
end
|
|
|
|
if self.requiredVar ~= nil then
|
|
-- COP Missions use a charVar to track since missionStatus is used to define menu settings.
|
|
-- When converting these, allow for the same structure of status, but with defined requiredVar (str)
|
|
-- and requiredValue (uint requirement). These are currently mutually exclusive, and treated
|
|
-- as such.
|
|
|
|
local status = player:getCharVar(self.requiredVar)
|
|
local hasAccepted = player:getQuestStatus(self.questArea, self.quest) == xi.questStatus.QUEST_ACCEPTED
|
|
|
|
return (not isRegistrant and (player:hasCompletedQuest(self.questArea, self.quest) or
|
|
(hasAccepted and status >= self.requiredValue))) or
|
|
(hasAccepted and status == self.requiredValue)
|
|
else
|
|
return player:getQuestStatus(self.questArea, self.quest) >= xi.questStatus.QUEST_ACCEPTED
|
|
end
|
|
end
|
|
|
|
---@diagnostic disable-next-line: duplicate-set-field
|
|
function BattlefieldQuest:checkSkipCutscene(player)
|
|
return player:getQuestStatus(self.questArea, self.quest) == xi.questStatus.QUEST_COMPLETED
|
|
end
|
|
|
|
---@diagnostic disable-next-line: duplicate-set-field
|
|
function BattlefieldQuest:onBattlefieldWin(player, battlefield)
|
|
local status = player:getQuestStatus(self.questArea, self.quest)
|
|
|
|
if status == xi.questStatus.QUEST_ACCEPTED then
|
|
player:setLocalVar('battlefieldWin', battlefield:getID())
|
|
end
|
|
|
|
local _, clearTime, partySize = battlefield:getRecord()
|
|
local canSkipCS = status ~= xi.questStatus.QUEST_ACCEPTED and 1 or 0
|
|
|
|
player:startEvent(32001, battlefield:getArea(), clearTime, partySize, battlefield:getTimeInside(), player:getZoneID(), self.index, canSkipCS)
|
|
end
|
|
|
|
-- returns false if out of time
|
|
function xi.battlefield.SendTimePrompts(battlefield, players)
|
|
local remainingTime = battlefield:getRemainingTime()
|
|
local lastTimeUpdate = battlefield:getLastTimeUpdate()
|
|
local timeLimit = battlefield:getTimeLimit()
|
|
local message = 0
|
|
|
|
players = players or battlefield:getPlayers()
|
|
|
|
if remainingTime < 600 and lastTimeUpdate > 600 and timeLimit > 600 then
|
|
message = 600
|
|
elseif remainingTime < 300 and lastTimeUpdate > 300 and timeLimit > 300 then
|
|
message = 300
|
|
elseif remainingTime < 60 and lastTimeUpdate > 60 and timeLimit > 60 then
|
|
message = 60
|
|
elseif remainingTime < 30 and lastTimeUpdate > 30 and timeLimit > 30 then
|
|
message = 30
|
|
elseif remainingTime < 10 and lastTimeUpdate > 10 and timeLimit > 10 then
|
|
message = 10
|
|
end
|
|
|
|
if message ~= 0 then
|
|
for i, player in pairs(players) do
|
|
player:messageBasic(xi.msg.basic.TIME_LEFT, message)
|
|
end
|
|
|
|
battlefield:setLastTimeUpdate(message)
|
|
end
|
|
|
|
return remainingTime > 0
|
|
end
|