landsandboat/scripts/globals/ancestry_moogle.lua
Corey Sotiropoulos 1cb2d545ea
Replace os.time() with GetSystemTime()
Simple find/replace.
This will make sure everything is using the same clock (earth_time::clock).
2025-07-03 23:38:14 -04:00

262 lines
8.9 KiB
Lua

-----------------------------------
-- Ancestry Moogle (Race Change)
--
-- Player must have CharVar '[RaceChange]Eligible' set to a non-zero value to use the service.
-----------------------------------
xi = xi or {}
xi.ancestryMoogle = {}
local settings =
{
-- Interactions disabled if set to false.
enabled = xi.settings.main.RACE_CHANGE_ENABLED or true,
-- The time in seconds that the player has to wait before they can use the service again.
-- Default is 14 days (1209600 seconds).
cooldown = xi.settings.main.RACE_CHANGE_COOLDOWN or 1209600,
}
local csidKey =
{
NOT_ELIGIBLE = 1,
RACE_CHANGE = 2,
}
local csidLookup =
{
[xi.zone.PORT_BASTOK] = { 480, 479 },
[xi.zone.PORT_SAN_DORIA] = { 833, 832 },
[xi.zone.WINDURST_WALLS] = { 580, 579 },
}
local swappableItems =
{
[0] = -- Female trading Male gear
{
xi.item.DANCERS_TIARA_M,
xi.item.DANCERS_CASAQUE_M,
xi.item.DANCERS_BANGLES_M,
xi.item.DANCERS_TIGHTS_M,
xi.item.DANCERS_TOE_SHOES_M,
xi.item.DANCERS_TIARA_M_P1,
xi.item.DANCERS_CASAQUE_M_P1,
xi.item.DANCERS_BANGLES_M_P1,
xi.item.DANCERS_TIGHTS_M_P1,
xi.item.DANCERS_TOE_SHOES_M_P1,
xi.item.MAXIXI_TIARA_M,
xi.item.MAXIXI_CASAQUE_M,
xi.item.MAXIXI_BANGLES_M,
xi.item.MAXIXI_TIGHTS_M,
xi.item.MAXIXI_TOE_SHOES_M,
xi.item.MAXIXI_TIARA_M_P1,
xi.item.MAXIXI_CASAQUE_M_P1,
xi.item.MAXIXI_BANGLES_M_P1,
xi.item.MAXIXI_TIGHTS_M_P1,
xi.item.MAXIXI_TOE_SHOES_M_P1,
xi.item.MAXIXI_TIARA_M_P2,
xi.item.MAXIXI_CASAQUE_M_P2,
xi.item.MAXIXI_BANGLES_M_P2,
xi.item.MAXIXI_TIGHTS_M_P2,
xi.item.MAXIXI_TOE_SHOES_M_P2,
xi.item.MAXIXI_TIARA_M_P3,
xi.item.MAXIXI_CASAQUE_M_P3,
xi.item.MAXIXI_BANGLES_M_P3,
xi.item.MAXIXI_TIGHTS_M_P3,
xi.item.MAXIXI_TOE_SHOES_M_P3,
},
[1] = -- Male trading Female gear
{
xi.item.DANCERS_TIARA_F,
xi.item.DANCERS_CASAQUE_F,
xi.item.DANCERS_BANGLES_F,
xi.item.DANCERS_TIGHTS_F,
xi.item.DANCERS_TOE_SHOES_F,
xi.item.DANCERS_TIARA_F_P1,
xi.item.DANCERS_CASAQUE_F_P1,
xi.item.DANCERS_BANGLES_F_P1,
xi.item.DANCERS_TIGHTS_F_P1,
xi.item.DANCERS_TOE_SHOES_F_P1,
xi.item.MAXIXI_TIARA_F,
xi.item.MAXIXI_CASAQUE_F,
xi.item.MAXIXI_BANGLES_F,
xi.item.MAXIXI_TIGHTS_F,
xi.item.MAXIXI_TOE_SHOES_F,
xi.item.MAXIXI_TIARA_F_P1,
xi.item.MAXIXI_CASAQUE_F_P1,
xi.item.MAXIXI_BANGLES_F_P1,
xi.item.MAXIXI_TIGHTS_F_P1,
xi.item.MAXIXI_TOE_SHOES_F_P1,
xi.item.MAXIXI_TIARA_F_P2,
xi.item.MAXIXI_CASAQUE_F_P2,
xi.item.MAXIXI_BANGLES_F_P2,
xi.item.MAXIXI_TIGHTS_F_P2,
xi.item.MAXIXI_TOE_SHOES_F_P2,
xi.item.MAXIXI_TIARA_F_P3,
xi.item.MAXIXI_CASAQUE_F_P3,
xi.item.MAXIXI_BANGLES_F_P3,
xi.item.MAXIXI_TIGHTS_F_P3,
xi.item.MAXIXI_TOE_SHOES_F_P3,
},
}
xi.ancestryMoogle.onTrade = function(player, npc, trade)
if not settings.enabled then
return
end
local confirmedItems = {}
for slotId = 0, 7 do
local item = trade:getItem(slotId)
if item then
local itemId = item:getID()
if utils.contains(itemId, swappableItems[player:getGender()]) then
print('Item eligible for swap: ' .. itemId)
-- Player submitted an eligible item from the opposite gender.
table.insert(confirmedItems, itemId)
end
end
end
for _, v in ipairs(confirmedItems) do
-- Give new item. Item ID + 1 for MtoF, -1 for FtoM.
-- getGender returns 0 for F and 1 for M.
if npcUtil.giveItem(player, player:getGender() == 1 and v - 1 or v + 1) then
trade:confirmItem(v, 1)
end
end
player:confirmTrade()
return true
end
xi.ancestryMoogle.onTrigger = function(player, npc)
if not settings.enabled then
return
end
local zoneID = player:getZoneID()
local raceChangeLast = player:getCharVar('[RaceChange]Last')
local raceChangeExpiry = player:getCharVar('[RaceChange]Eligible')
-- If the player is on cooldown, show a message and exit.
if raceChangeLast + settings.cooldown >= GetSystemTime() then
player:startEvent(csidLookup[zoneID][csidKey.NOT_ELIGIBLE],
236,
0, -- unknown, seen 0
0, -- unknown, seen 0
0, -- unknown, seen 0
0, -- unknown, time remaining for an unknown variant of the event
5, -- 0 is message when service not purchased, 1+ when on cooldown.
1, -- 1+ displays "Thank you for using the service!". Capture shows misc values.
0 -- unknown, seen random values.
)
return
end
-- Player must have "purchased" the service and have enough time left to use it.
if
raceChangeExpiry == 0 or -- Expired charvar
raceChangeExpiry - GetSystemTime() <= 0 -- If the charvar is set but expired.
then
player:startEvent(csidLookup[zoneID][csidKey.NOT_ELIGIBLE], 236)
player:setCharVar('[RaceChange]Eligible', 0)
return
end
-- The upcoming event need the Previous_Race NPC set to the player current race/face.
-- The Previous_Race NPC is 2 IDs higher than the Ancestry Moogle NPC.
local previousRaceNpc = GetNPCByID(npc:getID() + 2)
if previousRaceNpc then
previousRaceNpc:setLook({ race = player:getRace(), face = player:getFace() })
player:sendEntityUpdateToPlayer(previousRaceNpc, xi.entityUpdate.ENTITY_UPDATE, xi.updateType.UPDATE_ALL_CHAR)
end
player:startEvent(csidLookup[zoneID][csidKey.RACE_CHANGE],
player:getRace(), -- current race
bit.rshift(player:getFace(), 1), -- current face, flattened from 0-15 to 0-7
player:getSize(), -- current size
player:getFace() % 2, -- current hair color (face variant)
raceChangeExpiry - GetSystemTime(), -- Time left to use the service, in seconds.
0, -- unknown, seen 0
0, -- unknown, seen random values
0 -- unknown, seen random values
)
end
xi.ancestryMoogle.onEventFinish = function(player, csid, option, npc)
if not settings.enabled then
return
end
local zoneID = player:getZoneID()
local raceChangeExpiry = player:getCharVar('[RaceChange]Eligible')
if
csid == csidLookup[zoneID][csidKey.RACE_CHANGE] and
option ~= utils.EVENT_CANCELLED_OPTION
then
-- If timer expired between the event start and finish, reset
if
raceChangeExpiry == 0 or -- Expired charvar
raceChangeExpiry - GetSystemTime() <= 0 -- If the charvar is set but expired.
then
player:messageSpecial(zones[zoneID].text.UNABLE_RACE_CHANGE)
-- Must rezone character to exit the special event.
player:forceRezone()
return
end
-- Bits 8-11: Race ID (4 bits, values 1-8)
-- Bits 12-15: Face/Hair combination (4 bits, values 0-15)
-- Bits 16-17: Size (2 bits, values 0(small)-2(large))
local newRace = bit.band(bit.rshift(option, 8), 0xF)
local newFace = bit.band(bit.rshift(option, 12), 0xF)
local newSize = bit.band(bit.rshift(option, 16), 0x3)
-- Sanity checks
if
(not newRace or not newFace or not newSize) or
(newRace < xi.race.HUME_M or newRace > xi.race.GALKA) or
(newFace > 15) or -- 1A (0) to 8B (15)
(newSize > 2) -- 0 (small) to 2 (large)
then
printf('bad race change data: player=%s, newRace=%d, newFace=%d, newSize=%d',
player:getName(), newRace, newFace, newSize)
player:messageSpecial(zones[zoneID].text.UNABLE_RACE_CHANGE)
-- Must rezone character to exit the special event.
player:forceRezone()
return
end
printf('race change: player=%s, race %d -> %d, face %d -> %d, size %d -> %d',
player:getName(),
player:getRace(), newRace,
player:getFace(), newFace,
player:getSize(), newSize)
-- Set the timer to 0 so the player can't use the service again
-- This is done before the actual race change since it will teleport the player
-- and player may no longer be valid
player:setCharVar('[RaceChange]Eligible', 0)
player:setCharVar('[RaceChange]Last', GetSystemTime())
if not player:raceChange(newRace, newFace, newSize) then
player:messageSpecial(zones[zoneID].text.UNABLE_RACE_CHANGE)
-- Must rezone character to exit the special event.
player:forceRezone()
end
end
end