landsandboat/scripts/globals/dynamis.lua
2026-04-16 20:58:06 -04:00

1130 lines
39 KiB
Lua

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