landsandboat/scripts/globals/npc_util.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