From d209b953f084cc40211db4c68589e222d943a7f5 Mon Sep 17 00:00:00 2001 From: Critical Date: Mon, 25 May 2026 22:11:21 -0600 Subject: [PATCH] [lua, cpp] Assault Framework Rework Co-authored-by: Abdiah <62350957+dallano@users.noreply.github.com> --- scripts/enum/assault.lua | 135 +++-- scripts/globals/appraisal.lua | 1 - scripts/globals/assault.lua | 200 ------- scripts/globals/assault/container.lua | 496 ++++++++++++++++++ scripts/globals/assault/data.lua | 241 +++++++++ scripts/globals/assault/npc_handler.lua | 290 ++++++++++ scripts/globals/instance.lua | 52 +- .../npcs/Bhoy_Yhupplo.lua | 76 +-- .../zones/Aht_Urhgan_Whitegate/npcs/Famad.lua | 76 +-- .../Aht_Urhgan_Whitegate/npcs/Isdebaaq.lua | 76 +-- .../Aht_Urhgan_Whitegate/npcs/Lageegee.lua | 76 +-- .../Aht_Urhgan_Whitegate/npcs/Rytaal.lua | 130 +---- .../Aht_Urhgan_Whitegate/npcs/Yahsra.lua | 76 +-- src/map/instance.cpp | 16 +- src/map/lua/luautils.cpp | 31 +- src/map/utils/instanceutils.cpp | 20 +- src/map/utils/mobutils.cpp | 1 + src/map/utils/zoneutils.cpp | 4 +- tools/ci/sanity_checks/lua.sh | 1 + tools/ci/sanity_checks/lua_binding_usage.py | 5 + 20 files changed, 1203 insertions(+), 800 deletions(-) delete mode 100644 scripts/globals/assault.lua create mode 100644 scripts/globals/assault/container.lua create mode 100644 scripts/globals/assault/data.lua create mode 100644 scripts/globals/assault/npc_handler.lua diff --git a/scripts/enum/assault.lua b/scripts/enum/assault.lua index 60488da1f58..c60bdc42a03 100644 --- a/scripts/enum/assault.lua +++ b/scripts/enum/assault.lua @@ -43,7 +43,7 @@ xi.assault.mission = APKALLU_BREEDING = 26, WAMOURA_FARM_RAID = 27, EGG_CONSERVATION = 28, - OPERATION__BLACK_PEARL = 29, + OPERATION_BLACK_PEARL = 29, BETTER_THAN_ONE = 30, SEAGULL_GROUNDED = 31, REQUIEM = 32, @@ -52,7 +52,7 @@ xi.assault.mission = BUILDING_BRIDGES = 35, STOP_THE_BLOODSHED = 36, DEFUSE_THE_THREAT = 37, - OPERATION__SNAKE_EYES = 38, + OPERATION_SNAKE_EYES = 38, WAKE_THE_PUPPET = 39, THE_PRICE_IS_RIGHT = 40, GOLDEN_SALVAGE = 41, @@ -69,59 +69,82 @@ xi.assault.mission = NYZUL_ISLE_UNCHARTED_AREA_SURVEY = 52, } ----@enum xi.missionInfo -xi.assault.missionInfo = +---@enum xi.assault.mercenaryRank +xi.assault.mercenaryRank = { - [xi.assault.mission.LEUJAOAM_CLEANSING] = { suggestedLevel = 50, minimumPoints = 1000 }, - [xi.assault.mission.ORICHALCUM_SURVEY] = { suggestedLevel = 50, minimumPoints = 1200 }, - [xi.assault.mission.ESCORT_PROFESSOR_CHANOIX] = { suggestedLevel = 60, minimumPoints = 1100 }, - [xi.assault.mission.SHANARHA_GRASS_CONSERVATION] = { suggestedLevel = 50, minimumPoints = 1333 }, - [xi.assault.mission.COUNTING_SHEEP] = { suggestedLevel = 60, minimumPoints = 1166 }, - [xi.assault.mission.SUPPLIES_RECOVERY] = { suggestedLevel = 70, minimumPoints = 1000 }, - [xi.assault.mission.AZURE_EXPERIMENTS] = { suggestedLevel = 70, minimumPoints = 1000 }, - [xi.assault.mission.IMPERIAL_CODE] = { suggestedLevel = 70, minimumPoints = 1333 }, - [xi.assault.mission.RED_VERSUS_BLUE] = { suggestedLevel = 70, minimumPoints = 1666 }, - [xi.assault.mission.BLOODY_RONDO] = { suggestedLevel = 70, minimumPoints = 1500 }, - [xi.assault.mission.IMPERIAL_AGENT_RESCUE] = { suggestedLevel = 60, minimumPoints = 1100 }, - [xi.assault.mission.PREEMPTIVE_STRIKE] = { suggestedLevel = 60, minimumPoints = 1000 }, - [xi.assault.mission.SAGELORD_ELIMINATION] = { suggestedLevel = 70, minimumPoints = 1200 }, - [xi.assault.mission.BREAKING_MORALE] = { suggestedLevel = 60, minimumPoints = 1333 }, - [xi.assault.mission.THE_DOUBLE_AGENT] = { suggestedLevel = 70, minimumPoints = 1200 }, - [xi.assault.mission.IMPERIAL_TREASURE_RETRIEVAL] = { suggestedLevel = 50, minimumPoints = 1200 }, - [xi.assault.mission.BLITZKRIEG] = { suggestedLevel = 70, minimumPoints = 1533 }, - [xi.assault.mission.MARIDS_IN_THE_MIST] = { suggestedLevel = 70, minimumPoints = 1333 }, - [xi.assault.mission.AZURE_EXPERIMENTS] = { suggestedLevel = 70, minimumPoints = 1000 }, - [xi.assault.mission.THE_SUSANOO_SHUFFLE] = { suggestedLevel = 70, minimumPoints = 1500 }, - [xi.assault.mission.EXCAVATION_DUTY] = { suggestedLevel = 50, minimumPoints = 1100 }, - [xi.assault.mission.LEBROS_SUPPLIES] = { suggestedLevel = 60, minimumPoints = 1200 }, - [xi.assault.mission.TROLL_FUGITIVES] = { suggestedLevel = 70, minimumPoints = 1000 }, - [xi.assault.mission.EVADE_AND_ESCAPE] = { suggestedLevel = 70, minimumPoints = 1000 }, - [xi.assault.mission.SIEGEMASTER_ASSASSINATION] = { suggestedLevel = 70, minimumPoints = 1100 }, - [xi.assault.mission.APKALLU_BREEDING] = { suggestedLevel = 60, minimumPoints = 1300 }, - [xi.assault.mission.WAMOURA_FARM_RAID] = { suggestedLevel = 70, minimumPoints = 1166 }, - [xi.assault.mission.EGG_CONSERVATION] = { suggestedLevel = 70, minimumPoints = 1333 }, - [xi.assault.mission.OPERATION__BLACK_PEARL] = { suggestedLevel = 70, minimumPoints = 1400 }, - [xi.assault.mission.BETTER_THAN_ONE] = { suggestedLevel = 70, minimumPoints = 1500 }, - [xi.assault.mission.SEAGULL_GROUNDED] = { suggestedLevel = 70, minimumPoints = 1100 }, - [xi.assault.mission.REQUIEM] = { suggestedLevel = 70, minimumPoints = 1000 }, - [xi.assault.mission.SAVING_PRIVATE_RYAAF] = { suggestedLevel = 70, minimumPoints = 1100 }, - [xi.assault.mission.SHOOTING_DOWN_THE_BARON] = { suggestedLevel = 60, minimumPoints = 1100 }, - [xi.assault.mission.BUILDING_BRIDGES] = { suggestedLevel = 70, minimumPoints = 1200 }, - [xi.assault.mission.STOP_THE_BLOODSHED] = { suggestedLevel = 50, minimumPoints = 1000 }, - [xi.assault.mission.DEFUSE_THE_THREAT] = { suggestedLevel = 60, minimumPoints = 1600 }, - [xi.assault.mission.OPERATION__SNAKE_EYES] = { suggestedLevel = 70, minimumPoints = 1333 }, - [xi.assault.mission.WAKE_THE_PUPPET] = { suggestedLevel = 70, minimumPoints = 1200 }, - [xi.assault.mission.THE_PRICE_IS_RIGHT] = { suggestedLevel = 70, minimumPoints = 1500 }, - [xi.assault.mission.GOLDEN_SALVAGE] = { suggestedLevel = 60, minimumPoints = 1100 }, - [xi.assault.mission.LAMIA_NO_13] = { suggestedLevel = 70, minimumPoints = 1200 }, - [xi.assault.mission.EXTERMINATION] = { suggestedLevel = 70, minimumPoints = 1100 }, - [xi.assault.mission.DEMOLITION_DUTY] = { suggestedLevel = 50, minimumPoints = 1000 }, - [xi.assault.mission.SEARAT_SALVATION] = { suggestedLevel = 60, minimumPoints = 1166 }, - [xi.assault.mission.APKALLU_SEIZURE] = { suggestedLevel = 70, minimumPoints = 1000 }, - [xi.assault.mission.LOST_AND_FOUND] = { suggestedLevel = 60, minimumPoints = 1000 }, - [xi.assault.mission.DESERTER] = { suggestedLevel = 70, minimumPoints = 1000 }, - [xi.assault.mission.DESPERATELY_SEEKING_CEPHALOPODS] = { suggestedLevel = 70, minimumPoints = 1000 }, - [xi.assault.mission.BELLEROPHONS_BLISS] = { suggestedLevel = 70, minimumPoints = 1500 }, - [xi.assault.mission.NYZUL_ISLE_INVESTIGATION] = { suggestedLevel = 75, minimumPoints = nil }, - [xi.assault.mission.NYZUL_ISLE_UNCHARTED_AREA_SURVEY] = { suggestedLevel = 99, minimumPoints = nil }, + PRIVATE_SECOND_CLASS = 1, + PRIVATE_FIRST_CLASS = 2, + SUPERIOR_PRIVATE = 3, + LANCE_CORPORAL = 4, + CORPORAL = 5, + SERGEANT = 6, + SERGEANT_MAJOR = 7, + CHIEF_SERGEANT = 8, + SECOND_LIEUTENANT = 9, + FIRST_LIEUTENANT = 10, + CAPTAIN = 11, +} + +---@enum xi.assault.instance +xi.assault.instance = +{ + -- Ilrusi Atoll + GOLDEN_SALVAGE = 5500, + LAMIA_NO_13 = 5501, + EXTERMINATION = 5502, + DEMOLITION_DUTY = 5503, + SEARAT_SALVATION = 5504, + APKALLU_SEIZURE = 5505, + LOST_AND_FOUND = 5506, + DESERTER = 5507, + DESPERATELY_SEEKING_CEPHALOPODS = 5508, + BELLEROPHONS_BLISS = 5509, + + -- Periqia + SEAGULL_GROUNDED = 5601, + REQUIEM = 5602, + SAVING_PRIVATE_RYAAF = 5603, + SHOOTING_DOWN_THE_BARON = 5604, + BUILDING_BRIDGES = 5605, + STOP_THE_BLOODSHED = 5606, + DEFUSE_THE_THREAT = 5607, + OPERATION_SNAKE_EYES = 5608, + WAKE_THE_PUPPET = 5609, + THE_PRICE_IS_RIGHT = 5610, + + -- Lebros Cavern + EXCAVATION_DUTY = 6300, + LEBROS_SUPPLIES = 6301, + TROLL_FUGITIVES = 6302, + EVADE_AND_ESCAPE = 6303, + SIEGEMASTER_ASSASSINATION = 6304, + APKALLU_BREEDING = 6305, + WAMOURA_FARM_RAID = 6306, + EGG_CONSERVATION = 6307, + OPERATION_BLACK_PEARL = 6308, + BETTER_THAN_ONE = 6309, + + -- Mamool Ja Training Grounds + IMPERIAL_AGENT_RESCUE = 6600, + PREEMPTIVE_STRIKE = 6601, + SAGELORD_ELIMINATION = 6602, + BREAKING_MORALE = 6603, + THE_DOUBLE_AGENT = 6604, + IMPERIAL_TREASURE_RETRIEVAL = 6605, + BLITZKRIEG = 6606, + MARIDS_IN_THE_MIST = 6607, + AZURE_AILMENTS = 6608, + THE_SUSANOO_SHUFFLE = 6609, + + -- Leujaoam Sanctum + LEUJAOAM_CLEANSING = 6900, + ORICHALCUM_SURVEY = 6901, + ESCORT_PROFESSOR_CHANOIX = 6902, + SHANARHA_GRASS_CONSERVATION = 6903, + COUNTING_SHEEP = 6904, + SUPPLIES_RECOVERY = 6905, + AZURE_EXPERIMENTS = 6906, + IMPERIAL_CODE = 6907, + RED_VERSUS_BLUE = 6908, + BLOODY_RONDO = 6909, } diff --git a/scripts/globals/appraisal.lua b/scripts/globals/appraisal.lua index bd4321687fb..0c00f276eda 100644 --- a/scripts/globals/appraisal.lua +++ b/scripts/globals/appraisal.lua @@ -2,7 +2,6 @@ -- Appraisal Utilities -- desc: Common functionality for Appraisals ----------------------------------- -require('scripts/globals/assault') require('scripts/globals/npc_util') ----------------------------------- xi = xi or {} diff --git a/scripts/globals/assault.lua b/scripts/globals/assault.lua deleted file mode 100644 index 62a037db47e..00000000000 --- a/scripts/globals/assault.lua +++ /dev/null @@ -1,200 +0,0 @@ ------------------------------------ --- Assault Utilities --- desc: Common functionality for Assaults ------------------------------------ -require('scripts/globals/besieged') -require('scripts/globals/npc_util') ------------------------------------ -xi = xi or {} -xi.assault = xi.assault or {} - -xi.assault.assaultOrders = -{ - xi.ki.LEUJAOAM_ASSAULT_ORDERS, - xi.ki.MAMOOL_JA_ASSAULT_ORDERS, - xi.ki.LEBROS_ASSAULT_ORDERS, - xi.ki.PERIQIA_ASSAULT_ORDERS, - xi.ki.ILRUSI_ASSAULT_ORDERS, - xi.ki.NYZUL_ISLE_ASSAULT_ORDERS, -} - -xi.assault.getAssaultArea = function(player) - return math.floor((player:getCurrentAssault() - 1) / 10) -end - -xi.assault.hasOrders = function(player) - for _, assaultOrders in pairs(xi.assault.assaultOrders) do - if player:hasKeyItem(assaultOrders) then - return true - end - end - - return false -end - -xi.assault.onAssaultUpdate = function(player, csid, option, npc) - local ID = zones[player:getZoneID()] - - local cap = bit.band(option, 0x03) - if cap == 0 then - cap = 0 - elseif cap == 1 then - cap = 70 - elseif cap == 2 then - cap = 60 - else - cap = 50 - end - - player:setLocalVar('AssaultCap', cap) - - if - player:getGMLevel() == 0 and - player:getPartySize() < xi.settings.main.ASSAULT_MINIMUM - then - player:messageSpecial(ID.text.MEMBER_TOO_FAR - 1, xi.settings.main.ASSAULT_MINIMUM) - player:instanceEntry(npc, 1) - return - elseif player:checkSoloPartyAlliance() == 2 then - player:messageText(player, ID.text.MEMBER_NO_REQS + 1, false) - player:instanceEntry(npc, 1) - return - end -end - -xi.assault.onInstanceCreatedCallback = function(player, instance) - if instance then - instance:setLevelCap(player:getLocalVar('AssaultCap')) - player:setLocalVar('AssaultCap', 0) - player:setCharVar('Assault_Armband', 1) - player:delKeyItem(xi.ki.ASSAULT_ARMBAND) - else - local npc = player:getEventTarget() - player:messageText(player, zones[player:getZoneID()].text.CANNOT_ENTER, false) - player:instanceEntry(npc, 3) - end -end - -xi.assault.afterInstanceRegister = function(player, fireFlies) - local instance = player:getInstance() - local assaultID = player:getCurrentAssault() - local levelCap = instance:getLevelCap() - local ID = zones[player:getZoneID()] - - player:setCharVar('assaultEntered', assaultID) - player:messageSpecial(ID.text.ASSAULT_START_OFFSET + assaultID, assaultID) - player:messageSpecial(ID.text.TIME_TO_COMPLETE, instance:getTimeLimit()) - player:addTempItem(fireFlies) - - if levelCap ~= 0 then - player:addStatusEffect(xi.effect.LEVEL_RESTRICTION, { power = levelCap, origin = player }) - end - - for _, entity in pairs(ID.mob[assaultID].MOBS_START) do - SpawnMob(entity, instance) - end -end - -xi.assault.onInstanceFailure = function(instance) - local chars = instance:getChars() - local mobs = instance:getMobs() - - for _, entity in pairs(mobs) do - local mobID = entity:getID() - DespawnMob(mobID, instance) - end - - for _, entity in pairs(chars) do - entity:messageSpecial(zones[instance:getZone():getID()].text.MISSION_FAILED, 10, 10) - entity:startEvent(102) - end -end - -xi.assault.onInstanceComplete = function(instance, posX, posZ) - local chars = instance:getChars() - local ID = zones[instance:getZone():getID()] - - GetNPCByID(ID.npc.RUNE_OF_RELEASE, instance):setStatus(xi.status.NORMAL) - GetNPCByID(ID.npc.ANCIENT_LOCKBOX, instance):setStatus(xi.status.NORMAL) - - for _, entity in pairs(chars) do - entity:messageSpecial(ID.text.RUNE_UNLOCKED_POS, posX, posZ) - end -end - -xi.assault.instanceOnEventFinish = function(player, csid, zone) - if csid == 102 then - local instance = player:getInstance() - local chars = instance:getChars() - for _, entity in pairs(chars) do - entity:setPos(0, 0, 0, 0, zone) - end - end -end - -xi.assault.runeReleaseFinish = function(player, csid, option, npc) - if csid == 100 and option == 1 then - local instance = player:getInstance() - local chars = instance:getChars() - local zone = player:getZoneID() - local ID = zones[zone] - local playerpoints = math.max((#chars - 3) * 0.1, 0) - local points = 0 - local assaultID = player:getCurrentAssault() - local mobs = instance:getMobs() - local pointsArea = xi.assault.getAssaultArea(player) - - for _, entity in pairs(mobs) do - local mobID = entity:getID() - DespawnMob(mobID, instance) - end - - for _, entity in pairs(chars) do - if entity:getLocalVar('AssaultPointsAwarded') == 0 then - entity:setLocalVar('AssaultPointsAwarded', 1) - - local pointModifier = xi.assault.missionInfo[assaultID].minimumPoints - points = pointModifier - (pointModifier * playerpoints) - if entity:getCharVar('Assault_Armband') == 1 then - points = points * 1.1 - end - - if entity:hasCompletedAssault(assaultID) then - points = math.floor(points) - entity:setVar('AssaultPromotion', entity:getCharVar('AssaultPromotion') + 1) - entity:addAssaultPoint(pointsArea, points) - entity:messageSpecial(ID.text.ASSAULT_POINTS_OBTAINED, points) - else - points = math.floor(points * 1.5) - entity:setVar('AssaultPromotion', entity:getCharVar('AssaultPromotion') + 5) - entity:addAssaultPoint(pointsArea, points) - entity:messageSpecial(ID.text.ASSAULT_POINTS_OBTAINED, points) - end - - entity:setVar('AssaultComplete', 1) - entity:startEvent(102) - end - end - end -end - -xi.assault.adjustMobLevel = function(mob) - local instance = mob:getInstance() - local levelCap = instance:getLevelCap() - local reducedLevel = 0 - local entity = GetMobByID(mob:getID(), instance) - - if levelCap ~= 0 then - if levelCap == 70 then - reducedLevel = 5 - elseif levelCap == 60 then - reducedLevel = 15 - elseif levelCap == 50 then - reducedLevel = 25 - end - - if entity then - entity:setMobLevel(entity:getMainLvl() - reducedLevel) - end - end -end diff --git a/scripts/globals/assault/container.lua b/scripts/globals/assault/container.lua new file mode 100644 index 00000000000..cb9ece1a7f4 --- /dev/null +++ b/scripts/globals/assault/container.lua @@ -0,0 +1,496 @@ +----------------------------------- +-- Assault Instance Functions +----------------------------------- +xi = xi or {} +xi.assault = xi.assault or {} +xi.assault.contents = xi.assault.contents or {} +xi.assault.contentsByZone = xi.assault.contentsByZone or {} +----------------------------------- + +---@class InstanceAssault : TInteractionContainer +---@field id integer +---@field assaultID integer +---@field instanceID integer +---@field zoneID integer +---@field loot table +---@field releasePos table +---@field requiredProgress integer? +---@field afterInstanceRegister function +---@field onInstanceCreated function +---@field onInstanceProgressUpdate function +---@field onInstanceComplete function +---@field onAssaultFail function +---@field onEventUpdate function +---@field onEventFinish function +InstanceAssault = setmetatable({}, { __index = Container }) +InstanceAssault.__index = InstanceAssault + +InstanceAssault.__eq = function(self, other) + return self.id == other.id +end + +function InstanceAssault.getVarPrefix(assaultID) + return string.format('Assault[%d]', assaultID) +end + +-- Creates a new InstanceAssault container with the following params: +-- - assaultID: (required) ID of the assault +-- - instanceID: (required) ID of the global instance +-- - requiredOrders: (required) Key item orders needed to enter the assault +-- - zoneID: (required) ID of the zone +-- - assaultArea: (required) Area used for assault point currency +-- - suggestedLevel: (required) Minimum level to enter; affects points rewarded +-- - entranceParams: (required) Table of zone-in event parameters +-- - instanceID: instanceID of the assault +-- - entryEvent: { csid, ... } args unpacked into player:startEvent() at the Runic Portal +-- - confirmEvent: { csid, option } checked in onEventFinish to confirm zone-in +-- - memberEvent: { csid, option } args for party members joining the instance +-- - runeOfReleasePos: (optional) { x, y, z, rot } position of the Rune of Release NPC on completion +-- - releasePos: (optional) { x, z } grid coordinates for the rune-unlocked messageSpecial (A=0) +-- - ancientBoxPos: (optional) { x, y, z, rot } position of the Ancient Lockbox NPC on completion +-- - requiredProgress: (optional) Progress value at which the instance auto-completes +-- - basePoints: (optional) Base assault points before bonuses and penalties +-- - mobs: (optional) { { baseID = id, offset = n } } — spawns mobs from baseID to baseID+offset +-- - npcs: (optional) Same format as mobs; sets animation to NORMAL on instance creation +-- - wallNPCs: (optional) { npcID, ... } — NPCs set to OPEN_DOOR animation on instance creation +-- - loot: (optional) Ancient Lockbox reward table. Omit to use the zone's Ancient_Lockbox.lua. +-- - loot.appraisalReward = { { { itemId, weight }, ... } } group for unappraised gear +-- - loot.bonusLoot = { { { itemId, weight }, ... }, ... } one or more groups for consumables +-- Use xi.item.NONE with a weight for a chance of no drop within a group. +-- +-- The following functions can be overridden in individual assault files for custom behavior. +-- Call InstanceAssault.methodName(self, ...) within an override to utilize the default behavior of the functions. +-- - function content:afterInstanceRegister(player) +-- - function content:onInstanceCreated(instance) +-- - function content:onInstanceProgressUpdate(instance, progress) +-- - function content:onEventUpdate(player, csid, option, npc) +-- - function content:onEventFinish(player, csid, option, npc) +-- - function content:onLockboxOpen(player, npc) +-- - function content:onInstanceComplete(instance) +-- - function content:onAssaultFail(instance) +---@param data table +function InstanceAssault:new(data) + assert(type(data.assaultID) == 'number', 'InstanceAssault: assaultID (number) is required') + assert(type(data.instanceID) == 'number', 'InstanceAssault: instanceID (number) is required') + assert(type(data.zoneID) == 'number', 'InstanceAssault: zoneID (number) is required') + assert(type(data.suggestedLevel) == 'number', 'InstanceAssault: suggestedLevel (number) is required') + assert(type(data.entranceParams) == 'table', 'InstanceAssault: entranceParams (table) is required') + assert(data.assaultArea ~= nil, 'InstanceAssault: assaultArea is required') + assert(data.requiredOrders ~= nil, 'InstanceAssault: requiredOrders is required') + + local obj = Container:new(InstanceAssault.getVarPrefix(data.assaultID)) + setmetatable(obj, self) + + for key, value in pairs(data) do + obj[key] = value + end + + obj.entranceParams = obj.entranceParams or {} + obj.runeOfReleasePos = obj.runeOfReleasePos or {} + obj.ancientBoxPos = obj.ancientBoxPos or {} + obj.releasePos = obj.releasePos or {} + obj.loot = obj.loot or {} + obj.mobs = obj.mobs or {} + obj.npcs = obj.npcs or {} + obj.wallNPCs = obj.wallNPCs or {} + + return obj +end + +function InstanceAssault:afterInstanceRegister(player) + xi.assault.afterInstanceRegistration(player, self) +end + +function InstanceAssault:onInstanceCreated(instance) + xi.assault.onInstanceSetup(instance, self) +end + +function InstanceAssault:onInstanceProgressUpdate(instance, progress) + if + self.requiredProgress and + progress >= self.requiredProgress and + not instance:completed() + then + instance:complete() + end +end + +function InstanceAssault:onEventUpdate(player, csid, option, npc) +end + +function InstanceAssault:onEventFinish(player, csid, option, npc) +end + +function InstanceAssault:onLockboxOpen(player, npc) + if not self.loot or not self.loot.appraisalReward then + return + end + + xi.assault.assaultChestTrigger(player, npc, self.loot.appraisalReward, self.loot.bonusLoot or {}) +end + +function InstanceAssault:onInstanceComplete(instance) + local pos = self.releasePos + xi.assault.onInstanceComplete(instance, pos.x, pos.z) +end + +function InstanceAssault:onAssaultFail(instance) + xi.assault.onInstanceFailure(instance) +end + +function InstanceAssault:register() + local content = self + + -- Add container to global lookup + xi.assault.contents[content.assaultID] = content + xi.assault.contentsByZone[content.zoneID] = xi.assault.contentsByZone[content.zoneID] or {} + table.insert(xi.assault.contentsByZone[content.zoneID], content) + + -- Create a dynamic instance object + local instanceObject = {} + + -- Registry and entry requirements + instanceObject.registryRequirements = function(player) + return xi.assault.checkRequirements(player, content) and + player:hasKeyItem(xi.ki.ASSAULT_ARMBAND) + end + + instanceObject.entryRequirements = function(player) + return xi.assault.checkRequirements(player, content) + end + + -- Callback functions + instanceObject.afterInstanceRegister = function(player) + content:afterInstanceRegister(player) + end + + instanceObject.onInstanceCreated = function(instance) + content:onInstanceCreated(instance) + end + + instanceObject.onInstanceCreatedCallback = function(player, instance) + xi.assault.onInstanceCreatedCallback(player, instance, content) + end + + instanceObject.onInstanceTimeUpdate = function(instance, elapsed) + xi.instance.updateInstanceTime(instance, elapsed, zones[content.zoneID].text) + end + + instanceObject.onInstanceFailure = function(instance) + content:onAssaultFail(instance) + end + + instanceObject.onInstanceProgressUpdate = function(instance, progress) + content:onInstanceProgressUpdate(instance, progress) + end + + instanceObject.onInstanceComplete = function(instance) + content:onInstanceComplete(instance) + end + + instanceObject.onEventUpdate = function(player, csid, option, npc) + content:onEventUpdate(player, csid, option, npc) + end + + instanceObject.onEventFinish = function(player, csid, option, npc) + content:onEventFinish(player, csid, option, npc) + end + + return instanceObject +end + +----------------------------------- +-- Entry Prerequisites +----------------------------------- + +xi.assault.checkRequirements = function(player, content) + return player:hasKeyItem(content.requiredOrders) and + player:getCurrentAssault() == content.assaultID and + player:getCharVar('assaultEntered') == 0 and + player:getMainLvl() >= content.suggestedLevel +end + +xi.assault.hasOrders = function(player) + for _, assaultOrders in pairs(xi.assault.assaultOrders) do + if player:hasKeyItem(assaultOrders) then + return true + end + end + + return false +end + +----------------------------------- +-- Runic Portal Entry Flow +----------------------------------- + +-- Search first for eligible assaults. If failed search instead for eligible instances +xi.assault.onRunicTrigger = function(player, npc, zone) + local chosenAssault + for _, eligibleAssault in ipairs(xi.assault.contentsByZone[zone] or {}) do + if + xi.assault.checkRequirements(player, eligibleAssault) and + player:hasKeyItem(xi.ki.ASSAULT_ARMBAND) + then + chosenAssault = eligibleAssault + break + end + end + + if chosenAssault == nil then + if not xi.instance.onTrigger(player, npc, zone) then + player:messageSpecial(zones[player:getZoneID()].text.NOTHING_HAPPENS) + return + end + else + xi.instance.clearInstance(player) + player:setLocalVar('INSTANCE_ID', chosenAssault.instanceID) + player:startEvent(unpack(chosenAssault.entranceParams.entryEvent)) + end +end + +xi.assault.onAssaultUpdate = function(player, csid, option, npc) + local levelCap = xi.assault.levelCapByIndex[bit.band(option, 0x03)] + local ID = zones[player:getZoneID()] + + player:setLocalVar('AssaultCap', levelCap) + + if + player:getGMLevel() == 0 and + player:getPartySize() < xi.settings.main.ASSAULT_MINIMUM + then + player:messageSpecial(ID.text.MEMBER_TOO_FAR - 1, xi.settings.main.ASSAULT_MINIMUM) + player:instanceEntry(npc, 1) + return + elseif player:checkSoloPartyAlliance() == 2 then + player:messageText(player, ID.text.MEMBER_NO_REQS + 1, false) + player:instanceEntry(npc, 1) + return + end + + xi.instance.onEventUpdate(player, csid, option, npc) +end + +xi.assault.onEventFinish = function(player, csid, option, npc) + local assaultInfo = xi.assault.contents[player:getCurrentAssault()] + if assaultInfo then + xi.instance.onEventFinish(player, csid, option, npc, assaultInfo.entranceParams.confirmEvent) + else + xi.instance.onEventFinish(player, csid, option, npc) + end +end + +----------------------------------- +-- Instance Creation +----------------------------------- + +xi.assault.onInstanceCreatedCallback = function(player, instance, content) + if not instance then + local npc = player:getEventTarget() + player:messageText(player, zones[player:getZoneID()].text.CANNOT_ENTER, false) + player:instanceEntry(npc, 3) + return + end + + instance:setLevelCap(player:getLocalVar('AssaultCap')) + player:setLocalVar('AssaultCap', 0) + player:setCharVar('Assault_Armband', 1) + player:delKeyItem(xi.ki.ASSAULT_ARMBAND) + + if content then + xi.instance.onInstanceCreatedCallback(player, instance, content.entranceParams) + end +end + +xi.assault.onInstanceSetup = function(instance, content) + local ID = zones[content.zoneID] + local rPos = content.runeOfReleasePos + local aPos = content.ancientBoxPos + + if rPos and next(rPos) then + GetNPCByID(ID.npc.RUNE_OF_RELEASE, instance):setPos(rPos.x, rPos.y, rPos.z, rPos.rot) + end + + if aPos and next(aPos) then + GetNPCByID(ID.npc.ANCIENT_LOCKBOX, instance):setPos(aPos.x, aPos.y, aPos.z, aPos.rot) + end + + for _, group in pairs(content.mobs) do + for mobID = group.baseID, group.baseID + group.offset, 1 do + SpawnMob(mobID, instance) + end + end + + for _, group in pairs(content.npcs) do + for npcID = group.baseID, group.baseID + group.offset, 1 do + GetNPCByID(npcID, instance):setAnimation(xi.status.NORMAL) + end + end + + for _, npcID in ipairs(content.wallNPCs) do + GetNPCByID(npcID, instance):setAnimation(xi.animation.OPEN_DOOR) + end + + if content.loot and content.loot.appraisalReward then + local lockboxNPC = GetNPCByID(ID.npc.ANCIENT_LOCKBOX, instance) + if lockboxNPC then + lockboxNPC:addListener('ON_TRIGGER', 'LOCKBOX_TRIGGER', xi.assault.onLockboxTrigger) + end + end +end + +-- Runs once for each player as they enter +xi.assault.afterInstanceRegistration = function(player, content) + local instance = player:getInstance() + local assaultID = content.assaultID + local ID = zones[content.zoneID] + + player:setCharVar('assaultEntered', assaultID) + player:messageSpecial(ID.text.ASSAULT_START_OFFSET + assaultID, assaultID) + player:messageSpecial(ID.text.TIME_TO_COMPLETE, instance:getTimeLimit()) + + local areaData = xi.assault.areaData[content.assaultArea] + if areaData and areaData.firefly then + player:addTempItem(areaData.firefly) + end +end + +xi.assault.adjustMobLevel = function(mob) + local instance = mob:getInstance() + local levelCap = instance:getLevelCap() + local reducedLevel = 75 - levelCap + + if levelCap ~= 0 then + mob:setMobLevel(mob:getMainLvl() - reducedLevel) + end +end + +----------------------------------- +-- Completion / Failure +----------------------------------- + +xi.assault.onInstanceComplete = function(instance, posX, posZ) + local chars = instance:getChars() + local ID = zones[instance:getZone():getID()] + + GetNPCByID(ID.npc.RUNE_OF_RELEASE, instance):setStatus(xi.status.NORMAL) + GetNPCByID(ID.npc.ANCIENT_LOCKBOX, instance):setStatus(xi.status.NORMAL) + + for _, entity in pairs(chars) do + entity:messageSpecial(ID.text.RUNE_UNLOCKED_POS, posX, posZ) + end +end + +xi.assault.onInstanceFailure = function(instance) + local chars = instance:getChars() + local mobs = instance:getMobs() + local zoneID = instance:getZone():getID() + local player = next(chars) + if not player then + return + end + + local assaultID = player:getCurrentAssault() + local area = xi.assault.missionToArea[assaultID] + + for _, entity in pairs(mobs) do + local mobID = entity:getID() + DespawnMob(mobID, instance) + end + + for _, entity in pairs(chars) do + if area then + entity:addAssaultPoint(area, 100) + entity:messageSpecial(zones[zoneID].text.ASSAULT_POINTS_OBTAINED, 100) + end + + entity:messageSpecial(zones[zoneID].text.MISSION_FAILED, 10, 10) + entity:setCharVar('assaultEntered', 0) + entity:setCharVar('Assault_Armband', 0) + entity:startEvent(102) + end +end + +local function awardCompletionPoints(player, instance) + if not instance then + return + end + + local assaultID = player:getCurrentAssault() + local content = xi.assault.contents[assaultID] + local basePoints = content and content.basePoints + if not basePoints then + return + end + + local chars = instance:getChars() + local playerPointMod = math.max((#chars - 3) * 0.1, 0) + local basePointsForMember = basePoints - (basePoints * playerPointMod) -- Base points before per-member bonuses + local pointsArea = xi.assault.missionToArea[assaultID] + local zoneText = zones[instance:getZone():getID()].text + + for _, member in pairs(chars) do + if member:getLocalVar('AssaultPointsAwarded') == 0 then + member:setLocalVar('AssaultPointsAwarded', 1) + + local points = basePointsForMember + local promotionBonus = 1 + + -- Leader Bonus + if member:getCharVar('Assault_Armband') == 1 then + points = points * 1.1 + end + + -- First time completion bonus + if not member:hasCompletedAssault(assaultID) then + promotionBonus = 5 + points = points * 1.5 + end + + if pointsArea then + member:addAssaultPoint(pointsArea, math.floor(points)) + member:messageSpecial(zoneText.ASSAULT_POINTS_OBTAINED, math.floor(points)) + end + + member:setVar('AssaultPromotion', member:getCharVar('AssaultPromotion') + promotionBonus) + member:setVar('AssaultComplete', 1) + + member:setCharVar('assaultEntered', 0) + member:setCharVar('Assault_Armband', 0) + member:startEvent(102) + end + end + + -- Cleanup remaining mobs + for _, mob in pairs(instance:getMobs()) do + DespawnMob(mob:getID(), instance) + end +end + +local function exitToZone(player, exitZone) + local instance = player:getInstance() + if not instance then + return + end + + local chars = instance:getChars() + for _, entity in pairs(chars) do + entity:setPos(0, 0, 0, 0, exitZone) + end +end + +xi.assault.instanceOnEventFinish = function(player, csid, exitZone) + if csid == 102 then + exitToZone(player, exitZone) + end +end + +xi.assault.runeReleaseFinish = function(player, csid, option, npc, exitZone) + if csid == 100 and option == 1 then + awardCompletionPoints(player, player:getInstance()) + elseif csid == 102 and exitZone then + exitToZone(player, exitZone) + end +end + +xi.assault.InstanceAssault = InstanceAssault diff --git a/scripts/globals/assault/data.lua b/scripts/globals/assault/data.lua new file mode 100644 index 00000000000..bb149615b66 --- /dev/null +++ b/scripts/globals/assault/data.lua @@ -0,0 +1,241 @@ +----------------------------------- +-- Assault Data Tables +-- desc: Static data tables for the Assault system. All per-area +-- lookups, shop inventories, and level cap tables live here. +----------------------------------- +xi = xi or {} +xi.assault = xi.assault or {} +----------------------------------- + +-- Ordered list of all assault orders key items, used for hasOrders checks +xi.assault.assaultOrders = +{ + xi.ki.LEUJAOAM_ASSAULT_ORDERS, + xi.ki.MAMOOL_JA_ASSAULT_ORDERS, + xi.ki.LEBROS_ASSAULT_ORDERS, + xi.ki.PERIQIA_ASSAULT_ORDERS, + xi.ki.ILRUSI_ASSAULT_ORDERS, + xi.ki.NYZUL_ISLE_ASSAULT_ORDERS, +} + +xi.assault.areaData = +{ + [xi.assault.assaultArea.LEUJAOAM_SANCTUM] = + { + orders = xi.ki.LEUJAOAM_ASSAULT_ORDERS, + map = xi.ki.MAP_OF_LEUJAOAM_SANCTUM, + firefly = xi.item.CAGE_OF_AZOUPH_FIREFLIES, + }, + [xi.assault.assaultArea.MAMOOL_JA_TRAINING_GROUNDS] = + { + orders = xi.ki.MAMOOL_JA_ASSAULT_ORDERS, + map = xi.ki.MAP_OF_THE_TRAINING_GROUNDS, + firefly = xi.item.CAGE_OF_BHAFLAU_FIREFLIES, + }, + [xi.assault.assaultArea.LEBROS_CAVERN] = + { + orders = xi.ki.LEBROS_ASSAULT_ORDERS, + map = xi.ki.MAP_OF_LEBROS_CAVERN, + firefly = xi.item.CAGE_OF_ZHAYOLM_FIREFLIES, + }, + [xi.assault.assaultArea.PERIQIA] = + { + orders = xi.ki.PERIQIA_ASSAULT_ORDERS, + map = xi.ki.MAP_OF_PERIQIA, + firefly = xi.item.CAGE_OF_DVUCCA_FIREFLIES, + }, + [xi.assault.assaultArea.ILRUSI_ATOLL] = + { + orders = xi.ki.ILRUSI_ASSAULT_ORDERS, + map = xi.ki.MAP_OF_ILRUSI_ATOLL, + firefly = xi.item.CAGE_OF_REEF_FIREFLIES, + }, +} + +xi.assault.zoneToArea = +{ + [xi.zone.LEUJAOAM_SANCTUM] = xi.assault.assaultArea.LEUJAOAM_SANCTUM, + [xi.zone.MAMOOL_JA_TRAINING_GROUNDS] = xi.assault.assaultArea.MAMOOL_JA_TRAINING_GROUNDS, + [xi.zone.LEBROS_CAVERN] = xi.assault.assaultArea.LEBROS_CAVERN, + [xi.zone.PERIQIA] = xi.assault.assaultArea.PERIQIA, + [xi.zone.ILRUSI_ATOLL] = xi.assault.assaultArea.ILRUSI_ATOLL, +} + +xi.assault.missionsByArea = +{ + [xi.assault.assaultArea.LEUJAOAM_SANCTUM] = + { + xi.assault.mission.LEUJAOAM_CLEANSING, + xi.assault.mission.ORICHALCUM_SURVEY, + xi.assault.mission.ESCORT_PROFESSOR_CHANOIX, + xi.assault.mission.SHANARHA_GRASS_CONSERVATION, + xi.assault.mission.COUNTING_SHEEP, + xi.assault.mission.SUPPLIES_RECOVERY, + xi.assault.mission.AZURE_EXPERIMENTS, + xi.assault.mission.IMPERIAL_CODE, + xi.assault.mission.RED_VERSUS_BLUE, + xi.assault.mission.BLOODY_RONDO, + }, + [xi.assault.assaultArea.MAMOOL_JA_TRAINING_GROUNDS] = + { + xi.assault.mission.IMPERIAL_AGENT_RESCUE, + xi.assault.mission.PREEMPTIVE_STRIKE, + xi.assault.mission.SAGELORD_ELIMINATION, + xi.assault.mission.BREAKING_MORALE, + xi.assault.mission.THE_DOUBLE_AGENT, + xi.assault.mission.IMPERIAL_TREASURE_RETRIEVAL, + xi.assault.mission.BLITZKRIEG, + xi.assault.mission.MARIDS_IN_THE_MIST, + xi.assault.mission.AZURE_AILMENTS, + xi.assault.mission.THE_SUSANOO_SHUFFLE, + }, + [xi.assault.assaultArea.LEBROS_CAVERN] = + { + xi.assault.mission.EXCAVATION_DUTY, + xi.assault.mission.LEBROS_SUPPLIES, + xi.assault.mission.TROLL_FUGITIVES, + xi.assault.mission.EVADE_AND_ESCAPE, + xi.assault.mission.SIEGEMASTER_ASSASSINATION, + xi.assault.mission.APKALLU_BREEDING, + xi.assault.mission.WAMOURA_FARM_RAID, + xi.assault.mission.EGG_CONSERVATION, + xi.assault.mission.OPERATION_BLACK_PEARL, + xi.assault.mission.BETTER_THAN_ONE, + }, + [xi.assault.assaultArea.PERIQIA] = + { + xi.assault.mission.SEAGULL_GROUNDED, + xi.assault.mission.REQUIEM, + xi.assault.mission.SAVING_PRIVATE_RYAAF, + xi.assault.mission.SHOOTING_DOWN_THE_BARON, + xi.assault.mission.BUILDING_BRIDGES, + xi.assault.mission.STOP_THE_BLOODSHED, + xi.assault.mission.DEFUSE_THE_THREAT, + xi.assault.mission.OPERATION_SNAKE_EYES, + xi.assault.mission.WAKE_THE_PUPPET, + xi.assault.mission.THE_PRICE_IS_RIGHT, + }, + [xi.assault.assaultArea.ILRUSI_ATOLL] = + { + xi.assault.mission.GOLDEN_SALVAGE, + xi.assault.mission.LAMIA_NO_13, + xi.assault.mission.EXTERMINATION, + xi.assault.mission.DEMOLITION_DUTY, + xi.assault.mission.SEARAT_SALVATION, + xi.assault.mission.APKALLU_SEIZURE, + xi.assault.mission.LOST_AND_FOUND, + xi.assault.mission.DESERTER, + xi.assault.mission.DESPERATELY_SEEKING_CEPHALOPODS, + xi.assault.mission.BELLEROPHONS_BLISS, + }, + + [xi.assault.assaultArea.NYZUL_ISLE] = + { + xi.assault.mission.NYZUL_ISLE_INVESTIGATION, + xi.assault.mission.NYZUL_ISLE_UNCHARTED_AREA_SURVEY, + }, +} + +xi.assault.missionToArea = {} +for area, missions in pairs(xi.assault.missionsByArea) do + for _, missionId in ipairs(missions) do + xi.assault.missionToArea[missionId] = area + end +end + +xi.assault.levelCapByIndex = +{ + [0] = 0, -- No level cap + [1] = 70, + [2] = 60, + [3] = 50, +} + +-- Assault Point shop inventories per area +xi.assault.shops = +{ + [xi.assault.assaultArea.LEUJAOAM_SANCTUM] = + { + [1] = { itemid = xi.item.STOIC_EARRING, price = 3000 }, + [2] = { itemid = xi.item.UNFETTERED_RING, price = 5000 }, + [3] = { itemid = xi.item.TEMPERED_CHAIN, price = 8000 }, + [4] = { itemid = xi.item.POTENT_BELT, price = 10000 }, + [5] = { itemid = xi.item.MIRACULOUS_CAPE, price = 10000 }, + [6] = { itemid = xi.item.YIGIT_BULAWA, price = 10000 }, + [7] = { itemid = xi.item.IMPERIAL_BHUJ, price = 15000 }, + [8] = { itemid = xi.item.PAHLUWAN_PATAS, price = 15000 }, + [9] = { itemid = xi.item.AMIR_KOLLUKS, price = 15000 }, + [10] = { itemid = xi.item.PAHLUWAN_QALANSUWA, price = 20000 }, + [11] = { itemid = xi.item.YIGIT_SERAWEELS, price = 20000 }, + [12] = { itemid = xi.item.CIPHER_OF_OVJANGS_ALTER_EGO, price = 3000 }, + [13] = { itemid = xi.item.CIPHER_OF_MNEJINGS_ALTER_EGO, price = 3000 }, + }, + + [xi.assault.assaultArea.MAMOOL_JA_TRAINING_GROUNDS] = + { + [1] = { itemid = xi.item.ANTIVENOM_EARRING, price = 3000 }, + [2] = { itemid = xi.item.EBULLIENT_RING, price = 5000 }, + [3] = { itemid = xi.item.ENLIGHTENED_CHAIN, price = 8000 }, + [4] = { itemid = xi.item.SPECTRAL_BELT, price = 10000 }, + [5] = { itemid = xi.item.BULLSEYE_CAPE, price = 10000 }, + [6] = { itemid = xi.item.STORM_TULWAR, price = 15000 }, + [7] = { itemid = xi.item.IMPERIAL_NEZA, price = 15000 }, + [8] = { itemid = xi.item.STORM_TABAR, price = 15000 }, + [9] = { itemid = xi.item.YIGIT_GAGES, price = 20000 }, + [10] = { itemid = xi.item.AMIR_BOOTS, price = 20000 }, + [11] = { itemid = xi.item.PAHLUWAN_SERAWEELS, price = 20000 }, + [12] = { itemid = xi.item.CIPHER_OF_OVJANGS_ALTER_EGO, price = 3000 }, + [13] = { itemid = xi.item.CIPHER_OF_MNEJINGS_ALTER_EGO, price = 3000 }, + }, + + [xi.assault.assaultArea.LEBROS_CAVERN] = + { + [1] = { itemid = xi.item.INSOMNIA_EARRING, price = 3000 }, + [2] = { itemid = xi.item.HALE_RING, price = 5000 }, + [3] = { itemid = xi.item.CHIVALROUS_CHAIN, price = 8000 }, + [4] = { itemid = xi.item.PRECISE_BELT, price = 10000 }, + [5] = { itemid = xi.item.INTENSIFYING_CAPE, price = 10000 }, + [6] = { itemid = xi.item.IMPERIAL_POLE, price = 15000 }, + [7] = { itemid = xi.item.DOOMBRINGER, price = 15000 }, + [8] = { itemid = xi.item.SAYOSAMONJI, price = 15000 }, + [9] = { itemid = xi.item.PAHLUWAN_DASTANAS, price = 20000 }, + [10] = { itemid = xi.item.YIGIT_CRACKOWS, price = 20000 }, + [11] = { itemid = xi.item.AMIR_KORAZIN, price = 20000 }, + [12] = { itemid = xi.item.CIPHER_OF_OVJANGS_ALTER_EGO, price = 3000 }, + [13] = { itemid = xi.item.CIPHER_OF_MNEJINGS_ALTER_EGO, price = 3000 }, + }, + + [xi.assault.assaultArea.PERIQIA] = + { + [1] = { itemid = xi.item.VISION_EARRING, price = 3000 }, + [2] = { itemid = xi.item.UNYIELDING_RING, price = 5000 }, + [3] = { itemid = xi.item.FORTIFIED_CHAIN, price = 8000 }, + [4] = { itemid = xi.item.RESOLUTE_BELT, price = 10000 }, + [5] = { itemid = xi.item.BUSHIDO_CAPE, price = 10000 }, + [6] = { itemid = xi.item.KHANJAR, price = 15000 }, + [7] = { itemid = xi.item.HOTARUMARU, price = 15000 }, + [8] = { itemid = xi.item.IMPERIAL_GUN, price = 15000 }, + [9] = { itemid = xi.item.AMIR_PUGGAREE, price = 20000 }, + [10] = { itemid = xi.item.PAHLUWAN_CRACKOWS, price = 20000 }, + [11] = { itemid = xi.item.YIGIT_GOMLEK, price = 20000 }, + [12] = { itemid = xi.item.CIPHER_OF_OVJANGS_ALTER_EGO, price = 3000 }, + [13] = { itemid = xi.item.CIPHER_OF_MNEJINGS_ALTER_EGO, price = 3000 }, + }, + + [xi.assault.assaultArea.ILRUSI_ATOLL] = + { + [1] = { itemid = xi.item.VELOCITY_EARRING, price = 3000 }, + [2] = { itemid = xi.item.GARRULOUS_RING, price = 5000 }, + [3] = { itemid = xi.item.GRANDIOSE_CHAIN, price = 8000 }, + [4] = { itemid = xi.item.HURLING_BELT, price = 10000 }, + [5] = { itemid = xi.item.INVIGORATING_CAPE, price = 10000 }, + [6] = { itemid = xi.item.IMPERIAL_KAMAN, price = 15000 }, + [7] = { itemid = xi.item.STORM_ZAGHNAL, price = 15000 }, + [8] = { itemid = xi.item.STORM_FIFE, price = 15000 }, + [9] = { itemid = xi.item.YIGIT_TURBAN, price = 20000 }, + [10] = { itemid = xi.item.AMIR_DIRS, price = 20000 }, + [11] = { itemid = xi.item.PAHLUWAN_KHAZAGAND, price = 20000 }, + [12] = { itemid = xi.item.CIPHER_OF_OVJANGS_ALTER_EGO, price = 3000 }, + [13] = { itemid = xi.item.CIPHER_OF_MNEJINGS_ALTER_EGO, price = 3000 }, + }, +} diff --git a/scripts/globals/assault/npc_handler.lua b/scripts/globals/assault/npc_handler.lua new file mode 100644 index 00000000000..fff5b0193a0 --- /dev/null +++ b/scripts/globals/assault/npc_handler.lua @@ -0,0 +1,290 @@ +----------------------------------- +-- Assault Mission NPC Handler Functions (Rytaal / Mission Givers) +----------------------------------- +local ID = zones[xi.zone.AHT_URHGAN_WHITEGATE] +xi = xi or {} +xi.assault = xi.assault or {} +----------------------------------- + +----------------------------------- +-- Mission Giver Functions +----------------------------------- + +xi.assault.onMissionGiverTrigger = function(player, npc, eventOffset, assaultArea) + local rank = xi.besieged.getMercenaryRank(player) + local hasimperialIDtag = player:hasKeyItem(xi.ki.IMPERIAL_ARMY_ID_TAG) and 1 or 0 + local assaultPoints = player:getAssaultPoint(assaultArea) + local active = xi.extravaganza.campaignActive() + local cipher = 0 + + if + active == xi.extravaganza.campaign.SPRING_FALL or + active == xi.extravaganza.campaign.BOTH + then + cipher = 1 + end + + -- If the player is eligible for Assaults, show them the menu. Otherwise, show them the "not eligible" message. + if rank > 0 then + player:startEvent(eventOffset, rank, hasimperialIDtag, assaultPoints, player:getCurrentAssault(), cipher) + else + player:startEvent(eventOffset + 6) + end +end + +xi.assault.onMissionGiverUpdate = function(player, csid, option, npc, eventOffset, assaultArea) + local selectiontype = bit.band(option, 0xF) + local shop = xi.assault.shops[assaultArea] + + if + csid == eventOffset and + selectiontype == 2 + then + local item = bit.rshift(option, 14) + local choice = shop[item] + if not choice then + return + end + + local assaultPoints = player:getAssaultPoint(assaultArea) + local canEquip = player:canEquipItem(choice.itemid) and 2 or 0 + + player:updateEvent(0, 0, assaultPoints, 0, canEquip) + end +end + +xi.assault.onMissionGiverEventFinish = function(player, csid, option, npc, eventOffset, assaultArea) + if csid == eventOffset then + local selectiontype = bit.band(option, 0xF) + local shop = xi.assault.shops[assaultArea] + + -- Player selected assault mission + if + selectiontype == 1 and + npcUtil.giveKeyItem(player, xi.assault.areaData[assaultArea].orders) + then + player:addAssault(bit.rshift(option, 4)) + player:delKeyItem(xi.ki.IMPERIAL_ARMY_ID_TAG) + player:addKeyItem(xi.assault.areaData[assaultArea].map) + + -- Player selected to purchase an item + elseif selectiontype == 2 then + local item = bit.rshift(option, 14) + local choice = shop[item] + if choice and npcUtil.giveItem(player, choice.itemid) then + player:delAssaultPoint(assaultArea, choice.price) + end + end + end +end + +----------------------------------- +-- Rytaal Functions +----------------------------------- + +local function handleAssaultFinish(player, currentAssault) + -- Assault complete is set to 1 on assault or nyzul win + if player:getCharVar('AssaultComplete') == 1 then + player:messageText(player, ID.text.ASSAULT_COMPLETE) + player:completeAssault(currentAssault) + + elseif currentAssault == xi.assault.mission.NYZUL_ISLE_INVESTIGATION then + player:messageText(player, ID.text.NYZUL_FAIL) + player:delAssault(currentAssault) + + -- Players still receive 100 consolation assault points on fail + else + local area = xi.assault.missionToArea[currentAssault] + if area then + player:addAssaultPoint(area, 100) + end + + player:messageText(player, ID.text.ASSAULT_FAILED) + player:delAssault(currentAssault) + end + + for mapId = xi.ki.MAP_OF_LEUJAOAM_SANCTUM, xi.ki.MAP_OF_NYZUL_ISLE do + if player:hasKeyItem(mapId) then + player:delKeyItem(mapId) + end + end + + player:setCharVar('AssaultComplete', 0) + player:setCharVar('assaultEntered', 0) + player:setCharVar('Assault_Armband', 0) + + for _, orders in ipairs(xi.assault.assaultOrders) do + if player:hasKeyItem(orders) then + player:delKeyItem(orders) + end + end +end + +-- Max of 4 assault tags upon completion of all assaults and Second Lieutenant rank +local function getMaxTagStock(player) + if xi.besieged.getMercenaryRank(player) < xi.assault.mercenaryRank.SECOND_LIEUTENANT then + return 3 + end + + for missionID = 1, xi.assault.mission.NYZUL_ISLE_INVESTIGATION do + if not player:hasCompletedAssault(missionID) then + return 3 + end + end + + return 4 +end + +local function initializeTagStock(player, maxTagStock) + local tagStock = player:getCurrency('id_tags') + local tagDrawTime = player:getCharVar('tagDrawTime') -- Time when the player last drew a tag from a full stock; 0 if no timer is running + + if tagStock == 0 and tagDrawTime == 0 and player:getCharVar('tagStockInitialized') == 0 then + player:setCurrency('id_tags', maxTagStock) + player:setCharVar('tagStockInitialized', 1) + return maxTagStock + end + + return tagStock +end + +local function applyMaxStockBonus(player, maxTagStock, tagStock) + -- Player obtains a one time extra assault tag when first completing all assault missions + if + maxTagStock == 4 and + player:getCharVar('assaultMaxStockGranted') == 0 + then + tagStock = math.min(maxTagStock, tagStock + 1) + player:setCurrency('id_tags', tagStock) + player:setCharVar('assaultMaxStockGranted', 1) + end + + return tagStock +end + +local function replenishFromTimer(player, tagStock, maxTagStock, idTagPeriod) + local tagDrawTime = player:getCharVar('tagDrawTime') + + -- Award one tag per elapsed restock period since the last full tag draw + if + tagDrawTime > 0 and + tagStock < maxTagStock + then + local periodsElapsed = math.floor((GetSystemTime() - tagDrawTime) / idTagPeriod) + if periodsElapsed > 0 then + local periodsToApply = math.min(periodsElapsed, maxTagStock - tagStock) + tagStock = tagStock + periodsToApply + + -- Stop the timer when stock reaches the cap + if tagStock >= maxTagStock then + tagDrawTime = 0 + else + tagDrawTime = tagDrawTime + periodsToApply * idTagPeriod + end + + player:setCurrency('id_tags', tagStock) + player:setCharVar('tagDrawTime', tagDrawTime) + end + elseif + tagDrawTime > 0 and + tagStock == maxTagStock + then + tagDrawTime = 0 + player:setCharVar('tagDrawTime', 0) + end + + local vanaEpoch = 1009810800 -- Vanadiel epoch time base for restock timer + local allTagsTimeCS = tagDrawTime > 0 and (tagDrawTime - vanaEpoch) or 0 -- Timestamp passed to event to display the restock timer + + return tagStock, allTagsTimeCS +end + +local function calculateTags(player) + local idTagPeriod = player:hasKeyItem(xi.ki.RHAPSODY_IN_AZURE) and 600 or 86400 -- Restock is 1 tag per day, or 1 tag per 10 minutes with Rhapsody in Azure equipped + local maxTagStock = getMaxTagStock(player) + local tagStock = initializeTagStock(player, maxTagStock) + tagStock = applyMaxStockBonus(player, maxTagStock, tagStock) + + return replenishFromTimer(player, tagStock, maxTagStock, idTagPeriod) +end + +xi.assault.onRytaalTrigger = function(player, npc) + local currentAssault = player:getCurrentAssault() + + -- Player isn't high enough level or hasn't progressed far enough in TOAU to access Assaults + if + player:getMainLvl() < 50 or + player:getCurrentMission(xi.mission.log_id.TOAU) <= xi.mission.id.toau.IMMORTAL_SENTRIES + then + player:startEvent(270) + + return + end + + -- Player has returned from an assault, handle completion + if + currentAssault ~= 0 and + (player:getCharVar('assaultEntered') ~= 0 or + player:getCharVar('AssaultComplete') == 1) + then + handleAssaultFinish(player, currentAssault) + + return + end + + -- Player has not started an assault, give them a tag + local tagStock, allTagsTimeCS = calculateTags(player) + local tagsAvail = tagStock > 0 and 1 or 0 + local haveimperialIDtag = player:hasKeyItem(xi.ki.IMPERIAL_ARMY_ID_TAG) and 1 or 0 + player:startEvent(268, 2, tagStock, currentAssault, haveimperialIDtag, allTagsTimeCS, tagsAvail) +end + +xi.assault.onRytaalEventFinish = function(player, csid, option, npc) + -- Early return if not assault tag related event + if csid ~= 268 then + return + end + + -- Player selected to obtain a new tag + if + option == 1 and + not player:hasKeyItem(xi.ki.IMPERIAL_ARMY_ID_TAG) + then + local tagStock = player:getCurrency('id_tags') + if tagStock == 0 then + return + end + + if player:getCurrentAssault() ~= 0 then + player:messageSpecial(ID.text.CANNOT_ISSUE_TAG, xi.ki.IMPERIAL_ARMY_ID_TAG) + + return + end + + npcUtil.giveKeyItem(player, xi.ki.IMPERIAL_ARMY_ID_TAG) + player:setCharVar('tagStockInitialized', 1) + + -- Start the replenishment timer only when taking from a full stock + if tagStock == getMaxTagStock(player) then + player:setCharVar('tagDrawTime', GetSystemTime()) + end + + player:setCurrency('id_tags', tagStock - 1) + + -- Player selected selected to end an assault + elseif + option == 2 and + xi.assault.hasOrders(player) and + not player:hasKeyItem(xi.ki.IMPERIAL_ARMY_ID_TAG) + then + local currentAssault = player:getCurrentAssault() + for _, orders in ipairs(xi.assault.assaultOrders) do + if player:hasKeyItem(orders) then + player:delKeyItem(orders) + end + end + + npcUtil.giveKeyItem(player, xi.ki.IMPERIAL_ARMY_ID_TAG) + player:delAssault(currentAssault) + end +end diff --git a/scripts/globals/instance.lua b/scripts/globals/instance.lua index 534cc7dbaf4..0b1a3d39ca8 100644 --- a/scripts/globals/instance.lua +++ b/scripts/globals/instance.lua @@ -326,18 +326,22 @@ local checkEntryReqs = function(player, instanceId) end end +-- Clear up after possible failed loads +xi.instance.clearInstance = function(player) + player:setLocalVar('INSTANCE_REQUESTED', 0) + local existingInstance = player:getInstance() + if existingInstance then + existingInstance:fail() + end +end + xi.instance.onTrade = function(player, npc, trade) end xi.instance.onTrigger = function(player, npc, instanceZoneID) local zoneLookup = xi.instance.lookup[instanceZoneID] - -- Clear up after possible failed loads - player:setLocalVar('INSTANCE_REQUESTED', 0) - local existingInstance = player:getInstance() - if existingInstance then - existingInstance:fail() - end + xi.instance.clearInstance(player) -- Find the first instance you're valid for -- TODO: Handle being valid for multiple instances from the same entrance @@ -425,18 +429,19 @@ xi.instance.onEventUpdate = function(player, csid, option, npc) end -- 'Default' behavior. It's up to each instance whether or not they want to use this logic -xi.instance.onInstanceCreatedCallback = function(player, instance) - local zoneLookup = xi.instance.lookup[instance:getZone():getID()] +-- Can pass instance cutscene information directly if accessible from container, otherwise will try and find it from the player's instance +xi.instance.onInstanceCreatedCallback = function(player, instance, entryInfo) local instanceId = instance:getID() - -- Collect cs for party members - local lookupEntry - for _, entry in ipairs(zoneLookup) do - local entryInstanceId = entry[1] - if instanceId == entryInstanceId then - lookupEntry = entry + if entryInfo == nil then + -- Collect cs for party members + for _, entry in ipairs(xi.instance.lookup[instance:getZone():getID()]) do + local entryInstanceId = entry[1] + if instanceId == entryInstanceId then + entryInfo = entry - break + break + end end end @@ -455,7 +460,7 @@ xi.instance.onInstanceCreatedCallback = function(player, instance) -- player will be brought into instance either way -- this makes the animation trigger reliably v:release() - v:startEvent(unpack(lookupEntry[4])) + v:startEvent(unpack(entryInfo.memberEvent or entryInfo[4])) v:setInstance(instance) local npc = player:getEventTarget() @@ -480,7 +485,7 @@ xi.instance.onInstanceCreatedCallback = function(player, instance) end -- finally, send commander in - player:startEvent(unpack(lookupEntry[4])) -- will fail if previous event is working as it should, otherwise catches secondary event to enter + player:startEvent(unpack(entryInfo.memberEvent or entryInfo[4])) -- will fail if previous event is working as it should, otherwise catches secondary event to enter local npc = player:getEventTarget() if npc ~= nil then player:instanceEntry(npc, 4) @@ -493,13 +498,18 @@ xi.instance.onInstanceCreatedCallback = function(player, instance) end end -xi.instance.onEventFinish = function(player, csid, option, npc) +-- Default instance behavior. Unpacks the csid and option from the lookup table. +-- Can pass instance custcene information directly if accessible from container, +-- otherwise will try and find it from the player's instance and a table lookup. +xi.instance.onEventFinish = function(player, csid, option, npc, instanceInfo) local instance = player:getInstance() if instance then - local instanceZoneId = instance:getZone():getID() - local zoneLookup = xi.instance.lookup[instanceZoneId] - local csidEntry, optionEntry = unpack(zoneLookup[1][3]) + if not instanceInfo then + instanceInfo = xi.instance.lookup[instance:getZone():getID()][1][3] + end + + local csidEntry, optionEntry = unpack(instanceInfo) if csid == csidEntry and option == optionEntry then for _, v in ipairs(player:getParty()) do diff --git a/scripts/zones/Aht_Urhgan_Whitegate/npcs/Bhoy_Yhupplo.lua b/scripts/zones/Aht_Urhgan_Whitegate/npcs/Bhoy_Yhupplo.lua index abc7bffda06..8e69b9cdaea 100644 --- a/scripts/zones/Aht_Urhgan_Whitegate/npcs/Bhoy_Yhupplo.lua +++ b/scripts/zones/Aht_Urhgan_Whitegate/npcs/Bhoy_Yhupplo.lua @@ -7,86 +7,16 @@ ---@type TNpcEntity local entity = {} -local items = -{ - [ 1] = { itemId = xi.item.VELOCITY_EARRING, price = 3000 }, - [ 2] = { itemId = xi.item.GARRULOUS_RING, price = 5000 }, - [ 3] = { itemId = xi.item.GRANDIOSE_CHAIN, price = 8000 }, - [ 4] = { itemId = xi.item.HURLING_BELT, price = 10000 }, - [ 5] = { itemId = xi.item.INVIGORATING_CAPE, price = 10000 }, - [ 6] = { itemId = xi.item.IMPERIAL_KAMAN, price = 15000 }, - [ 7] = { itemId = xi.item.STORM_ZAGHNAL, price = 15000 }, - [ 8] = { itemId = xi.item.STORM_FIFE, price = 15000 }, - [ 9] = { itemId = xi.item.YIGIT_TURBAN, price = 20000 }, - [10] = { itemId = xi.item.AMIR_DIRS, price = 20000 }, - [11] = { itemId = xi.item.PAHLUWAN_KHAZAGAND, price = 20000 }, - [12] = { itemId = xi.item.CIPHER_OF_OVJANGS_ALTER_EGO, price = 3000 }, - [13] = { itemId = xi.item.CIPHER_OF_MNEJINGS_ALTER_EGO, price = 3000 }, -} - entity.onTrigger = function(player, npc) - local rank = xi.besieged.getMercenaryRank(player) - local haveimperialIDtag = player:hasKeyItem(xi.ki.IMPERIAL_ARMY_ID_TAG) and 1 or 0 - local assaultPoints = player:getCurrency('ilrusi_assault_point') - local cipher = 0 - local active = xi.extravaganza.campaignActive() - - if - active == xi.extravaganza.campaign.SPRING_FALL or - active == xi.extravaganza.campaign.BOTH - then - cipher = 1 - end - - if rank > 0 then - player:startEvent(277, rank, haveimperialIDtag, assaultPoints, player:getCurrentAssault(), cipher) - else - player:startEvent(283) - end + xi.assault.onMissionGiverTrigger(player, npc, 277, xi.assault.assaultArea.ILRUSI_ATOLL) end entity.onEventUpdate = function(player, csid, option, npc) - local selectiontype = bit.band(option, 0xF) - if csid == 277 and selectiontype == 2 then - local item = bit.rshift(option, 14) - local choice = items[item] - local assaultPoints = player:getCurrency('ilrusi_assault_point') - local canEquip = player:canEquipItem(choice.itemId) and 2 or 0 - - player:updateEvent(0, 0, assaultPoints, 0, canEquip) - end + xi.assault.onMissionGiverUpdate(player, csid, option, npc, 277, xi.assault.assaultArea.ILRUSI_ATOLL) end entity.onEventFinish = function(player, csid, option, npc) - if csid == 277 then - local selectiontype = bit.band(option, 0xF) - - -- Taken assault mission - if - selectiontype == 1 and - npcUtil.giveKeyItem(player, xi.ki.ILRUSI_ASSAULT_ORDERS) - then - player:addAssault(bit.rshift(option, 4)) - player:delKeyItem(xi.ki.IMPERIAL_ARMY_ID_TAG) - player:addKeyItem(xi.ki.MAP_OF_ILRUSI_ATOLL) - - -- Purchased an item - elseif selectiontype == 2 then - local choice = items[bit.rshift(option, 14)] - if not choice then - return - end - - local currency = player:getCurrency('ilrusi_assault_point') - if currency < choice.price then - return - end - - if npcUtil.giveItem(player, choice.itemId) then - player:delCurrency('ilrusi_assault_point', choice.price) - end - end - end + xi.assault.onMissionGiverEventFinish(player, csid, option, npc, 277, xi.assault.assaultArea.ILRUSI_ATOLL) end return entity diff --git a/scripts/zones/Aht_Urhgan_Whitegate/npcs/Famad.lua b/scripts/zones/Aht_Urhgan_Whitegate/npcs/Famad.lua index 47e50cd8f5b..86079a66327 100644 --- a/scripts/zones/Aht_Urhgan_Whitegate/npcs/Famad.lua +++ b/scripts/zones/Aht_Urhgan_Whitegate/npcs/Famad.lua @@ -7,86 +7,16 @@ ---@type TNpcEntity local entity = {} -local items = -{ - [ 1] = { itemId = xi.item.INSOMNIA_EARRING, price = 3000 }, - [ 2] = { itemId = xi.item.HALE_RING, price = 5000 }, - [ 3] = { itemId = xi.item.CHIVALROUS_CHAIN, price = 8000 }, - [ 4] = { itemId = xi.item.PRECISE_BELT, price = 10000 }, - [ 5] = { itemId = xi.item.INTENSIFYING_CAPE, price = 10000 }, - [ 6] = { itemId = xi.item.IMPERIAL_POLE, price = 15000 }, - [ 7] = { itemId = xi.item.DOOMBRINGER, price = 15000 }, - [ 8] = { itemId = xi.item.SAYOSAMONJI, price = 15000 }, - [ 9] = { itemId = xi.item.PAHLUWAN_DASTANAS, price = 20000 }, - [10] = { itemId = xi.item.YIGIT_CRACKOWS, price = 20000 }, - [11] = { itemId = xi.item.AMIR_KORAZIN, price = 20000 }, - [12] = { itemId = xi.item.CIPHER_OF_OVJANGS_ALTER_EGO, price = 3000 }, - [13] = { itemId = xi.item.CIPHER_OF_MNEJINGS_ALTER_EGO, price = 3000 }, -} - entity.onTrigger = function(player, npc) - local rank = xi.besieged.getMercenaryRank(player) - local haveimperialIDtag = player:hasKeyItem(xi.ki.IMPERIAL_ARMY_ID_TAG) and 1 or 0 - local assaultPoints = player:getCurrency('lebros_assault_point') - local cipher = 0 - local active = xi.extravaganza.campaignActive() - - if - active == xi.extravaganza.campaign.SPRING_FALL or - active == xi.extravaganza.campaign.BOTH - then - cipher = 1 - end - - if rank > 0 then - player:startEvent(275, rank, haveimperialIDtag, assaultPoints, player:getCurrentAssault(), cipher) - else - player:startEvent(281) - end + xi.assault.onMissionGiverTrigger(player, npc, 275, xi.assault.assaultArea.LEBROS_CAVERN) end entity.onEventUpdate = function(player, csid, option, npc) - local selectiontype = bit.band(option, 0xF) - if csid == 275 and selectiontype == 2 then - local item = bit.rshift(option, 14) - local choice = items[item] - local assaultPoints = player:getCurrency('lebros_assault_point') - local canEquip = player:canEquipItem(choice.itemId) and 2 or 0 - - player:updateEvent(0, 0, assaultPoints, 0, canEquip) - end + xi.assault.onMissionGiverUpdate(player, csid, option, npc, 275, xi.assault.assaultArea.LEBROS_CAVERN) end entity.onEventFinish = function(player, csid, option, npc) - if csid == 275 then - local selectiontype = bit.band(option, 0xF) - - -- Taken assault mission - if - selectiontype == 1 and - npcUtil.giveKeyItem(player, xi.ki.LEBROS_ASSAULT_ORDERS) - then - player:addAssault(bit.rshift(option, 4)) - player:delKeyItem(xi.ki.IMPERIAL_ARMY_ID_TAG) - player:addKeyItem(xi.ki.MAP_OF_LEBROS_CAVERN) - - -- Purchased an item - elseif selectiontype == 2 then - local choice = items[bit.rshift(option, 14)] - if not choice then - return - end - - local currency = player:getCurrency('lebros_assault_point') - if currency < choice.price then - return - end - - if npcUtil.giveItem(player, choice.itemId) then - player:delCurrency('lebros_assault_point', choice.price) - end - end - end + xi.assault.onMissionGiverEventFinish(player, csid, option, npc, 275, xi.assault.assaultArea.LEBROS_CAVERN) end return entity diff --git a/scripts/zones/Aht_Urhgan_Whitegate/npcs/Isdebaaq.lua b/scripts/zones/Aht_Urhgan_Whitegate/npcs/Isdebaaq.lua index 6bc3d7dd8ab..8af6f6fb344 100644 --- a/scripts/zones/Aht_Urhgan_Whitegate/npcs/Isdebaaq.lua +++ b/scripts/zones/Aht_Urhgan_Whitegate/npcs/Isdebaaq.lua @@ -7,86 +7,16 @@ ---@type TNpcEntity local entity = {} -local items = -{ - [ 1] = { itemId = xi.item.ANTIVENOM_EARRING, price = 3000 }, - [ 2] = { itemId = xi.item.EBULLIENT_RING, price = 5000 }, - [ 3] = { itemId = xi.item.ENLIGHTENED_CHAIN, price = 8000 }, - [ 4] = { itemId = xi.item.SPECTRAL_BELT, price = 10000 }, - [ 5] = { itemId = xi.item.BULLSEYE_CAPE, price = 10000 }, - [ 6] = { itemId = xi.item.STORM_TULWAR, price = 15000 }, - [ 7] = { itemId = xi.item.IMPERIAL_NEZA, price = 15000 }, - [ 8] = { itemId = xi.item.STORM_TABAR, price = 15000 }, - [ 9] = { itemId = xi.item.YIGIT_GAGES, price = 20000 }, - [10] = { itemId = xi.item.AMIR_BOOTS, price = 20000 }, - [11] = { itemId = xi.item.PAHLUWAN_SERAWEELS, price = 20000 }, - [12] = { itemId = xi.item.CIPHER_OF_OVJANGS_ALTER_EGO, price = 3000 }, - [13] = { itemId = xi.item.CIPHER_OF_MNEJINGS_ALTER_EGO, price = 3000 }, -} - entity.onTrigger = function(player, npc) - local rank = xi.besieged.getMercenaryRank(player) - local haveimperialIDtag = player:hasKeyItem(xi.ki.IMPERIAL_ARMY_ID_TAG) and 1 or 0 - local assaultPoints = player:getCurrency('mamool_assault_point') - local cipher = 0 - local active = xi.extravaganza.campaignActive() - - if - active == xi.extravaganza.campaign.SPRING_FALL or - active == xi.extravaganza.campaign.BOTH - then - cipher = 1 - end - - if rank > 0 then - player:startEvent(274, rank, haveimperialIDtag, assaultPoints, player:getCurrentAssault(), cipher) - else - player:startEvent(280) - end + xi.assault.onMissionGiverTrigger(player, npc, 274, xi.assault.assaultArea.MAMOOL_JA_TRAINING_GROUNDS) end entity.onEventUpdate = function(player, csid, option, npc) - local selectiontype = bit.band(option, 0xF) - if csid == 274 and selectiontype == 2 then - local item = bit.rshift(option, 14) - local choice = items[item] - local assaultPoints = player:getCurrency('mamool_assault_point') - local canEquip = player:canEquipItem(choice.itemId) and 2 or 0 - - player:updateEvent(0, 0, assaultPoints, 0, canEquip) - end + xi.assault.onMissionGiverUpdate(player, csid, option, npc, 274, xi.assault.assaultArea.MAMOOL_JA_TRAINING_GROUNDS) end entity.onEventFinish = function(player, csid, option, npc) - if csid == 274 then - local selectiontype = bit.band(option, 0xF) - - -- Taken assault mission - if - selectiontype == 1 and - npcUtil.giveKeyItem(player, xi.ki.MAMOOL_JA_ASSAULT_ORDERS) - then - player:addAssault(bit.rshift(option, 4)) - player:delKeyItem(xi.ki.IMPERIAL_ARMY_ID_TAG) - player:addKeyItem(xi.ki.MAP_OF_THE_TRAINING_GROUNDS) - - -- Purchased an item - elseif selectiontype == 2 then - local choice = items[bit.rshift(option, 14)] - if not choice then - return - end - - local currency = player:getCurrency('mamool_assault_point') - if currency < choice.price then - return - end - - if npcUtil.giveItem(player, choice.itemId) then - player:delCurrency('mamool_assault_point', choice.price) - end - end - end + xi.assault.onMissionGiverEventFinish(player, csid, option, npc, 274, xi.assault.assaultArea.MAMOOL_JA_TRAINING_GROUNDS) end return entity diff --git a/scripts/zones/Aht_Urhgan_Whitegate/npcs/Lageegee.lua b/scripts/zones/Aht_Urhgan_Whitegate/npcs/Lageegee.lua index 47723e6c00d..276222ac6a5 100644 --- a/scripts/zones/Aht_Urhgan_Whitegate/npcs/Lageegee.lua +++ b/scripts/zones/Aht_Urhgan_Whitegate/npcs/Lageegee.lua @@ -7,86 +7,16 @@ ---@type TNpcEntity local entity = {} -local items = -{ - [ 1] = { itemId = xi.item.VISION_EARRING, price = 3000 }, - [ 2] = { itemId = xi.item.UNYIELDING_RING, price = 5000 }, - [ 3] = { itemId = xi.item.FORTIFIED_CHAIN, price = 8000 }, - [ 4] = { itemId = xi.item.RESOLUTE_BELT, price = 10000 }, - [ 5] = { itemId = xi.item.BUSHIDO_CAPE, price = 10000 }, - [ 6] = { itemId = xi.item.KHANJAR, price = 15000 }, - [ 7] = { itemId = xi.item.HOTARUMARU, price = 15000 }, - [ 8] = { itemId = xi.item.IMPERIAL_GUN, price = 15000 }, - [ 9] = { itemId = xi.item.AMIR_PUGGAREE, price = 20000 }, - [10] = { itemId = xi.item.PAHLUWAN_CRACKOWS, price = 20000 }, - [11] = { itemId = xi.item.YIGIT_GOMLEK, price = 20000 }, - [12] = { itemId = xi.item.CIPHER_OF_OVJANGS_ALTER_EGO, price = 3000 }, - [13] = { itemId = xi.item.CIPHER_OF_MNEJINGS_ALTER_EGO, price = 3000 }, -} - entity.onTrigger = function(player, npc) - local rank = xi.besieged.getMercenaryRank(player) - local haveimperialIDtag = player:hasKeyItem(xi.ki.IMPERIAL_ARMY_ID_TAG) and 1 or 0 - local assaultPoints = player:getCurrency('periqia_assault_point') - local cipher = 0 - local active = xi.extravaganza.campaignActive() - - if - active == xi.extravaganza.campaign.SPRING_FALL or - active == xi.extravaganza.campaign.BOTH - then - cipher = 1 - end - - if rank > 0 then - player:startEvent(276, rank, haveimperialIDtag, assaultPoints, player:getCurrentAssault(), cipher) - else - player:startEvent(282) - end + xi.assault.onMissionGiverTrigger(player, npc, 276, xi.assault.assaultArea.PERIQIA) end entity.onEventUpdate = function(player, csid, option, npc) - local selectiontype = bit.band(option, 0xF) - if csid == 276 and selectiontype == 2 then - local item = bit.rshift(option, 14) - local choice = items[item] - local assaultPoints = player:getCurrency('periqia_assault_point') - local canEquip = player:canEquipItem(choice.itemId) and 2 or 0 - - player:updateEvent(0, 0, assaultPoints, 0, canEquip) - end + xi.assault.onMissionGiverUpdate(player, csid, option, npc, 276, xi.assault.assaultArea.PERIQIA) end entity.onEventFinish = function(player, csid, option, npc) - if csid == 276 then - local selectiontype = bit.band(option, 0xF) - - -- Taken assault mission - if - selectiontype == 1 and - npcUtil.giveKeyItem(player, xi.ki.PERIQIA_ASSAULT_ORDERS) - then - player:addAssault(bit.rshift(option, 4)) - player:delKeyItem(xi.ki.IMPERIAL_ARMY_ID_TAG) - player:addKeyItem(xi.ki.MAP_OF_PERIQIA) - - -- Purchased an item - elseif selectiontype == 2 then - local choice = items[bit.rshift(option, 14)] - if not choice then - return - end - - local currency = player:getCurrency('periqia_assault_point') - if currency < choice.price then - return - end - - if npcUtil.giveItem(player, choice.itemId) then - player:delCurrency('periqia_assault_point', choice.price) - end - end - end + xi.assault.onMissionGiverEventFinish(player, csid, option, npc, 276, xi.assault.assaultArea.PERIQIA) end return entity diff --git a/scripts/zones/Aht_Urhgan_Whitegate/npcs/Rytaal.lua b/scripts/zones/Aht_Urhgan_Whitegate/npcs/Rytaal.lua index 51cc6f66d40..77b0e3526f4 100644 --- a/scripts/zones/Aht_Urhgan_Whitegate/npcs/Rytaal.lua +++ b/scripts/zones/Aht_Urhgan_Whitegate/npcs/Rytaal.lua @@ -3,141 +3,15 @@ -- NPC: Rytaal -- !pos 112.002 -1.338 -45.038 50 ----------------------------------- -local ID = zones[xi.zone.AHT_URHGAN_WHITEGATE] ------------------------------------ ---@type TNpcEntity local entity = {} entity.onTrigger = function(player, npc) - local currentAssault = player:getCurrentAssault() - - if - player:getCurrentMission(xi.mission.log_id.TOAU) <= xi.mission.id.toau.IMMORTAL_SENTRIES or - player:getMainLvl() <= 49 - then - player:startEvent(270) - elseif currentAssault ~= 0 and player:getCharVar('assaultEntered') ~= 0 then - if player:getCharVar('AssaultComplete') == 1 then - player:messageText(player, ID.text.ASSAULT_COMPLETE) - player:completeAssault(currentAssault) - elseif currentAssault == 51 then - player:messageText(player, ID.text.NYZUL_FAIL) - player:delAssault(currentAssault) - else - player:addAssaultPoint(xi.assault.getAssaultArea(player), 100) - player:messageText(player, ID.text.ASSAULT_FAILED) - player:delAssault(currentAssault) - end - - player:setCharVar('AssaultComplete', 0) - player:setCharVar('assaultEntered', 0) - player:setCharVar('Assault_Armband', 0) - - for _, orders in pairs(xi.assault.assaultOrders) do - if player:hasKeyItem(orders) then - player:delKeyItem(orders) - end - end - - for maps = xi.ki.MAP_OF_LEUJAOAM_SANCTUM, xi.ki.MAP_OF_NYZUL_ISLE do - if player:hasKeyItem(maps) then - player:delKeyItem(maps) - end - end - elseif - player:getCurrentMission(xi.mission.log_id.TOAU) > xi.mission.id.toau.PRESIDENT_SALAHEEM or - (player:getCurrentMission(xi.mission.log_id.TOAU) == xi.mission.id.toau.PRESIDENT_SALAHEEM and - player:getCharVar('ToAU3Progress') >= 1) - then - local currentTime = GetSystemTime() - local refreshTime = player:getCharVar('nextTagTime') - local idTagPeriod = 86400 - - if player:hasKeyItem(xi.ki.RHAPSODY_IN_AZURE) then - idTagPeriod = 600 - end - - local diffPeriod = math.floor((currentTime - refreshTime) / idTagPeriod) - local tagStock = player:getCurrency('id_tags') - local allTagsTimeCS = (refreshTime - 1009897200) + (diffPeriod * idTagPeriod) - local haveimperialIDtag = 0 - local tagsAvail = 0 - - while currentTime >= refreshTime and tagStock < 3 do - refreshTime = refreshTime + idTagPeriod - tagStock = tagStock + 1 - end - - player:setCurrency('id_tags', tagStock) - player:setCharVar('nextTagTime', refreshTime) - - if player:hasKeyItem(xi.ki.IMPERIAL_ARMY_ID_TAG) then - haveimperialIDtag = 1 - end - - if tagStock > 0 then - tagsAvail = 1 - end - - player:startEvent(268, 2, tagStock, currentAssault, haveimperialIDtag, allTagsTimeCS, tagsAvail) - else - -- Something went worng, clear all data - player:setCharVar('AssaultComplete', 0) - player:setCharVar('assaultEntered', 0) - player:setCharVar('Assault_Armband', 0) - player:delAssault(currentAssault) - for _, orders in pairs(xi.assault.assaultOrders) do - if player:hasKeyItem(orders) then - player:delKeyItem(orders) - end - end - end + xi.assault.onRytaalTrigger(player, npc) end entity.onEventFinish = function(player, csid, option, npc) - local tagStock = player:getCurrency('id_tags') - - if - csid == 268 and - option == 1 and - not player:hasKeyItem(xi.ki.IMPERIAL_ARMY_ID_TAG) and - tagStock > 0 - then - if player:getCurrentAssault() ~= 0 then - player:messageSpecial(ID.text.CANNOT_ISSUE_TAG, xi.ki.IMPERIAL_ARMY_ID_TAG) - return - end - - npcUtil.giveKeyItem(player, xi.ki.IMPERIAL_ARMY_ID_TAG) - - local idTagPeriod = 86400 - - if player:hasKeyItem(xi.ki.RHAPSODY_IN_AZURE) then - idTagPeriod = 600 - end - - if tagStock >= 3 then - player:setCharVar('nextTagTime', GetSystemTime() + idTagPeriod) - end - - player:setCurrency('id_tags', tagStock - 1) - elseif - csid == 268 and - option == 2 and - xi.assault.hasOrders(player) and - not player:hasKeyItem(xi.ki.IMPERIAL_ARMY_ID_TAG) - then - local currentAssault = player:getCurrentAssault() - - for _, orders in pairs(xi.assault.assaultOrders) do - if player:hasKeyItem(orders) then - player:delKeyItem(orders) - end - end - - npcUtil.giveKeyItem(player, xi.ki.IMPERIAL_ARMY_ID_TAG) - player:delAssault(currentAssault) - end + xi.assault.onRytaalEventFinish(player, csid, option, npc) end return entity diff --git a/scripts/zones/Aht_Urhgan_Whitegate/npcs/Yahsra.lua b/scripts/zones/Aht_Urhgan_Whitegate/npcs/Yahsra.lua index 61b4e0ccabf..1f6e0e1a813 100644 --- a/scripts/zones/Aht_Urhgan_Whitegate/npcs/Yahsra.lua +++ b/scripts/zones/Aht_Urhgan_Whitegate/npcs/Yahsra.lua @@ -7,86 +7,16 @@ ---@type TNpcEntity local entity = {} -local items = -{ - [ 1] = { itemId = xi.item.STOIC_EARRING, price = 3000 }, - [ 2] = { itemId = xi.item.UNFETTERED_RING, price = 5000 }, - [ 3] = { itemId = xi.item.TEMPERED_CHAIN, price = 8000 }, - [ 4] = { itemId = xi.item.POTENT_BELT, price = 10000 }, - [ 5] = { itemId = xi.item.MIRACULOUS_CAPE, price = 10000 }, - [ 6] = { itemId = xi.item.YIGIT_BULAWA, price = 10000 }, - [ 7] = { itemId = xi.item.IMPERIAL_BHUJ, price = 15000 }, - [ 8] = { itemId = xi.item.PAHLUWAN_PATAS, price = 15000 }, - [ 9] = { itemId = xi.item.AMIR_KOLLUKS, price = 15000 }, - [10] = { itemId = xi.item.PAHLUWAN_QALANSUWA, price = 20000 }, - [11] = { itemId = xi.item.YIGIT_SERAWEELS, price = 20000 }, - [12] = { itemId = xi.item.CIPHER_OF_OVJANGS_ALTER_EGO, price = 3000 }, - [13] = { itemId = xi.item.CIPHER_OF_MNEJINGS_ALTER_EGO, price = 3000 }, -} - entity.onTrigger = function(player, npc) - local rank = xi.besieged.getMercenaryRank(player) - local haveimperialIDtag = player:hasKeyItem(xi.ki.IMPERIAL_ARMY_ID_TAG) and 1 or 0 - local assaultPoints = player:getCurrency('leujaoam_assault_point') - local cipher = 0 - local active = xi.extravaganza.campaignActive() - - if - active == xi.extravaganza.campaign.SPRING_FALL or - active == xi.extravaganza.campaign.BOTH - then - cipher = 1 - end - - if rank > 0 then - player:startEvent(273, rank, haveimperialIDtag, assaultPoints, player:getCurrentAssault(), cipher) - else - player:startEvent(279) - end + xi.assault.onMissionGiverTrigger(player, npc, 273, xi.assault.assaultArea.LEUJAOAM_SANCTUM) end entity.onEventUpdate = function(player, csid, option, npc) - local selectiontype = bit.band(option, 0xF) - if csid == 273 and selectiontype == 2 then - local item = bit.rshift(option, 14) - local choice = items[item] - local assaultPoints = player:getCurrency('leujaoam_assault_point') - local canEquip = player:canEquipItem(choice.itemId) and 2 or 0 - - player:updateEvent(0, 0, assaultPoints, 0, canEquip) - end + xi.assault.onMissionGiverUpdate(player, csid, option, npc, 273, xi.assault.assaultArea.LEUJAOAM_SANCTUM) end entity.onEventFinish = function(player, csid, option, npc) - if csid == 273 then - local selectiontype = bit.band(option, 0xF) - - -- Taken assault mission - if - selectiontype == 1 and - npcUtil.giveKeyItem(player, xi.ki.LEUJAOAM_ASSAULT_ORDERS) - then - player:addAssault(bit.rshift(option, 4)) - player:delKeyItem(xi.ki.IMPERIAL_ARMY_ID_TAG) - player:addKeyItem(xi.ki.MAP_OF_LEUJAOAM_SANCTUM) - - -- Purchased an item - elseif selectiontype == 2 then - local choice = items[bit.rshift(option, 14)] - if not choice then - return - end - - local currency = player:getCurrency('leujaoam_assault_point') - if currency < choice.price then - return - end - - if npcUtil.giveItem(player, choice.itemId) then - player:delCurrency('leujaoam_assault_point', choice.price) - end - end - end + xi.assault.onMissionGiverEventFinish(player, csid, option, npc, 273, xi.assault.assaultArea.LEUJAOAM_SANCTUM) end return entity diff --git a/src/map/instance.cpp b/src/map/instance.cpp index 9116d11c955..8473a57fd5d 100644 --- a/src/map/instance.cpp +++ b/src/map/instance.cpp @@ -19,6 +19,7 @@ =========================================================================== */ +#include #include #include "instance.h" @@ -108,10 +109,17 @@ void CInstance::LoadInstance() // Add to Lua cache // TODO: This will happen more often than needed, but not so often that it's a performance concern - const auto zone = m_zone->getName(); - const auto name = m_instanceName; - const auto filename = fmt::format("./scripts/zones/{}/instances/{}.lua", zone, name); - luautils::CacheLuaObjectFromFile(filename); + const auto zone = m_zone->getName(); + const auto name = m_instanceName; + const auto assaultPath = fmt::format("./scripts/assaults/{}/{}.lua", zone, name); + if (std::filesystem::exists(assaultPath)) + { + luautils::CacheLuaObjectFromFile(assaultPath, true); + } + else + { + luautils::CacheLuaObjectFromFile(fmt::format("./scripts/zones/{}/instances/{}.lua", zone, name)); + } } else { diff --git a/src/map/lua/luautils.cpp b/src/map/lua/luautils.cpp index 85745a49557..a77a804ba4c 100644 --- a/src/map/lua/luautils.cpp +++ b/src/map/lua/luautils.cpp @@ -98,6 +98,7 @@ #include "zone_entities.h" #include +#include #include #include #include @@ -4631,10 +4632,8 @@ void AfterInstanceRegister(CBaseEntity* PChar) TracyZoneScoped; - auto zone = PChar->loc.zone->getName(); - auto instance = PChar->PInstance->GetName(); - - auto afterInstanceRegister = lua["xi"]["zones"][zone]["instances"][instance]["afterInstanceRegister"]; + auto instanceData = instanceutils::GetInstanceData(PChar->PInstance->GetID()); + auto afterInstanceRegister = GetCacheEntryFromFilename(instanceData.filename)["afterInstanceRegister"]; if (!afterInstanceRegister.valid()) { return; @@ -4742,10 +4741,8 @@ void OnInstanceCreated(CInstance* PInstance) { TracyZoneScoped; - auto zone = PInstance->GetZone()->getName(); - auto name = PInstance->GetName(); - - auto onInstanceCreated = lua["xi"]["zones"][zone]["instances"][name]["onInstanceCreated"]; + auto instanceData = instanceutils::GetInstanceData(PInstance->GetID()); + auto onInstanceCreated = GetCacheEntryFromFilename(instanceData.filename)["onInstanceCreated"]; if (!onInstanceCreated.valid()) { return; @@ -4763,10 +4760,8 @@ void OnInstanceProgressUpdate(CInstance* PInstance) { TracyZoneScoped; - auto zone = PInstance->GetZone()->getName(); - auto name = PInstance->GetName(); - - auto onInstanceProgressUpdate = lua["xi"]["zones"][zone]["instances"][name]["onInstanceProgressUpdate"]; + auto instanceData = instanceutils::GetInstanceData(PInstance->GetID()); + auto onInstanceProgressUpdate = GetCacheEntryFromFilename(instanceData.filename)["onInstanceProgressUpdate"]; if (!onInstanceProgressUpdate.valid()) { return; @@ -4785,10 +4780,8 @@ void OnInstanceStageChange(CInstance* PInstance) { TracyZoneScoped; - auto zone = PInstance->GetZone()->getName(); - auto name = PInstance->GetName(); - - auto onInstanceStageChange = lua["xi"]["zones"][zone]["instances"][name]["onInstanceStageChange"]; + auto instanceData = instanceutils::GetInstanceData(PInstance->GetID()); + auto onInstanceStageChange = GetCacheEntryFromFilename(instanceData.filename)["onInstanceStageChange"]; if (!onInstanceStageChange.valid()) { return; @@ -4806,10 +4799,8 @@ void OnInstanceComplete(CInstance* PInstance) { TracyZoneScoped; - auto zone = PInstance->GetZone()->getName(); - auto name = PInstance->GetName(); - - auto onInstanceComplete = lua["xi"]["zones"][zone]["instances"][name]["onInstanceComplete"]; + auto instanceData = instanceutils::GetInstanceData(PInstance->GetID()); + auto onInstanceComplete = GetCacheEntryFromFilename(instanceData.filename)["onInstanceComplete"]; if (!onInstanceComplete.valid()) { return; diff --git a/src/map/utils/instanceutils.cpp b/src/map/utils/instanceutils.cpp index e825a4eade0..2beb0f7f129 100644 --- a/src/map/utils/instanceutils.cpp +++ b/src/map/utils/instanceutils.cpp @@ -21,6 +21,8 @@ #include "instanceutils.h" +#include + #include "common/database.h" #include "common/logging.h" @@ -115,13 +117,23 @@ auto LoadInstances(const std::vector& instanceIds) -> void // Meta data data.instance_zone_name = rset->get("zone_name"); data.entrance_zone_name = rset->get("zone_name"); - data.filename = fmt::format("./scripts/zones/{}/instances/{}.lua", data.instance_zone_name, data.instance_name); + + // Determine if instance exists at new assault path + data.filename = fmt::format("./scripts/assaults/{}/{}.lua", data.instance_zone_name, data.instance_name); + if (std::filesystem::exists(data.filename)) + { + luautils::CacheLuaObjectFromFile(data.filename, true); + } + + // If not, fall back to regular instance path + else + { + data.filename = fmt::format("./scripts/zones/{}/instances/{}.lua", data.instance_zone_name, data.instance_name); + luautils::CacheLuaObjectFromFile(data.filename); + } // Add to data cache InstanceData[data.id] = data; - - // Add to Lua cache - luautils::CacheLuaObjectFromFile(data.filename); } } diff --git a/src/map/utils/mobutils.cpp b/src/map/utils/mobutils.cpp index a39f2acea7c..4feef5be5ee 100644 --- a/src/map/utils/mobutils.cpp +++ b/src/map/utils/mobutils.cpp @@ -1366,6 +1366,7 @@ void SetupBattlefieldMob(CMobEntity* PMob) // do not roam around PMob->setMobMod(MOBMOD_ROAM_RESET_FACING, 1); + PMob->setMobMod(MOBMOD_ROAM_DISTANCE, 0); PMob->m_maxRoamDistance = 0.0f; if ((PMob->m_bcnmID != 864) && (PMob->m_bcnmID != 704) && (PMob->m_bcnmID != 706)) { diff --git a/src/map/utils/zoneutils.cpp b/src/map/utils/zoneutils.cpp index b31be88e223..e93c9ab6384 100644 --- a/src/map/utils/zoneutils.cpp +++ b/src/map/utils/zoneutils.cpp @@ -1359,7 +1359,9 @@ void AfterZoneIn(CBaseEntity* PEntity) return; } - if (!PChar->PBattlefield || !PChar->PBattlefield->isEntered(PChar)) + const bool inBattlefield = PChar->PBattlefield && PChar->PBattlefield->isEntered(PChar); + const bool inCappedInstance = PChar->PInstance && PChar->PInstance->GetLevelCap() > 0; + if (!inBattlefield && !inCappedInstance) { GetZone(PChar->getZone())->updateCharLevelRestriction(PChar); } diff --git a/tools/ci/sanity_checks/lua.sh b/tools/ci/sanity_checks/lua.sh index 186e7420dd0..1b7796fc804 100755 --- a/tools/ci/sanity_checks/lua.sh +++ b/tools/ci/sanity_checks/lua.sh @@ -78,6 +78,7 @@ global_objects=( BattlefieldQuest Limbus SeasonalEvent + InstanceAssault onBattlefieldHandlerInitialize applyResistanceAddEffect diff --git a/tools/ci/sanity_checks/lua_binding_usage.py b/tools/ci/sanity_checks/lua_binding_usage.py index 17b39f7c547..27408dcf0ab 100644 --- a/tools/ci/sanity_checks/lua_binding_usage.py +++ b/tools/ci/sanity_checks/lua_binding_usage.py @@ -114,6 +114,11 @@ def main(): function_names.append("find") function_names.append("sub") function_names.append("getStatusEffectBySource") + function_names.append("afterInstanceRegister") + function_names.append("onInstanceCreated") + function_names.append("onAssaultFail") + function_names.append("onInstanceProgressUpdate") + function_names.append("onInstanceComplete") # root_dir needs a trailing slash (i.e. /root/dir/) for filename in glob.iglob("./scripts/" + "**/*.lua", recursive=True):