landsandboat/scripts/globals/moghouse.lua
Zach Toogood 13d9ae5baa Rename isInMogHouse() to inMogHouse()
For brevity.

Also includes: inMogHouseInHomeNation()
2025-12-09 16:49:58 +00:00

462 lines
17 KiB
Lua

-----------------------------------
-- Mog House related functions
-----------------------------------
require('scripts/globals/npc_util')
require('scripts/globals/quests')
-----------------------------------
xi = xi or {}
xi.moghouse = xi.moghouse or {}
-----------------------------------
-- Mog Locker constants
-----------------------------------
local mogLockerStartTimestamp = 1009810800 -- unix timestamp for 2001/12/31 15:00
local mogLockerTimestampVarName = 'mog-locker-expiry-timestamp'
xi.moghouse.MOGLOCKER_ALZAHBI_VALID_DAYS = 7
xi.moghouse.MOGLOCKER_ALLAREAS_VALID_DAYS = 5
xi.moghouse.MOGLOCKER_PLAYERVAR_ACCESS_TYPE = 'mog-locker-access-type'
xi.moghouse.lockerAccessType =
{
ALZAHBI = 0,
ALLAREAS = 1,
}
xi.moghouse.moghouseZones =
{
xi.zone.AL_ZAHBI, -- 49
xi.zone.AHT_URHGAN_WHITEGATE, -- 50
xi.zone.SOUTHERN_SAN_DORIA_S, -- 80
xi.zone.BASTOK_MARKETS_S, -- 87
xi.zone.WINDURST_WATERS_S, -- 94
xi.zone.RESIDENTIAL_AREA, -- 219
xi.zone.SOUTHERN_SAN_DORIA, -- 230
xi.zone.NORTHERN_SAN_DORIA, -- 231
xi.zone.PORT_SAN_DORIA, -- 232
xi.zone.BASTOK_MINES, -- 234
xi.zone.BASTOK_MARKETS, -- 235
xi.zone.PORT_BASTOK, -- 236
xi.zone.WINDURST_WATERS, -- 238
xi.zone.WINDURST_WALLS, -- 239
xi.zone.PORT_WINDURST, -- 240
xi.zone.WINDURST_WOODS, -- 241
xi.zone.RULUDE_GARDENS, -- 243
xi.zone.UPPER_JEUNO, -- 244
xi.zone.LOWER_JEUNO, -- 245
xi.zone.PORT_JEUNO, -- 246
xi.zone.WESTERN_ADOULIN, -- 256
xi.zone.EASTERN_ADOULIN, -- 257
}
xi.moghouse.moghouse2FUnlockCSs =
{
[xi.zone.SOUTHERN_SAN_DORIA] = 3535,
[xi.zone.NORTHERN_SAN_DORIA] = 904,
[xi.zone.PORT_SAN_DORIA] = 820,
[xi.zone.BASTOK_MINES] = 610,
[xi.zone.BASTOK_MARKETS] = 604,
[xi.zone.PORT_BASTOK] = 456,
[xi.zone.WINDURST_WATERS] = 1086,
[xi.zone.WINDURST_WALLS] = 547,
[xi.zone.PORT_WINDURST] = 903,
[xi.zone.WINDURST_WOODS] = 885,
}
local offsetAxis =
{
X = 1,
Z = 3,
R = 4,
}
-- [zoneId] = { [Entrance] ={x, y, z, rot, offsetAxis, max } }
xi.moghouse.exits =
{
-- Single Exit Zones -------------------------------------------------------------------------
[xi.zone.BASTOK_MINES] = { [1] = { 117, 0.99, -72, 127, offsetAxis.Z, 5.0 } },
[xi.zone.PORT_BASTOK] = { [1] = { 60, 8.5, -239, 192, offsetAxis.X, 5.0 } },
[xi.zone.AL_ZAHBI] = { [1] = { 40, 0, -61, 191, offsetAxis.X, 2.0 } },
[xi.zone.AHT_URHGAN_WHITEGATE] = { [1] = { -101, 0, -80, 255, offsetAxis.Z, 2.0 } },
[xi.zone.SOUTHERN_SAN_DORIA_S] = { [1] = { 148.5, -2, 116, 95, offsetAxis.X, 2.0 } },
[xi.zone.WINDURST_WATERS_S] = { [1] = { 160, 0.99, -11, 191, offsetAxis.X, 2.0 } },
[xi.zone.SOUTHERN_SAN_DORIA] = { [1] = { 159.5, -2, 160, 95, offsetAxis.Z, 2.0 } },
[xi.zone.NORTHERN_SAN_DORIA] = { [1] = { 130, -0.2, -3, 160, offsetAxis.X, 2.0 } },
[xi.zone.PORT_SAN_DORIA] = { [1] = { 79.4, -16, -135.5, 165, offsetAxis.R, 1.8 } },
[xi.zone.WINDURST_WATERS] = { [1] = { 160, -2.65, -53.7, 192, offsetAxis.X, 2.5 } },
[xi.zone.WINDURST_WALLS] = { [1] = { -249, -2.65, -120, 0, offsetAxis.Z, 2.5 } },
[xi.zone.PORT_WINDURST] = { [1] = { 198, -15.65, 258, 65, offsetAxis.X, 2.5 } },
[xi.zone.WINDURST_WOODS] = { [1] = { -130, -7.65, 40, 0, offsetAxis.Z, 2.5 } },
[xi.zone.RULUDE_GARDENS] = { [1] = { 46.5, 10, -73, 192, offsetAxis.X, 1.8 } },
[xi.zone.UPPER_JEUNO] = { [1] = { 47, -5, -79.3, 172, offsetAxis.R, 1.0 } },
[xi.zone.LOWER_JEUNO] = { [1] = { 42, -5, 85, 85, offsetAxis.R, 1.0 } },
[xi.zone.PORT_JEUNO] = { [1] = { -193, -5, -0, 0, offsetAxis.Z, 1.0 } },
[xi.zone.WESTERN_ADOULIN] = { [1] = { 0, 0.75, -142, 223, offsetAxis.R, 4.0 } },
[xi.zone.EASTERN_ADOULIN] = { [1] = { -56, -0.15, -127, 191, offsetAxis.X, 4.0 } },
--- Multiple Exit Zones ----------------------------------------------------------------------
[xi.zone.BASTOK_MARKETS_S] = {
[1] = { -177, -8, -30, 128, offsetAxis.Z, 2.0 },
[2] = { -177, -8, 30, 128, offsetAxis.Z, 2.0 }
},
[xi.zone.BASTOK_MARKETS] = {
[1] = { -177, -8, -30, 128, offsetAxis.Z, 2.0 },
[2] = { -177, -8, 30, 128, offsetAxis.Z, 2.0 }
},
}
local moghouseZoneLines =
{
-- [Zoneline ID] = entrance no.
[ 812805498] = 1, -- (230) SOUTHERN_SANDORIA
[ 812871034] = 1, -- ( 80) SOUTHERN_SANDORIA_S
[ 846359930] = 1, -- (231) NORTHERN_SANDORIA
[ 879979898] = 1, -- ( 87) BASTOK_MARKETS_S [Entrance 1]
[ 846425466] = 2, -- ( 87) BASTOK_MARKETS_S [Entrance 2]
[ 879914362] = 1, -- (232) PORT_SANDORIA
[ 913468794] = 1, -- (238) WINDURST_WATERS
[ 913534330] = 1, -- ( 94) WINDURST_WATERS_S
[ 947023226] = 1, -- (238) WINDURST_WALLS
[1668443514] = 1, -- (235) BASTOK_MARKETS [Entrance 1]
[1634889082] = 2, -- (235) BASTOK_MARKETS [Entrance 2]
[1701997946] = 1, -- (240) PORT_WINDURST
[1735552378] = 1, -- (234) BASTOK_MINES
[1769106810] = 1, -- (236) PORT_BASTOK
[1802661242] = 1, -- (241) WINDURST_WOODS
[1836215674] = 1, -- (243) RULUDE_GARDENS
[1869770106] = 1, -- (244) UPPER_JEUNO
[1936878970] = 1, -- (246) PORT_JEUNO
[1970433402] = 1, -- (245) LOWER_JEUNO
[2003987834] = 1, -- ( 48) AL_ZAHBI
[2037542266] = 1, -- ( 50) AHT_URHGAN_WHITEGATE
[ 947088762] = 1, -- (256) WESTERN_ADOULIN
[1634954618] = 1, -- (257) EASTERN_ADOULIN
}
xi.moghouse.inMogHouseInHomeNation = function(player)
if not player:inMogHouse() then
return false
end
local currentZone = player:getZoneID()
local nation = player:getNation()
-- TODO: Simplify nested conditions
if nation == xi.nation.BASTOK then
if
currentZone >= xi.zone.BASTOK_MINES and
currentZone <= xi.zone.PORT_BASTOK
then
return true
end
elseif nation == xi.nation.SANDORIA then
if
currentZone >= xi.zone.SOUTHERN_SAN_DORIA and
currentZone <= xi.zone.PORT_SAN_DORIA
then
return true
end
else
if
currentZone >= xi.zone.WINDURST_WATERS and
currentZone <= xi.zone.WINDURST_WOODS
then
return true
end
end
return false
end
xi.moghouse.set2ndFloorStyle = function(player, style)
-- 0x0080: This bit and the next track which 2F decoration style is being used (0: SANDORIA, 1: BASTOK, 2: WINDURST, 3: PATIO)
-- 0x0100: ^ As above
local mhflag = player:getMoghouseFlag()
utils.mask.setBit(mhflag, 0x0080, utils.mask.getBit(style, 0))
utils.mask.setBit(mhflag, 0x0100, utils.mask.getBit(style, 1))
player:setMoghouseFlag(mhflag)
end
xi.moghouse.getAvailableMusic = function(player)
-- See documentation/songdata.txt or documentation/MusicIDs.txt for song data.
local possibleSongs = {}
local orchestrion = player:findItem(xi.item.ORCHESTRION)
local spinet = player:findItem(xi.item.SPINET)
local nanaaStatue1 = player:findItem(xi.item.NANAA_MIHGO_STATUE)
local nanaaStatue2 = player:findItem(xi.item.NANAA_MIHGO_STATUE_II)
local hasOrchestrion = orchestrion and orchestrion:isInstalled()
local hasSpinet = spinet and spinet:isInstalled()
local hasNanaaStatue1 = nanaaStatue1 and nanaaStatue1:isInstalled()
local hasNanaaStatue2 = nanaaStatue2 and nanaaStatue2:isInstalled()
-- NOTE: Since Spinet, Nanaa Mihgo Statue I, and Nanaa Mihgo Statue II are promotional-only items,
-- : it is extremely difficult to get them and test what they do when used together.
-- : We're completely guessing how they interact with each other.
-- : TODO: Do these overwrite eachother in some way, or do they work together (as we've implemented
-- : them here)?
if not hasOrchestrion then
-- https://www.bg-wiki.com/ffxi/Orchestrion
if hasSpinet then
table.insert(possibleSongs, 112) -- Selbina
table.insert(possibleSongs, 196) -- Fighters of the Crystal
table.insert(possibleSongs, 230) -- A New Horizon
table.insert(possibleSongs, 187) -- Ragnarok
table.insert(possibleSongs, 215) -- Clash of Standards
table.insert(possibleSongs, 47) -- Echoes of Creation
table.insert(possibleSongs, 49) -- Luck of the Mog
table.insert(possibleSongs, 50) -- Feast of the Ladies
table.insert(possibleSongs, 51) -- Abyssea
table.insert(possibleSongs, 52) -- Melodies Errant
table.insert(possibleSongs, 109) -- Ronfaure
table.insert(possibleSongs, 251) -- Autumn Footfalls
table.insert(possibleSongs, 48) -- Main Theme
table.insert(possibleSongs, 126) -- Mog House
end
if hasNanaaStatue1 then
table.insert(possibleSongs, 69) -- Distant Worlds (Nanaa Mihgo's Version)
end
if hasNanaaStatue2 then
table.insert(possibleSongs, 59) -- The Pioneers (Nanaa Mihgo's Version)
end
end
return possibleSongs
end
xi.moghouse.trySetMusic = function(player)
local possibleSongs = xi.moghouse.getAvailableMusic(player)
if #possibleSongs > 0 then
-- This needs a moment before music changes can take effect
player:timer(1000, function(playerArg)
playerArg:changeMusic(6, utils.randomEntry(possibleSongs))
end)
end
end
xi.moghouse.onMoghouseZoneEvent = function(player, prevZone)
-- Handle players zoning in their Mog House
if player:inMogHouse() then
return xi.moghouse.onMoghouseZoneIn(player, prevZone)
end
-- Handle players zoning out of their Mog House
if
player:getXPos() == 0 and
player:getYPos() == 0 and
player:getZPos() == 0
then
local zoneId = player:getZoneID()
local prevZoneId = player:getPreviousZone()
local prevZoneLineID = player:getPreviousZoneLineID()
local moghouseEntrance = zoneId == prevZoneId and moghouseZoneLines[prevZoneLineID] or 1
local x, y, z, r, randomizedAxis, randomMax = unpack(xi.moghouse.exits[zoneId][moghouseEntrance])
local randomOffset = math.random(-randomMax * 1000, randomMax * 1000) -- offset -/+ from center point
local offsetValue = randomOffset / 1000 -- 0.000 - N.N00 variance
-- A few moghouses are rotated so we handle them first.
if randomizedAxis == offsetAxis.R then
local lineRot = (r + 64) % 256 -- Calculate the perpendicular angle (90 degrees / 64 units offset) from the rotaion of r.
local lineRadians = lineRot * (math.pi * 2 / 256) -- Convert to radians.
x = x + (offsetValue * math.cos(lineRadians))
z = z - (offsetValue * math.sin(lineRadians))
elseif randomizedAxis == offsetAxis.X then
x = x + offsetValue
elseif randomizedAxis == offsetAxis.Z then
z = z + offsetValue
end
player:setPos(x, y, z, r)
end
return -1
end
xi.moghouse.onMoghouseZoneIn = function(player, prevZone)
local cs = -1
player:eraseAllStatusEffect()
player:delStatusEffectSilent(xi.effect.POISON)
player:delStatusEffectSilent(xi.effect.BLINDNESS)
player:delStatusEffectSilent(xi.effect.PARALYSIS)
player:delStatusEffectSilent(xi.effect.SILENCE)
player:setPos(0, 0, 0, 192)
-- Moghouse data (bit-packed)
-- 0x0001: SANDORIA exit quest flag
-- 0x0002: BASTOK exit quest flag
-- 0x0004: WINDURST exit quest flag
-- 0x0008: JEUNO exit quest flag
-- 0x0010: WEST_AHT_URHGAN exit quest flag
-- 0x0020: Unlocked Moghouse2F flag
-- 0x0040: Moghouse 2F tracker flag (0: default, 1: using 2F)
-- 0x0080: This bit and the next track which 2F decoration style is being used (0: SANDORIA, 1: BASTOK, 2: WINDURST, 3: PATIO)
-- 0x0100: ^ As above
local mhflag = player:getMoghouseFlag()
local growingFlowers = bit.band(mhflag, 0x0001) > 0
local aLadysHeart = bit.band(mhflag, 0x0002) > 0
local flowerChild = bit.band(mhflag, 0x0004) > 0
local unlocked2ndFloor = bit.band(mhflag, 0x0020) > 0
local using2ndFloor = bit.band(mhflag, 0x0040) > 0
-- NOTE: You can test these quest conditions with:
-- Reset: !exec player:setMoghouseFlag(0)
-- Complete quests: !exec player:setMoghouseFlag(7)
if
xi.moghouse.inMogHouseInHomeNation(player) and
growingFlowers and
aLadysHeart and
flowerChild and
not unlocked2ndFloor and
not using2ndFloor
then
cs = xi.moghouse.moghouse2FUnlockCSs[player:getZoneID()]
player:setMoghouseFlag(mhflag + 0x0020) -- Set unlock flag now, rather than in onEventFinish
local nation = player:getNation()
xi.moghouse.set2ndFloorStyle(player, nation)
end
xi.moghouse.trySetMusic(player)
return cs
end
xi.moghouse.moogleTrade = function(player, npc, trade)
if player:inMogHouse() then
local numBronze = trade:getItemQty(xi.item.IMPERIAL_BRONZE_PIECE)
if numBronze > 0 then
if xi.moghouse.addMogLockerExpiryTime(player, numBronze) then
player:tradeComplete()
player:messageSpecial(zones[player:getZoneID()].text.MOG_LOCKER_OFFSET + 2, xi.moghouse.getMogLockerExpiryTimestamp(player))
end
end
local eggComponents =
{
xi.item.EGG_LOCKER,
xi.item.EGG_TABLE,
xi.item.EGG_STOOL,
xi.item.EGG_LANTERN,
}
if npcUtil.tradeHasExactly(trade, eggComponents) then
if npcUtil.giveItem(player, xi.item.EGG_BUFFET) then
player:confirmTrade()
end
elseif npcUtil.tradeHasExactly(trade, xi.item.EGG_BUFFET) then
if npcUtil.giveItem(player, eggComponents) then
player:confirmTrade()
end
end
end
end
xi.moghouse.moogleTrigger = function(player, npc)
if player:inMogHouse() then
local lockerTs = xi.moghouse.getMogLockerExpiryTimestamp(player)
if lockerTs ~= nil then
if lockerTs == -1 then -- Expired
player:messageSpecial(zones[player:getZoneID()].text.MOG_LOCKER_OFFSET + 1, xi.item.IMPERIAL_BRONZE_PIECE)
else
player:messageSpecial(zones[player:getZoneID()].text.MOG_LOCKER_OFFSET, lockerTs)
end
end
player:sendMenu(xi.menuType.MOOGLE)
end
end
xi.moghouse.moogleEventUpdate = function(player, csid, option, npc)
end
xi.moghouse.moogleEventFinish = function(player, csid, option, npc)
end
-- Unlocks a mog locker for a player. Returns the 'expired' timestamp (-1)
xi.moghouse.unlockMogLocker = function(player)
player:setCharVar(mogLockerTimestampVarName, -1)
-- Safety check in case some servers auto-set 80 slots for mog locker items.
if player:getContainerSize(xi.inv.MOGLOCKER) == 0 then
player:changeContainerSize(xi.inv.MOGLOCKER, 30)
end
return -1
end
-- Sets the mog locker access type (all area or alzahbi only). Returns the new access type.
xi.moghouse.setMogLockerAccessType = function(player, accessType)
player:setCharVar(xi.moghouse.MOGLOCKER_PLAYERVAR_ACCESS_TYPE, accessType)
return accessType
end
-- Gets the mog locker access type (all area or alzahbi only). Returns the new access type.
xi.moghouse.getMogLockerAccessType = function(player)
return player:getCharVar(xi.moghouse.MOGLOCKER_PLAYERVAR_ACCESS_TYPE)
end
-- Gets the expiry time for your locker. A return value of -1 is expired. A return value of nil means mog locker hasn't been unlocked.
xi.moghouse.getMogLockerExpiryTimestamp = function(player)
local expiryTime = player:getCharVar(mogLockerTimestampVarName)
if expiryTime == 0 then
return nil
end
local now = GetSystemTime() - mogLockerStartTimestamp
if now > expiryTime then
player:setCharVar(mogLockerTimestampVarName, -1)
return -1
end
return expiryTime
end
-- Adds time to your mog locker, given the number of bronze coins.
-- The amount of time per bronze is affected by the access type
-- The expiry time itself is the number of seconds past 2001/12/31 15:00
-- Returns true if time was added successfully, false otherwise.
xi.moghouse.addMogLockerExpiryTime = function(player, numBronze)
local accessType = xi.moghouse.getMogLockerAccessType(player)
local numDaysPerBronze = 5
if accessType == xi.moghouse.lockerAccessType.ALZAHBI then
numDaysPerBronze = 7
end
local currentTs = xi.moghouse.getMogLockerExpiryTimestamp(player)
if currentTs == nil then
return false
end
if currentTs == -1 then
currentTs = GetSystemTime() - mogLockerStartTimestamp
end
local timeIncrease = 60 * 60 * 24 * numDaysPerBronze * numBronze
local newTs = currentTs + timeIncrease
player:setCharVar(mogLockerTimestampVarName, newTs)
-- Send an invent size packet to enable the items if they weren't.
player:changeContainerSize(xi.inv.MOGLOCKER, 0)
return true
end