2023-08-28 21:02:28 -04:00
|
|
|
require('scripts/globals/interaction/quest')
|
2013-03-18 01:18:40 +00:00
|
|
|
|
2024-08-30 18:13:51 -04:00
|
|
|
---@class utils
|
2025-09-26 00:23:17 +02:00
|
|
|
utils = utils or {}
|
2013-03-18 01:18:40 +00:00
|
|
|
|
2023-10-01 13:21:10 -04:00
|
|
|
-- Event cancelled constant, replaces the hardcoded value of 1073741824 in many
|
|
|
|
|
-- scripts.
|
|
|
|
|
utils.EVENT_CANCELLED_OPTION = bit.lshift(1, 30)
|
|
|
|
|
|
2021-02-08 15:19:27 -05:00
|
|
|
-- Max uint32 constant, replaces negative values in event parameters
|
|
|
|
|
-- Note: If correcting a negative value, this is *already* -1, adjust accordingly!
|
|
|
|
|
utils.MAX_UINT32 = 4294967295
|
2021-06-06 06:55:07 -04:00
|
|
|
utils.MAX_INT32 = 2147483647
|
2021-02-08 15:19:27 -05:00
|
|
|
|
2021-12-05 19:59:44 +02:00
|
|
|
-- Used to keep the linter quiet
|
2024-08-30 18:13:51 -04:00
|
|
|
---@param ... any
|
|
|
|
|
---@return nil
|
2021-12-05 19:59:44 +02:00
|
|
|
function utils.unused(...)
|
|
|
|
|
end
|
|
|
|
|
|
2022-09-28 10:08:09 -07:00
|
|
|
-- bind and related functions are from https://stackoverflow.com/a/18229720
|
|
|
|
|
local unpack = unpack or table.unpack
|
|
|
|
|
|
|
|
|
|
local function packn(...)
|
2022-09-28 13:17:29 -07:00
|
|
|
return { n = select('#', ...), ... }
|
2022-09-28 10:08:09 -07:00
|
|
|
end
|
|
|
|
|
|
|
|
|
|
local function unpackn(t)
|
|
|
|
|
return unpack(t, 1, t.n)
|
|
|
|
|
end
|
|
|
|
|
|
|
|
|
|
local function mergen(...)
|
2022-09-28 13:17:29 -07:00
|
|
|
local res = { n = 0 }
|
2022-09-28 10:08:09 -07:00
|
|
|
for i = 1, select('#', ...) do
|
|
|
|
|
local t = select(i, ...)
|
|
|
|
|
for j = 1, t.n do
|
|
|
|
|
res.n = res.n + 1
|
|
|
|
|
res[res.n] = t[j]
|
|
|
|
|
end
|
|
|
|
|
end
|
2022-11-22 15:55:35 -05:00
|
|
|
|
2022-09-28 10:08:09 -07:00
|
|
|
return res
|
|
|
|
|
end
|
|
|
|
|
|
2023-08-24 15:53:48 +01:00
|
|
|
-- https://stackoverflow.com/questions/49979017/how-to-get-current-function-call-stack-depth-in-lua
|
|
|
|
|
-- NOTE: Supposedly this is slow. Only use this during debugging and not for live!
|
|
|
|
|
function utils.getStackDepth()
|
|
|
|
|
local depth = 0
|
|
|
|
|
while true do
|
|
|
|
|
if not debug.getinfo(3 + depth) then
|
|
|
|
|
break
|
|
|
|
|
end
|
|
|
|
|
|
|
|
|
|
depth = depth + 1
|
|
|
|
|
end
|
|
|
|
|
|
|
|
|
|
return depth
|
|
|
|
|
end
|
|
|
|
|
|
|
|
|
|
-- https://www.lua.org/pil/23.1.1.html
|
|
|
|
|
function utils.getObjectFromScope(objName, depth)
|
|
|
|
|
local idx = 1
|
|
|
|
|
while true do
|
|
|
|
|
local name, value = debug.getlocal(depth, idx)
|
|
|
|
|
if not name then
|
|
|
|
|
break
|
|
|
|
|
end
|
|
|
|
|
|
|
|
|
|
if name == objName then
|
|
|
|
|
return value
|
|
|
|
|
end
|
|
|
|
|
|
|
|
|
|
idx = idx + 1
|
|
|
|
|
end
|
|
|
|
|
|
|
|
|
|
return nil
|
|
|
|
|
end
|
|
|
|
|
|
|
|
|
|
function utils.getDebugPrinter(printEntityName, settingOrCondition, prefix)
|
|
|
|
|
return function(...)
|
|
|
|
|
if settingOrCondition then
|
2023-08-24 18:10:39 -04:00
|
|
|
local t = { ... }
|
2023-08-24 15:53:48 +01:00
|
|
|
if prefix then
|
|
|
|
|
t = { prefix, ... }
|
|
|
|
|
end
|
|
|
|
|
|
|
|
|
|
local str = tostring(unpack(t))
|
|
|
|
|
|
|
|
|
|
print(str)
|
|
|
|
|
|
|
|
|
|
local depth = utils.getStackDepth()
|
|
|
|
|
local player = utils.getObjectFromScope(printEntityName, depth + 1)
|
|
|
|
|
if player then
|
2023-12-09 23:54:00 +00:00
|
|
|
player:printToPlayer(str, xi.msg.channel.SYSTEM_3, '')
|
2023-08-24 15:53:48 +01:00
|
|
|
end
|
|
|
|
|
end
|
|
|
|
|
end
|
|
|
|
|
end
|
|
|
|
|
|
|
|
|
|
function utils.getDebugPlayerPrinter(settingOrCondition, prefix)
|
2023-08-28 21:02:28 -04:00
|
|
|
return utils.getDebugPrinter('player', settingOrCondition, prefix)
|
2023-08-24 15:53:48 +01:00
|
|
|
end
|
|
|
|
|
|
2024-08-30 18:13:51 -04:00
|
|
|
---@param func function
|
|
|
|
|
---@param ... any
|
|
|
|
|
---@return function
|
2022-09-28 10:08:09 -07:00
|
|
|
function utils.bind(func, ...)
|
|
|
|
|
local args = packn(...)
|
|
|
|
|
return function(...)
|
|
|
|
|
return func(unpackn(mergen(args, packn(...))))
|
|
|
|
|
end
|
|
|
|
|
end
|
|
|
|
|
|
2025-04-16 05:32:17 -06:00
|
|
|
-- Appends two array-style tables into a new one
|
|
|
|
|
-- Creates and returns a new iterable table containing all elements from both input arrays `a` and `b`,
|
|
|
|
|
---@nodiscard
|
|
|
|
|
---@param a table
|
|
|
|
|
---@param b table
|
|
|
|
|
---@return table
|
|
|
|
|
function utils.appendArrays(a, b)
|
|
|
|
|
local result = {}
|
|
|
|
|
for i = 1, #a do
|
|
|
|
|
result[#result + 1] = a[i]
|
|
|
|
|
end
|
|
|
|
|
|
|
|
|
|
for i = 1, #b do
|
|
|
|
|
result[#result + 1] = b[i]
|
|
|
|
|
end
|
|
|
|
|
|
|
|
|
|
return result
|
|
|
|
|
end
|
|
|
|
|
|
2022-10-31 08:16:39 -07:00
|
|
|
-- Creates a slice of an input table and returns a new table
|
2024-08-30 18:13:51 -04:00
|
|
|
---@nodiscard
|
|
|
|
|
---@param inputTable table
|
|
|
|
|
---@param first integer?
|
|
|
|
|
---@param last integer?
|
|
|
|
|
---@param step integer?
|
|
|
|
|
---@return table
|
2022-10-31 08:16:39 -07:00
|
|
|
function utils.slice(inputTable, first, last, step)
|
|
|
|
|
local slicedTable = {}
|
|
|
|
|
first = first or 1
|
|
|
|
|
last = last or #inputTable
|
|
|
|
|
step = step or 1
|
|
|
|
|
local position = 1
|
|
|
|
|
|
|
|
|
|
for i = first, last, step do
|
|
|
|
|
slicedTable[position] = inputTable[i]
|
|
|
|
|
position = position + 1
|
|
|
|
|
end
|
|
|
|
|
|
|
|
|
|
return slicedTable
|
|
|
|
|
end
|
|
|
|
|
|
2022-01-31 09:42:34 -05:00
|
|
|
-- Shuffles a table and returns a new table containing the randomized result.
|
2024-08-30 18:13:51 -04:00
|
|
|
---@nodiscard
|
|
|
|
|
---@param inputTable table
|
|
|
|
|
---@return table
|
2022-01-31 09:42:34 -05:00
|
|
|
function utils.shuffle(inputTable)
|
|
|
|
|
local shuffledTable = {}
|
2016-03-10 21:12:33 +00:00
|
|
|
|
2022-01-31 09:42:34 -05:00
|
|
|
for _, v in ipairs(inputTable) do
|
|
|
|
|
local pos = math.random(1, #shuffledTable + 1)
|
|
|
|
|
table.insert(shuffledTable, pos, v)
|
2016-03-10 21:12:33 +00:00
|
|
|
end
|
2022-01-31 09:42:34 -05:00
|
|
|
|
|
|
|
|
return shuffledTable
|
2016-03-10 21:12:33 +00:00
|
|
|
end
|
2022-11-22 15:55:35 -05:00
|
|
|
|
2022-09-28 10:07:37 -07:00
|
|
|
utils.append = nil
|
|
|
|
|
|
|
|
|
|
-- Recursively appends the input table into the provided base table.
|
|
|
|
|
-- Non-table keys are overwritten by input.
|
2024-08-30 18:13:51 -04:00
|
|
|
---@param base table
|
|
|
|
|
---@param input table
|
|
|
|
|
---@return table
|
2022-09-28 10:07:37 -07:00
|
|
|
function utils.append(base, input)
|
|
|
|
|
for k, v in pairs(input) do
|
|
|
|
|
local baseValue = base[k]
|
|
|
|
|
if baseValue ~= nil and type(baseValue) == 'table' and type(v) == 'table' then
|
|
|
|
|
utils.append(baseValue, v)
|
|
|
|
|
else
|
|
|
|
|
base[k] = v
|
|
|
|
|
end
|
|
|
|
|
end
|
2022-11-22 15:55:35 -05:00
|
|
|
|
2022-09-28 10:07:37 -07:00
|
|
|
return base
|
|
|
|
|
end
|
|
|
|
|
|
|
|
|
|
-- Returns a new table with the two input tables joined together.
|
|
|
|
|
-- Values from second input have higher priority.
|
2024-08-30 18:13:51 -04:00
|
|
|
---@nodiscard
|
|
|
|
|
---@param input1 table
|
|
|
|
|
---@param input2 table
|
|
|
|
|
---@return table
|
2022-09-28 10:07:37 -07:00
|
|
|
function utils.join(input1, input2)
|
|
|
|
|
local result = {}
|
|
|
|
|
utils.append(result, input1)
|
|
|
|
|
utils.append(result, input2)
|
|
|
|
|
return result
|
|
|
|
|
end
|
2016-03-10 21:12:33 +00:00
|
|
|
|
2020-08-16 08:50:59 -06:00
|
|
|
-- Generates a random permutation of integers >= min_val and <= max_val
|
|
|
|
|
-- If a min_val isn't given, 1 is used (assumes permutation of lua indices)
|
2024-08-30 18:13:51 -04:00
|
|
|
---@nodiscard
|
|
|
|
|
---@param max_val integer
|
|
|
|
|
---@param min_val integer?
|
|
|
|
|
---@return table
|
2020-08-16 08:50:59 -06:00
|
|
|
function utils.permgen(max_val, min_val)
|
|
|
|
|
local indices = {}
|
|
|
|
|
min_val = min_val or 1
|
|
|
|
|
|
|
|
|
|
if min_val >= max_val then
|
|
|
|
|
for iter = min_val, max_val, -1 do
|
|
|
|
|
indices[iter] = iter
|
|
|
|
|
end
|
|
|
|
|
else
|
|
|
|
|
for iter = min_val, max_val, 1 do
|
|
|
|
|
indices[iter] = iter
|
|
|
|
|
end
|
|
|
|
|
end
|
|
|
|
|
|
|
|
|
|
return utils.shuffle(indices)
|
|
|
|
|
end
|
|
|
|
|
|
2022-01-31 09:42:09 -05:00
|
|
|
-- Generates a table of unique values given a range and number of entries. This should
|
|
|
|
|
-- only be used when you need unique random values smaller than the input range. Use
|
|
|
|
|
-- utils.shuffle() or utils.permgen() directly if length of array is equal to the input
|
|
|
|
|
-- list.
|
|
|
|
|
-- Examples:
|
|
|
|
|
-- Input: (1, 3, 2) Sample Output: { 3, 1 } (randomized)
|
|
|
|
|
-- Input: (1, 10, 3) Sample Output: { 4, 9, 2 } (randomized)
|
2024-08-30 18:13:51 -04:00
|
|
|
---@nodiscard
|
|
|
|
|
---@param minVal integer
|
|
|
|
|
---@param maxVal integer
|
|
|
|
|
---@param numEntries integer
|
|
|
|
|
---@return table
|
2022-01-31 09:42:09 -05:00
|
|
|
function utils.uniqueRandomTable(minVal, maxVal, numEntries)
|
|
|
|
|
local resultTable = {}
|
|
|
|
|
local shuffledTable = utils.permgen(maxVal, minVal)
|
|
|
|
|
|
|
|
|
|
if numEntries > #shuffledTable then
|
2023-08-28 21:02:28 -04:00
|
|
|
print('utils.uniqueRandomTable(): numEntries exceeds length of shuffledTable!')
|
2024-07-23 13:23:22 +01:00
|
|
|
return resultTable
|
2022-01-31 09:42:09 -05:00
|
|
|
end
|
|
|
|
|
|
|
|
|
|
for i = 1, numEntries do
|
|
|
|
|
resultTable[i] = shuffledTable[i]
|
|
|
|
|
end
|
|
|
|
|
|
|
|
|
|
return resultTable
|
|
|
|
|
end
|
|
|
|
|
|
2024-08-30 18:13:51 -04:00
|
|
|
---@nodiscard
|
|
|
|
|
---@param input number
|
2025-12-24 00:16:56 +01:00
|
|
|
---@param minValue number
|
|
|
|
|
---@param maxValue number
|
2024-08-30 18:13:51 -04:00
|
|
|
---@return number
|
2025-12-24 00:16:56 +01:00
|
|
|
function utils.clamp(input, minValue, maxValue)
|
|
|
|
|
if minValue == nil then
|
|
|
|
|
print('utils.clamp() -> invalid "min" value.')
|
|
|
|
|
return input
|
|
|
|
|
end
|
|
|
|
|
|
|
|
|
|
if maxValue == nil then
|
|
|
|
|
print('utils.clamp() -> invalid "max" value.')
|
|
|
|
|
return input
|
|
|
|
|
end
|
|
|
|
|
|
|
|
|
|
if minValue > maxValue then
|
|
|
|
|
print('utils.clamp() -> "min" value.is higher than "max" value.')
|
|
|
|
|
return input
|
|
|
|
|
end
|
|
|
|
|
|
|
|
|
|
if input < minValue then
|
|
|
|
|
return minValue
|
|
|
|
|
end
|
|
|
|
|
|
|
|
|
|
if input > maxValue then
|
|
|
|
|
return maxValue
|
2013-03-18 01:18:40 +00:00
|
|
|
end
|
2022-11-22 15:55:35 -05:00
|
|
|
|
2020-02-22 19:37:33 -08:00
|
|
|
return input
|
|
|
|
|
end
|
2013-03-18 01:18:40 +00:00
|
|
|
|
2022-11-16 22:37:31 -08:00
|
|
|
-- Returns a table containing all the elements in the specified range.
|
|
|
|
|
-- Source: https://github.com/mebens/range
|
2024-08-30 18:13:51 -04:00
|
|
|
---@nodiscard
|
|
|
|
|
---@param from number
|
|
|
|
|
---@param to number
|
|
|
|
|
---@param step integer?
|
|
|
|
|
---@return table
|
|
|
|
|
---@overload fun(from: string, to: string, step: integer?): table
|
|
|
|
|
---@overload fun(from: table, to: table, step: integer?): table
|
2022-11-16 22:37:31 -08:00
|
|
|
function utils.range(from, to, step)
|
|
|
|
|
local t = {}
|
|
|
|
|
local argType = type(from)
|
|
|
|
|
step = step or 1
|
|
|
|
|
|
2023-08-31 20:31:20 +01:00
|
|
|
if argType == 'number' then
|
2022-11-16 22:37:31 -08:00
|
|
|
for i = from, to, step do t[#t + 1] = i end
|
2023-08-31 20:31:20 +01:00
|
|
|
elseif argType == 'string' then
|
2022-11-16 22:37:31 -08:00
|
|
|
local e = string.byte(to)
|
|
|
|
|
for i = string.byte(from), e, step do t[#t + 1] = string.char(i) end
|
2023-08-31 20:31:20 +01:00
|
|
|
elseif argType == 'table' then
|
2022-11-16 22:37:31 -08:00
|
|
|
local metaNext = getmetatable(from).__next
|
|
|
|
|
|
|
|
|
|
if metaNext then
|
|
|
|
|
local i = from
|
|
|
|
|
|
|
|
|
|
while i < to do
|
|
|
|
|
t[#t + 1] = i
|
|
|
|
|
i = metaNext(i, step)
|
|
|
|
|
end
|
|
|
|
|
|
|
|
|
|
t[#t + 1] = to
|
|
|
|
|
end
|
|
|
|
|
end
|
|
|
|
|
|
|
|
|
|
return t
|
|
|
|
|
end
|
|
|
|
|
|
|
|
|
|
-----------------------------------
|
|
|
|
|
--
|
|
|
|
|
-- Functional
|
|
|
|
|
--
|
|
|
|
|
-- Functional methods provide a means to simplify logic that consists in
|
|
|
|
|
-- simple operations when iterating a table.
|
|
|
|
|
-- In general, they can make code much more concise and readable, but they
|
|
|
|
|
-- can also end up making it a cluttered mess, so use your judgement
|
|
|
|
|
-- when deciding if you want to use these methods
|
|
|
|
|
-----------------------------------
|
|
|
|
|
|
|
|
|
|
-- Given a table and a mapping function, returns a new table created by
|
|
|
|
|
-- applying the given mapping function to the given table elements
|
2024-08-30 18:13:51 -04:00
|
|
|
---@nodiscard
|
|
|
|
|
---@param tbl table
|
|
|
|
|
---@param func function
|
|
|
|
|
---@return table
|
2022-11-16 22:37:31 -08:00
|
|
|
function utils.map(tbl, func)
|
|
|
|
|
local t = {}
|
2023-07-24 23:48:17 +01:00
|
|
|
|
2022-11-16 22:37:31 -08:00
|
|
|
for k, v in pairs(tbl) do
|
|
|
|
|
t[k] = func(k, v)
|
|
|
|
|
end
|
2023-07-24 23:48:17 +01:00
|
|
|
|
2022-11-16 22:37:31 -08:00
|
|
|
return t
|
|
|
|
|
end
|
|
|
|
|
|
2025-04-16 05:32:17 -06:00
|
|
|
-- Iterates through the table and calls the callback on each key, value pair.
|
|
|
|
|
---@param tbl table
|
|
|
|
|
---@param func function
|
|
|
|
|
function utils.each(tbl, func)
|
|
|
|
|
for k, v in pairs(tbl) do
|
|
|
|
|
func(k, v)
|
|
|
|
|
end
|
|
|
|
|
end
|
|
|
|
|
|
2022-11-16 22:37:31 -08:00
|
|
|
-- Given a table and a filter function, returns a new table composed of the
|
|
|
|
|
-- elements that pass the given filter.
|
2023-08-31 20:31:20 +01:00
|
|
|
-- e.g: utils.filter({ 'a', 'b', 'c', 'd' }, function(k, v) return v >= 'c' end) --> { 'c', 'd }
|
2024-08-30 18:13:51 -04:00
|
|
|
---@nodiscard
|
|
|
|
|
---@param tbl table
|
|
|
|
|
---@param func function
|
|
|
|
|
---@return table
|
2022-11-16 22:37:31 -08:00
|
|
|
function utils.filter(tbl, func)
|
|
|
|
|
local out = {}
|
|
|
|
|
|
|
|
|
|
for k, v in pairs(tbl) do
|
|
|
|
|
if func(k, v) then
|
|
|
|
|
out[k] = v
|
|
|
|
|
end
|
|
|
|
|
end
|
|
|
|
|
|
|
|
|
|
return out
|
|
|
|
|
end
|
|
|
|
|
|
|
|
|
|
-- Given a table and a filter function, returns a new table composed of the
|
|
|
|
|
-- elements that pass the given filter.
|
|
|
|
|
-- Unlike utils.filter, this method will return an iterable table.
|
2023-08-31 20:31:20 +01:00
|
|
|
-- e.g utils.filterArray({ 'a', 'b', 'c', 'd' }, function(k, v) return v >= 'c' end) --> { 1 => 'c', 2 => 'd' }
|
2024-08-30 18:13:51 -04:00
|
|
|
---@nodiscard
|
|
|
|
|
---@param tbl table
|
|
|
|
|
---@param func function
|
|
|
|
|
---@return table
|
2022-11-16 22:37:31 -08:00
|
|
|
function utils.filterArray(tbl, func)
|
|
|
|
|
local out = {}
|
|
|
|
|
|
|
|
|
|
for k, v in pairs(tbl) do
|
|
|
|
|
if func(k, v) then
|
|
|
|
|
table.insert(out, v)
|
|
|
|
|
end
|
|
|
|
|
end
|
|
|
|
|
|
|
|
|
|
return out
|
|
|
|
|
end
|
|
|
|
|
|
|
|
|
|
-- Returns true if any member of the given table passes the given
|
|
|
|
|
-- predicate function
|
2024-08-30 18:13:51 -04:00
|
|
|
---@nodiscard
|
|
|
|
|
---@param tbl table
|
|
|
|
|
---@param predicate function
|
|
|
|
|
---@return boolean
|
2022-11-16 22:37:31 -08:00
|
|
|
function utils.any(tbl, predicate)
|
|
|
|
|
for k, v in pairs(tbl) do
|
|
|
|
|
if predicate(k, v) then
|
|
|
|
|
return true
|
|
|
|
|
end
|
|
|
|
|
end
|
|
|
|
|
|
|
|
|
|
return false
|
|
|
|
|
end
|
|
|
|
|
|
2025-04-18 05:59:15 -06:00
|
|
|
-- Returns true if all members of the given table pass the given
|
|
|
|
|
-- predicate function
|
|
|
|
|
---@nodiscard
|
|
|
|
|
---@param tbl table
|
|
|
|
|
---@param predicate function
|
|
|
|
|
---@return boolean
|
|
|
|
|
function utils.all(tbl, predicate)
|
|
|
|
|
for k, v in pairs(tbl) do
|
|
|
|
|
if not predicate(k, v) then
|
|
|
|
|
return false
|
|
|
|
|
end
|
|
|
|
|
end
|
|
|
|
|
|
|
|
|
|
return true
|
|
|
|
|
end
|
|
|
|
|
|
2022-11-16 22:37:31 -08:00
|
|
|
-- Returns the sum of applying the given function to each element of the given table
|
|
|
|
|
-- e.g: utils.sum({ 1, 2, 3 }, function(k, v) return v end) --> 6
|
2024-08-30 18:13:51 -04:00
|
|
|
---@nodiscard
|
|
|
|
|
---@param tbl table
|
|
|
|
|
---@param func function
|
|
|
|
|
---@return number
|
2022-11-16 22:37:31 -08:00
|
|
|
function utils.sum(tbl, func)
|
|
|
|
|
local sum = 0
|
|
|
|
|
|
|
|
|
|
for k, v in pairs(tbl) do
|
|
|
|
|
sum = sum + func(k, v)
|
|
|
|
|
end
|
|
|
|
|
|
|
|
|
|
return sum
|
|
|
|
|
end
|
|
|
|
|
|
|
|
|
|
-- To be used with utils.sum.
|
|
|
|
|
-- Used to count the number of times an element in a table
|
|
|
|
|
-- matches the given predicate
|
2023-08-31 20:31:20 +01:00
|
|
|
-- e.g: utils.sum({ 'a, 'a', 'b' }, utils.counter(function (k, v) return v == 'a' end)) --> 2
|
2022-11-16 22:37:31 -08:00
|
|
|
function utils.counter(predicate)
|
2023-07-24 23:48:17 +01:00
|
|
|
return function(k, v)
|
2022-11-16 22:37:31 -08:00
|
|
|
if predicate(k, v) then
|
|
|
|
|
return 1
|
|
|
|
|
else
|
|
|
|
|
return 0
|
|
|
|
|
end
|
|
|
|
|
end
|
|
|
|
|
end
|
|
|
|
|
|
2024-08-30 18:13:51 -04:00
|
|
|
---@nodiscard
|
|
|
|
|
---@param attacker CBaseEntity
|
|
|
|
|
---@param target CBaseEntity
|
|
|
|
|
---@param skill any TODO: This is currently unused
|
|
|
|
|
---@param maxDamage any
|
|
|
|
|
---@param minimumPercentage any
|
|
|
|
|
---@return integer
|
2022-12-03 09:20:16 -05:00
|
|
|
function utils.conalDamageAdjustment(attacker, target, skill, maxDamage, minimumPercentage)
|
2020-10-20 17:39:21 -07:00
|
|
|
-- #TODO: Currently all cone attacks use static 45 degree (360 scale) angles in core, when cone attacks
|
2020-10-19 08:16:51 -07:00
|
|
|
-- have different angles and there's a method to fetch the angle, use a line like the below
|
2022-12-03 09:20:16 -05:00
|
|
|
-- local coneAngle = skill:getConalAngle()
|
2023-08-31 20:31:20 +01:00
|
|
|
local coneAngle = 32 -- 256-degree based, equivalent to '45 degrees' on 360 degree scale
|
2020-10-19 08:16:51 -07:00
|
|
|
|
2023-08-31 20:31:20 +01:00
|
|
|
-- #TODO: Conal attacks hit targets in a cone with a center line of the 'primary' target (the mob's
|
2020-10-20 17:39:21 -07:00
|
|
|
-- highest enmity target). These primary targets can be within 128 degrees of the mob's front. However,
|
|
|
|
|
-- there's currently no way for a conal skill to store (and later check) the primary target a mob skill
|
2023-08-31 20:31:20 +01:00
|
|
|
-- was trying to hit. Therefore the 'damage drop off' here is based from an origin of the mob's rotation
|
2020-10-20 17:39:21 -07:00
|
|
|
-- instead. Should conal skills become capable of identifying their primary target, this should be changed
|
|
|
|
|
-- to be based on the degree difference from the primary target instead.
|
2022-12-03 09:20:16 -05:00
|
|
|
local conalAnglePower = coneAngle - math.abs(attacker:getFacingAngle(target))
|
2020-10-19 08:16:51 -07:00
|
|
|
|
2022-12-03 09:20:16 -05:00
|
|
|
if conalAnglePower < 0 then
|
2020-10-20 17:39:21 -07:00
|
|
|
-- #TODO The below print will be a valid print upon fixing to-do above relating to beam center orgin
|
2022-12-03 09:20:16 -05:00
|
|
|
conalAnglePower = 0
|
2020-10-19 08:16:51 -07:00
|
|
|
end
|
|
|
|
|
|
|
|
|
|
-- Calculate the amount of damage to add above the minimum percentage based on how close
|
|
|
|
|
-- the target is to the center of the conal (0 degrees from the attacker's facing)
|
2022-12-03 09:20:16 -05:00
|
|
|
local minimumDamage = maxDamage * minimumPercentage
|
|
|
|
|
local damagePerAngle = (maxDamage - minimumDamage) / coneAngle
|
|
|
|
|
local additionalDamage = damagePerAngle * conalAnglePower
|
2020-10-19 08:16:51 -07:00
|
|
|
|
2023-08-24 18:10:39 -04:00
|
|
|
local finalDamage = math.max(1, math.ceil(minimumDamage + additionalDamage))
|
2020-10-19 08:16:51 -07:00
|
|
|
|
2022-12-03 09:20:16 -05:00
|
|
|
return finalDamage
|
2020-10-19 08:16:51 -07:00
|
|
|
end
|
|
|
|
|
|
2024-08-30 18:13:51 -04:00
|
|
|
---@nodiscard
|
|
|
|
|
---@param actor CBaseEntity
|
|
|
|
|
---@param job xi.job
|
2023-05-31 12:10:20 +02:00
|
|
|
function utils.getActiveJobLevel(actor, job)
|
|
|
|
|
local jobLevel = 0
|
|
|
|
|
|
|
|
|
|
if actor:getMainJob() == job then
|
|
|
|
|
jobLevel = actor:getMainLvl()
|
|
|
|
|
elseif actor:getSubJob() == job then
|
|
|
|
|
jobLevel = actor:getSubLvl()
|
|
|
|
|
end
|
|
|
|
|
|
|
|
|
|
return jobLevel
|
|
|
|
|
end
|
|
|
|
|
|
2021-10-13 12:16:06 -04:00
|
|
|
-- System Strength Bonus table. This is used by xi.mobskills.mobBreathMove, but determines weakness of
|
2022-12-19 20:22:31 +01:00
|
|
|
-- a defending system, vs the attacking system. This table is indexed by the attacker.
|
2021-06-12 06:25:23 -04:00
|
|
|
-- This table can scale beyond two values, but at this time, no data has been recorded.
|
|
|
|
|
-- Values: 1 == Bonus, -1 == Weakness, 0 == Default (No Weakness or Bonus)
|
|
|
|
|
local systemStrengthTable =
|
|
|
|
|
{
|
|
|
|
|
[xi.eco.BEAST ] = { [xi.eco.LIZARD ] = 1, [xi.eco.PLANTOID] = -1, },
|
|
|
|
|
[xi.eco.LIZARD ] = { [xi.eco.VERMIN ] = 1, [xi.eco.BEAST ] = -1, },
|
|
|
|
|
[xi.eco.VERMIN ] = { [xi.eco.PLANTOID] = 1, [xi.eco.LIZARD ] = -1, },
|
|
|
|
|
[xi.eco.PLANTOID] = { [xi.eco.BEAST ] = 1, [xi.eco.VERMIN ] = -1, },
|
|
|
|
|
[xi.eco.AQUAN ] = { [xi.eco.AMORPH ] = 1, [xi.eco.BIRD ] = -1, },
|
|
|
|
|
[xi.eco.AMORPH ] = { [xi.eco.BIRD ] = 1, [xi.eco.AQUAN ] = -1, },
|
|
|
|
|
[xi.eco.BIRD ] = { [xi.eco.AQUAN ] = 1, [xi.eco.AMORPH ] = -1, },
|
|
|
|
|
[xi.eco.UNDEAD ] = { [xi.eco.ARCANA ] = 1, },
|
|
|
|
|
[xi.eco.ARCANA ] = { [xi.eco.UNDEAD ] = 1, },
|
|
|
|
|
[xi.eco.DRAGON ] = { [xi.eco.DEMON ] = 1, },
|
|
|
|
|
[xi.eco.DEMON ] = { [xi.eco.DRAGON ] = 1, },
|
2022-12-19 20:22:31 +01:00
|
|
|
[xi.eco.LUMINIAN] = { [xi.eco.LUMINION] = 1, },
|
|
|
|
|
[xi.eco.LUMINION] = { [xi.eco.LUMINIAN] = 1, },
|
2021-06-12 06:25:23 -04:00
|
|
|
}
|
|
|
|
|
|
2024-08-30 18:13:51 -04:00
|
|
|
---@nodiscard
|
|
|
|
|
---@param attackerSystem xi.eco
|
|
|
|
|
---@param defenderSystem xi.eco
|
|
|
|
|
---@return integer
|
2023-04-20 00:40:48 -04:00
|
|
|
function utils.getEcosystemStrengthBonus(attackerSystem, defenderSystem)
|
2021-06-12 06:25:23 -04:00
|
|
|
for k, v in pairs(systemStrengthTable) do
|
|
|
|
|
if k == attackerSystem then
|
|
|
|
|
for defId, weakValue in pairs(systemStrengthTable[k]) do
|
|
|
|
|
if defId == defenderSystem then
|
|
|
|
|
return weakValue
|
|
|
|
|
end
|
|
|
|
|
end
|
2015-10-14 16:55:59 -04:00
|
|
|
end
|
|
|
|
|
end
|
|
|
|
|
|
2020-02-22 19:37:33 -08:00
|
|
|
return 0
|
|
|
|
|
end
|
2019-12-20 19:14:59 -05:00
|
|
|
|
2020-10-05 14:20:28 -04:00
|
|
|
-- utils.mask contains functions for bitmask variables
|
|
|
|
|
utils.mask =
|
|
|
|
|
{
|
|
|
|
|
-- return mask's pos-th bit as bool
|
2024-08-30 18:13:51 -04:00
|
|
|
---@nodiscard
|
|
|
|
|
---@param mask integer
|
|
|
|
|
---@param pos integer
|
|
|
|
|
---@return boolean
|
2020-10-05 14:20:28 -04:00
|
|
|
getBit = function(mask, pos)
|
|
|
|
|
return bit.band(mask, bit.lshift(1, pos)) ~= 0
|
|
|
|
|
end,
|
|
|
|
|
|
|
|
|
|
-- return value of mask after setting its pos-th bit
|
|
|
|
|
-- val can be bool or number. if number, any non-zero value will be treated as true.
|
2024-08-30 18:13:51 -04:00
|
|
|
---@param mask integer
|
|
|
|
|
---@param pos integer
|
|
|
|
|
---@param val number|boolean
|
|
|
|
|
---@return integer
|
2020-10-05 14:20:28 -04:00
|
|
|
setBit = function(mask, pos, val)
|
|
|
|
|
local state = false
|
|
|
|
|
|
2023-08-28 21:02:28 -04:00
|
|
|
if type(val) == 'boolean' then
|
2020-10-05 14:20:28 -04:00
|
|
|
state = val
|
2023-08-28 21:02:28 -04:00
|
|
|
elseif type(val) == 'number' then
|
2020-10-05 14:20:28 -04:00
|
|
|
state = (val ~= 0)
|
|
|
|
|
end
|
|
|
|
|
|
|
|
|
|
if state then
|
|
|
|
|
-- turn bit on
|
|
|
|
|
return bit.bor(mask, bit.lshift(1, pos))
|
|
|
|
|
else
|
|
|
|
|
-- turn bit off
|
|
|
|
|
return bit.band(mask, bit.bnot(bit.lshift(1, pos)))
|
|
|
|
|
end
|
|
|
|
|
end,
|
|
|
|
|
|
|
|
|
|
-- return number of true bits in mask of length len
|
|
|
|
|
-- if len is omitted, assume 32
|
2024-08-30 18:13:51 -04:00
|
|
|
---@nodiscard
|
|
|
|
|
---@param mask integer
|
|
|
|
|
---@param len integer?
|
|
|
|
|
---@return integer
|
2020-10-05 14:20:28 -04:00
|
|
|
countBits = function(mask, len)
|
|
|
|
|
if not len then
|
|
|
|
|
len = 32
|
|
|
|
|
end
|
|
|
|
|
|
|
|
|
|
local count = 0
|
|
|
|
|
|
|
|
|
|
for i = 0, len - 1 do
|
2020-10-06 09:11:35 -04:00
|
|
|
count = count + bit.band(bit.rshift(mask, i), 1)
|
2020-10-05 14:20:28 -04:00
|
|
|
end
|
|
|
|
|
|
|
|
|
|
return count
|
|
|
|
|
end,
|
|
|
|
|
|
|
|
|
|
-- are all bits true in mask of length len?
|
|
|
|
|
-- if len is omitted, assume 32
|
2024-08-30 18:13:51 -04:00
|
|
|
---@nodiscard
|
|
|
|
|
---@param mask integer
|
|
|
|
|
---@param len integer?
|
|
|
|
|
---@return boolean
|
2020-10-05 14:20:28 -04:00
|
|
|
isFull = function(mask, len)
|
|
|
|
|
if not len then
|
|
|
|
|
len = 32
|
|
|
|
|
end
|
|
|
|
|
|
2021-06-02 20:43:44 +02:00
|
|
|
local fullMask = (2 ^ len) - 1
|
2020-10-05 14:20:28 -04:00
|
|
|
|
|
|
|
|
return bit.band(mask, fullMask) == fullMask
|
|
|
|
|
end,
|
2025-02-15 05:23:34 -06:00
|
|
|
|
|
|
|
|
splitBits = function(bits, size)
|
|
|
|
|
local result = {}
|
|
|
|
|
local mask = bit.lshift(1, size) - 1
|
|
|
|
|
|
|
|
|
|
while bits > 0 do
|
|
|
|
|
result[#result + 1] = bit.band(bits, mask)
|
|
|
|
|
bits = bit.rshift(bits, size)
|
|
|
|
|
end
|
|
|
|
|
|
|
|
|
|
return result
|
|
|
|
|
end,
|
|
|
|
|
|
|
|
|
|
varSplit = function(option, splitBit)
|
|
|
|
|
splitBit = splitBit or 16
|
|
|
|
|
local mask = bit.lshift(1, splitBit) - 1
|
|
|
|
|
|
|
|
|
|
return bit.band(option, mask), bit.rshift(option, splitBit)
|
|
|
|
|
end,
|
2020-10-05 18:54:45 -04:00
|
|
|
}
|
2021-05-01 00:53:48 +02:00
|
|
|
|
|
|
|
|
function utils.prequire(...)
|
|
|
|
|
local ok, result = pcall(require, ...)
|
|
|
|
|
if ok then
|
|
|
|
|
return result
|
|
|
|
|
else
|
2022-09-17 10:50:57 -04:00
|
|
|
local vars = { ... }
|
2025-09-26 00:23:17 +02:00
|
|
|
print(string.format('Error while trying to load \'%s\': %s', vars[1], result))
|
2021-05-01 00:53:48 +02:00
|
|
|
end
|
|
|
|
|
end
|
2021-05-27 13:44:52 -04:00
|
|
|
|
2021-07-05 12:30:12 -04:00
|
|
|
-- Checks to see if a specific value is contained in a table. This is often
|
|
|
|
|
-- used for tables that do not define specific indices.
|
|
|
|
|
-- See: Sigil NPCs
|
2024-08-30 18:13:51 -04:00
|
|
|
---@nodiscard
|
|
|
|
|
---@param value any
|
|
|
|
|
---@param collection table?
|
|
|
|
|
---@return boolean
|
2021-05-30 06:02:01 -04:00
|
|
|
function utils.contains(value, collection)
|
2023-08-24 23:27:30 +01:00
|
|
|
if collection == nil then
|
|
|
|
|
return false
|
|
|
|
|
end
|
|
|
|
|
|
2021-08-11 14:16:25 -07:00
|
|
|
for _, v in pairs(collection) do
|
2021-05-30 06:02:01 -04:00
|
|
|
if value == v then
|
|
|
|
|
return true
|
|
|
|
|
end
|
|
|
|
|
end
|
|
|
|
|
|
|
|
|
|
return false
|
|
|
|
|
end
|
|
|
|
|
|
2021-07-05 12:30:12 -04:00
|
|
|
-- Checks to see if a specific key is contained in the table. This is used by
|
|
|
|
|
-- tables that contain specific indices that may be non-sequential.
|
|
|
|
|
-- See: xi.teleport.escape
|
2024-08-30 18:13:51 -04:00
|
|
|
---@nodiscard
|
|
|
|
|
---@param keyVal string|integer
|
|
|
|
|
---@param collection table
|
|
|
|
|
---@return boolean
|
2021-07-04 09:21:46 -04:00
|
|
|
function utils.hasKey(keyVal, collection)
|
|
|
|
|
for k, _ in pairs(collection) do
|
|
|
|
|
if k == keyVal then
|
|
|
|
|
return true
|
|
|
|
|
end
|
|
|
|
|
end
|
|
|
|
|
|
|
|
|
|
return false
|
|
|
|
|
end
|
|
|
|
|
|
2021-06-16 13:51:03 +03:00
|
|
|
-- Selects a random entry from a table, returns the index and the entry
|
|
|
|
|
-- https://gist.github.com/jdev6/1e7ff30671edf88d03d4
|
2024-08-30 18:13:51 -04:00
|
|
|
---@nodiscard
|
|
|
|
|
---@param t table
|
|
|
|
|
---@return integer|string, any
|
2021-12-05 19:59:44 +02:00
|
|
|
function utils.randomEntryIdx(t)
|
2021-06-16 13:51:03 +03:00
|
|
|
local keys = {}
|
2022-04-10 12:48:30 -04:00
|
|
|
|
|
|
|
|
for key, _ in pairs(t) do
|
2022-10-23 10:54:18 -04:00
|
|
|
keys[#keys + 1] = key
|
2021-06-16 13:51:03 +03:00
|
|
|
end
|
2022-04-10 12:48:30 -04:00
|
|
|
|
2021-06-16 13:51:03 +03:00
|
|
|
local index = keys[math.random(1, #keys)]
|
|
|
|
|
return index, t[index]
|
|
|
|
|
end
|
|
|
|
|
|
2024-08-30 18:13:51 -04:00
|
|
|
---@nodiscard
|
|
|
|
|
---@param t table
|
|
|
|
|
---@return any
|
2021-12-05 19:59:44 +02:00
|
|
|
function utils.randomEntry(t)
|
|
|
|
|
local _, item = utils.randomEntryIdx(t)
|
|
|
|
|
return item
|
|
|
|
|
end
|
|
|
|
|
|
2021-05-27 13:44:52 -04:00
|
|
|
-- Helper functions for Interaction Framework Quests
|
|
|
|
|
-- These should only be used when working between quests, or outside
|
|
|
|
|
-- of the quest script itself. Quest vars will be deleted automatically
|
|
|
|
|
-- when that quest:complete(player) is called!
|
2024-08-30 18:13:51 -04:00
|
|
|
---@deprecated Use xi.quest or xi.mission functions
|
|
|
|
|
---@nodiscard
|
|
|
|
|
---@param player CBaseEntity
|
|
|
|
|
---@param logId xi.questLog
|
|
|
|
|
---@param questId integer
|
|
|
|
|
---@param varName string
|
|
|
|
|
---@return integer
|
2021-05-27 13:44:52 -04:00
|
|
|
function utils.getQuestVar(player, logId, questId, varName)
|
|
|
|
|
local charVarName = Quest.getVarPrefix(logId, questId) .. varName
|
|
|
|
|
return player:getCharVar(charVarName)
|
|
|
|
|
end
|
|
|
|
|
|
2024-08-30 18:13:51 -04:00
|
|
|
---@deprecated Use xi.quest or xi.mission functions
|
|
|
|
|
---@param player CBaseEntity
|
|
|
|
|
---@param logId xi.questLog
|
|
|
|
|
---@param questId integer
|
|
|
|
|
---@param varName string
|
|
|
|
|
---@param value integer
|
|
|
|
|
---@return nil
|
2021-05-27 13:44:52 -04:00
|
|
|
function utils.setQuestVar(player, logId, questId, varName, value)
|
|
|
|
|
local charVarName = Quest.getVarPrefix(logId, questId) .. varName
|
|
|
|
|
player:setCharVar(charVarName, value)
|
|
|
|
|
end
|
2021-05-28 16:25:41 -04:00
|
|
|
|
2023-08-28 21:02:28 -04:00
|
|
|
-- utils.splitStr('a.b.c', '.') => { 'a', 'b', 'c' }
|
2024-08-30 18:13:51 -04:00
|
|
|
---@nodiscard
|
|
|
|
|
---@param s string
|
|
|
|
|
---@param sep string
|
|
|
|
|
---@return table
|
2021-08-17 21:23:10 +03:00
|
|
|
function utils.splitStr(s, sep)
|
|
|
|
|
local fields = {}
|
2023-08-28 21:02:28 -04:00
|
|
|
local pattern = string.format('([^%s]+)', sep)
|
2022-11-22 15:55:35 -05:00
|
|
|
local _ = string.gsub(s, pattern, function(c)
|
|
|
|
|
fields[#fields + 1] = c
|
|
|
|
|
end)
|
|
|
|
|
|
2021-08-17 21:23:10 +03:00
|
|
|
return fields
|
|
|
|
|
end
|
2022-06-02 16:48:35 -04:00
|
|
|
|
2023-07-19 19:11:51 +01:00
|
|
|
-- Remove whitespace from the beginning and end of a string
|
2024-08-30 18:13:51 -04:00
|
|
|
---@nodiscard
|
|
|
|
|
---@param s string
|
|
|
|
|
---@return string, integer
|
2023-07-19 19:11:51 +01:00
|
|
|
function utils.trimStr(s)
|
2023-08-28 21:02:28 -04:00
|
|
|
local s1 = string.gsub(s, '^s%+', '')
|
|
|
|
|
return string.gsub(s1, '%s+$', '')
|
2023-07-19 19:11:51 +01:00
|
|
|
end
|
|
|
|
|
|
|
|
|
|
-- Split a single string argument into multiple arguments
|
2024-08-30 18:13:51 -04:00
|
|
|
---@nodiscard
|
|
|
|
|
---@param s string
|
|
|
|
|
---@return table
|
2023-07-19 19:11:51 +01:00
|
|
|
function utils.splitArg(s)
|
2023-08-28 21:02:28 -04:00
|
|
|
local comma = string.gsub(s, ',', ' ')
|
|
|
|
|
local spaces = string.gsub(comma, '%s+', ' ')
|
2023-07-19 19:11:51 +01:00
|
|
|
local trimmed = utils.trimStr(spaces)
|
|
|
|
|
|
2023-08-28 21:02:28 -04:00
|
|
|
return utils.splitStr(trimmed, ' ')
|
2023-07-19 19:11:51 +01:00
|
|
|
end
|
|
|
|
|
|
2024-08-30 18:13:51 -04:00
|
|
|
---@param mob CBaseEntity
|
|
|
|
|
---@param hideDuration integer
|
|
|
|
|
---@param pos table?
|
|
|
|
|
---@param disAnim xi.animationString?
|
|
|
|
|
---@param reapAnim xi.animationString?
|
|
|
|
|
---@return nil
|
2022-06-02 16:48:35 -04:00
|
|
|
function utils.mobTeleport(mob, hideDuration, pos, disAnim, reapAnim)
|
|
|
|
|
if hideDuration == nil then
|
|
|
|
|
hideDuration = 5000
|
|
|
|
|
end
|
|
|
|
|
|
|
|
|
|
if disAnim == nil then
|
2024-08-30 18:13:51 -04:00
|
|
|
disAnim = xi.animationString.STATUS_DISAPPEAR
|
2022-06-02 16:48:35 -04:00
|
|
|
end
|
|
|
|
|
|
|
|
|
|
if reapAnim == nil then
|
2024-08-30 18:13:51 -04:00
|
|
|
reapAnim = xi.animationString.STATUS_VISIBLE
|
2022-06-02 16:48:35 -04:00
|
|
|
end
|
|
|
|
|
|
|
|
|
|
if pos == nil then
|
|
|
|
|
pos = mob:getPos()
|
|
|
|
|
end
|
|
|
|
|
|
|
|
|
|
local mobSpeed = mob:getSpeed()
|
|
|
|
|
|
|
|
|
|
if hideDuration < 1000 then
|
|
|
|
|
hideDuration = 1000
|
|
|
|
|
end
|
|
|
|
|
|
|
|
|
|
if mob:isDead() then
|
|
|
|
|
return
|
|
|
|
|
end
|
|
|
|
|
|
2024-11-01 13:47:09 +00:00
|
|
|
-- TODO: Temporary workaround
|
|
|
|
|
---@diagnostic disable: param-type-mismatch
|
|
|
|
|
|
2022-06-02 16:48:35 -04:00
|
|
|
mob:entityAnimationPacket(disAnim)
|
|
|
|
|
mob:hideName(true)
|
|
|
|
|
mob:setUntargetable(true)
|
2022-11-13 21:28:26 +00:00
|
|
|
mob:setAutoAttackEnabled(false)
|
|
|
|
|
mob:setMagicCastingEnabled(false)
|
|
|
|
|
mob:setMobAbilityEnabled(false)
|
2022-06-02 16:48:35 -04:00
|
|
|
mob:setPos(pos, 0)
|
2025-01-20 20:38:30 -05:00
|
|
|
mob:setBaseSpeed(0)
|
2022-06-02 16:48:35 -04:00
|
|
|
|
2024-11-01 13:47:09 +00:00
|
|
|
-- TODO: Temporary workaround
|
|
|
|
|
---@diagnostic enable: param-type-mismatch
|
|
|
|
|
|
2022-06-02 16:48:35 -04:00
|
|
|
mob:timer(hideDuration, function(mobArg)
|
|
|
|
|
mobArg:setPos(pos, 0)
|
|
|
|
|
mobArg:hideName(false)
|
|
|
|
|
mobArg:setUntargetable(false)
|
2022-11-13 21:28:26 +00:00
|
|
|
mobArg:setAutoAttackEnabled(true)
|
|
|
|
|
mobArg:setMagicCastingEnabled(true)
|
|
|
|
|
mobArg:setMobAbilityEnabled(true)
|
2025-01-20 20:38:30 -05:00
|
|
|
mobArg:setBaseSpeed(mobSpeed)
|
2022-06-02 16:48:35 -04:00
|
|
|
mobArg:entityAnimationPacket(reapAnim)
|
|
|
|
|
|
|
|
|
|
if mobArg:isDead() then
|
|
|
|
|
return
|
|
|
|
|
end
|
|
|
|
|
end)
|
|
|
|
|
end
|
2022-06-12 13:57:13 -06:00
|
|
|
|
2023-09-07 08:18:40 -04:00
|
|
|
-----------------------------------
|
2023-05-31 12:10:20 +02:00
|
|
|
-- Spatial position utilities
|
2023-09-07 08:18:40 -04:00
|
|
|
-----------------------------------
|
2024-08-30 18:13:51 -04:00
|
|
|
---@type number
|
2022-06-12 13:57:13 -06:00
|
|
|
local ffxiRotConversionFactor = 360.0 / 255.0
|
|
|
|
|
|
2024-08-30 18:13:51 -04:00
|
|
|
---@nodiscard
|
|
|
|
|
---@param ffxiRot number
|
|
|
|
|
---@return number
|
2022-06-12 13:57:13 -06:00
|
|
|
function utils.ffxiRotToDegrees(ffxiRot)
|
|
|
|
|
return ffxiRotConversionFactor * ffxiRot
|
|
|
|
|
end
|
|
|
|
|
|
2024-08-30 18:13:51 -04:00
|
|
|
---@nodiscard
|
|
|
|
|
---@param origin table
|
|
|
|
|
---@param translation table
|
|
|
|
|
---@return table
|
2022-06-12 13:57:13 -06:00
|
|
|
function utils.lateralTranslateWithOriginRotation(origin, translation)
|
|
|
|
|
local degrees = utils.ffxiRotToDegrees(origin.rot)
|
|
|
|
|
local rads = math.rad(degrees)
|
2022-12-03 09:20:16 -05:00
|
|
|
local newCoords = {}
|
2022-06-12 13:57:13 -06:00
|
|
|
|
2022-12-03 09:20:16 -05:00
|
|
|
newCoords.x = origin.x + ((math.cos(rads) * translation.x) + (math.sin(rads) * translation.z))
|
|
|
|
|
newCoords.z = origin.z + ((math.cos(rads) * translation.z) - (math.sin(rads) * translation.x))
|
|
|
|
|
newCoords.y = origin.y
|
|
|
|
|
newCoords.rot = origin.rot
|
2022-06-12 13:57:13 -06:00
|
|
|
|
2022-12-03 09:20:16 -05:00
|
|
|
return newCoords
|
2022-06-12 13:57:13 -06:00
|
|
|
end
|
2022-08-25 11:52:22 -07:00
|
|
|
|
2024-08-30 18:13:51 -04:00
|
|
|
---@nodiscard
|
|
|
|
|
---@param origin table
|
|
|
|
|
---@param offset number
|
|
|
|
|
---@param radians number
|
|
|
|
|
---@return table
|
2022-08-25 11:52:22 -07:00
|
|
|
function utils.getNearPosition(origin, offset, radians)
|
2022-08-26 18:41:33 -07:00
|
|
|
local destination =
|
|
|
|
|
{
|
2022-08-25 11:52:22 -07:00
|
|
|
x = origin.x + math.cos(2 * math.pi - radians) * offset,
|
|
|
|
|
y = origin.y,
|
|
|
|
|
z = origin.z + math.sin(2 * math.pi - radians) * offset
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return destination
|
|
|
|
|
end
|
|
|
|
|
|
2024-08-30 18:13:51 -04:00
|
|
|
---@nodiscard
|
|
|
|
|
---@param A table
|
|
|
|
|
---@param B table
|
|
|
|
|
---@param ignoreVertical boolean?
|
|
|
|
|
---@return number
|
2022-08-25 11:52:22 -07:00
|
|
|
function utils.distance(A, B, ignoreVertical)
|
|
|
|
|
return math.sqrt(utils.distanceSquared(A, B, ignoreVertical))
|
|
|
|
|
end
|
|
|
|
|
|
2024-08-30 18:13:51 -04:00
|
|
|
---@nodiscard
|
|
|
|
|
---@param A table
|
|
|
|
|
---@param B table
|
|
|
|
|
---@param ignoreVertical boolean?
|
|
|
|
|
---@return number
|
2022-08-25 11:52:22 -07:00
|
|
|
function utils.distanceSquared(A, B, ignoreVertical)
|
|
|
|
|
local dX = B.x - A.x
|
|
|
|
|
local dY = (ignoreVertical and 0.0) or B.y - A.y
|
|
|
|
|
local dZ = B.z - A.z
|
|
|
|
|
|
|
|
|
|
return dX * dX + dY * dY + dZ * dZ
|
|
|
|
|
end
|
|
|
|
|
|
2024-08-30 18:13:51 -04:00
|
|
|
---@nodiscard
|
|
|
|
|
---@param A table
|
|
|
|
|
---@param B table
|
|
|
|
|
---@param within number
|
|
|
|
|
---@param ignoreVertical boolean
|
|
|
|
|
---@return boolean
|
2022-08-25 11:52:22 -07:00
|
|
|
function utils.distanceWithin(A, B, within, ignoreVertical)
|
|
|
|
|
return utils.distanceSquared(A, B, ignoreVertical) <= within * within
|
|
|
|
|
end
|
|
|
|
|
|
2024-08-30 18:13:51 -04:00
|
|
|
---@nodiscard
|
|
|
|
|
---@param A table
|
|
|
|
|
---@param B table
|
|
|
|
|
---@return number
|
2022-08-25 11:52:22 -07:00
|
|
|
function utils.getWorldAngle(A, B)
|
|
|
|
|
-- lua math.atan functions like cpp atan2. On top of this, all ffxi world angles are positive.
|
|
|
|
|
-- So we have to adjust the range of lua's math.atan from [-pi, pi] to [0, 2pi]
|
|
|
|
|
-- Similarly, cpp's atanf gets its range adjusted from [-pi/2, pi/2] to [0, 2pi]
|
|
|
|
|
-- It does this by checking the quadrant, which math.atan does automatically.
|
|
|
|
|
-- Finally, ffxi rotations go in the opposite direction from standard rotations.
|
|
|
|
|
local radians = math.atan((B.z - A.z) / (B.x - A.x)) * -1.0
|
|
|
|
|
|
|
|
|
|
if B.x > A.x and B.z > A.z then
|
|
|
|
|
-- Quadrant 1
|
|
|
|
|
radians = radians + 2 * math.pi
|
|
|
|
|
elseif B.x > A.x then
|
|
|
|
|
-- Quadrant 4
|
|
|
|
|
radians = radians + 0
|
|
|
|
|
elseif B.z > A.z then
|
|
|
|
|
-- Quadrant 2
|
|
|
|
|
radians = radians + math.pi
|
|
|
|
|
else
|
|
|
|
|
-- Quadrant 3
|
|
|
|
|
radians = radians + math.pi
|
|
|
|
|
end
|
|
|
|
|
|
|
|
|
|
return radians
|
|
|
|
|
end
|
|
|
|
|
|
2024-08-30 18:13:51 -04:00
|
|
|
---@nodiscard
|
|
|
|
|
---@param A table
|
|
|
|
|
---@param B table
|
|
|
|
|
---@return number
|
2022-08-25 11:52:22 -07:00
|
|
|
function utils.getWorldRotation(A, B)
|
|
|
|
|
return utils.angleToRotation(utils.getWorldAngle(A, B))
|
|
|
|
|
end
|
|
|
|
|
|
2024-08-30 18:13:51 -04:00
|
|
|
---@nodiscard
|
|
|
|
|
---@param a number
|
|
|
|
|
---@param b number
|
|
|
|
|
---@return number
|
2022-08-25 11:52:22 -07:00
|
|
|
function utils.getAngleDifference(a, b)
|
|
|
|
|
local diff = math.abs(b - a)
|
2022-11-22 15:55:35 -05:00
|
|
|
if diff > math.pi then
|
|
|
|
|
diff = 2 * math.pi - diff
|
|
|
|
|
end
|
|
|
|
|
|
2022-08-25 11:52:22 -07:00
|
|
|
return diff
|
|
|
|
|
end
|
|
|
|
|
|
|
|
|
|
-- Returns whether the angle formed between A and B with origin is <= within radians.
|
2024-08-30 18:13:51 -04:00
|
|
|
---@nodiscard
|
|
|
|
|
---@param origin table
|
|
|
|
|
---@param A table
|
|
|
|
|
---@param B table
|
|
|
|
|
---@param within number
|
|
|
|
|
---@return boolean
|
2022-08-25 11:52:22 -07:00
|
|
|
function utils.angleWithin(origin, A, B, within)
|
|
|
|
|
return utils.getAngleDifference(utils.getWorldAngle(origin, A), utils.getWorldAngle(origin, B)) <= within
|
|
|
|
|
end
|
|
|
|
|
|
|
|
|
|
local ffxiRotationToAngleFactor = 2.0 * math.pi / 256.0
|
2023-05-31 12:10:20 +02:00
|
|
|
local ffxiAngleToRotationFactor = 256.0 / (2.0 * math.pi)
|
2022-08-25 11:52:22 -07:00
|
|
|
|
2024-08-30 18:13:51 -04:00
|
|
|
---@nodiscard
|
|
|
|
|
---@param ffxiRotation number
|
|
|
|
|
---@return number
|
2022-08-25 11:52:22 -07:00
|
|
|
function utils.rotationToAngle(ffxiRotation)
|
|
|
|
|
return ffxiRotation * ffxiRotationToAngleFactor
|
|
|
|
|
end
|
|
|
|
|
|
2024-08-30 18:13:51 -04:00
|
|
|
---@nodiscard
|
|
|
|
|
---@param radians number
|
|
|
|
|
---@return number
|
2022-08-25 11:52:22 -07:00
|
|
|
function utils.angleToRotation(radians)
|
|
|
|
|
return radians * ffxiAngleToRotationFactor
|
2022-08-26 18:41:33 -07:00
|
|
|
end
|
2023-05-29 07:52:57 -04:00
|
|
|
|
2024-12-23 10:03:09 -05:00
|
|
|
-- Function to calculate the cross product
|
|
|
|
|
local function crossProduct(x1, y1, x2, y2)
|
|
|
|
|
return x1 * y2 - y1 * x2
|
|
|
|
|
end
|
|
|
|
|
|
|
|
|
|
-- Function to check if two points are on the same side of a line
|
|
|
|
|
---@nodiscard
|
|
|
|
|
---@param line table
|
|
|
|
|
---@param pos1 table
|
|
|
|
|
---@param pos2 table
|
|
|
|
|
---@return boolean
|
2024-12-27 13:26:10 -05:00
|
|
|
function utils.sameSideOfLine(line, pos1, pos2)
|
2024-12-23 10:03:09 -05:00
|
|
|
-- Calculate vectors
|
|
|
|
|
local v1x, v1y = pos1.x - line[1][1], pos1.z - line[1][2]
|
|
|
|
|
local v2x, v2y = pos2.x - line[1][1], pos2.z - line[1][2]
|
|
|
|
|
local lx, ly = line[2][1] - line[1][1], line[2][2] - line[1][2]
|
|
|
|
|
|
|
|
|
|
-- Calculate cross products
|
|
|
|
|
local cross1 = crossProduct(lx, ly, v1x, v1y)
|
|
|
|
|
local cross2 = crossProduct(lx, ly, v2x, v2y)
|
|
|
|
|
|
|
|
|
|
-- Check if cross products have the same sign
|
|
|
|
|
return cross1 * cross2 >= 0
|
|
|
|
|
end
|
|
|
|
|
|
2023-09-19 04:19:19 -05:00
|
|
|
-- Converts a number to a binary string
|
2024-08-30 18:13:51 -04:00
|
|
|
---@nodiscard
|
|
|
|
|
---@param x integer
|
|
|
|
|
---@return string
|
2023-09-19 04:19:19 -05:00
|
|
|
function utils.intToBinary(x)
|
|
|
|
|
local bin = ''
|
|
|
|
|
|
|
|
|
|
while x > 1 do
|
|
|
|
|
bin = tostring(x % 2) .. bin
|
|
|
|
|
x = math.floor(x / 2)
|
|
|
|
|
end
|
|
|
|
|
|
|
|
|
|
bin = tostring(x) .. bin
|
|
|
|
|
|
|
|
|
|
return bin
|
|
|
|
|
end
|
2024-08-29 23:12:20 +01:00
|
|
|
|
2024-08-30 18:13:51 -04:00
|
|
|
---@nodiscard
|
|
|
|
|
---@param value integer
|
|
|
|
|
---@return integer, integer, integer, integer
|
2024-08-29 23:12:20 +01:00
|
|
|
function utils.toBytes(value)
|
|
|
|
|
local byte0 = bit.rshift(bit.band(value, 0x000000FF), 0)
|
|
|
|
|
local byte1 = bit.rshift(bit.band(value, 0x0000FF00), 8)
|
|
|
|
|
local byte2 = bit.rshift(bit.band(value, 0x00FF0000), 16)
|
|
|
|
|
local byte3 = bit.rshift(bit.band(value, 0xFF000000), 24)
|
|
|
|
|
return byte0, byte1, byte2, byte3
|
|
|
|
|
end
|
|
|
|
|
|
2024-08-30 18:13:51 -04:00
|
|
|
---@nodiscard
|
|
|
|
|
---@param value integer
|
|
|
|
|
---@return integer, integer
|
2024-08-29 23:12:20 +01:00
|
|
|
function utils.toWords(value)
|
|
|
|
|
local word0 = bit.rshift(bit.band(value, 0x0000FFFF), 0)
|
|
|
|
|
local word1 = bit.rshift(bit.band(value, 0xFFFF0000), 16)
|
|
|
|
|
return word0, word1
|
|
|
|
|
end
|
2022-12-04 16:53:43 -06:00
|
|
|
|
2024-12-19 21:00:34 -05:00
|
|
|
-- Draws in target to position if any conditions are met.
|
|
|
|
|
-- Conditions must be met for "wait" seconds.
|
|
|
|
|
-- Can set offset from and degrees around position to place target.
|
|
|
|
|
--[[
|
|
|
|
|
table =
|
|
|
|
|
{
|
|
|
|
|
conditions = { boolean, ... },
|
|
|
|
|
position =
|
|
|
|
|
{
|
|
|
|
|
x = number,
|
|
|
|
|
y = number,
|
|
|
|
|
z = number,
|
|
|
|
|
rot = integer,
|
|
|
|
|
},
|
|
|
|
|
offset = number,
|
|
|
|
|
degrees = number,
|
|
|
|
|
wait = integer,
|
|
|
|
|
}
|
|
|
|
|
--]]
|
|
|
|
|
---@param target CBaseEntity
|
|
|
|
|
---@param table table
|
|
|
|
|
---@return boolean
|
|
|
|
|
function utils.drawIn(target, table)
|
|
|
|
|
if table.position then
|
|
|
|
|
local nextDrawIn = target:getLocalVar('[Draw-In]WaitTime')
|
|
|
|
|
local conditions = table.conditions and table.conditions or { true }
|
|
|
|
|
for _, condition in ipairs(conditions) do
|
|
|
|
|
if condition then
|
|
|
|
|
if nextDrawIn > 0 then
|
2025-07-03 21:20:08 -04:00
|
|
|
if GetSystemTime() > nextDrawIn then
|
2024-12-19 21:00:34 -05:00
|
|
|
local position = {}
|
|
|
|
|
if table.position then
|
|
|
|
|
position.x = table.position.x and table.position.x or table.position[1]
|
|
|
|
|
position.y = table.position.y and table.position.y or table.position[2]
|
|
|
|
|
position.z = table.position.z and table.position.z or table.position[3]
|
|
|
|
|
position.rot = table.position.rot and table.position.rot or table.position[4]
|
|
|
|
|
end
|
2025-01-01 09:05:13 -05:00
|
|
|
|
2024-12-19 21:00:34 -05:00
|
|
|
local offset = table.offset and table.offset or 0
|
|
|
|
|
local degrees = table.degrees and table.degrees or 0
|
|
|
|
|
|
|
|
|
|
DrawIn(target, position, offset, degrees)
|
|
|
|
|
target:setLocalVar('[Draw-In]WaitTime', 0)
|
|
|
|
|
return true
|
|
|
|
|
end
|
2025-01-01 09:05:13 -05:00
|
|
|
|
2024-12-19 21:00:34 -05:00
|
|
|
return false
|
|
|
|
|
else
|
|
|
|
|
local wait = table.wait and table.wait or 1
|
2025-07-03 21:20:08 -04:00
|
|
|
target:setLocalVar('[Draw-In]WaitTime', GetSystemTime() + wait)
|
2024-12-19 21:00:34 -05:00
|
|
|
return false
|
|
|
|
|
end
|
|
|
|
|
end
|
|
|
|
|
end
|
2022-12-04 16:53:43 -06:00
|
|
|
end
|
2025-01-01 09:05:13 -05:00
|
|
|
|
2024-12-19 21:00:34 -05:00
|
|
|
target:setLocalVar('[Draw-In]WaitTime', 0)
|
|
|
|
|
return false
|
2022-12-04 16:53:43 -06:00
|
|
|
end
|
2025-02-19 16:18:45 +01:00
|
|
|
|
|
|
|
|
function utils.defaultIfNil(inputValue, defaultValue)
|
|
|
|
|
if inputValue == nil then
|
2026-02-16 08:04:04 -08:00
|
|
|
-- local info = debug.getinfo(2, 'Sl')
|
|
|
|
|
-- print(string.format('nil value encounted at %s:%i, defaulting to %s', info.source, info.currentline, tostring(defaultValue)))
|
2025-02-19 16:18:45 +01:00
|
|
|
|
|
|
|
|
return defaultValue
|
|
|
|
|
end
|
|
|
|
|
|
|
|
|
|
return inputValue
|
|
|
|
|
end
|
2025-07-03 23:04:18 -04:00
|
|
|
|
2025-09-19 11:42:59 -05:00
|
|
|
-- Selects loot from a structured table of loot groups
|
2025-09-19 10:28:25 -05:00
|
|
|
-- loot is chosen by randomly assigning based on weights from loot groups within lootTable. Example:
|
|
|
|
|
--[[
|
|
|
|
|
lootTable =
|
|
|
|
|
{
|
2025-09-19 11:42:36 -05:00
|
|
|
{
|
2025-09-19 11:42:59 -05:00
|
|
|
{ itemId = xi.item.GIL, amount = 10000, weight = 1000 },
|
2025-09-19 11:42:36 -05:00
|
|
|
},
|
|
|
|
|
|
2025-09-19 10:28:25 -05:00
|
|
|
{
|
|
|
|
|
quantity = 2,
|
2025-09-19 11:42:59 -05:00
|
|
|
{ itemId = xi.item.REMEDY, weight = 900 },
|
|
|
|
|
{ itemId = 0, weight = 100 },
|
2025-09-19 10:28:25 -05:00
|
|
|
},
|
|
|
|
|
|
|
|
|
|
{
|
2025-09-19 11:42:59 -05:00
|
|
|
{ itemId = xi.item.REMEDY, weight = 200 },
|
|
|
|
|
{ itemId = 0, weight = 800 },
|
2025-09-19 10:28:25 -05:00
|
|
|
},
|
|
|
|
|
|
|
|
|
|
--]]
|
|
|
|
|
|
2025-09-19 11:42:59 -05:00
|
|
|
---@param actor CBaseEntity
|
2025-09-19 10:28:25 -05:00
|
|
|
---@param lootTable table<table>
|
|
|
|
|
---@return table
|
|
|
|
|
function utils.selectFromLootGroups(actor, lootTable)
|
|
|
|
|
local selectedLoot = {}
|
|
|
|
|
if not actor or not actor:getName() then
|
|
|
|
|
return selectedLoot
|
|
|
|
|
end
|
|
|
|
|
|
2025-09-19 11:14:37 -05:00
|
|
|
for i, lootGroup in ipairs(lootTable) do
|
2025-09-19 10:28:25 -05:00
|
|
|
local max = 0
|
|
|
|
|
|
|
|
|
|
-- not ipairs because we might have string keys alongside the individual items in each lootGroup
|
|
|
|
|
for j, entry in pairs(lootGroup) do
|
|
|
|
|
if type(entry) == 'table' then
|
|
|
|
|
max = max + entry.weight
|
|
|
|
|
|
2025-09-19 11:42:59 -05:00
|
|
|
if entry.itemId == nil then
|
2025-09-19 10:28:25 -05:00
|
|
|
print(fmt('[ERROR] Player ({}) has encountered nil item at index {} of lootGroup with index {}', actor:getName(), j, i))
|
|
|
|
|
end
|
|
|
|
|
end
|
|
|
|
|
end
|
|
|
|
|
|
|
|
|
|
local quantity = lootGroup.quantity or 1
|
|
|
|
|
|
|
|
|
|
for j = 1, quantity do
|
|
|
|
|
local roll = math.random(max)
|
|
|
|
|
local current = 0
|
|
|
|
|
|
|
|
|
|
for _, entry in pairs(lootGroup) do
|
|
|
|
|
if type(entry) == 'table' then
|
|
|
|
|
current = current + entry.weight
|
|
|
|
|
|
|
|
|
|
if current >= roll then
|
|
|
|
|
-- xi.item.NONE gives a chance to drop nothing from a group
|
2025-09-19 11:42:59 -05:00
|
|
|
if entry.itemId == 0 or entry.itemId == nil then
|
2025-09-19 10:28:25 -05:00
|
|
|
break
|
|
|
|
|
end
|
|
|
|
|
|
|
|
|
|
table.insert(selectedLoot, entry)
|
|
|
|
|
|
|
|
|
|
break
|
|
|
|
|
end
|
|
|
|
|
end
|
|
|
|
|
end
|
|
|
|
|
end
|
|
|
|
|
end
|
|
|
|
|
|
|
|
|
|
return selectedLoot
|
|
|
|
|
end
|