----------------------------------- -- Dynamis ----------------------------------- require('scripts/globals/battlefield') require('scripts/globals/missions') require('scripts/globals/npc_util') ----------------------------------- xi = xi or {} xi.dynamis = xi.dynamis or {} local entryInfo = { --[[ [zone] = { csBit = the bit in the Dynamis_Status player variable that records whether player has beaten this dynamis this bit number is also given to the start Dynamis event and message. csVial = event ID for cutscene where Cornelia gives you the vial of shrouded sand csBeat = event ID for cutscene after you have beaten this Dynamis csMenu = event ID for entering Dynamis beatVar = variable used to denote players who have beaten this Dynamis, but not yet viewed the cutscene beatKI = key item given as reward for this Dynamis enterPos = coordinates where player will be placed when entering this Dynamis reqs = function that returns true if player meets requirements for entering this Dynamis minimum level and timer are checked separately } --]] [xi.zone.SOUTHERN_SAN_DORIA] = { csBit = 1, csVial = 686, csFirst = 692, csBeat = 698, csMenu = 961, beatVar = 'DynaSandoria_Win', beatKI = xi.ki.HYDRA_CORPS_COMMAND_SCEPTER, enterPos = { 161.838, -2.000, 161.673, 93, 185 }, }, [xi.zone.BASTOK_MINES] = { csBit = 2, csVial = 203, csFirst = 209, csBeat = 215, csMenu = 597, beatVar = 'DynaBastok_Win', beatKI = xi.ki.HYDRA_CORPS_EYEGLASS, enterPos = { 116.482, 0.994, -72.121, 128, 186 }, }, [xi.zone.WINDURST_WALLS] = { csBit = 3, csVial = 455, csFirst = 459, csBeat = 465, csMenu = 513, beatVar = 'DynaWindurst_Win', beatKI = xi.ki.HYDRA_CORPS_LANTERN, enterPos = { -221.988, 1.000, -120.184, 0, 187 }, }, [xi.zone.RULUDE_GARDENS] = { csBit = 4, csVial = 10016, csFirst = 10020, csBeat = 10026, csMenu = 10176, beatVar = 'DynaJeuno_Win', beatKI = xi.ki.HYDRA_CORPS_TACTICAL_MAP, enterPos = { 48.930, 10.002, -71.032, 195, 188 }, }, [xi.zone.BEAUCEDINE_GLACIER] = { csBit = 5, csFirst = 128, csBeat = 134, csMenu = 229, beatVar = 'DynaBeaucedine_Win', beatKI = xi.ki.HYDRA_CORPS_INSIGNIA, enterPos = { -284.751, -39.923, -422.948, 235, 134 }, reqs = function(player) return player:hasKeyItem(xi.ki.HYDRA_CORPS_COMMAND_SCEPTER) and player:hasKeyItem(xi.ki.HYDRA_CORPS_EYEGLASS) and player:hasKeyItem(xi.ki.HYDRA_CORPS_LANTERN) and player:hasKeyItem(xi.ki.HYDRA_CORPS_TACTICAL_MAP) end, }, [xi.zone.XARCABARD] = { csBit = 6, csFirst = 26, csBeat = 32, csMenu = 205, beatVar = 'DynaXarcabard_Win', beatKI = xi.ki.HYDRA_CORPS_BATTLE_STANDARD, enterPos = { 569.312, -0.098, -270.158, 90, 135 }, reqs = function(player) return player:hasKeyItem(xi.ki.HYDRA_CORPS_INSIGNIA) end, }, -- TODO: Make absolutely sure that winning Xarcabard does NOT allow early access to dreamlands BEFORE CoP 3-5 [xi.zone.VALKURM_DUNES] = { csBit = 7, csFirst = 33, csBeat = 39, csMenu = 58, beatVar = 'DynaValkurm_Win', beatKI = xi.ki.DYNAMIS_VALKURM_SLIVER, enterPos = { 100, -8, 131, 47, 39 }, reqs = function(player) return player:hasCompletedMission(xi.mission.log_id.COP, xi.mission.id.cop.DARKNESS_NAMED) or xi.settings.main.FREE_COP_DYNAMIS == 1 end, }, [xi.zone.BUBURIMU_PENINSULA] = { csBit = 8, csFirst = 40, csBeat = 46, csMenu = 64, beatVar = 'DynaBuburimu_Win', beatKI = xi.ki.DYNAMIS_BUBURIMU_SLIVER, enterPos = { 155, -1, -169, 170, 40 }, reqs = function(player) return player:hasCompletedMission(xi.mission.log_id.COP, xi.mission.id.cop.DARKNESS_NAMED) or xi.settings.main.FREE_COP_DYNAMIS == 1 end, }, [xi.zone.QUFIM_ISLAND] = { csBit = 9, csFirst = 22, csBeat = 28, csMenu = 48, beatVar = 'DynaQufim_Win', beatKI = xi.ki.DYNAMIS_QUFIM_SLIVER, enterPos = { -19, -17, 104, 253, 41 }, reqs = function(player) return player:hasCompletedMission(xi.mission.log_id.COP, xi.mission.id.cop.DARKNESS_NAMED) or xi.settings.main.FREE_COP_DYNAMIS == 1 end, }, [xi.zone.TAVNAZIAN_SAFEHOLD] = { csBit = 10, csFirst = 614, -- TODO: Tavnazian winning CS changes Param2 (part of dialogue) depending on CoP progress ranging from 0 ('she will pay dearly') up to 3 ('you know this girl well') -- The exact mission breaking points for each bit need captures or testimonies; Param1 seems to be 0 or garbage csBeat = 615, csMenu = 624, beatVar = 'DynaTavnazia_Win', beatKI = xi.ki.DYNAMIS_TAVNAZIA_SLIVER, enterPos = { 0.1, -7, -21, 190, 42 }, reqs = function(player) return player:hasKeyItem(xi.ki.DYNAMIS_BUBURIMU_SLIVER) and player:hasKeyItem(xi.ki.DYNAMIS_QUFIM_SLIVER) and player:hasKeyItem(xi.ki.DYNAMIS_VALKURM_SLIVER) end, }, } local dynaInfo = { --[[ [zone] = { entryPos = default entry coordinates (used for GMs warping to zone, for example) ejectPos = coordinates outside, where players will be sent when running out of time or using Somnial Threshold } --]] [xi.zone.DYNAMIS_SAN_DORIA] = { beatVar = 'DynaSandoria_Win', beatKI = xi.ki.HYDRA_CORPS_COMMAND_SCEPTER, beatTitle = xi.title.DYNAMIS_SAN_DORIA_INTERLOPER, entryPos = { 161.838, -2.000, 161.673, 93 }, ejectPos = { 161.000, -2.000, 161.000, 94, 230 }, }, [xi.zone.DYNAMIS_BASTOK] = { beatVar = 'DynaBastok_Win', beatKI = xi.ki.HYDRA_CORPS_EYEGLASS, beatTitle = xi.title.DYNAMIS_BASTOK_INTERLOPER, entryPos = { 116.482, 0.994, -72.121, 128 }, ejectPos = { 112.000, 0.994, -72.000, 127, 234 }, }, [xi.zone.DYNAMIS_WINDURST] = { beatVar = 'DynaWindurst_Win', beatKI = xi.ki.HYDRA_CORPS_LANTERN, beatTitle = xi.title.DYNAMIS_WINDURST_INTERLOPER, entryPos = { -221.988, 1.000, -120.184, 0 }, ejectPos = { -217.000, 1.000, -119.000, 94, 239 }, }, [xi.zone.DYNAMIS_JEUNO] = { beatVar = 'DynaJeuno_Win', beatKI = xi.ki.HYDRA_CORPS_TACTICAL_MAP, beatTitle = xi.title.DYNAMIS_JEUNO_INTERLOPER, entryPos = { 48.930, 10.002, -71.032, 195 }, ejectPos = { 48.930, 10.002, -71.032, 195, 243 }, }, [xi.zone.DYNAMIS_BEAUCEDINE] = { beatVar = 'DynaBeaucedine_Win', beatKI = xi.ki.HYDRA_CORPS_INSIGNIA, beatTitle = xi.title.DYNAMIS_BEAUCEDINE_INTERLOPER, entryPos = { -284.751, -39.923, -422.948, 235 }, ejectPos = { -284.751, -39.923, -422.948, 235, 111 }, }, [xi.zone.DYNAMIS_XARCABARD] = { beatVar = 'DynaXarcabard_Win', beatKI = xi.ki.HYDRA_CORPS_BATTLE_STANDARD, beatTitle = xi.title.DYNAMIS_XARCABARD_INTERLOPER, entryPos = { 569.312, -0.098, -270.158, 90 }, ejectPos = { 569.312, -0.098, -270.158, 90, 112 }, }, [xi.zone.DYNAMIS_VALKURM] = { beatVar = 'DynaValkurm_Win', beatKI = xi.ki.DYNAMIS_VALKURM_SLIVER, beatTitle = xi.title.DYNAMIS_VALKURM_INTERLOPER, entryPos = { 100, -8, 131, 47 }, ejectPos = { 119, -9, 131, 52, 103 }, }, [xi.zone.DYNAMIS_BUBURIMU] = { beatVar = 'DynaBuburimu_Win', beatKI = xi.ki.DYNAMIS_BUBURIMU_SLIVER, beatTitle = xi.title.DYNAMIS_BUBURIMU_INTERLOPER, entryPos = { 155, -1, -169, 170 }, ejectPos = { 154, -1, -170, 190, 118 }, }, [xi.zone.DYNAMIS_QUFIM] = { beatVar = 'DynaQufim_Win', beatKI = xi.ki.DYNAMIS_QUFIM_SLIVER, beatTitle = xi.title.DYNAMIS_QUFIM_INTERLOPER, entryPos = { -19, -17, 104, 253 }, ejectPos = { 18, -19, 162, 240, 126 }, }, [xi.zone.DYNAMIS_TAVNAZIA] = { beatVar = 'DynaTavnazia_Win', beatKI = xi.ki.DYNAMIS_TAVNAZIA_SLIVER, beatTitle = xi.title.DYNAMIS_TAVNAZIA_INTERLOPER, entryPos = { 0.1, -7, -21, 190 }, ejectPos = { 0, -7, -23, 195, 26 }, }, } ----------------------------------- -- local functions ----------------------------------- local function arg3(player, bit) local csVar = player:getCharVar('Dynamis_Status') local timeKI = player:hasKeyItem(xi.ki.RHAPSODY_IN_AZURE) and 65536 or 0 if csVar == 0 then return 1 + timeKI -- first time visiting any dynamis zone elseif not utils.mask.getBit(csVar, bit) then return 2 + timeKI -- first time visiting this dynamis zone else return 3 + timeKI -- have visited this dynamis zone more than once end end local function handleEntryTime(player) local realDay = GetSystemTime() if xi.settings.main.DYNA_MIDNIGHT_RESET then realDay = JstMidnight() - 86400 end local dynaWaitxDay = player:getCharVar('dynaWaitxDay') if (dynaWaitxDay + xi.settings.main.BETWEEN_2DYNA_WAIT_TIME * 60 * 60) < realDay and not player:hasKeyItem(xi.ki.RHAPSODY_IN_AZURE) then player:setCharVar('dynaWaitxDay', realDay) end end ----------------------------------- -- global functions ----------------------------------- xi.dynamis.entryNpcOnTrigger = function(player, npc) local zoneId = player:getZoneID() local info = entryInfo[zoneId] local ID = zones[zoneId] local dynaMask = player:getCharVar('Dynamis_Status') local unlockingDyna = utils.mask.getBit(dynaMask, 0) local tavnaziaFirst = false -- Tavnazia is unique; plays the first time cs directly on trigger without message or transporting if info.csBit == 10 and info.reqs(player) and not utils.mask.getBit(dynaMask, info.csBit) then player:startEvent(info.csFirst) player:setCharVar('Dynamis_Status', utils.mask.setBit(dynaMask, info.csBit, true)) -- set to skip menu after getting this CS tavnaziaFirst = not tavnaziaFirst -- player has access but is on a job below required level elseif player:hasKeyItem(xi.ki.PRISMATIC_HOURGLASS) and player:getMainLvl() < xi.settings.main.DYNA_LEVEL_MIN then player:messageSpecial(ID.text.PLAYERS_HAVE_NOT_REACHED_LEVEL) -- default message always prints except in cases above and not for shrouded sand or winning cs elseif not unlockingDyna and player:getCharVar(info.beatVar) ~= 1 then player:messageSpecial(ID.text.DYNA_NPC_DEFAULT_MESSAGE) end -- all cutscenes and menus are blocked behind base requirements; 'unlockingDyna' needs to be checked to access shroud cs after zoning into xarcabard if not tavnaziaFirst and player:getMainLvl() >= xi.settings.main.DYNA_LEVEL_MIN and (player:hasKeyItem(xi.ki.PRISMATIC_HOURGLASS) or unlockingDyna) then -- shrouded sand cutscene if unlockingDyna and info.csVial and not player:hasKeyItem(xi.ki.VIAL_OF_SHROUDED_SAND) then player:startEvent(info.csVial) -- victory cutscene elseif player:getCharVar(info.beatVar) == 1 then -- NOTE: The hourglass and shrouded sand parameter is only required for Beaucedine, but has no -- effect on the others. player:startEvent(info.csBeat, info.beatKI, 0, xi.ki.PRISMATIC_HOURGLASS , xi.ki.VIAL_OF_SHROUDED_SAND) -- dynamis entry elseif not info.reqs or info.reqs(player) then local realDay = GetSystemTime() local dynaWaitxDay = player:getCharVar('dynaWaitxDay') local sjobOption = info.csBit > 6 and 1 or 0 if (dynaWaitxDay + xi.settings.main.BETWEEN_2DYNA_WAIT_TIME * 60 * 60) < realDay then -- params: bit, cutscene option, Prismatic Hourglass KI, sJob option, junk, Shrouded Sand KI, Timeless Hourglass item ID, Perpetual Hourglass item ID player:startEvent(info.csMenu, info.csBit, arg3(player, info.csBit), xi.ki.PRISMATIC_HOURGLASS, sjobOption, 0, xi.ki.VIAL_OF_SHROUDED_SAND, 4236, 4237) else local dayRemaining = math.floor(((dynaWaitxDay + xi.settings.main.BETWEEN_2DYNA_WAIT_TIME * 60 * 60) - realDay) / 3456) player:messageSpecial(ID.text.YOU_CANNOT_ENTER_DYNAMIS, dayRemaining, info.csBit) end end end end xi.dynamis.entryNpcOnEventFinish = function(player, csid, option, npc) local info = entryInfo[player:getZoneID()] local dynaMask = player:getCharVar('Dynamis_Status') -- shrouded sand cutscene if info.csVial and csid == info.csVial then npcUtil.giveKeyItem(player, xi.ki.VIAL_OF_SHROUDED_SAND) player:setCharVar('Dynamis_Status', utils.mask.setBit(dynaMask, 0, false)) -- victory cutscene elseif csid == info.csBeat then player:setCharVar(info.beatVar, 0) -- dynamis entry; the check for csFirst is needed, see below elseif csid == info.csMenu and (option == 0 or option == 1) or csid == info.csFirst then player:setCharVar('Dynamis_subjob', option) player:setCharVar('Dynamis_Entry', 1) player:setCharVar('Dynamis_Status', utils.mask.setBit(dynaMask, info.csBit, true)) handleEntryTime(player) -- first visit cutscene plays after choosing to enter, except Tavnazia as seen above if not utils.mask.getBit(dynaMask, info.csBit) then player:startEvent(info.csFirst) -- this will loop back to this same block to trigger setPos end player:setPos(unpack(info.enterPos)) end end xi.dynamis.zoneOnInitialize = function(zone) local zoneId = zone:getID() local ID = zones[zoneId] local timeExtensionMobs = ID.mob.TIME_EXTENSION local refillMobs = ID.mob.REFILL_STATUE -- spawn one of each grouped TEs if timeExtensionMobs then for _, v in pairs(timeExtensionMobs) do local group = {} if type(v.mob) == 'number' then group = { v.mob } elseif type(v.mob) == 'table' then group = { unpack(v.mob) } end local teId = group[math.random(1, #group)] DisallowRespawn(teId, false) SpawnMob(teId) end end -- spawn one of each grouped refill statue if refillMobs then for _, g in pairs(refillMobs) do local group = {} for _, m in pairs(g) do table.insert(group, m.mob) end local spawnId = group[math.random(1, #group)] DisallowRespawn(spawnId, false) SpawnMob(spawnId) end end for _, mob in pairs(zone:getMobs()) do mob:setCarefulPathing(true) end end xi.dynamis.zoneOnZoneIn = function(player, prevZone) local zoneId = player:getZoneID() local info = dynaInfo[zoneId] local ID = zones[zoneId] local cs = -1 if player:getCharVar('Dynamis_Entry') == 1 or player:getGMLevel() > 0 then if player:getCharVar('Dynamis_subjob') == 1 then player:addStatusEffect(xi.effect.SJ_RESTRICTION, { origin = player }) end player:addStatusEffect(xi.effect.DYNAMIS, { duration = 3600, origin = player, tick = 3, icon = 0 }) player:timer(5500, function(playerArg) playerArg:messageSpecial(ID.text.DYNAMIS_TIME_BEGIN, 60, xi.ki.PRISMATIC_HOURGLASS) end) player:setCharVar('Dynamis_Entry', 0) player:setCharVar('Dynamis_subjob', 0) end if not player:hasStatusEffect(xi.effect.DYNAMIS) then cs = 100 -- eject event (same event in all dynamis zones) elseif player:getXPos() == 0 and player:getYPos() == 0 and player:getZPos() == 0 then player:setPos(unpack(info.entryPos)) end return cs end xi.dynamis.zoneOnEventFinish = function(player, csid, option, npc) local zoneId = player:getZoneID() local info = dynaInfo[zoneId] if csid == 100 then player:setPos(unpack(info.ejectPos)) end end xi.dynamis.somnialThresholdOnTrigger = function(player, npc) -- ability to unlock SJ message local canUnlockSJ = player:hasStatusEffect(xi.effect.SJ_RESTRICTION) and 1 or 0 -- bitmask controls options in the menu. 1 = Leave Dynamis. 2 = Unlock support jobs. 4 = Nothing (quit menu) local menuBits = 5 + (canUnlockSJ * 2) player:startEvent(101, 0x27, canUnlockSJ, menuBits) end xi.dynamis.somnialThresholdOnEventFinish = function(player, csid, option, npc) local zoneId = player:getZoneID() local info = dynaInfo[zoneId] local ID = zones[zoneId] if csid == 100 then player:setPos(unpack(info.ejectPos)) elseif option == 1 then player:startEvent(100) elseif option == 2 then player:messageSpecial(ID.text.DYNAMIS_SUB_UNLOCKED) player:delStatusEffectSilent(xi.effect.SJ_RESTRICTION) end end xi.dynamis.megaBossOnDeath = function(mob, player, optParams) local zoneId = player:getZoneID() local info = dynaInfo[zoneId] player:addTitle(info.beatTitle) if not player:hasKeyItem(info.beatKI) then npcUtil.giveKeyItem(player, info.beatKI) player:setCharVar(info.beatVar, 1) end end xi.dynamis.timeExtensionOnDeath = function(mob, player, optParams) local mobId = mob:getID() local zoneId = mob:getZoneID() local ID = zones[zoneId] local timeExtensionMobs = ID.mob.TIME_EXTENSION if timeExtensionMobs then local found = false local te = nil local group = {} -- find this TE's group for _, t in pairs(timeExtensionMobs) do if type(t.mob) == 'number' then group = { t.mob } elseif type(t.mob) == 'table' then group = { unpack(t.mob) } end for _, g in pairs(group) do if g == mobId then found = true te = t break end end if found then break end end -- TODO: Refactor the above loops to not need the 'found' variable, and only use -- non-nil te value. if found and te then -- award KI and extension to those who have not yet received it local effect = player:getStatusEffect(xi.effect.DYNAMIS) if effect and not player:hasKeyItem(te.ki) then npcUtil.giveKeyItem(player, te.ki) local oldDuration = effect:getDuration() effect:setDuration(oldDuration + te.minutes * 60 * 1000) player:setLocalVar('dynamis_lasttimeupdate', effect:getTimeRemaining() / 1000) player:messageSpecial(ID.text.DYNAMIS_TIME_EXTEND, te.minutes) end -- spawn a new mob in this group if optParams.isKiller then local teId = group[math.random(1, #group)] if teId ~= mobId then DisallowRespawn(mobId, true) DisallowRespawn(teId, false) end GetMobByID(teId):setRespawnTime(85) end else printf('[xi.dynamis.timeExtensionOnDeath] called in zone %i on mob %s that does not appear in a time extension group.', zoneId, mob:getName()) end else printf('[xi.dynamis.timeExtensionOnDeath] called on mob %s in zone %i that does not have a TIME_EXTENSION table in its IDs.', mob:getName(), zoneId) end end xi.dynamis.refillStatueOnSpawn = function(mob) local mobId = mob:getID() local zoneId = mob:getZoneID() local ID = zones[zoneId] local refillMobs = ID.mob.REFILL_STATUE if refillMobs then local found = false -- set this statue's eye color for _, g in pairs(refillMobs) do for _, m in pairs(g) do if m.mob == mobId then found = true mob:setAnimationSub(m.eye) break end end if found then break end end if not found then printf('[xi.dynamis.refillStatueOnSpawn] called in zone %i on mob %i that does not appear in a refill statue group.', zoneId, mobId) end else printf('[xi.dynamis.refillStatueOnSpawn] called on mob %i in zone %i that does not have a REFILL_STATUE table in its IDs.', mobId, zoneId) end end xi.dynamis.refillStatueOnDeath = function(mob, player, optParams) local mobId = mob:getID() local zoneId = mob:getZoneID() local ID = zones[zoneId] local refillMobs = ID.mob.REFILL_STATUE if refillMobs then local found = false local group = {} local eye = nil -- find this statue's group and eye color for _, g in pairs(refillMobs) do group = {} for _, m in pairs(g) do table.insert(group, m.mob) if m.mob == mobId then found = true eye = m.eye end end if found then break end end if found then if optParams.isKiller then -- MP or HP refill if eye == xi.dynamis.eye.BLUE or eye == xi.dynamis.eye.GREEN then local zone = mob:getZone() local players = zone:getPlayers() for name, playerObj in pairs(players) do if mob:checkDistance(playerObj) < 30 then if eye == xi.dynamis.eye.BLUE then local amt = playerObj:getMaxMP() - playerObj:getMP() playerObj:restoreMP(amt) playerObj:messageBasic(xi.msg.basic.RECOVERS_MP, 0, amt) else local amt = playerObj:getMaxHP() - playerObj:getHP() playerObj:restoreHP(amt) playerObj:messageBasic(xi.msg.basic.RECOVERS_HP, 0, amt) end end end end mob:setAnimationSub(xi.dynamis.eye.NONE) -- spawn a new mob in this group local nextId = group[math.random(1, #group)] if nextId ~= mobId then DisallowRespawn(mobId, true) DisallowRespawn(nextId, false) end GetMobByID(nextId):setRespawnTime(300) -- 5 minutes end else printf('[xi.dynamis.refillStatueOnDeath] called in zone %i on mob %i that does not appear in a refill statue group.', zoneId, mobId) end else printf('[xi.dynamis.refillStatueOnDeath] called on mob %i in zone %i that does not have a REFILL_STATUE table in its IDs.', mobId, zoneId) end end xi.dynamis.qmOnTrade = function(player, npc, trade) local npcId = npc:getID() local zoneId = npc:getZoneID() local ID = zones[zoneId] local qmNpcs = ID.npc.QM if qmNpcs then local info = qmNpcs[npcId] if info then for _, v in pairs(info.trade) do if npcUtil.tradeHasExactly(trade, v.item) then local mobId if type(v.mob) == 'table' then mobId = v.mob[math.random(1, #v.mob)] else mobId = v.mob end if mobId and npcUtil.popFromQM(player, npc, mobId, { hide = 0, radius = 2 }) then player:confirmTrade() end break end end else printf('[xi.dynamis.qmOnTrade] called on in zone %i on npc %i (%s) that does not appear in QM data.', zoneId, npcId, npc:getName()) end else printf('[xi.dynamis.qmOnTrade] called on npc %i (%s) in zone %i that does not have a QM group in its IDs.', npcId, npc:getName(), zoneId) end end xi.dynamis.qmOnTrigger = function(player, npc) local npcId = npc:getID() local zoneId = npc:getZoneID() local ID = zones[zoneId] local qmNpcs = ID.npc.QM if qmNpcs then local info = qmNpcs[npcId] if info then if info.param then player:startEvent(102, unpack(info.param)) elseif info.trade and #info.trade == 1 and info.trade[1].item and type(info.trade[1].item) == 'number' and ID.text.OMINOUS_PRESENCE then player:messageSpecial(ID.text.OMINOUS_PRESENCE, info.trade[1].item) end else printf('[xi.dynamis.qmOnTrigger] called on in zone %i on npc %i (%s) that does not appear in QM data.', zoneId, npcId, npc:getName()) end else printf('[xi.dynamis.qmOnTrigger] called on npc %i (%s) in zone %i that does not have a QM group in its IDs.', npcId, npc:getName(), zoneId) end end ----------------------------------- -- xi.dynamis.getDynamisMapList -- Produces a bitmask for the goblin ancient currency NPCs ----------------------------------- local dynamisMapKI = { xi.ki.MAP_OF_DYNAMIS_SAN_DORIA, xi.ki.MAP_OF_DYNAMIS_BASTOK, xi.ki.MAP_OF_DYNAMIS_WINDURST, xi.ki.MAP_OF_DYNAMIS_JEUNO, xi.ki.MAP_OF_DYNAMIS_BEAUCEDINE, xi.ki.MAP_OF_DYNAMIS_XARCABARD, xi.ki.MAP_OF_DYNAMIS_VALKURM, xi.ki.MAP_OF_DYNAMIS_BUBURIMU, xi.ki.MAP_OF_DYNAMIS_QUFIM, xi.ki.MAP_OF_DYNAMIS_TAVNAZIA, } xi.dynamis.getDynamisMapList = function(player) local bitmask = 0 for position, keyItem in ipairs(dynamisMapKI) do if player:hasKeyItem(keyItem) then bitmask = bitmask + bit.lshift(1, position) end end return bitmask end -- todo: fix these to use tables local function getExtensions(player) local count = 0 for i = xi.ki.CRIMSON_GRANULES_OF_TIME, xi.ki.OBSIDIAN_GRANULES_OF_TIME do if player:hasKeyItem(i) then count = count + 1 end end return count end xi.dynamis.procMonster = function(mob, player) if player and player:getAllegiance() == 1 then local master = player:getMaster() if master then player = master end local extensions = getExtensions(player) if extensions > 2 then if player:hasStatusEffect(xi.effect.SJ_RESTRICTION) and math.random(1, 100) == 1 then mob:setLocalVar('dynamis_proc', 4) mob:addStatusEffect(xi.effect.TERROR, { duration = 30, origin = player }) mob:weaknessTrigger(3) elseif extensions == 5 then mob:setLocalVar('dynamis_proc', 3) mob:addStatusEffect(xi.effect.TERROR, { duration = 30, origin = player }) mob:weaknessTrigger(2) elseif extensions == 4 then mob:setLocalVar('dynamis_proc', 2) mob:addStatusEffect(xi.effect.TERROR, { duration = 30, origin = player }) mob:weaknessTrigger(1) elseif extensions == 3 then mob:setLocalVar('dynamis_proc', 1) mob:addStatusEffect(xi.effect.TERROR, { duration = 30, origin = player }) mob:weaknessTrigger(0) end end end end xi.dynamis.hourglassAndCurrencyExchangeNPCLookup = { -- Haggleblix [xi.zone.BEADEAUX] = { baseCs = 130, currency = { xi.item.ONE_BYNE_BILL, xi.item.ONE_HUNDRED_BYNE_BILL, xi.item.TEN_THOUSAND_BYNE_BILL, }, shop = { 7, xi.item.LOCK_OF_SIRENS_HAIR, 8, xi.item.VIAL_OF_SLIME_JUICE, 9, xi.item.CHUNK_OF_WOOTZ_ORE, 12, xi.item.BOTTLE_OF_CANTARELLA, 20, xi.item.FLASK_OF_MARKSMANS_OIL, 25, xi.item.WOOTZ_INGOT, 33, xi.item.KOH_I_NOOR, } }, -- Antiquix [xi.zone.CASTLE_OZTROJA] = { baseCs = 50, currency = { xi.item.TUKUKU_WHITESHELL, xi.item.LUNGO_NANGO_JADESHELL, xi.item.RIMILALA_STRIPESHELL, }, shop = { 7, xi.item.PIECE_OF_ANGEL_SKIN, 8, xi.item.COLOSSAL_SKULL, 9, xi.item.LANCEWOOD_LOG, 23, xi.item.CHRONOS_TOOTH, 24, xi.item.CHUNK_OF_RELIC_STEEL, 25, xi.item.PIECE_OF_LANCEWOOD_LUMBER, 28, xi.item.DAMASCUS_INGOT, } }, -- Lootblox [xi.zone.DAVOI] = { baseCs = 130, currency = { xi.item.ORDELLE_BRONZEPIECE, xi.item.MONTIONT_SILVERPIECE, xi.item.RANPERRE_GOLDPIECE, }, shop = { 5, xi.item.TWINCOON, 6, xi.item.PILE_OF_RELIC_IRON, 7, xi.item.JAR_OF_GOBLIN_GREASE, 8, xi.item.GRIFFON_HIDE, 23, xi.item.SQUARE_OF_GRIFFON_LEATHER, 25, xi.item.BEHEMOTH_HORN, 28, xi.item.MAMMOTH_TUSK, } }, } xi.dynamis.mapShopCosts = { [xi.ki.MAP_OF_DYNAMIS_SAN_DORIA] = 10000, [xi.ki.MAP_OF_DYNAMIS_BASTOK] = 10000, [xi.ki.MAP_OF_DYNAMIS_WINDURST] = 10000, [xi.ki.MAP_OF_DYNAMIS_JEUNO] = 10000, [xi.ki.MAP_OF_DYNAMIS_BEAUCEDINE] = 15000, [xi.ki.MAP_OF_DYNAMIS_XARCABARD] = 20000, [xi.ki.MAP_OF_DYNAMIS_VALKURM] = 10000, [xi.ki.MAP_OF_DYNAMIS_BUBURIMU] = 10000, [xi.ki.MAP_OF_DYNAMIS_QUFIM] = 10000, [xi.ki.MAP_OF_DYNAMIS_TAVNAZIA] = 20000, } xi.dynamis.hourglassAndCurrencyExchangeNPCOnTrade = function(player, npc, trade) local gil = trade:getGil() local count = trade:getItemCount() local tradeItem = trade:getItemId(0) local zoneId = player:getZoneID() local baseCs = xi.dynamis.hourglassAndCurrencyExchangeNPCLookup[zoneId].baseCs local currency = xi.dynamis.hourglassAndCurrencyExchangeNPCLookup[zoneId].currency local shop = xi.dynamis.hourglassAndCurrencyExchangeNPCLookup[zoneId].shop -- Zero this out, just in case player:setLocalVar('currencyExchange', 0) if player:hasKeyItem(xi.ki.VIAL_OF_SHROUDED_SAND) then -- buy prismatic hourglass if gil == xi.settings.main.PRISMATIC_HOURGLASS_COST and count == 1 and not player:hasKeyItem(xi.ki.PRISMATIC_HOURGLASS) then player:startEvent(baseCs + 4) -- return timeless hourglass for refund elseif count == 1 and trade:hasItemQty(xi.item.TIMELESS_HOURGLASS, 1) then player:startEvent(baseCs + 23) -- currency exchanges elseif -- 1's -> 100's count == xi.settings.main.CURRENCY_EXCHANGE_RATE and trade:hasItemQty(currency[1], xi.settings.main.CURRENCY_EXCHANGE_RATE) then player:startEvent(baseCs + 5, xi.settings.main.CURRENCY_EXCHANGE_RATE) elseif -- 100's -> 10'000's count == xi.settings.main.CURRENCY_EXCHANGE_RATE and trade:hasItemQty(currency[2], xi.settings.main.CURRENCY_EXCHANGE_RATE) then player:startEvent(baseCs + 6, xi.settings.main.CURRENCY_EXCHANGE_RATE) elseif -- 10'000's to 100's count == 1 and trade:hasItemQty(currency[3], 1) and tradeItem == currency[3] then player:setLocalVar('currencyExchange', currency[2]) player:startEvent(baseCs + 8, tradeItem, currency[2], xi.settings.main.CURRENCY_EXCHANGE_RATE) elseif -- (optional) 100's to 1's xi.settings.main.ENABLE_EXCHANGE_100S_TO_1S and count == 1 and trade:hasItemQty(currency[2], 1) and tradeItem == currency[2] then player:setLocalVar('currencyExchange', currency[1]) player:startEvent(baseCs + 8, tradeItem, currency[1], xi.settings.main.CURRENCY_EXCHANGE_RATE) -- shop else local item local price for i = 1, 13, 2 do price = shop[i] item = shop[i + 1] if count == price and trade:hasItemQty(currency[2], price) then player:setLocalVar('hundredItemBought', item) player:startEvent(baseCs + 7, currency[2], price, item) break end end end end end xi.dynamis.hourglassAndCurrencyExchangeNPCOnTrigger = function(player, npc) local zoneId = player:getZoneID() local baseCs = xi.dynamis.hourglassAndCurrencyExchangeNPCLookup[zoneId].baseCs local currency = xi.dynamis.hourglassAndCurrencyExchangeNPCLookup[zoneId].currency if player:hasKeyItem(xi.ki.VIAL_OF_SHROUDED_SAND) then player:startEvent(baseCs + 3, currency[1], xi.settings.main.CURRENCY_EXCHANGE_RATE, currency[2], xi.settings.main.CURRENCY_EXCHANGE_RATE, currency[3], xi.settings.main.PRISMATIC_HOURGLASS_COST, xi.item.TIMELESS_HOURGLASS, xi.settings.main.TIMELESS_HOURGLASS_COST) else player:startEvent(baseCs + 0) end end xi.dynamis.hourglassAndCurrencyExchangeNPCOnEventUpdate = function(player, csid, option, npc) local zoneId = player:getZoneID() local ID = zones[zoneId] local baseCs = xi.dynamis.hourglassAndCurrencyExchangeNPCLookup[zoneId].baseCs local currency = xi.dynamis.hourglassAndCurrencyExchangeNPCLookup[zoneId].currency local shop = xi.dynamis.hourglassAndCurrencyExchangeNPCLookup[zoneId].shop if csid == baseCs + 3 then -- asking about hourglasses if option == 1 then if not player:hasItem(xi.item.TIMELESS_HOURGLASS) then -- must figure out what changes here to prevent the additional dialog -- player:updateEvent(?) end -- shop elseif option == 2 then player:updateEvent(unpack(shop, 1, 8)) elseif option == 3 then player:updateEvent(unpack(shop, 9, 14)) -- offer to trade down from a 10k elseif option == 10 then player:updateEvent(currency[3], currency[2], xi.settings.main.CURRENCY_EXCHANGE_RATE) -- main menu (param1 = dynamis map bitmask, param2 = gil) elseif option == 11 then player:updateEvent(xi.dynamis.getDynamisMapList(player), player:getGil()) -- maps elseif xi.dynamis.mapShopCosts[option] ~= nil then local price = xi.dynamis.mapShopCosts[option] if price > player:getGil() then player:messageSpecial(ID.text.NOT_ENOUGH_GIL) else player:delGil(price) npcUtil.giveKeyItem(player, option) end player:updateEvent(xi.dynamis.getDynamisMapList(player), player:getGil()) end end end xi.dynamis.hourglassAndCurrencyExchangeNPCOnEventFinish = function(player, csid, option, npc) local zoneId = player:getZoneID() local ID = zones[zoneId] local baseCs = xi.dynamis.hourglassAndCurrencyExchangeNPCLookup[zoneId].baseCs local currency = xi.dynamis.hourglassAndCurrencyExchangeNPCLookup[zoneId].currency -- bought prismatic hourglass if csid == baseCs + 4 then player:tradeComplete() npcUtil.giveKeyItem(player, xi.ki.PRISMATIC_HOURGLASS) -- refund timeless hourglass elseif csid == baseCs + 13 then player:tradeComplete() npcUtil.giveCurrency(player, 'gil', xi.settings.main.TIMELESS_HOURGLASS_COST) -- singles to hundreds elseif csid == baseCs + 5 then if player:getFreeSlotsCount() == 0 then player:messageSpecial(ID.text.ITEM_CANNOT_BE_OBTAINED, currency[2]) else player:tradeComplete() player:addItem(currency[2]) player:messageSpecial(ID.text.ITEM_OBTAINED, currency[2]) end -- hundreds to 10k pieces elseif csid == baseCs + 6 then if player:getFreeSlotsCount() == 0 then player:messageSpecial(ID.text.ITEM_CANNOT_BE_OBTAINED, currency[3]) else player:tradeComplete() player:addItem(currency[3]) player:messageSpecial(ID.text.ITEM_OBTAINED, currency[3]) end -- 10k pieces to hundreds (or hundreds to singles) elseif csid == baseCs + 8 then local currencyExchange = player:getLocalVar('currencyExchange') local slotsReq = math.ceil(xi.settings.main.CURRENCY_EXCHANGE_RATE / 99) if player:getFreeSlotsCount() < slotsReq then player:messageSpecial(ID.text.ITEM_CANNOT_BE_OBTAINED, currencyExchange) else player:tradeComplete() for i = 1, slotsReq do if i < slotsReq or (xi.settings.main.CURRENCY_EXCHANGE_RATE % 99) == 0 then player:addItem(currencyExchange, 99) else player:addItem(currencyExchange, xi.settings.main.CURRENCY_EXCHANGE_RATE % 99) end end player:messageSpecial(ID.text.ITEMS_OBTAINED, currencyExchange, xi.settings.main.CURRENCY_EXCHANGE_RATE) end -- Zero this out, just in case player:setLocalVar('currencyExchange', 0) -- bought item from shop elseif csid == baseCs + 7 then local item = player:getLocalVar('hundredItemBought') if player:getFreeSlotsCount() == 0 then player:messageSpecial(ID.text.ITEM_CANNOT_BE_OBTAINED, item) else player:tradeComplete() player:addItem(item) player:messageSpecial(ID.text.ITEM_OBTAINED, item) end player:setLocalVar('hundredItemBought', 0) end end xi.dynamis.mobInfo = function(mob) -- No gil, no mug, enhanced damage 1.5x mob:setMobMod(xi.mobMod.GIL_MAX, -1) mob:setMobMod(xi.mobMod.MUG_GIL, -1) mob:setMobMod(xi.mobMod.BASE_DAMAGE_MULTIPLIER, 150) end