1092 lines
35 KiB
Lua
1092 lines
35 KiB
Lua
--[[
|
|
Helper functions for common NPC tasks.
|
|
|
|
npcUtil.popFromQM(player, qm, mobId, params)
|
|
npcUtil.pickNewPosition(npc, positionTable, allowCurrentPosition)
|
|
npcUtil.giveCurrency(player, currency, amount, useTreasurePoolMsg)
|
|
npcUtil.giveItem(player, items, params)
|
|
npcUtil.giveKeyItem(player, keyitems)
|
|
npcUtil.completeMission(player, logId, missionId, params)
|
|
npcUtil.completeQuest(player, area, quest, params)
|
|
npcUtil.tradeHas(trade, items)
|
|
npcUtil.tradeHasExactly(trade, items)
|
|
npcUtil.queueMove(npc, point, delay)
|
|
npcUtil.UpdateNPCSpawnPoint(id, minTime, maxTime, posTable, serverVar)
|
|
npcUtil.castingAnimation(npc, magicType, phaseDuration, func)
|
|
npcUtil.fishingAnimation(npc, phaseDuration, func)
|
|
--]]
|
|
|
|
---@class npcUtil
|
|
npcUtil = {}
|
|
|
|
--[[
|
|
Pop mob(s) from question mark NPC.
|
|
If any mob is already spawned, return false.
|
|
Params (table) can contain the following parameters:
|
|
|
|
radius (number)
|
|
if set, spawn mobs randomly within radius of NPC
|
|
claim (boolean, default true)
|
|
do spawned mobs automatically aggro the player
|
|
hide (number, default xi.settings.main.FORCE_SPAWN_QM_RESET_TIME)
|
|
how long to hide the QM for after mobs die
|
|
message (number)
|
|
if set a message will play if a entity spawns
|
|
|
|
--]]
|
|
---@param player CBaseEntity
|
|
---@param qm CBaseEntity
|
|
---@param mobId integer|integer[]
|
|
---@param params table?
|
|
---@return boolean
|
|
function npcUtil.popFromQM(player, qm, mobId, params)
|
|
local qmId = qm:getID()
|
|
|
|
-- default params
|
|
if not params then
|
|
params = {}
|
|
end
|
|
|
|
if params.claim == nil or type(params.claim) ~= 'boolean' then
|
|
params.claim = true
|
|
end
|
|
|
|
if params.hide == nil or type(params.hide) ~= 'number' then
|
|
params.hide = xi.settings.main.FORCE_SPAWN_QM_RESET_TIME
|
|
end
|
|
|
|
-- get list of mobs to pop
|
|
local mobIds = {}
|
|
if type(mobId) == 'number' then
|
|
table.insert(mobIds, mobId)
|
|
elseif type(mobId) == 'table' then
|
|
for _, v in pairs(mobId) do
|
|
if type(v) == 'number' then
|
|
table.insert(mobIds, v)
|
|
end
|
|
end
|
|
end
|
|
|
|
-- make sure none are spawned, translate table from integers to CLuaBaseEntities
|
|
local mobs = {}
|
|
for k, v in pairs(mobIds) do
|
|
local mob = GetMobByID(v)
|
|
if mob == nil or mob:isSpawned() then
|
|
return false
|
|
else
|
|
mobs[k] = mob
|
|
end
|
|
end
|
|
|
|
-- hide qm
|
|
if params.hide > 0 then
|
|
qm:setStatus(xi.status.DISAPPEAR)
|
|
end
|
|
|
|
-- spawn mobs and give each a listener that will show QM after they are all dead
|
|
for _, mob in pairs(mobs) do
|
|
-- choose random position uniformly from within radius
|
|
if params.radius and type(params.radius) == 'number' then
|
|
local r = params.radius * math.sqrt(math.random())
|
|
local theta = math.random() * 2 * math.pi
|
|
local x = r * math.cos(theta)
|
|
local z = r * math.sin(theta)
|
|
mob:setSpawn(qm:getXPos() + x, qm:getYPos(), qm:getZPos() + z)
|
|
end
|
|
|
|
-- spawn
|
|
mob:spawn()
|
|
|
|
-- mobs that pop from QM should get their steal items reset even if they didn't die on previous pop
|
|
mob:itemStolen(false)
|
|
mob:itemDespoiled(false)
|
|
|
|
-- claim
|
|
if params.claim then
|
|
mob:updateClaim(player)
|
|
end
|
|
|
|
-- Distribute enmity
|
|
if type(params.enmityPlayerList) == 'table' then
|
|
-- Add 1 CE to ensure mobs go after spawner first in case params.claim == false
|
|
mob:addEnmity(player, 1, 0)
|
|
|
|
for _, member in ipairs(params.enmityPlayerList) do
|
|
mob:addEnmity(member, 1, 0)
|
|
end
|
|
end
|
|
|
|
-- look
|
|
if params.look then
|
|
mob:lookAt(player:getPos())
|
|
end
|
|
|
|
-- reappear the QM when all spawned mobs are dead, plus params.hide seconds
|
|
if params.hide > 0 then
|
|
local myId = mob:getID()
|
|
mob:setLocalVar('qm', qmId)
|
|
mob:addListener('DESPAWN', 'QM_'..myId, function(m)
|
|
m:removeListener('QM_'..myId)
|
|
|
|
for _, v in pairs(mobs) do
|
|
if v:isAlive() then
|
|
return false
|
|
end
|
|
end
|
|
|
|
GetNPCByID(m:getLocalVar('qm')):updateNPCHideTime(params.hide)
|
|
end)
|
|
end
|
|
|
|
-- add in a spawn message if has one
|
|
if params.message then
|
|
player:messageSpecial(params.message)
|
|
end
|
|
end
|
|
|
|
return true
|
|
end
|
|
|
|
--[[
|
|
Queue a position change for an NPC. We do this because if you setPos() an NPC
|
|
immediately after you setStatus(xi.status.DISAPPEAR) it, the QM does not hide
|
|
on the players' screens.
|
|
|
|
point may be any of the following formats:
|
|
{ x, y, z }
|
|
{ x, y, z, rot }
|
|
{ x = x, y = y, z = z }
|
|
{ x = x, y = y, z = z, rot = r }
|
|
--]]
|
|
---@param x number
|
|
---@param y number
|
|
---@param z number
|
|
---@param r integer?
|
|
---@return function
|
|
---@overload fun(posTable: { ['x']: number, ['y']: number, ['z']: number, ['rot']: integer? }): nil
|
|
local function doMove(x, y, z, r)
|
|
if not r then
|
|
r = 0
|
|
end
|
|
|
|
---@param entity CBaseEntity
|
|
return function(entity)
|
|
entity:setPos(x, y, z, r)
|
|
end
|
|
end
|
|
|
|
---@param npc CBaseEntity
|
|
---@param point { ['x']: number, ['y']: number, ['z']: number, ['rot']: integer? }
|
|
---@param delay integer?
|
|
---@return nil
|
|
function npcUtil.queueMove(npc, point, delay)
|
|
if not delay then
|
|
delay = 3000
|
|
end
|
|
|
|
if point.rot then
|
|
point = { point.x, point.y, point.z, point.rot }
|
|
elseif point.x then
|
|
point = { point.x, point.y, point.z }
|
|
end
|
|
|
|
local x, y, z, rot = unpack(point)
|
|
|
|
-- TODO: Current LLS version is treating all table entries as nil-able, where only rot is. Ignore
|
|
-- until resolved.
|
|
---@diagnostic disable-next-line: param-type-mismatch
|
|
npc:queue(delay, doMove(x, y, z, rot))
|
|
end
|
|
|
|
-- Picks a new position for an NPC and excluding the current position.
|
|
-- INPUT: npc = npcID, position = 2D table with coords: index, { x, y, z }
|
|
-- RETURN: table index
|
|
---@param npcID integer
|
|
---@param positionTable table
|
|
---@param allowCurrentPosition boolean?
|
|
---@return table
|
|
function npcUtil.pickNewPosition(npcID, positionTable, allowCurrentPosition)
|
|
local npc = GetNPCByID(npcID)
|
|
local positionIndex = 1 -- Default to position one in the table if it can't be found.
|
|
local tableSize = 0
|
|
local newPosition = math.random(1, tableSize)
|
|
allowCurrentPosition = allowCurrentPosition or false
|
|
|
|
for i, v in ipairs(positionTable) do -- Looking for the current position
|
|
|
|
if not allowCurrentPosition then
|
|
-- Finding by comparing the NPC's coords
|
|
if
|
|
npc and
|
|
math.floor(v[1]) == math.floor(npc:getXPos()) and
|
|
math.floor(v[2]) == math.floor(npc:getYPos()) and
|
|
math.floor(v[3]) == math.floor(npc:getZPos())
|
|
then
|
|
positionIndex = i -- Found where the NPC is!
|
|
end
|
|
end
|
|
|
|
tableSize = tableSize + 1 -- Counting the array size
|
|
end
|
|
|
|
if not allowCurrentPosition then
|
|
-- Pick a new pos that isn't the current
|
|
repeat
|
|
newPosition = math.random(1, tableSize)
|
|
until (newPosition ~= positionIndex)
|
|
end
|
|
|
|
return { ['x'] = positionTable[newPosition][1], ['y'] = positionTable[newPosition][2], ['z'] = positionTable[newPosition][3] }
|
|
end
|
|
|
|
--[[
|
|
Give item(s) to player.
|
|
If player has inventory space, give items, display message, and return true.
|
|
If not, do not give items, display a message to indicate this, and return false.
|
|
|
|
Examples of valid items parameter:
|
|
npcUtil.giveItem(player, xi.item.CHUNK_OF_COPPER_ORE, 1) -- copper ore x1
|
|
npcUtil.giveItem(player, { xi.item.CHUNK_OF_COPPER_ORE, 1 , xi.item.CHUNK_OF_TIN_ORE, 1 }) -- copper ore x1, tin ore x1
|
|
npcUtil.giveItem(player, { { xi.item.CHUNK_OF_COPPER_ORE, 2 } }) -- copper ore x2
|
|
npcUtil.giveItem(player, { { xi.item.CHUNK_OF_COPPER_ORE, 12 }, { xi.item.CHUNK_OF_TIN_ORE, 3 } }) -- copper ore x12 tin ore x3
|
|
npcUtil.giveItem(target, { { xi.item.CHUNK_OF_COPPER_ORE, math.random(3, 15) } }) -- random 3-15 copper ores
|
|
enum can be found in scripts/enum/item.lua
|
|
|
|
params (table) can contain the following parameters:
|
|
|
|
silent (boolean, default false)
|
|
if set, displays no messages
|
|
fromTrade (boolean, default false)
|
|
if set, when player has no room for items, display
|
|
"Try trading again after sorting your inventory"
|
|
instead of
|
|
"Come back again after sorting your inventory"
|
|
multiple (boolean default false)
|
|
if set, force message type as multiples version
|
|
eg. You obtain 1 chunk of rock salt!
|
|
--]]
|
|
|
|
---@class itemQuantityEntry : { [xi.item]: xi.item, [integer]: integer }
|
|
|
|
---@class multipleItemList
|
|
---@field [integer] { [integer]: xi.item, [integer]: integer }|xi.item
|
|
|
|
---@param player CBaseEntity
|
|
---@param items xi.item|itemQuantityEntry|multipleItemList
|
|
---@param params { silent: boolean?, fromTrade: boolean?, multiple: boolean? }?
|
|
---@return boolean
|
|
function npcUtil.giveItem(player, items, params)
|
|
params = params or {}
|
|
params.silent = params.silent or false
|
|
local ID = zones[player:getZoneID()]
|
|
|
|
-- create table of items, with key/val of itemId/itemQty
|
|
local givenItems = {}
|
|
if type(items) == 'number' then
|
|
table.insert(givenItems, { items, 1 })
|
|
elseif type(items) == 'table' then
|
|
for _, v in pairs(items) do
|
|
local itemId = nil
|
|
local quantity = 1
|
|
if type(v) == 'number' then
|
|
itemId = v
|
|
elseif
|
|
type(v) == 'table' and
|
|
#v == 2 and
|
|
type(v[1]) == 'number' and
|
|
type(v[2]) == 'number'
|
|
then
|
|
itemId = v[1]
|
|
quantity = v[2]
|
|
end
|
|
|
|
if
|
|
not itemId or
|
|
itemId == 0
|
|
then
|
|
print(string.format('ERROR: invalid items parameter given to npcUtil.giveItem in zone %s.', player:getZoneName()))
|
|
return false
|
|
end
|
|
|
|
table.insert(givenItems, { itemId, quantity })
|
|
end
|
|
end
|
|
|
|
-- does player have enough inventory space?
|
|
if player:getFreeSlotsCount() < #givenItems then
|
|
if not params.silent then
|
|
local messageId = params.fromTrade and (ID.text.ITEM_CANNOT_BE_OBTAINED + 4) or ID.text.ITEM_CANNOT_BE_OBTAINED
|
|
player:messageSpecial(messageId, givenItems[1][1])
|
|
end
|
|
|
|
return false
|
|
end
|
|
|
|
-- give items to player
|
|
local messagedItems = {}
|
|
for _, v in pairs(givenItems) do
|
|
if player:addItem({ id = v[1], quantity = v[2], silent = true }) then
|
|
if
|
|
not params.silent and
|
|
not messagedItems[v[1]]
|
|
then
|
|
if
|
|
v[2] > 1 or
|
|
params.multiple
|
|
then
|
|
player:messageSpecial(ID.text.ITEM_OBTAINED + 9, v[1], v[2])
|
|
else
|
|
player:messageSpecial(ID.text.ITEM_OBTAINED, v[1])
|
|
end
|
|
|
|
messagedItems[v[1]] = true
|
|
end
|
|
else
|
|
if
|
|
not params.silent and
|
|
#givenItems == 1
|
|
then
|
|
player:messageSpecial(ID.text.ITEM_CANNOT_BE_OBTAINED, v[1])
|
|
end
|
|
|
|
return false
|
|
end
|
|
end
|
|
|
|
return true
|
|
end
|
|
|
|
--[[
|
|
Give temp item(s) to player.
|
|
If player has inventory space, give items, display message, and return true.
|
|
If not, do not give items, display a message to indicate this, and return false.
|
|
|
|
Examples of valid items parameter:
|
|
640 -- copper ore x1
|
|
{ 640, 641 } -- copper ore x1, tin ore x1
|
|
{ { 640, 2 } } -- copper ore x2
|
|
{ { 640, 2 }, 641 } -- copper ore x2, tin ore x1
|
|
|
|
params (table) can contain the following parameters:
|
|
|
|
silent (boolean, default false)
|
|
if set, displays no messages
|
|
fromTrade (boolean, default false)
|
|
if set, when player has no room for items, display
|
|
"Try trading again after sorting your inventory"
|
|
instead of
|
|
"Come back again after sorting your inventory"
|
|
--]]
|
|
---@param player CBaseEntity
|
|
---@param items xi.item|itemQuantityEntry|multipleItemList
|
|
---@param params { silent: boolean? }?
|
|
---@return boolean
|
|
function npcUtil.giveTempItem(player, items, params)
|
|
params = params or {}
|
|
local ID = zones[player:getZoneID()]
|
|
|
|
-- create table of items, with key/val of itemId/itemQty
|
|
local givenItems = {}
|
|
if type(items) == 'number' then
|
|
table.insert(givenItems, { items, 1 })
|
|
elseif type(items) == 'table' then
|
|
for _, v in pairs(items) do
|
|
if type(v) == 'number' then
|
|
table.insert(givenItems, { v, 1 })
|
|
elseif
|
|
type(v) == 'table' and
|
|
#v == 2 and
|
|
type(v[1]) == 'number' and
|
|
type(v[2]) == 'number'
|
|
then
|
|
table.insert(givenItems, { v[1], v[2] })
|
|
else
|
|
print(string.format('ERROR: invalid items parameter given to npcUtil.giveTempItem in zone %s.', player:getZoneName()))
|
|
return false
|
|
end
|
|
end
|
|
end
|
|
|
|
-- give items to player
|
|
local messagedItems = {}
|
|
for _, v in pairs(givenItems) do
|
|
if player:addTempItem(v[1], v[2]) then
|
|
if not params.silent and not messagedItems[v[1]] then
|
|
if v[2] > 1 then
|
|
player:messageSpecial(ID.text.ITEM_OBTAINED + 9, v[1], v[2])
|
|
else
|
|
player:messageSpecial(ID.text.ITEM_OBTAINED, v[1])
|
|
end
|
|
end
|
|
|
|
messagedItems[v[1]] = true
|
|
elseif #givenItems == 1 then
|
|
if not params.silent then
|
|
player:messageSpecial(ID.text.ITEM_CANNOT_BE_OBTAINED, givenItems[1][1])
|
|
end
|
|
|
|
return false
|
|
end
|
|
end
|
|
|
|
return true
|
|
end
|
|
|
|
--[[
|
|
Give currency to a player.
|
|
Message is displayed showing currency obtained.
|
|
|
|
Examples of valid parameters:
|
|
gil, 500
|
|
bayld, 1000
|
|
--]]
|
|
---@param player CBaseEntity
|
|
---@param currency string
|
|
---@param amount integer
|
|
---@param useTreasurePoolMsg boolean?
|
|
---@return boolean
|
|
function npcUtil.giveCurrency(player, currency, amount, useTreasurePoolMsg)
|
|
local ID = zones[player:getZoneID()]
|
|
|
|
if type(currency) ~= 'string' or type(amount) ~= 'number' then
|
|
print(string.format('ERROR: invalid parameter given to npcUtil.giveCurrency in zone %s.', player:getZoneName()))
|
|
return false
|
|
end
|
|
|
|
currency = string.lower(currency)
|
|
|
|
local currencyTypes =
|
|
{
|
|
['gil'] = { 'GIL_OBTAINED', xi.settings.main.GIL_RATE },
|
|
['bayld'] = { 'BAYLD_OBTAINED', xi.settings.main.BAYLD_RATE }
|
|
}
|
|
|
|
local currencyType = currencyTypes[currency]
|
|
|
|
if not currencyType then
|
|
print(string.format('ERROR: invalid currency \'%s\' given to npcUtil.giveCurrency in zone %s.', currency, player:getZoneName()))
|
|
return false
|
|
end
|
|
|
|
local messageId = ID.text[currencyType[1]]
|
|
if not messageId then
|
|
print(string.format('ERROR: no message ID defined for currency \'%s\' given to npcUtil.giveCurrency in zone %s.', currency, player:getZoneName()))
|
|
return false
|
|
end
|
|
|
|
amount = amount * currencyType[2]
|
|
|
|
if currency == 'gil' then
|
|
player:addGil(amount)
|
|
else
|
|
player:addCurrency(currency, amount)
|
|
end
|
|
|
|
if useTreasurePoolMsg then
|
|
player:messageSystem(xi.msg.system.OBTAINS_GIL, amount)
|
|
else
|
|
player:messageSpecial(messageId, amount)
|
|
end
|
|
|
|
return true
|
|
end
|
|
|
|
--[[
|
|
Give key item(s) to player.
|
|
Message is displayed showing key items obtained.
|
|
|
|
Examples of valid keyitems parameter:
|
|
xi.ki.ZERUHN_REPORT
|
|
{ xi.ki.PALBOROUGH_MINES_LOGS }
|
|
{ xi.ki.BLUE_ACIDITY_TESTER, xi.ki.RED_ACIDITY_TESTER }
|
|
--]]
|
|
---@param player CBaseEntity
|
|
---@param keyitems xi.keyItem|{ [integer]: xi.keyItem }
|
|
---@param msgId integer?
|
|
function npcUtil.giveKeyItem(player, keyitems, msgId)
|
|
local ID = zones[player:getZoneID()]
|
|
local givenKeyItems = type(keyitems) == 'table' and keyitems or { keyitems }
|
|
|
|
-- give key items to player, with message
|
|
|
|
for _, keyItemId in ipairs(givenKeyItems) do
|
|
---@cast keyItemId xi.keyItem
|
|
if not player:hasKeyItem(keyItemId) then
|
|
player:addKeyItem(keyItemId)
|
|
|
|
if msgId then
|
|
player:messageSpecial(msgId, keyItemId)
|
|
else
|
|
player:messageSpecial(ID.text.KEYITEM_OBTAINED, keyItemId)
|
|
end
|
|
end
|
|
end
|
|
|
|
return true
|
|
end
|
|
|
|
--[[
|
|
Give a reward (Hidden Quests)
|
|
If hidden quest rewards items, and the player cannot carry them, return false.
|
|
Otherwise, return true.
|
|
|
|
Example of usage with params (all params are optional):
|
|
npcUtil.giveReward(player, {
|
|
item = { { 640, 2 }, 641 }, -- see npcUtil.giveItem for formats
|
|
itemParams = { -- see npcUtil.giveItem for formats
|
|
fromTrade = true,
|
|
},
|
|
keyItem = xi.ki.ZERUHN_REPORT, -- see npcUtil.giveKeyItem for formats
|
|
fameArea = xi.fameArea.NORG, -- Required for Fame to be applied
|
|
fame = 120, -- fame defaults to 30 if not set
|
|
bayld = 500,
|
|
gil = 200,
|
|
exp = 1000,
|
|
title = xi.title.ENTRANCE_DENIED,
|
|
var = { 'foo1', 'foo2' } -- variable(s) to set to 0. string or table
|
|
})
|
|
--]]
|
|
|
|
---@class rewardParam
|
|
---@field item xi.item|itemQuantityEntry|multipleItemList?
|
|
---@field itemParams { silent: boolean?, fromTrade: boolean?, multiple: boolean? }?
|
|
---@field keyItem xi.keyItem|{ [integer]: xi.keyItem }?
|
|
---@field ki xi.keyItem|{ [integer]: xi.keyItem }?
|
|
---@field fame integer?
|
|
---@field fameArea xi.fameArea?
|
|
---@field bayld integer?
|
|
---@field gil integer?
|
|
---@field title xi.title?
|
|
---@field var string|string[]?
|
|
---@field exp integer?
|
|
|
|
---@param player CBaseEntity
|
|
---@param params rewardParam
|
|
---@return boolean
|
|
function npcUtil.giveReward(player, params)
|
|
params = params or {}
|
|
|
|
-- load text ids
|
|
local ID = zones[player:getZoneID()]
|
|
|
|
-- item(s) plus message. return false if player lacks inventory space.
|
|
if params['item'] ~= nil then
|
|
if not npcUtil.giveItem(player, params['item'], params['itemParams']) then
|
|
return false
|
|
end
|
|
end
|
|
|
|
-- key item(s), fame, gil, bayld, xp, and title
|
|
if params['keyItem'] ~= nil then
|
|
npcUtil.giveKeyItem(player, params['keyItem'])
|
|
end
|
|
|
|
if params['fame'] == nil then
|
|
params['fame'] = 30
|
|
end
|
|
|
|
if
|
|
params['fameArea'] ~= nil and
|
|
params['fameArea']['fame_area'] ~= nil and
|
|
type(params['fame']) == 'number'
|
|
then
|
|
player:addFame(params['fameArea'], params['fame'])
|
|
end
|
|
|
|
if params['gil'] ~= nil and type(params['gil']) == 'number' then
|
|
player:addGil(params['gil'] * xi.settings.main.GIL_RATE)
|
|
player:messageSpecial(ID.text.GIL_OBTAINED, params['gil'] * xi.settings.main.GIL_RATE)
|
|
end
|
|
|
|
if params['bayld'] ~= nil and type(params['bayld']) == 'number' then
|
|
player:addCurrency('bayld', params['bayld'] * xi.settings.main.BAYLD_RATE)
|
|
player:messageSpecial(ID.text.BAYLD_OBTAINED, params['bayld'] * xi.settings.main.BAYLD_RATE)
|
|
end
|
|
|
|
if params['exp'] ~= nil and type(params['exp']) == 'number' then
|
|
player:addExp(params['exp'] * xi.settings.main.EXP_RATE)
|
|
end
|
|
|
|
if params['title'] ~= nil then
|
|
player:addTitle(params['title'])
|
|
end
|
|
|
|
if params['var'] ~= nil then
|
|
local playerVarsToZero = {}
|
|
if type(params['var']) == 'table' then
|
|
---@cast params['var'] string[]
|
|
playerVarsToZero = params['var']
|
|
elseif type(params['var']) == 'string' then
|
|
table.insert(playerVarsToZero, params['var'])
|
|
end
|
|
|
|
for _, v in pairs(playerVarsToZero) do
|
|
player:setCharVar(v, 0)
|
|
end
|
|
end
|
|
|
|
return true
|
|
end
|
|
|
|
--[[
|
|
Complete a quest.
|
|
If quest rewards items, and the player cannot carry them, return false.
|
|
Otherwise, return true.
|
|
|
|
Example of usage with params (all params are optional):
|
|
npcUtil.completeQuest(player, xi.questLog.SANDORIA, xi.quest.id.sandoria.ROSEL_THE_ARMORER, {
|
|
item = { { 640, 2 }, 641 }, -- see npcUtil.giveItem for formats
|
|
itemParams = { -- see npcUtil.giveItem for formats
|
|
fromTrade = true,
|
|
},
|
|
keyItem = xi.ki.ZERUHN_REPORT, -- see npcUtil.giveKeyItem for formats
|
|
fameArea = xi.fameArea.NORG, -- Required for Fame to be applied
|
|
fame = 120, -- fame defaults to 30 if not set
|
|
bayld = 500,
|
|
gil = 200,
|
|
exp = 1000,
|
|
title = xi.title.ENTRANCE_DENIED,
|
|
var = { 'foo1', 'foo2' } -- variable(s) to set to 0. string or table
|
|
})
|
|
--]]
|
|
|
|
---@param player CBaseEntity
|
|
---@param area xi.questLog
|
|
---@param quest integer
|
|
---@param params rewardParam
|
|
---@return boolean
|
|
function npcUtil.completeQuest(player, area, quest, params)
|
|
params = params or {}
|
|
|
|
-- load text ids
|
|
local ID = zones[player:getZoneID()]
|
|
|
|
-- item(s) plus message. return false if player lacks inventory space.
|
|
if params['item'] ~= nil then
|
|
if not npcUtil.giveItem(player, params['item'], params['itemParams']) then
|
|
return false
|
|
end
|
|
end
|
|
|
|
-- key item(s), fame, gil, bayld, xp, and title
|
|
if params['keyItem'] ~= nil then
|
|
npcUtil.giveKeyItem(player, params['keyItem'])
|
|
end
|
|
|
|
-- Note: fameArea is a required numeric parameter in order for fame to be applied. Fame areas
|
|
-- are not a one to one mapping of log ids, and it should not be used as a fallback.
|
|
if params['fameArea'] ~= nil and type(params['fameArea']) == 'number' then
|
|
if params['fame'] == nil then
|
|
params['fame'] = 30
|
|
end
|
|
|
|
player:addFame(params['fameArea'], params['fame'])
|
|
end
|
|
|
|
if params['gil'] ~= nil and type(params['gil']) == 'number' then
|
|
player:addGil(params['gil'] * xi.settings.main.GIL_RATE)
|
|
player:messageSpecial(ID.text.GIL_OBTAINED, params['gil'] * xi.settings.main.GIL_RATE)
|
|
end
|
|
|
|
if params['bayld'] ~= nil and type(params['bayld']) == 'number' then
|
|
player:addCurrency('bayld', params['bayld'] * xi.settings.main.BAYLD_RATE)
|
|
player:messageSpecial(ID.text.BAYLD_OBTAINED, params['bayld'] * xi.settings.main.BAYLD_RATE)
|
|
end
|
|
|
|
if params['exp'] ~= nil and type(params['exp']) == 'number' then
|
|
player:addExp(params['exp'] * xi.settings.main.EXP_RATE)
|
|
end
|
|
|
|
if params['title'] ~= nil then
|
|
player:addTitle(params['title'])
|
|
end
|
|
|
|
if params['var'] ~= nil then
|
|
local playerVarsToZero = {}
|
|
if type(params['var']) == 'table' then
|
|
---@cast params['var'] string[]
|
|
playerVarsToZero = params['var']
|
|
elseif type(params['var']) == 'string' then
|
|
table.insert(playerVarsToZero, params['var'])
|
|
end
|
|
|
|
for _, v in ipairs(playerVarsToZero) do
|
|
player:setCharVar(v, 0)
|
|
end
|
|
end
|
|
|
|
local logId
|
|
if type(area) == 'number' then
|
|
logId = area
|
|
elseif area['quest_log'] then
|
|
logId = area['quest_log']
|
|
end
|
|
|
|
-- successfully complete the quest
|
|
if logId then
|
|
player:completeQuest(logId, quest)
|
|
else
|
|
print('ERROR: invalid logId encountered in npcUtil.completeQuest')
|
|
end
|
|
|
|
return true
|
|
end
|
|
|
|
--[[
|
|
Complete a Mission.
|
|
If quest rewards items, and the player cannot carry them, return false.
|
|
Otherwise, return true.
|
|
|
|
Example of usage with params (all params are optional):
|
|
npcUtil.completeMission(player, xi.questLog.SANDORIA, xi.quest.id.sandoria.ROSEL_THE_ARMORER, {
|
|
item = { { 640, 2 }, 641 }, -- see npcUtil.giveItem for formats
|
|
itemParams = { -- see npcUtil.giveItem for formats
|
|
fromTrade = true,
|
|
},
|
|
keyItem = xi.ki.ZERUHN_REPORT, -- see npcUtil.giveKeyItem for formats
|
|
bayld = 500,
|
|
gil = 200,
|
|
exp = 1000,
|
|
title = xi.title.ENTRANCE_DENIED,
|
|
})
|
|
--]]
|
|
function npcUtil.completeMission(player, logId, missionId, params)
|
|
params = params or {}
|
|
|
|
-- load text ids
|
|
local ID = zones[player:getZoneID()]
|
|
|
|
-- item(s) plus message. return false if player lacks inventory space.
|
|
if params['item'] ~= nil then
|
|
if not npcUtil.giveItem(player, params['item'], params['itemParams']) then
|
|
return false
|
|
end
|
|
end
|
|
|
|
-- key item(s), fame, gil, bayld, xp, and title
|
|
if params['keyItem'] ~= nil then
|
|
npcUtil.giveKeyItem(player, params['keyItem'])
|
|
end
|
|
|
|
if params['gil'] ~= nil and type(params['gil']) == 'number' then
|
|
player:addGil(params['gil'] * xi.settings.main.GIL_RATE)
|
|
player:messageSpecial(ID.text.GIL_OBTAINED, params['gil'] * xi.settings.main.GIL_RATE)
|
|
end
|
|
|
|
if params['bayld'] ~= nil and type(params['bayld']) == 'number' then
|
|
player:addCurrency('bayld', params['bayld'] * xi.settings.main.BAYLD_RATE)
|
|
player:messageSpecial(ID.text.BAYLD_OBTAINED, params['bayld'] * xi.settings.main.BAYLD_RATE)
|
|
end
|
|
|
|
if params['exp'] ~= nil and type(params['exp']) == 'number' then
|
|
player:addExp(params['exp'] * xi.settings.main.EXP_RATE)
|
|
end
|
|
|
|
if params['title'] ~= nil then
|
|
player:addTitle(params['title'])
|
|
end
|
|
|
|
-- successfully complete the mission
|
|
if logId then
|
|
player:completeMission(logId, missionId)
|
|
else
|
|
print('ERROR: Invalid logId encountered in npcUtil.completeMission')
|
|
end
|
|
|
|
-- Set Rank points before potentially increasing rank; Allows for repeatable missions
|
|
-- that provide rank up on the first completion to not interfere.
|
|
if params['rankPoints'] ~= nil and type(params['rankPoints']) == 'number' then
|
|
-- TODO: Verify 4000 cap, this was taken from missions.lua
|
|
player:setRankPoints(math.min(player:getRankPoints() + params['rankPoints'], 4000))
|
|
end
|
|
|
|
-- Add some safety for rank, and only set rank if it increases
|
|
if
|
|
params['rank'] ~= nil and
|
|
type(params['rank']) == 'number' and
|
|
player:getRank(player:getNation()) < params['rank']
|
|
then
|
|
player:setRank(params['rank'])
|
|
player:setRankPoints(0)
|
|
end
|
|
|
|
-- TODO: Do we need to support multiple missions being set?
|
|
if
|
|
params['nextMission'] ~= nil and
|
|
type(params['nextMission'][1]) == 'number' and
|
|
type(params['nextMission'][2]) == 'number'
|
|
then
|
|
player:addMission(params['nextMission'][1], params['nextMission'][2])
|
|
end
|
|
|
|
return true
|
|
end
|
|
|
|
--[[
|
|
check whether trade has all required items
|
|
if yes, confirm all the items and return true
|
|
if no, return false
|
|
|
|
valid examples of items:
|
|
640 -- copper ore x1
|
|
{ 640, 641 } -- copper ore x1, tin ore x1
|
|
{ 640, 640 } -- copper ore x2
|
|
{ { 640, 2 } } -- copper ore x2
|
|
{ { 640, 2 }, 641 } -- copper ore x2, tin ore x1
|
|
{ 640, { 'gil', 200 } } -- copper ore x1, gil x200
|
|
--]]
|
|
function npcUtil.tradeHas(trade, items, exact)
|
|
if type(exact) ~= 'boolean' then
|
|
exact = false
|
|
end
|
|
|
|
-- create table of traded items, with key/val of itemId/itemQty
|
|
local tradedItems = {}
|
|
local itemId
|
|
local itemQty
|
|
for i = 0, trade:getSlotCount()-1 do
|
|
itemId = trade:getItemId(i)
|
|
itemQty = trade:getItemQty(itemId)
|
|
tradedItems[itemId] = itemQty
|
|
end
|
|
|
|
-- create table of needed items, with key/val of itemId/itemQty
|
|
local neededItems = {}
|
|
if type(items) == 'number' then
|
|
neededItems[items] = 1
|
|
elseif type(items) == 'table' then
|
|
local itemIdNeeded
|
|
local itemQtyNeeded
|
|
for _, v in pairs(items) do
|
|
if type(v) == 'number' then
|
|
itemIdNeeded = v
|
|
itemQtyNeeded = 1
|
|
elseif
|
|
type(v) == 'table' and
|
|
#v == 2 and
|
|
type(v[1]) == 'number' and
|
|
type(v[2]) == 'number'
|
|
then
|
|
itemIdNeeded = v[1]
|
|
itemQtyNeeded = v[2]
|
|
elseif
|
|
type(v) == 'table' and
|
|
#v == 2 and
|
|
type(v[1]) == 'string' and
|
|
type(v[2]) == 'number' and
|
|
string.lower(v[1]) == 'gil'
|
|
then
|
|
itemIdNeeded = 65535
|
|
itemQtyNeeded = v[2]
|
|
else
|
|
print('ERROR: invalid value contained within items parameter given to npcUtil.tradeHas.')
|
|
itemIdNeeded = nil
|
|
end
|
|
|
|
if itemIdNeeded ~= nil then
|
|
neededItems[itemIdNeeded] = (neededItems[itemIdNeeded] == nil) and itemQtyNeeded or neededItems[itemIdNeeded] + itemQtyNeeded
|
|
end
|
|
end
|
|
else
|
|
print('ERROR: invalid items parameter given to npcUtil.tradeHas.')
|
|
return false
|
|
end
|
|
|
|
-- determine whether all needed items have been traded. return false if not.
|
|
for k, v in pairs(neededItems) do
|
|
local tradedQty = (tradedItems[k] == nil) and 0 or tradedItems[k]
|
|
if v > tradedQty then
|
|
return false
|
|
else
|
|
tradedItems[k] = tradedQty - v
|
|
end
|
|
end
|
|
|
|
-- if an exact trade was requested, check if any excess items were traded. if so, return false.
|
|
if exact then
|
|
for k, v in pairs(tradedItems) do
|
|
if v > 0 then
|
|
return false
|
|
end
|
|
end
|
|
end
|
|
|
|
-- confirm items
|
|
for k, v in pairs(neededItems) do
|
|
trade:confirmItem(k, v)
|
|
end
|
|
|
|
return true
|
|
end
|
|
|
|
--[[
|
|
check whether trade has exactly required items
|
|
if yes, confirm all the items and return true
|
|
if no, return false
|
|
|
|
valid examples of items:
|
|
640 -- copper ore x1
|
|
{ 640, 641 } -- copper ore x1, tin ore x1
|
|
{ 640, 640 } -- copper ore x2
|
|
{ { 640, 2 } } -- copper ore x2
|
|
{ { 640, 2 }, 641 } -- copper ore x2, tin ore x1
|
|
{ 640, { 'gil', 200 } } -- copper ore x1, gil x200
|
|
--]]
|
|
function npcUtil.tradeHasExactly(trade, items)
|
|
return npcUtil.tradeHas(trade, items, true)
|
|
end
|
|
|
|
-- Checks to see if a trade only contains one item, but the total count can be variable
|
|
function npcUtil.tradeHasOnly(trade, itemID)
|
|
return npcUtil.tradeHasExactly(trade, { { itemID, trade:getItemCount() } })
|
|
end
|
|
|
|
-- Checks to see if a single item in a list is contained in the trade
|
|
function npcUtil.tradeSetInList(trade, itemList)
|
|
for k, v in ipairs(itemList) do
|
|
if npcUtil.tradeHasExactly(trade, itemList[k]) then
|
|
return true
|
|
end
|
|
end
|
|
|
|
return false
|
|
end
|
|
|
|
-----------------------------------
|
|
-- UpdateNPCSpawnPoint
|
|
-----------------------------------
|
|
|
|
function npcUtil.UpdateNPCSpawnPoint(id, minTime, maxTime, posTable, serverVar)
|
|
local npc = GetNPCByID(id)
|
|
if not npc then
|
|
return
|
|
end
|
|
|
|
local respawnTime = math.random(minTime, maxTime)
|
|
local newPosition = npcUtil.pickNewPosition(npc:getID(), posTable, true)
|
|
serverVar = serverVar or nil -- serverVar is optional
|
|
|
|
if serverVar then
|
|
if GetServerVariable(serverVar) <= GetSystemTime() then
|
|
npc:hideNPC(1) -- hide so the NPC is not 'moving' through the zone
|
|
npc:setPos(newPosition.x, newPosition.y, newPosition.z)
|
|
end
|
|
end
|
|
|
|
npc:timer(respawnTime * 1000, function(npcArg)
|
|
npcUtil.UpdateNPCSpawnPoint(id, minTime, maxTime, posTable, serverVar)
|
|
end)
|
|
end
|
|
|
|
function npcUtil.fishingAnimation(npc, phaseDuration, func)
|
|
func = func or function(npcArg)
|
|
-- return true to not loop again
|
|
return false
|
|
end
|
|
|
|
if func(npc) then
|
|
return
|
|
end
|
|
|
|
npc:timer(phaseDuration * 1000, function(npcArg)
|
|
local anims =
|
|
{
|
|
[xi.anim.FISHING_NPC] = { duration = 5, nextAnim = { xi.anim.FISHING_START } },
|
|
[xi.anim.FISHING_START] = { duration = 10, nextAnim = { xi.anim.FISHING_FISH } },
|
|
[xi.anim.FISHING_FISH] =
|
|
{
|
|
duration = 10,
|
|
nextAnim =
|
|
{
|
|
xi.anim.FISHING_CAUGHT,
|
|
xi.anim.FISHING_ROD_BREAK,
|
|
xi.anim.FISHING_LINE_BREAK,
|
|
}
|
|
},
|
|
|
|
[xi.anim.FISHING_ROD_BREAK] = { duration = 3, nextAnim = { xi.anim.FISHING_NPC } },
|
|
[xi.anim.FISHING_LINE_BREAK] = { duration = 3, nextAnim = { xi.anim.FISHING_NPC } },
|
|
[xi.anim.FISHING_CAUGHT] = { duration = 5, nextAnim = { xi.anim.FISHING_NPC } },
|
|
[xi.anim.FISHING_STOP] = { duration = 3, nextAnim = { xi.anim.FISHING_NPC } },
|
|
}
|
|
|
|
local anim = anims[npcArg:getAnimation()]
|
|
local nextAnimationId = xi.anim.FISHING_NPC
|
|
local nextAnimationDuration = 10
|
|
local nextAnim = nil
|
|
if anim then
|
|
nextAnim = anim.nextAnim[math.random(1, #anim.nextAnim)]
|
|
end
|
|
|
|
if nextAnim then
|
|
nextAnimationId = nextAnim
|
|
if anims[nextAnimationId] then
|
|
nextAnimationDuration = anims[nextAnimationId].duration
|
|
end
|
|
end
|
|
|
|
npcArg:setAnimation(nextAnimationId)
|
|
npcUtil.fishingAnimation(npcArg, nextAnimationDuration, func)
|
|
end)
|
|
end
|
|
|
|
function npcUtil.castingAnimation(npc, magicType, phaseDuration, func)
|
|
func = func or function(npcArg)
|
|
-- return true to not loop again
|
|
return false
|
|
end
|
|
|
|
if func(npc) then
|
|
return
|
|
end
|
|
|
|
npc:timer(phaseDuration * 1000, function(npcArg)
|
|
local anims =
|
|
{
|
|
[xi.magic.spellGroup.BLACK] = { start = 'cabk', duration = 2000, stop = 'shbk' },
|
|
[xi.magic.spellGroup.WHITE] = { start = 'cawh', duration = 1800, stop = 'shwh' },
|
|
}
|
|
npcArg:entityAnimationPacket(anims[magicType].start)
|
|
npcArg:timer(anims[magicType].duration, function(npcTimerArg)
|
|
npcTimerArg:entityAnimationPacket(anims[magicType].stop)
|
|
end)
|
|
|
|
npcUtil.castingAnimation(npcArg, magicType, phaseDuration, func)
|
|
end)
|
|
end
|
|
|
|
function npcUtil.showCrate(crate)
|
|
crate:setStatus(xi.status.NORMAL)
|
|
crate:setUntargetable(false)
|
|
crate:resetLocalVars()
|
|
end
|
|
|
|
function npcUtil.disappearCrate(crate)
|
|
if crate:isNPC() then
|
|
crate:entityAnimationPacket(xi.animationString.STATUS_DISAPPEAR)
|
|
crate:timer(3000, function(npc)
|
|
npc:setUntargetable(true)
|
|
npc:setStatus(xi.status.DISAPPEAR)
|
|
end)
|
|
else
|
|
-- Some crates, such as Recover Crates in Limbus, are actually mobs that look like NPCs
|
|
DespawnMob(crate:getID())
|
|
end
|
|
end
|
|
|
|
-- Opens a crate and sets an 'opened' var so it cannot be opened again.
|
|
-- - crate: The npc crate to open
|
|
-- - callback: The callback function to call if crate is successfully opened. Return true to leave crate open.
|
|
function npcUtil.openCrate(crate, callback)
|
|
if crate:getLocalVar('opened') == 0 then
|
|
crate:setLocalVar('opened', 1)
|
|
local shouldDisappear = not callback()
|
|
crate:entityAnimationPacket(xi.animationString.OPEN_CRATE_GLOW)
|
|
|
|
if shouldDisappear then
|
|
crate:timer(7000, function(npc)
|
|
npcUtil.disappearCrate(npc)
|
|
end)
|
|
end
|
|
end
|
|
end
|