landsandboat/scripts/globals/fishing_contest.lua
sruon 9b0853e13b Exdata definitions
Escutcheons
Weapon unlock
Soul plate
Soul reflectors
Fish

Co-Authored-By: atom0s <atom0s@users.noreply.github.com>
2026-04-04 13:59:18 +02:00

427 lines
16 KiB
Lua

-----------------------------------
-- Fish Ranking Contest Functions
-----------------------------------
xi = xi or {}
xi.fishingContest = xi.fishingContest or {}
xi.fishingContest.fish =
{
-- NOTE: If you don't want certain fish included in the contest (era restrictions, or whatever),
-- then you must comment out the lines containing unwanted fish. The C++ core calls the
-- random function in this file to select a fish for each contest, which uses this table.
{ id = xi.item.GRIMMONITE, name = 'grimmonite' , realName = 'Grimmonite' },
{ id = xi.item.RYUGU_TITAN, name = 'ryugu_titan' , realName = 'Ryugu Titan' },
{ id = xi.item.GIANT_DONKO_1, name = 'giant_donko' , realName = 'Giant Donko' },
{ id = xi.item.JUNGLE_CATFISH, name = 'jungle_catfish' , realName = 'Jungle Catfish' },
{ id = xi.item.GIANT_CHIRAI, name = 'giant_chirai' , realName = 'Giant Chirai' },
{ id = xi.item.CAVE_CHERAX, name = 'cave_cherax' , realName = 'Cave Cherax' },
{ id = xi.item.ARMORED_PISCES, name = 'armored_pisces' , realName = 'Armored Pisces' },
{ id = xi.item.TRICORN, name = 'tricorn' , realName = 'Tricorn' },
{ id = xi.item.EMPEROR_FISH, name = 'emperor_fish' , realName = 'Emperor Fish' },
{ id = xi.item.MONKE_ONKE_1, name = 'monke-onke' , realName = 'Monke-Onke' },
{ id = xi.item.TAKITARO, name = 'takitaro' , realName = 'Takitaro' },
{ id = xi.item.GIANT_CATFISH_1, name = 'giant_catfish' , realName = 'Giant Catfish' },
{ id = xi.item.BLADEFISH_1, name = 'bladefish' , realName = 'Bladefish' },
{ id = xi.item.GIGANT_SQUID, name = 'gigant_squid' , realName = 'Gigant Squid' },
{ id = xi.item.SEA_ZOMBIE, name = 'sea_zombie' , realName = 'Sea Zombie' },
{ id = xi.item.TITANICTUS, name = 'titanictus' , realName = 'Titanictus' },
{ id = xi.item.GAVIAL_FISH, name = 'gavial_fish' , realName = 'Gavial Fish' },
{ id = xi.item.THREE_EYED_FISH_1, name = 'three-eyed_fish' , realName = 'Three-eyed Fish' },
{ id = xi.item.BHEFHEL_MARLIN_1, name = 'bhefhel_marlin' , realName = 'Bhefhel Marlin' },
{ id = xi.item.GUGRU_TUNA_1, name = 'gugru_tuna' , realName = 'Gugru Tuna' },
{ id = xi.item.TITANIC_SAWFISH, name = 'titanic_sawfish' , realName = 'Titanic Sawfish' },
{ id = xi.item.GUGRUSAURUS, name = 'gugrusaurus' , realName = 'Gugrusaurus' },
{ id = xi.item.LIK, name = 'lik' , realName = 'Lik' },
}
xi.fishingContest.criteria =
{
SIZE = 0,
WEIGHT = 1,
BOTH = 2,
}
xi.fishingContest.measure =
{
GREATEST = 0,
SMALLEST = 1,
}
xi.fishingContest.status =
{
CONTESTING = 0, -- Usually used for building manual contest, but a simple 5 minute buffer
OPENING = 1, -- 30 mins, warning to players that contest is opening soon
ACCEPTING = 2, -- 2 week window during which trades are accepted
RELEASING = 3, -- Submissions closed, preparing to release the results
PRESENTING = 4, -- Results posted, contestants may claim their prizes
HIATUS = 5, -- Approx 35 minute window after presentations
CLOSED = 6, -- Only used when contest is closed and inactive. No automatic progression from this stage
}
xi.fishingContest.reward =
{
-- Defines the reward at each rank
[ 1] = { gil = 50000, item = xi.item.PELICAN_RING, title = xi.title.GOLD_HOOK },
[ 2] = { gil = 25000, item = xi.item.PELICAN_RING, title = xi.title.MYTHRIL_HOOK },
[ 3] = { gil = 10000, item = xi.item.PELICAN_RING, title = xi.title.SILVER_HOOK },
[ 4] = { gil = 1000, item = xi.item.PELICAN_RING, title = xi.title.COPPER_HOOK },
[ 5] = { gil = 1000, item = xi.item.PELICAN_RING, title = xi.title.COPPER_HOOK },
[ 6] = { gil = 1000, item = xi.item.PELICAN_RING, title = xi.title.COPPER_HOOK },
[ 7] = { gil = 1000, item = xi.item.PELICAN_RING, title = xi.title.COPPER_HOOK },
[ 8] = { gil = 1000, item = xi.item.PELICAN_RING, title = xi.title.COPPER_HOOK },
[ 9] = { gil = 1000, item = xi.item.PELICAN_RING, title = xi.title.COPPER_HOOK },
[10] = { gil = 1000, item = xi.item.PELICAN_RING, title = xi.title.COPPER_HOOK },
[11] = { item = xi.item.PELICAN_RING, },
[12] = { item = xi.item.PELICAN_RING, },
[13] = { item = xi.item.PELICAN_RING, },
[14] = { item = xi.item.PELICAN_RING, },
[15] = { item = xi.item.PELICAN_RING, },
[16] = { item = xi.item.PELICAN_RING, },
[17] = { item = xi.item.PELICAN_RING, },
[18] = { item = xi.item.PELICAN_RING, },
[19] = { item = xi.item.PELICAN_RING, },
[20] = { item = xi.item.PELICAN_RING, },
}
xi.fishingContest.interval =
{
-- These intervals indicate how long each phase lasts before being automatically progressed.
-- Total cycle is 28 days, 35 minutes based on captures. May be varied if contest is manually controlled
[xi.fishingContest.status.CONTESTING] = 300, -- 00 days 00 hours 05 mins
[xi.fishingContest.status.OPENING] = 1500, -- 00 days 00 hours 25 mins
[xi.fishingContest.status.ACCEPTING] = 1209600, -- 14 days 00 hours 00 mins
[xi.fishingContest.status.RELEASING] = 1800, -- 00 days 00 hours 30 mins
[xi.fishingContest.status.PRESENTING] = 1206000, -- 13 days 23 hours 00 minutes
[xi.fishingContest.status.HIATUS] = 2100, -- 00 days 00 hours 35 minutes
[xi.fishingContest.status.CLOSED] = 0xFFFFFFFF, -- Must be manually controlled
}
-----------------------------------
-- LOCAL FUNCTIONS
-----------------------------------
-- Determine the slot that contains the fish being traded
local function findFishSlot(trade, fish)
for i = 0, trade:getSlotCount() - 1 do
if trade:getItemId(i) == fish then
return i
end
end
return 0
end
local function giveFish(player, params)
params = params or {}
local ID = zones[player:getZoneID()]
local fishid = params.id
-- does player have enough inventory space?
if player:getFreeSlotsCount() < 1 then
local messageId = ID.text.ITEM_CANNOT_BE_OBTAINED + 4
player:messageSpecial(messageId, fishid)
return false
end
-- give items to player
local fishItem = player:addItem({ id = fishid, quantity = params.quantity or 1 })
if fishItem then
if params.exdata then
fishItem:setExData(params.exdata)
end
player:messageSpecial(ID.text.ITEM_OBTAINED, fishid)
else
player:messageSpecial(ID.text.ITEM_CANNOT_BE_OBTAINED, fishid)
return false
end
return true
end
local function getAwardInteger(awardTable)
return bit.lshift(awardTable['rank4to10'], 24) + bit.lshift(awardTable['rank3'], 16) + bit.lshift(awardTable['rank2'], 8) + awardTable['rank1']
end
-- Determine if the player has a reward pending in the current contest
-- This will not permit rewarding for ongoing contests until it is ACTUALLY at the presenting stage.
local function isRewardAvailable(player)
local awards = player:getContestRewardStatus()
if awards ~= nil then
if
awards['rank'] > 0 and
awards['rank'] <= 20
then
return 1
end
end
return 0
end
-- This returns the data necessary to send the packet with the 'time remaining until stage change' data to the player
local function getTimeRemaining(changeTime)
local timeTable = {}
local currentTime = GetSystemTime()
if changeTime > currentTime then
local timeDelta = changeTime - currentTime
timeTable =
{
['days'] = math.floor(timeDelta / (60 * 60 * 24)),
['hours'] = math.floor((timeDelta / (60 * 60)) % 24),
['minutes'] = math.floor((timeDelta / 60) % 60),
}
else
timeTable =
{
['days'] = 0,
['hours'] = 0,
['minutes'] = 0,
}
end
return timeTable
end
-- Provides the score of the submission
local function scoreFish(length, weight, criteria)
local score = 0
if criteria == xi.fishingContest.criteria.SIZE then
score = length
elseif criteria == xi.fishingContest.criteria.WEIGHT then
score = weight
else
score = length + weight
end
return score
end
-- Determines the rewards to be granted based on rank and share
local function getPlayerReward(player)
local status = player:getContestRewardStatus()
if status ~= nil then
local rank = status['rank']
local share = status['share'] or 1
if
rank > 0 and
rank <= 20
then
local reward = xi.fishingContest.reward[rank]
if reward.gil then
reward.gil = reward.gil / share
end
return reward
end
end
return nil
end
-----------------------------------
-- GLOBAL FUNCTIONS
-----------------------------------
xi.fishingContest.selectContestFish = function()
return utils.randomEntry(xi.fishingContest.fish)['id']
end
xi.fishingContest.getStageChangeTime = function(stage, stageStartTime)
-- If the value is invalid, send back the 'never change' time
if
stage >= xi.fishingContest.status.CONTESTING and
stage < xi.fishingContest.status.CLOSED
then
return stageStartTime + xi.fishingContest.interval[stage]
end
return xi.fishingContest.interval[xi.fishingContest.status.CLOSED]
end
xi.fishingContest.onStageProgress = function(newStage)
-- TODO: Capture NPC text on stage progressions
end
xi.fishingContest.onTrigger = function(player, npc)
local contest = GetFishingContest()
if contest == nil then
return
end
player:startEvent(10006, {
[0] = contest['status'] or xi.fishingContest.status.CLOSED,
[1] = contest['fishid'] or 0, -- Fish ID for the contest
[2] = contest['criteria'] or 0, -- 0 = Size, 1 = Weight, 2 = Size + Weight
[3] = contest['measure'] or 0, -- 0 = greatest, 1 = smallest
})
end
xi.fishingContest.onTrade = function(player, npc, trade)
-- Handle fish submission
local contest = GetFishingContest()
if contest == nil then
return
end
-- Check to see if the traded fish is the right one
if
contest['status'] == xi.fishingContest.status.ACCEPTING and
npcUtil.tradeHasExactly(trade, contest['fishid'])
then
local fishItem = trade:getItem(findFishSlot(trade, contest['fishid']))
local fishData = fishItem:getExData()
if fishData == nil then
return
elseif
fishData.isRanked or
fishData.size == 0 or
fishData.weight == 0
then
-- Fish has already been ranked previously
player:startEvent(10007, { [4] = 1 })
else
-- Fish is a valid entry, not previously ranked
-- Player local vars used to hold submission data until end of event
player:setLocalVar('[FishContest]Length', fishData.size)
player:setLocalVar('[FishContest]Weight', fishData.weight)
player:startEvent(10007, {
[5] = scoreFish(fishData.size, fishData.weight, contest['criteria']),
[6] = player:getContestScore(),
})
end
end
end
xi.fishingContest.onEventUpdate = function(player, csid, option, npc)
local contest = GetFishingContest()
if contest == nil then
return
end
if csid == 10006 then
if option == 147 then
-- Award History
player:updateEvent({ [5] = getAwardInteger(player:getContestRankHistory()) })
elseif option == 148 then
-- Pushed by client after informing of ranking status? Leaving open for now
elseif option == 149 then
-- View Current Rules | Ranking Results | Receive Award
if contest['status'] == xi.fishingContest.status.ACCEPTING then
player:updateEvent({
[0] = contest['status'],
[1] = contest['fishid'],
[2] = contest['criteria'],
[3] = contest['measure'],
})
elseif contest['status'] == xi.fishingContest.status.PRESENTING then
player:updateEvent({
[0] = contest['status'],
[1] = contest['fishid'],
[2] = contest['criteria'],
[3] = contest['measure'],
[4] = isRewardAvailable(player),
})
end
elseif option == 150 then
-- Initial option
local time = getTimeRemaining(contest['changetime'])
if contest['status'] == xi.fishingContest.status.ACCEPTING then
player:updateEvent({
[0] = contest['status'],
[1] = contest['fishid'],
[2] = contest['criteria'],
[3] = contest['measure'],
[4] = isRewardAvailable(player),
[5] = time['days'],
[6] = time['hours'],
[7] = time['minutes'],
})
elseif contest['status'] == xi.fishingContest.status.PRESENTING then
player:updateEvent({
[0] = contest['status'],
[1] = contest['fishid'],
[2] = contest['criteria'],
[3] = contest['measure'],
[4] = isRewardAvailable(player),
[5] = time['days'],
[6] = time['hours'],
[7] = time['minutes'],
})
end
elseif option == 151 then
-- Confirm request to confirm entry
player:updateEvent({
[0] = contest['status'],
[1] = contest['fishid'],
[2] = contest['criteria'],
[3] = contest['measure'],
[5] = player:getContestScore(),
})
elseif option == 152 then
-- Confirm request to withdraw entry
player:withdrawContestFish()
end
elseif csid == 10007 then
if option == 144 then
if player:getGil() < 500 then
-- Player does not have enough gil
player:messageSpecial(zones[player:getZoneID()].text.INSUFFICIENT_GIL)
return
else
player:updateEvent()
end
elseif option == 145 then
player:updateEvent()
end
end
end
xi.fishingContest.onEventFinish = function(player, csid, option, npc)
local contest = GetFishingContest()
if contest == nil then
return
end
if csid == 10007 then
-- Give the player back a fish with the same stats, but tagged for Ranking
if option == 145 then
local length = player:getLocalVar('[FishContest]Length')
local weight = player:getLocalVar('[FishContest]Weight')
local obtained = giveFish(player, { id = contest['fishid'],
quantity = 1,
exdata = { size = length, weight = weight, isRanked = true } })
if obtained then
player:confirmTrade()
player:delGil(500) -- Pay the registration fee of 500 gil.
player:submitContestFish(scoreFish(length, weight, contest['criteria']))
player:setLocalVar('[FishContest]Length', 0)
player:setLocalVar('[FishContest]Weight', 0)
end
end
elseif csid == 10006 then
if
option == 149 and
isRewardAvailable(player) > 0 and
contest['status'] == xi.fishingContest.status.PRESENTING
then
-- Issue the player a reward and flag it as awarded.
local reward = getPlayerReward(player)
if
reward ~= nil and
npcUtil.giveReward(player, reward)
then
player:claimContestReward()
end
end
end
end