initial commit
This commit is contained in:
commit
61b2683818
1
.gitignore
vendored
Normal file
1
.gitignore
vendored
Normal file
@ -0,0 +1 @@
|
|||||||
|
/build
|
11
.luarc.json
Normal file
11
.luarc.json
Normal file
@ -0,0 +1,11 @@
|
|||||||
|
{
|
||||||
|
"$schema": "https://raw.githubusercontent.com/sumneko/vscode-lua/master/setting/schema.json",
|
||||||
|
"Lua.runtime.version": "LuaJIT",
|
||||||
|
"Lua.workspace.checkThirdParty": false,
|
||||||
|
"Lua.workspace.library": [
|
||||||
|
"${3rd}/love2d/library"
|
||||||
|
],
|
||||||
|
"Lua.workspace.userThirdParty": [
|
||||||
|
"love2d"
|
||||||
|
]
|
||||||
|
}
|
6
.vimrc.lua
Normal file
6
.vimrc.lua
Normal file
@ -0,0 +1,6 @@
|
|||||||
|
local map = vim.api.nvim_set_keymap
|
||||||
|
|
||||||
|
map('n', '<leader>l', ":execute 'silent !kitty love src &' | redraw!<cr>", {
|
||||||
|
silent = true,
|
||||||
|
noremap = true
|
||||||
|
})
|
17
build.sh
Executable file
17
build.sh
Executable file
@ -0,0 +1,17 @@
|
|||||||
|
#!/bin/sh
|
||||||
|
|
||||||
|
BUILD_NAME="love2d-project"
|
||||||
|
ZIP_OPTIONS="-x '*.xcf'"
|
||||||
|
|
||||||
|
# Remove existing contents
|
||||||
|
rm -r build
|
||||||
|
mkdir build
|
||||||
|
|
||||||
|
# Build project
|
||||||
|
echo Building project...
|
||||||
|
cd src
|
||||||
|
zip -q $ZIP_OPTIONS -r ../build/$BUILD_NAME.love *
|
||||||
|
cd - > /dev/null
|
||||||
|
|
||||||
|
# Jobs done
|
||||||
|
echo Done.
|
11
src/conf.lua
Normal file
11
src/conf.lua
Normal file
@ -0,0 +1,11 @@
|
|||||||
|
function love.conf(t)
|
||||||
|
t.title = "Love2D project"
|
||||||
|
|
||||||
|
t.console = true
|
||||||
|
|
||||||
|
t.window.width = 1280
|
||||||
|
t.window.height = 720
|
||||||
|
t.window.resizable = true
|
||||||
|
|
||||||
|
t.modules.joystick = false
|
||||||
|
end
|
8
src/helpers/darken.lua
Normal file
8
src/helpers/darken.lua
Normal file
@ -0,0 +1,8 @@
|
|||||||
|
local vivid = require("lib.vivid")
|
||||||
|
local hueShift = require("helpers.hue-shift")
|
||||||
|
local BLUE_HUE = 240/360
|
||||||
|
|
||||||
|
return function(r, g, b)
|
||||||
|
return hueShift(BLUE_HUE, 0.08, vivid.desaturate(0.1, vivid.darken(0.1, r, g, b)))
|
||||||
|
end
|
||||||
|
|
8
src/helpers/hue-shift.lua
Normal file
8
src/helpers/hue-shift.lua
Normal file
@ -0,0 +1,8 @@
|
|||||||
|
local lerp = require("lib.lume").lerp
|
||||||
|
local vivid = require("lib.vivid")
|
||||||
|
|
||||||
|
return function (target_hue, amount, r, g, b)
|
||||||
|
local h, s, l = vivid.RGBtoHSL(r, g, b)
|
||||||
|
h = lerp(h, target_hue, amount)
|
||||||
|
return vivid.HSLtoRGB(h, s, l)
|
||||||
|
end
|
4
src/helpers/is-in-rect.lua
Normal file
4
src/helpers/is-in-rect.lua
Normal file
@ -0,0 +1,4 @@
|
|||||||
|
return function(x0, y0, x, y, w, h)
|
||||||
|
return x0 >= x and x0 < x+w and y0 >= y and y0 < y+h
|
||||||
|
end
|
||||||
|
|
8
src/helpers/lighten.lua
Normal file
8
src/helpers/lighten.lua
Normal file
@ -0,0 +1,8 @@
|
|||||||
|
local vivid = require("lib.vivid")
|
||||||
|
local hueShift = require("helpers.hue-shift")
|
||||||
|
local YELLOW_HUE = 60/360
|
||||||
|
|
||||||
|
return function(r, g, b)
|
||||||
|
return hueShift(YELLOW_HUE, 0.05, vivid.saturate(0.1, vivid.lighten(0.1, r, g, b)))
|
||||||
|
end
|
||||||
|
|
17
src/helpers/pick-text-color.lua
Normal file
17
src/helpers/pick-text-color.lua
Normal file
@ -0,0 +1,17 @@
|
|||||||
|
-- Pick a color for text that would be approriate depending on the given
|
||||||
|
-- background color.
|
||||||
|
-- If background color is bright, returns black.
|
||||||
|
-- IF background color is dark, returns white.
|
||||||
|
return function(p1, p2, p3)
|
||||||
|
local r, g, b
|
||||||
|
if p1 and p2 and p3 then
|
||||||
|
r, g, b = p1, p2, p3
|
||||||
|
else
|
||||||
|
r, g, b = p1[1], p1[2], p1[3]
|
||||||
|
end
|
||||||
|
if r+g+b < 1.5 then
|
||||||
|
return 1, 1, 1, 1
|
||||||
|
else
|
||||||
|
return 0, 0, 0, 1
|
||||||
|
end
|
||||||
|
end
|
279
src/lib/brinevector.lua
Normal file
279
src/lib/brinevector.lua
Normal file
@ -0,0 +1,279 @@
|
|||||||
|
-- LAST UPDATED: 9/25/18
|
||||||
|
-- added Vector.getCeil and v.ceil
|
||||||
|
-- added Vector.clamp
|
||||||
|
-- changed float to double as per Mike Pall's advice
|
||||||
|
-- added Vector.floor
|
||||||
|
-- added fallback to table if luajit ffi is not detected (used for unit tests)
|
||||||
|
|
||||||
|
--[[
|
||||||
|
BrineVector: a luajit ffi-accelerated vector library
|
||||||
|
|
||||||
|
Copyright 2018 Brian Sarfati
|
||||||
|
|
||||||
|
Permission is hereby granted, free of charge, to any person obtaining a copy of this software
|
||||||
|
and associated documentation files (the "Software"), to deal in the Software without restriction,
|
||||||
|
including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense,
|
||||||
|
and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so,
|
||||||
|
subject to the following conditions:
|
||||||
|
|
||||||
|
The above copyright notice and this permission notice shall be included in all copies or
|
||||||
|
substantial portions of the Software.
|
||||||
|
|
||||||
|
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED,
|
||||||
|
INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE
|
||||||
|
AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM,
|
||||||
|
DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||||
|
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||||
|
--]]
|
||||||
|
|
||||||
|
local ffi
|
||||||
|
local VECTORTYPE = "cdata"
|
||||||
|
|
||||||
|
if jit and jit.status() then
|
||||||
|
ffi = require "ffi"
|
||||||
|
ffi.cdef[[
|
||||||
|
typedef struct {
|
||||||
|
double x;
|
||||||
|
double y;
|
||||||
|
} brinevector;
|
||||||
|
]]
|
||||||
|
else
|
||||||
|
VECTORTYPE = "table"
|
||||||
|
end
|
||||||
|
|
||||||
|
local Vector = {}
|
||||||
|
setmetatable(Vector,Vector)
|
||||||
|
|
||||||
|
local special_properties = {
|
||||||
|
length = "getLength",
|
||||||
|
normalized = "getNormalized",
|
||||||
|
angle = "getAngle",
|
||||||
|
length2 = "getLengthSquared",
|
||||||
|
copy = "getCopy",
|
||||||
|
inverse = "getInverse",
|
||||||
|
floor = "getFloor",
|
||||||
|
ceil = "getCeil",
|
||||||
|
}
|
||||||
|
|
||||||
|
function Vector.__index(t, k)
|
||||||
|
if special_properties[k] then
|
||||||
|
return Vector[special_properties[k]](t)
|
||||||
|
end
|
||||||
|
return rawget(Vector,k)
|
||||||
|
end
|
||||||
|
|
||||||
|
function Vector.getLength(v)
|
||||||
|
return math.sqrt(v.x * v.x + v.y * v.y)
|
||||||
|
end
|
||||||
|
|
||||||
|
function Vector.getLengthSquared(v)
|
||||||
|
return v.x*v.x + v.y*v.y
|
||||||
|
end
|
||||||
|
|
||||||
|
function Vector.getNormalized(v)
|
||||||
|
local length = v.length
|
||||||
|
if length == 0 then return Vector(0,0) end
|
||||||
|
return Vector(v.x / length, v.y / length)
|
||||||
|
end
|
||||||
|
|
||||||
|
function Vector.getAngle(v)
|
||||||
|
return math.atan2(v.y, v.x)
|
||||||
|
end
|
||||||
|
|
||||||
|
function Vector.getCopy(v)
|
||||||
|
return Vector(v.x,v.y)
|
||||||
|
end
|
||||||
|
|
||||||
|
function Vector.getInverse(v)
|
||||||
|
return Vector(1 / v.x, 1 / v.y)
|
||||||
|
end
|
||||||
|
|
||||||
|
function Vector.getFloor(v)
|
||||||
|
return Vector(math.floor(v.x), math.floor(v.y))
|
||||||
|
end
|
||||||
|
|
||||||
|
function Vector.getCeil(v)
|
||||||
|
return Vector(math.ceil(v.x), math.ceil(v.y))
|
||||||
|
end
|
||||||
|
|
||||||
|
function Vector.__newindex(t,k,v)
|
||||||
|
if k == "length" then
|
||||||
|
local res = t.normalized * v
|
||||||
|
t.x = res.x
|
||||||
|
t.y = res.y
|
||||||
|
return
|
||||||
|
end
|
||||||
|
if k == "angle" then
|
||||||
|
local res = t:angled(v)
|
||||||
|
t.x = res.x
|
||||||
|
t.y = res.y
|
||||||
|
return
|
||||||
|
end
|
||||||
|
if t == Vector then
|
||||||
|
rawset(t,k,v)
|
||||||
|
else
|
||||||
|
error("Cannot assign a new property '" .. k .. "' to a Vector", 2)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
function Vector.angled(v, angle)
|
||||||
|
local length = v.length
|
||||||
|
return Vector(math.cos(angle) * length, math.sin(angle) * length)
|
||||||
|
end
|
||||||
|
|
||||||
|
function Vector.trim(v,mag)
|
||||||
|
if v.length < mag then return v end
|
||||||
|
return v.normalized * mag
|
||||||
|
end
|
||||||
|
|
||||||
|
function Vector.split(v)
|
||||||
|
return v.x, v.y
|
||||||
|
end
|
||||||
|
|
||||||
|
local function clamp(x, min, max)
|
||||||
|
-- because Mike Pall says math.min and math.max are JIT-optimized
|
||||||
|
return math.min(math.max(min, x), max)
|
||||||
|
end
|
||||||
|
|
||||||
|
function Vector.clamp(v, topleft, bottomright)
|
||||||
|
-- clamps a vector to a certain bounding box about the origin
|
||||||
|
return Vector(
|
||||||
|
clamp(v.x, topleft.x, bottomright.x),
|
||||||
|
clamp(v.y, topleft.y, bottomright.y)
|
||||||
|
)
|
||||||
|
end
|
||||||
|
|
||||||
|
function Vector.hadamard(v1, v2) -- also known as "Componentwise multiplication"
|
||||||
|
return Vector(v1.x * v2.x, v1.y * v2.y)
|
||||||
|
end
|
||||||
|
|
||||||
|
function Vector.rotated(v, angle)
|
||||||
|
local cos = math.cos(angle)
|
||||||
|
local sin = math.sin(angle)
|
||||||
|
return Vector(v.x * cos - v.y * sin, v.x * sin + v.y * cos)
|
||||||
|
end
|
||||||
|
|
||||||
|
local iteraxes_lookup = {
|
||||||
|
xy = {"x","y"},
|
||||||
|
yx = {"y","x"}
|
||||||
|
}
|
||||||
|
local function iteraxes(ordertable, i)
|
||||||
|
i = i + 1
|
||||||
|
if i > 2 then return nil end
|
||||||
|
return i, ordertable[i]
|
||||||
|
end
|
||||||
|
|
||||||
|
function Vector.axes(order)
|
||||||
|
return iteraxes, iteraxes_lookup[order or "yx"], 0
|
||||||
|
end
|
||||||
|
|
||||||
|
local function iterpairs(vector, k)
|
||||||
|
if k == nil then
|
||||||
|
k = "x"
|
||||||
|
elseif k == "x" then
|
||||||
|
k = "y"
|
||||||
|
else
|
||||||
|
k = nil
|
||||||
|
end
|
||||||
|
return k, vector[k]
|
||||||
|
end
|
||||||
|
|
||||||
|
function Vector.__pairs(v)
|
||||||
|
return iterpairs, v, nil
|
||||||
|
end
|
||||||
|
|
||||||
|
if ffi then
|
||||||
|
function Vector.isVector(arg)
|
||||||
|
return ffi.istype("brinevector",arg)
|
||||||
|
end
|
||||||
|
else
|
||||||
|
function Vector.isVector(arg)
|
||||||
|
return type(arg) == VECTORTYPE and arg.x and arg.y
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
function Vector.__add(v1, v2)
|
||||||
|
return Vector(v1.x + v2.x, v1.y + v2.y)
|
||||||
|
end
|
||||||
|
|
||||||
|
function Vector.__sub(v1, v2)
|
||||||
|
return Vector(v1.x - v2.x, v1.y - v2.y)
|
||||||
|
end
|
||||||
|
|
||||||
|
function Vector.__mul(v1, op)
|
||||||
|
-- acts as a dot multiplication if op is a vector
|
||||||
|
-- if op is a scalar then works as usual
|
||||||
|
if type(v1) == "number" then
|
||||||
|
return Vector(op.x * v1, op.y * v1)
|
||||||
|
end
|
||||||
|
if type(op) == VECTORTYPE then
|
||||||
|
return v1.x * op.x + v1.y * op.y
|
||||||
|
else
|
||||||
|
return Vector(v1.x * op, v1.y * op)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
function Vector.__div(v1, op)
|
||||||
|
if type(op) == "number" then
|
||||||
|
if op == 0 then error("Vector NaN occured", 2) end
|
||||||
|
return Vector(v1.x / op, v1.y / op)
|
||||||
|
elseif type(op) == VECTORTYPE then
|
||||||
|
if op.x * op.y == 0 then error("Vector NaN occured", 2) end
|
||||||
|
return Vector(v1.x / op.x, v1.y / op.y)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
function Vector.__unm(v)
|
||||||
|
return Vector(-v.x, -v.y)
|
||||||
|
end
|
||||||
|
|
||||||
|
function Vector.__eq(v1,v2)
|
||||||
|
if (not Vector.isVector(v1)) or (not Vector.isVector(v2)) then return false end
|
||||||
|
return v1.x == v2.x and v1.y == v2.y
|
||||||
|
end
|
||||||
|
|
||||||
|
function Vector.__mod(v1,v2) -- ran out of symbols, so i chose % for the hadamard product
|
||||||
|
return Vector(v1.x * v2.x, v1.y * v2.y)
|
||||||
|
end
|
||||||
|
|
||||||
|
function Vector.__tostring(t)
|
||||||
|
return string.format("Vector{%.4f, %.4f}",t.x,t.y)
|
||||||
|
end
|
||||||
|
|
||||||
|
function Vector.__concat(str, v)
|
||||||
|
return tostring(str) .. tostring(v)
|
||||||
|
end
|
||||||
|
|
||||||
|
function Vector.__len(v)
|
||||||
|
return math.sqrt(v.x * v.x + v.y * v.y)
|
||||||
|
end
|
||||||
|
|
||||||
|
if ffi then
|
||||||
|
function Vector.__call(t,x,y)
|
||||||
|
return ffi.new("brinevector",x or 0,y or 0)
|
||||||
|
end
|
||||||
|
else
|
||||||
|
function Vector.__call(t,x,y)
|
||||||
|
return setmetatable({x = x or 0, y = y or 0}, Vector)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
local dirs = {
|
||||||
|
up = Vector(0,-1),
|
||||||
|
down = Vector(0,1),
|
||||||
|
left = Vector(-1,0),
|
||||||
|
right = Vector(1,0),
|
||||||
|
top = Vector(0,-1),
|
||||||
|
bottom = Vector(0,1)
|
||||||
|
}
|
||||||
|
|
||||||
|
function Vector.dir(dir)
|
||||||
|
return dirs[dir] and dirs[dir].copy or Vector()
|
||||||
|
end
|
||||||
|
|
||||||
|
|
||||||
|
if ffi then
|
||||||
|
ffi.metatype("brinevector",Vector)
|
||||||
|
end
|
||||||
|
|
||||||
|
return Vector
|
773
src/lib/bump.lua
Normal file
773
src/lib/bump.lua
Normal file
@ -0,0 +1,773 @@
|
|||||||
|
local bump = {
|
||||||
|
_VERSION = 'bump v3.1.7',
|
||||||
|
_URL = 'https://github.com/kikito/bump.lua',
|
||||||
|
_DESCRIPTION = 'A collision detection library for Lua',
|
||||||
|
_LICENSE = [[
|
||||||
|
MIT LICENSE
|
||||||
|
|
||||||
|
Copyright (c) 2014 Enrique García Cota
|
||||||
|
|
||||||
|
Permission is hereby granted, free of charge, to any person obtaining a
|
||||||
|
copy of this software and associated documentation files (the
|
||||||
|
"Software"), to deal in the Software without restriction, including
|
||||||
|
without limitation the rights to use, copy, modify, merge, publish,
|
||||||
|
distribute, sublicense, and/or sell copies of the Software, and to
|
||||||
|
permit persons to whom the Software is furnished to do so, subject to
|
||||||
|
the following conditions:
|
||||||
|
|
||||||
|
The above copyright notice and this permission notice shall be included
|
||||||
|
in all copies or substantial portions of the Software.
|
||||||
|
|
||||||
|
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS
|
||||||
|
OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
|
||||||
|
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
|
||||||
|
IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY
|
||||||
|
CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT,
|
||||||
|
TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
|
||||||
|
SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||||
|
]]
|
||||||
|
}
|
||||||
|
|
||||||
|
------------------------------------------
|
||||||
|
-- Auxiliary functions
|
||||||
|
------------------------------------------
|
||||||
|
local DELTA = 1e-10 -- floating-point margin of error
|
||||||
|
|
||||||
|
local abs, floor, ceil, min, max = math.abs, math.floor, math.ceil, math.min, math.max
|
||||||
|
|
||||||
|
local function sign(x)
|
||||||
|
if x > 0 then return 1 end
|
||||||
|
if x == 0 then return 0 end
|
||||||
|
return -1
|
||||||
|
end
|
||||||
|
|
||||||
|
local function nearest(x, a, b)
|
||||||
|
if abs(a - x) < abs(b - x) then return a else return b end
|
||||||
|
end
|
||||||
|
|
||||||
|
local function assertType(desiredType, value, name)
|
||||||
|
if type(value) ~= desiredType then
|
||||||
|
error(name .. ' must be a ' .. desiredType .. ', but was ' .. tostring(value) .. '(a ' .. type(value) .. ')')
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
local function assertIsPositiveNumber(value, name)
|
||||||
|
if type(value) ~= 'number' or value <= 0 then
|
||||||
|
error(name .. ' must be a positive integer, but was ' .. tostring(value) .. '(' .. type(value) .. ')')
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
local function assertIsRect(x,y,w,h)
|
||||||
|
assertType('number', x, 'x')
|
||||||
|
assertType('number', y, 'y')
|
||||||
|
assertIsPositiveNumber(w, 'w')
|
||||||
|
assertIsPositiveNumber(h, 'h')
|
||||||
|
end
|
||||||
|
|
||||||
|
local defaultFilter = function()
|
||||||
|
return 'slide'
|
||||||
|
end
|
||||||
|
|
||||||
|
------------------------------------------
|
||||||
|
-- Rectangle functions
|
||||||
|
------------------------------------------
|
||||||
|
|
||||||
|
local function rect_getNearestCorner(x,y,w,h, px, py)
|
||||||
|
return nearest(px, x, x+w), nearest(py, y, y+h)
|
||||||
|
end
|
||||||
|
|
||||||
|
-- This is a generalized implementation of the liang-barsky algorithm, which also returns
|
||||||
|
-- the normals of the sides where the segment intersects.
|
||||||
|
-- Returns nil if the segment never touches the rect
|
||||||
|
-- Notice that normals are only guaranteed to be accurate when initially ti1, ti2 == -math.huge, math.huge
|
||||||
|
local function rect_getSegmentIntersectionIndices(x,y,w,h, x1,y1,x2,y2, ti1,ti2)
|
||||||
|
ti1, ti2 = ti1 or 0, ti2 or 1
|
||||||
|
local dx, dy = x2-x1, y2-y1
|
||||||
|
local nx, ny
|
||||||
|
local nx1, ny1, nx2, ny2 = 0,0,0,0
|
||||||
|
local p, q, r
|
||||||
|
|
||||||
|
for side = 1,4 do
|
||||||
|
if side == 1 then nx,ny,p,q = -1, 0, -dx, x1 - x -- left
|
||||||
|
elseif side == 2 then nx,ny,p,q = 1, 0, dx, x + w - x1 -- right
|
||||||
|
elseif side == 3 then nx,ny,p,q = 0, -1, -dy, y1 - y -- top
|
||||||
|
else nx,ny,p,q = 0, 1, dy, y + h - y1 -- bottom
|
||||||
|
end
|
||||||
|
|
||||||
|
if p == 0 then
|
||||||
|
if q <= 0 then return nil end
|
||||||
|
else
|
||||||
|
r = q / p
|
||||||
|
if p < 0 then
|
||||||
|
if r > ti2 then return nil
|
||||||
|
elseif r > ti1 then ti1,nx1,ny1 = r,nx,ny
|
||||||
|
end
|
||||||
|
else -- p > 0
|
||||||
|
if r < ti1 then return nil
|
||||||
|
elseif r < ti2 then ti2,nx2,ny2 = r,nx,ny
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
return ti1,ti2, nx1,ny1, nx2,ny2
|
||||||
|
end
|
||||||
|
|
||||||
|
-- Calculates the minkowsky difference between 2 rects, which is another rect
|
||||||
|
local function rect_getDiff(x1,y1,w1,h1, x2,y2,w2,h2)
|
||||||
|
return x2 - x1 - w1,
|
||||||
|
y2 - y1 - h1,
|
||||||
|
w1 + w2,
|
||||||
|
h1 + h2
|
||||||
|
end
|
||||||
|
|
||||||
|
local function rect_containsPoint(x,y,w,h, px,py)
|
||||||
|
return px - x > DELTA and py - y > DELTA and
|
||||||
|
x + w - px > DELTA and y + h - py > DELTA
|
||||||
|
end
|
||||||
|
|
||||||
|
local function rect_isIntersecting(x1,y1,w1,h1, x2,y2,w2,h2)
|
||||||
|
return x1 < x2+w2 and x2 < x1+w1 and
|
||||||
|
y1 < y2+h2 and y2 < y1+h1
|
||||||
|
end
|
||||||
|
|
||||||
|
local function rect_getSquareDistance(x1,y1,w1,h1, x2,y2,w2,h2)
|
||||||
|
local dx = x1 - x2 + (w1 - w2)/2
|
||||||
|
local dy = y1 - y2 + (h1 - h2)/2
|
||||||
|
return dx*dx + dy*dy
|
||||||
|
end
|
||||||
|
|
||||||
|
local function rect_detectCollision(x1,y1,w1,h1, x2,y2,w2,h2, goalX, goalY)
|
||||||
|
goalX = goalX or x1
|
||||||
|
goalY = goalY or y1
|
||||||
|
|
||||||
|
local dx, dy = goalX - x1, goalY - y1
|
||||||
|
local x,y,w,h = rect_getDiff(x1,y1,w1,h1, x2,y2,w2,h2)
|
||||||
|
|
||||||
|
local overlaps, ti, nx, ny
|
||||||
|
|
||||||
|
if rect_containsPoint(x,y,w,h, 0,0) then -- item was intersecting other
|
||||||
|
local px, py = rect_getNearestCorner(x,y,w,h, 0, 0)
|
||||||
|
local wi, hi = min(w1, abs(px)), min(h1, abs(py)) -- area of intersection
|
||||||
|
ti = -wi * hi -- ti is the negative area of intersection
|
||||||
|
overlaps = true
|
||||||
|
else
|
||||||
|
local ti1,ti2,nx1,ny1 = rect_getSegmentIntersectionIndices(x,y,w,h, 0,0,dx,dy, -math.huge, math.huge)
|
||||||
|
|
||||||
|
-- item tunnels into other
|
||||||
|
if ti1
|
||||||
|
and ti1 < 1
|
||||||
|
and (abs(ti1 - ti2) >= DELTA) -- special case for rect going through another rect's corner
|
||||||
|
and (0 < ti1 + DELTA
|
||||||
|
or 0 == ti1 and ti2 > 0)
|
||||||
|
then
|
||||||
|
ti, nx, ny = ti1, nx1, ny1
|
||||||
|
overlaps = false
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
if not ti then return end
|
||||||
|
|
||||||
|
local tx, ty
|
||||||
|
|
||||||
|
if overlaps then
|
||||||
|
if dx == 0 and dy == 0 then
|
||||||
|
-- intersecting and not moving - use minimum displacement vector
|
||||||
|
local px, py = rect_getNearestCorner(x,y,w,h, 0,0)
|
||||||
|
if abs(px) < abs(py) then py = 0 else px = 0 end
|
||||||
|
nx, ny = sign(px), sign(py)
|
||||||
|
tx, ty = x1 + px, y1 + py
|
||||||
|
else
|
||||||
|
-- intersecting and moving - move in the opposite direction
|
||||||
|
local ti1, _
|
||||||
|
ti1,_,nx,ny = rect_getSegmentIntersectionIndices(x,y,w,h, 0,0,dx,dy, -math.huge, 1)
|
||||||
|
if not ti1 then return end
|
||||||
|
tx, ty = x1 + dx * ti1, y1 + dy * ti1
|
||||||
|
end
|
||||||
|
else -- tunnel
|
||||||
|
tx, ty = x1 + dx * ti, y1 + dy * ti
|
||||||
|
end
|
||||||
|
|
||||||
|
return {
|
||||||
|
overlaps = overlaps,
|
||||||
|
ti = ti,
|
||||||
|
move = {x = dx, y = dy},
|
||||||
|
normal = {x = nx, y = ny},
|
||||||
|
touch = {x = tx, y = ty},
|
||||||
|
itemRect = {x = x1, y = y1, w = w1, h = h1},
|
||||||
|
otherRect = {x = x2, y = y2, w = w2, h = h2}
|
||||||
|
}
|
||||||
|
end
|
||||||
|
|
||||||
|
------------------------------------------
|
||||||
|
-- Grid functions
|
||||||
|
------------------------------------------
|
||||||
|
|
||||||
|
local function grid_toWorld(cellSize, cx, cy)
|
||||||
|
return (cx - 1)*cellSize, (cy-1)*cellSize
|
||||||
|
end
|
||||||
|
|
||||||
|
local function grid_toCell(cellSize, x, y)
|
||||||
|
return floor(x / cellSize) + 1, floor(y / cellSize) + 1
|
||||||
|
end
|
||||||
|
|
||||||
|
-- grid_traverse* functions are based on "A Fast Voxel Traversal Algorithm for Ray Tracing",
|
||||||
|
-- by John Amanides and Andrew Woo - http://www.cse.yorku.ca/~amana/research/grid.pdf
|
||||||
|
-- It has been modified to include both cells when the ray "touches a grid corner",
|
||||||
|
-- and with a different exit condition
|
||||||
|
|
||||||
|
local function grid_traverse_initStep(cellSize, ct, t1, t2)
|
||||||
|
local v = t2 - t1
|
||||||
|
if v > 0 then
|
||||||
|
return 1, cellSize / v, ((ct + v) * cellSize - t1) / v
|
||||||
|
elseif v < 0 then
|
||||||
|
return -1, -cellSize / v, ((ct + v - 1) * cellSize - t1) / v
|
||||||
|
else
|
||||||
|
return 0, math.huge, math.huge
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
local function grid_traverse(cellSize, x1,y1,x2,y2, f)
|
||||||
|
local cx1,cy1 = grid_toCell(cellSize, x1,y1)
|
||||||
|
local cx2,cy2 = grid_toCell(cellSize, x2,y2)
|
||||||
|
local stepX, dx, tx = grid_traverse_initStep(cellSize, cx1, x1, x2)
|
||||||
|
local stepY, dy, ty = grid_traverse_initStep(cellSize, cy1, y1, y2)
|
||||||
|
local cx,cy = cx1,cy1
|
||||||
|
|
||||||
|
f(cx, cy)
|
||||||
|
|
||||||
|
-- The default implementation had an infinite loop problem when
|
||||||
|
-- approaching the last cell in some occassions. We finish iterating
|
||||||
|
-- when we are *next* to the last cell
|
||||||
|
while abs(cx - cx2) + abs(cy - cy2) > 1 do
|
||||||
|
if tx < ty then
|
||||||
|
tx, cx = tx + dx, cx + stepX
|
||||||
|
f(cx, cy)
|
||||||
|
else
|
||||||
|
-- Addition: include both cells when going through corners
|
||||||
|
if tx == ty then f(cx + stepX, cy) end
|
||||||
|
ty, cy = ty + dy, cy + stepY
|
||||||
|
f(cx, cy)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
-- If we have not arrived to the last cell, use it
|
||||||
|
if cx ~= cx2 or cy ~= cy2 then f(cx2, cy2) end
|
||||||
|
|
||||||
|
end
|
||||||
|
|
||||||
|
local function grid_toCellRect(cellSize, x,y,w,h)
|
||||||
|
local cx,cy = grid_toCell(cellSize, x, y)
|
||||||
|
local cr,cb = ceil((x+w) / cellSize), ceil((y+h) / cellSize)
|
||||||
|
return cx, cy, cr - cx + 1, cb - cy + 1
|
||||||
|
end
|
||||||
|
|
||||||
|
------------------------------------------
|
||||||
|
-- Responses
|
||||||
|
------------------------------------------
|
||||||
|
|
||||||
|
local touch = function(world, col, x,y,w,h, goalX, goalY, filter)
|
||||||
|
return col.touch.x, col.touch.y, {}, 0
|
||||||
|
end
|
||||||
|
|
||||||
|
local cross = function(world, col, x,y,w,h, goalX, goalY, filter)
|
||||||
|
local cols, len = world:project(col.item, x,y,w,h, goalX, goalY, filter)
|
||||||
|
return goalX, goalY, cols, len
|
||||||
|
end
|
||||||
|
|
||||||
|
local slide = function(world, col, x,y,w,h, goalX, goalY, filter)
|
||||||
|
goalX = goalX or x
|
||||||
|
goalY = goalY or y
|
||||||
|
|
||||||
|
local tch, move = col.touch, col.move
|
||||||
|
if move.x ~= 0 or move.y ~= 0 then
|
||||||
|
if col.normal.x ~= 0 then
|
||||||
|
goalX = tch.x
|
||||||
|
else
|
||||||
|
goalY = tch.y
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
col.slide = {x = goalX, y = goalY}
|
||||||
|
|
||||||
|
x,y = tch.x, tch.y
|
||||||
|
local cols, len = world:project(col.item, x,y,w,h, goalX, goalY, filter)
|
||||||
|
return goalX, goalY, cols, len
|
||||||
|
end
|
||||||
|
|
||||||
|
local bounce = function(world, col, x,y,w,h, goalX, goalY, filter)
|
||||||
|
goalX = goalX or x
|
||||||
|
goalY = goalY or y
|
||||||
|
|
||||||
|
local tch, move = col.touch, col.move
|
||||||
|
local tx, ty = tch.x, tch.y
|
||||||
|
|
||||||
|
local bx, by = tx, ty
|
||||||
|
|
||||||
|
if move.x ~= 0 or move.y ~= 0 then
|
||||||
|
local bnx, bny = goalX - tx, goalY - ty
|
||||||
|
if col.normal.x == 0 then bny = -bny else bnx = -bnx end
|
||||||
|
bx, by = tx + bnx, ty + bny
|
||||||
|
end
|
||||||
|
|
||||||
|
col.bounce = {x = bx, y = by}
|
||||||
|
x,y = tch.x, tch.y
|
||||||
|
goalX, goalY = bx, by
|
||||||
|
|
||||||
|
local cols, len = world:project(col.item, x,y,w,h, goalX, goalY, filter)
|
||||||
|
return goalX, goalY, cols, len
|
||||||
|
end
|
||||||
|
|
||||||
|
------------------------------------------
|
||||||
|
-- World
|
||||||
|
------------------------------------------
|
||||||
|
|
||||||
|
local World = {}
|
||||||
|
local World_mt = {__index = World}
|
||||||
|
|
||||||
|
-- Private functions and methods
|
||||||
|
|
||||||
|
local function sortByWeight(a,b) return a.weight < b.weight end
|
||||||
|
|
||||||
|
local function sortByTiAndDistance(a,b)
|
||||||
|
if a.ti == b.ti then
|
||||||
|
local ir, ar, br = a.itemRect, a.otherRect, b.otherRect
|
||||||
|
local ad = rect_getSquareDistance(ir.x,ir.y,ir.w,ir.h, ar.x,ar.y,ar.w,ar.h)
|
||||||
|
local bd = rect_getSquareDistance(ir.x,ir.y,ir.w,ir.h, br.x,br.y,br.w,br.h)
|
||||||
|
return ad < bd
|
||||||
|
end
|
||||||
|
return a.ti < b.ti
|
||||||
|
end
|
||||||
|
|
||||||
|
local function addItemToCell(self, item, cx, cy)
|
||||||
|
self.rows[cy] = self.rows[cy] or setmetatable({}, {__mode = 'v'})
|
||||||
|
local row = self.rows[cy]
|
||||||
|
row[cx] = row[cx] or {itemCount = 0, x = cx, y = cy, items = setmetatable({}, {__mode = 'k'})}
|
||||||
|
local cell = row[cx]
|
||||||
|
self.nonEmptyCells[cell] = true
|
||||||
|
if not cell.items[item] then
|
||||||
|
cell.items[item] = true
|
||||||
|
cell.itemCount = cell.itemCount + 1
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
local function removeItemFromCell(self, item, cx, cy)
|
||||||
|
local row = self.rows[cy]
|
||||||
|
if not row or not row[cx] or not row[cx].items[item] then return false end
|
||||||
|
|
||||||
|
local cell = row[cx]
|
||||||
|
cell.items[item] = nil
|
||||||
|
cell.itemCount = cell.itemCount - 1
|
||||||
|
if cell.itemCount == 0 then
|
||||||
|
self.nonEmptyCells[cell] = nil
|
||||||
|
end
|
||||||
|
return true
|
||||||
|
end
|
||||||
|
|
||||||
|
local function getDictItemsInCellRect(self, cl,ct,cw,ch)
|
||||||
|
local items_dict = {}
|
||||||
|
for cy=ct,ct+ch-1 do
|
||||||
|
local row = self.rows[cy]
|
||||||
|
if row then
|
||||||
|
for cx=cl,cl+cw-1 do
|
||||||
|
local cell = row[cx]
|
||||||
|
if cell and cell.itemCount > 0 then -- no cell.itemCount > 1 because tunneling
|
||||||
|
for item,_ in pairs(cell.items) do
|
||||||
|
items_dict[item] = true
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
return items_dict
|
||||||
|
end
|
||||||
|
|
||||||
|
local function getCellsTouchedBySegment(self, x1,y1,x2,y2)
|
||||||
|
|
||||||
|
local cells, cellsLen, visited = {}, 0, {}
|
||||||
|
|
||||||
|
grid_traverse(self.cellSize, x1,y1,x2,y2, function(cx, cy)
|
||||||
|
local row = self.rows[cy]
|
||||||
|
if not row then return end
|
||||||
|
local cell = row[cx]
|
||||||
|
if not cell or visited[cell] then return end
|
||||||
|
|
||||||
|
visited[cell] = true
|
||||||
|
cellsLen = cellsLen + 1
|
||||||
|
cells[cellsLen] = cell
|
||||||
|
end)
|
||||||
|
|
||||||
|
return cells, cellsLen
|
||||||
|
end
|
||||||
|
|
||||||
|
local function getInfoAboutItemsTouchedBySegment(self, x1,y1, x2,y2, filter)
|
||||||
|
local cells, len = getCellsTouchedBySegment(self, x1,y1,x2,y2)
|
||||||
|
local cell, rect, l,t,w,h, ti1,ti2, tii0,tii1
|
||||||
|
local visited, itemInfo, itemInfoLen = {},{},0
|
||||||
|
for i=1,len do
|
||||||
|
cell = cells[i]
|
||||||
|
for item in pairs(cell.items) do
|
||||||
|
if not visited[item] then
|
||||||
|
visited[item] = true
|
||||||
|
if (not filter or filter(item)) then
|
||||||
|
rect = self.rects[item]
|
||||||
|
l,t,w,h = rect.x,rect.y,rect.w,rect.h
|
||||||
|
|
||||||
|
ti1,ti2 = rect_getSegmentIntersectionIndices(l,t,w,h, x1,y1, x2,y2, 0, 1)
|
||||||
|
if ti1 and ((0 < ti1 and ti1 < 1) or (0 < ti2 and ti2 < 1)) then
|
||||||
|
-- the sorting is according to the t of an infinite line, not the segment
|
||||||
|
tii0,tii1 = rect_getSegmentIntersectionIndices(l,t,w,h, x1,y1, x2,y2, -math.huge, math.huge)
|
||||||
|
itemInfoLen = itemInfoLen + 1
|
||||||
|
itemInfo[itemInfoLen] = {item = item, ti1 = ti1, ti2 = ti2, weight = min(tii0,tii1)}
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
table.sort(itemInfo, sortByWeight)
|
||||||
|
return itemInfo, itemInfoLen
|
||||||
|
end
|
||||||
|
|
||||||
|
local function getResponseByName(self, name)
|
||||||
|
local response = self.responses[name]
|
||||||
|
if not response then
|
||||||
|
error(('Unknown collision type: %s (%s)'):format(name, type(name)))
|
||||||
|
end
|
||||||
|
return response
|
||||||
|
end
|
||||||
|
|
||||||
|
|
||||||
|
-- Misc Public Methods
|
||||||
|
|
||||||
|
function World:addResponse(name, response)
|
||||||
|
self.responses[name] = response
|
||||||
|
end
|
||||||
|
|
||||||
|
function World:project(item, x,y,w,h, goalX, goalY, filter)
|
||||||
|
assertIsRect(x,y,w,h)
|
||||||
|
|
||||||
|
goalX = goalX or x
|
||||||
|
goalY = goalY or y
|
||||||
|
filter = filter or defaultFilter
|
||||||
|
|
||||||
|
local collisions, len = {}, 0
|
||||||
|
|
||||||
|
local visited = {}
|
||||||
|
if item ~= nil then visited[item] = true end
|
||||||
|
|
||||||
|
-- This could probably be done with less cells using a polygon raster over the cells instead of a
|
||||||
|
-- bounding rect of the whole movement. Conditional to building a queryPolygon method
|
||||||
|
local tl, tt = min(goalX, x), min(goalY, y)
|
||||||
|
local tr, tb = max(goalX + w, x+w), max(goalY + h, y+h)
|
||||||
|
local tw, th = tr-tl, tb-tt
|
||||||
|
|
||||||
|
local cl,ct,cw,ch = grid_toCellRect(self.cellSize, tl,tt,tw,th)
|
||||||
|
|
||||||
|
local dictItemsInCellRect = getDictItemsInCellRect(self, cl,ct,cw,ch)
|
||||||
|
|
||||||
|
for other,_ in pairs(dictItemsInCellRect) do
|
||||||
|
if not visited[other] then
|
||||||
|
visited[other] = true
|
||||||
|
|
||||||
|
local responseName = filter(item, other)
|
||||||
|
if responseName then
|
||||||
|
local ox,oy,ow,oh = self:getRect(other)
|
||||||
|
local col = rect_detectCollision(x,y,w,h, ox,oy,ow,oh, goalX, goalY)
|
||||||
|
|
||||||
|
if col then
|
||||||
|
col.other = other
|
||||||
|
col.item = item
|
||||||
|
col.type = responseName
|
||||||
|
|
||||||
|
len = len + 1
|
||||||
|
collisions[len] = col
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
table.sort(collisions, sortByTiAndDistance)
|
||||||
|
|
||||||
|
return collisions, len
|
||||||
|
end
|
||||||
|
|
||||||
|
function World:countCells()
|
||||||
|
local count = 0
|
||||||
|
for _,row in pairs(self.rows) do
|
||||||
|
for _,_ in pairs(row) do
|
||||||
|
count = count + 1
|
||||||
|
end
|
||||||
|
end
|
||||||
|
return count
|
||||||
|
end
|
||||||
|
|
||||||
|
function World:hasItem(item)
|
||||||
|
return not not self.rects[item]
|
||||||
|
end
|
||||||
|
|
||||||
|
function World:getItems()
|
||||||
|
local items, len = {}, 0
|
||||||
|
for item,_ in pairs(self.rects) do
|
||||||
|
len = len + 1
|
||||||
|
items[len] = item
|
||||||
|
end
|
||||||
|
return items, len
|
||||||
|
end
|
||||||
|
|
||||||
|
function World:countItems()
|
||||||
|
local len = 0
|
||||||
|
for _ in pairs(self.rects) do len = len + 1 end
|
||||||
|
return len
|
||||||
|
end
|
||||||
|
|
||||||
|
function World:getRect(item)
|
||||||
|
local rect = self.rects[item]
|
||||||
|
if not rect then
|
||||||
|
error('Item ' .. tostring(item) .. ' must be added to the world before getting its rect. Use world:add(item, x,y,w,h) to add it first.')
|
||||||
|
end
|
||||||
|
return rect.x, rect.y, rect.w, rect.h
|
||||||
|
end
|
||||||
|
|
||||||
|
function World:toWorld(cx, cy)
|
||||||
|
return grid_toWorld(self.cellSize, cx, cy)
|
||||||
|
end
|
||||||
|
|
||||||
|
function World:toCell(x,y)
|
||||||
|
return grid_toCell(self.cellSize, x, y)
|
||||||
|
end
|
||||||
|
|
||||||
|
|
||||||
|
--- Query methods
|
||||||
|
|
||||||
|
function World:queryRect(x,y,w,h, filter)
|
||||||
|
|
||||||
|
assertIsRect(x,y,w,h)
|
||||||
|
|
||||||
|
local cl,ct,cw,ch = grid_toCellRect(self.cellSize, x,y,w,h)
|
||||||
|
local dictItemsInCellRect = getDictItemsInCellRect(self, cl,ct,cw,ch)
|
||||||
|
|
||||||
|
local items, len = {}, 0
|
||||||
|
|
||||||
|
local rect
|
||||||
|
for item,_ in pairs(dictItemsInCellRect) do
|
||||||
|
rect = self.rects[item]
|
||||||
|
if (not filter or filter(item))
|
||||||
|
and rect_isIntersecting(x,y,w,h, rect.x, rect.y, rect.w, rect.h)
|
||||||
|
then
|
||||||
|
len = len + 1
|
||||||
|
items[len] = item
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
return items, len
|
||||||
|
end
|
||||||
|
|
||||||
|
function World:queryPoint(x,y, filter)
|
||||||
|
local cx,cy = self:toCell(x,y)
|
||||||
|
local dictItemsInCellRect = getDictItemsInCellRect(self, cx,cy,1,1)
|
||||||
|
|
||||||
|
local items, len = {}, 0
|
||||||
|
|
||||||
|
local rect
|
||||||
|
for item,_ in pairs(dictItemsInCellRect) do
|
||||||
|
rect = self.rects[item]
|
||||||
|
if (not filter or filter(item))
|
||||||
|
and rect_containsPoint(rect.x, rect.y, rect.w, rect.h, x, y)
|
||||||
|
then
|
||||||
|
len = len + 1
|
||||||
|
items[len] = item
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
return items, len
|
||||||
|
end
|
||||||
|
|
||||||
|
function World:querySegment(x1, y1, x2, y2, filter)
|
||||||
|
local itemInfo, len = getInfoAboutItemsTouchedBySegment(self, x1, y1, x2, y2, filter)
|
||||||
|
local items = {}
|
||||||
|
for i=1, len do
|
||||||
|
items[i] = itemInfo[i].item
|
||||||
|
end
|
||||||
|
return items, len
|
||||||
|
end
|
||||||
|
|
||||||
|
function World:querySegmentWithCoords(x1, y1, x2, y2, filter)
|
||||||
|
local itemInfo, len = getInfoAboutItemsTouchedBySegment(self, x1, y1, x2, y2, filter)
|
||||||
|
local dx, dy = x2-x1, y2-y1
|
||||||
|
local info, ti1, ti2
|
||||||
|
for i=1, len do
|
||||||
|
info = itemInfo[i]
|
||||||
|
ti1 = info.ti1
|
||||||
|
ti2 = info.ti2
|
||||||
|
|
||||||
|
info.weight = nil
|
||||||
|
info.x1 = x1 + dx * ti1
|
||||||
|
info.y1 = y1 + dy * ti1
|
||||||
|
info.x2 = x1 + dx * ti2
|
||||||
|
info.y2 = y1 + dy * ti2
|
||||||
|
end
|
||||||
|
return itemInfo, len
|
||||||
|
end
|
||||||
|
|
||||||
|
|
||||||
|
--- Main methods
|
||||||
|
|
||||||
|
function World:add(item, x,y,w,h)
|
||||||
|
local rect = self.rects[item]
|
||||||
|
if rect then
|
||||||
|
error('Item ' .. tostring(item) .. ' added to the world twice.')
|
||||||
|
end
|
||||||
|
assertIsRect(x,y,w,h)
|
||||||
|
|
||||||
|
self.rects[item] = {x=x,y=y,w=w,h=h}
|
||||||
|
|
||||||
|
local cl,ct,cw,ch = grid_toCellRect(self.cellSize, x,y,w,h)
|
||||||
|
for cy = ct, ct+ch-1 do
|
||||||
|
for cx = cl, cl+cw-1 do
|
||||||
|
addItemToCell(self, item, cx, cy)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
return item
|
||||||
|
end
|
||||||
|
|
||||||
|
function World:remove(item)
|
||||||
|
local x,y,w,h = self:getRect(item)
|
||||||
|
|
||||||
|
self.rects[item] = nil
|
||||||
|
local cl,ct,cw,ch = grid_toCellRect(self.cellSize, x,y,w,h)
|
||||||
|
for cy = ct, ct+ch-1 do
|
||||||
|
for cx = cl, cl+cw-1 do
|
||||||
|
removeItemFromCell(self, item, cx, cy)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
function World:update(item, x2,y2,w2,h2)
|
||||||
|
local x1,y1,w1,h1 = self:getRect(item)
|
||||||
|
w2,h2 = w2 or w1, h2 or h1
|
||||||
|
assertIsRect(x2,y2,w2,h2)
|
||||||
|
|
||||||
|
if x1 ~= x2 or y1 ~= y2 or w1 ~= w2 or h1 ~= h2 then
|
||||||
|
|
||||||
|
local cellSize = self.cellSize
|
||||||
|
local cl1,ct1,cw1,ch1 = grid_toCellRect(cellSize, x1,y1,w1,h1)
|
||||||
|
local cl2,ct2,cw2,ch2 = grid_toCellRect(cellSize, x2,y2,w2,h2)
|
||||||
|
|
||||||
|
if cl1 ~= cl2 or ct1 ~= ct2 or cw1 ~= cw2 or ch1 ~= ch2 then
|
||||||
|
|
||||||
|
local cr1, cb1 = cl1+cw1-1, ct1+ch1-1
|
||||||
|
local cr2, cb2 = cl2+cw2-1, ct2+ch2-1
|
||||||
|
local cyOut
|
||||||
|
|
||||||
|
for cy = ct1, cb1 do
|
||||||
|
cyOut = cy < ct2 or cy > cb2
|
||||||
|
for cx = cl1, cr1 do
|
||||||
|
if cyOut or cx < cl2 or cx > cr2 then
|
||||||
|
removeItemFromCell(self, item, cx, cy)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
for cy = ct2, cb2 do
|
||||||
|
cyOut = cy < ct1 or cy > cb1
|
||||||
|
for cx = cl2, cr2 do
|
||||||
|
if cyOut or cx < cl1 or cx > cr1 then
|
||||||
|
addItemToCell(self, item, cx, cy)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
end
|
||||||
|
|
||||||
|
local rect = self.rects[item]
|
||||||
|
rect.x, rect.y, rect.w, rect.h = x2,y2,w2,h2
|
||||||
|
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
function World:move(item, goalX, goalY, filter)
|
||||||
|
local actualX, actualY, cols, len = self:check(item, goalX, goalY, filter)
|
||||||
|
|
||||||
|
self:update(item, actualX, actualY)
|
||||||
|
|
||||||
|
return actualX, actualY, cols, len
|
||||||
|
end
|
||||||
|
|
||||||
|
function World:check(item, goalX, goalY, filter)
|
||||||
|
filter = filter or defaultFilter
|
||||||
|
|
||||||
|
local visited = {[item] = true}
|
||||||
|
local visitedFilter = function(itm, other)
|
||||||
|
if visited[other] then return false end
|
||||||
|
return filter(itm, other)
|
||||||
|
end
|
||||||
|
|
||||||
|
local cols, len = {}, 0
|
||||||
|
|
||||||
|
local x,y,w,h = self:getRect(item)
|
||||||
|
|
||||||
|
local projected_cols, projected_len = self:project(item, x,y,w,h, goalX,goalY, visitedFilter)
|
||||||
|
|
||||||
|
while projected_len > 0 do
|
||||||
|
local col = projected_cols[1]
|
||||||
|
len = len + 1
|
||||||
|
cols[len] = col
|
||||||
|
|
||||||
|
visited[col.other] = true
|
||||||
|
|
||||||
|
local response = getResponseByName(self, col.type)
|
||||||
|
|
||||||
|
goalX, goalY, projected_cols, projected_len = response(
|
||||||
|
self,
|
||||||
|
col,
|
||||||
|
x, y, w, h,
|
||||||
|
goalX, goalY,
|
||||||
|
visitedFilter
|
||||||
|
)
|
||||||
|
end
|
||||||
|
|
||||||
|
return goalX, goalY, cols, len
|
||||||
|
end
|
||||||
|
|
||||||
|
|
||||||
|
-- Public library functions
|
||||||
|
|
||||||
|
bump.newWorld = function(cellSize)
|
||||||
|
cellSize = cellSize or 64
|
||||||
|
assertIsPositiveNumber(cellSize, 'cellSize')
|
||||||
|
local world = setmetatable({
|
||||||
|
cellSize = cellSize,
|
||||||
|
rects = {},
|
||||||
|
rows = {},
|
||||||
|
nonEmptyCells = {},
|
||||||
|
responses = {}
|
||||||
|
}, World_mt)
|
||||||
|
|
||||||
|
world:addResponse('touch', touch)
|
||||||
|
world:addResponse('cross', cross)
|
||||||
|
world:addResponse('slide', slide)
|
||||||
|
world:addResponse('bounce', bounce)
|
||||||
|
|
||||||
|
return world
|
||||||
|
end
|
||||||
|
|
||||||
|
bump.rect = {
|
||||||
|
getNearestCorner = rect_getNearestCorner,
|
||||||
|
getSegmentIntersectionIndices = rect_getSegmentIntersectionIndices,
|
||||||
|
getDiff = rect_getDiff,
|
||||||
|
containsPoint = rect_containsPoint,
|
||||||
|
isIntersecting = rect_isIntersecting,
|
||||||
|
getSquareDistance = rect_getSquareDistance,
|
||||||
|
detectCollision = rect_detectCollision
|
||||||
|
}
|
||||||
|
|
||||||
|
bump.responses = {
|
||||||
|
touch = touch,
|
||||||
|
cross = cross,
|
||||||
|
slide = slide,
|
||||||
|
bounce = bounce
|
||||||
|
}
|
||||||
|
|
||||||
|
return bump
|
257
src/lib/hump/camera.lua
Normal file
257
src/lib/hump/camera.lua
Normal file
@ -0,0 +1,257 @@
|
|||||||
|
--[[
|
||||||
|
Copyright (c) 2010-2015 Matthias Richter
|
||||||
|
|
||||||
|
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||||
|
of this software and associated documentation files (the "Software"), to deal
|
||||||
|
in the Software without restriction, including without limitation the rights
|
||||||
|
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||||
|
copies of the Software, and to permit persons to whom the Software is
|
||||||
|
furnished to do so, subject to the following conditions:
|
||||||
|
|
||||||
|
The above copyright notice and this permission notice shall be included in
|
||||||
|
all copies or substantial portions of the Software.
|
||||||
|
|
||||||
|
Except as contained in this notice, the name(s) of the above copyright holders
|
||||||
|
shall not be used in advertising or otherwise to promote the sale, use or
|
||||||
|
other dealings in this Software without prior written authorization.
|
||||||
|
|
||||||
|
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||||
|
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||||
|
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||||
|
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||||
|
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||||
|
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
||||||
|
THE SOFTWARE.
|
||||||
|
]]--
|
||||||
|
|
||||||
|
local _PATH = (...):match('^(.*[%./])[^%.%/]+$') or ''
|
||||||
|
local cos, sin = math.cos, math.sin
|
||||||
|
|
||||||
|
local camera = {}
|
||||||
|
camera.__index = camera
|
||||||
|
|
||||||
|
-- Movement interpolators (for camera locking/windowing)
|
||||||
|
camera.smooth = {}
|
||||||
|
|
||||||
|
function camera.smooth.none()
|
||||||
|
return function(dx,dy) return dx,dy end
|
||||||
|
end
|
||||||
|
|
||||||
|
function camera.smooth.linear(speed)
|
||||||
|
assert(type(speed) == "number", "Invalid parameter: speed = "..tostring(speed))
|
||||||
|
return function(dx,dy, s)
|
||||||
|
-- normalize direction
|
||||||
|
local d = math.sqrt(dx*dx+dy*dy)
|
||||||
|
local dts = math.min((s or speed) * love.timer.getDelta(), d) -- prevent overshooting the goal
|
||||||
|
if d > 0 then
|
||||||
|
dx,dy = dx/d, dy/d
|
||||||
|
end
|
||||||
|
|
||||||
|
return dx*dts, dy*dts
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
function camera.smooth.damped(stiffness)
|
||||||
|
assert(type(stiffness) == "number", "Invalid parameter: stiffness = "..tostring(stiffness))
|
||||||
|
return function(dx,dy, s)
|
||||||
|
local dts = love.timer.getDelta() * (s or stiffness)
|
||||||
|
return dx*dts, dy*dts
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
|
||||||
|
local function new(x,y, zoom, rot, smoother)
|
||||||
|
x,y = x or love.graphics.getWidth()/2, y or love.graphics.getHeight()/2
|
||||||
|
zoom = zoom or 1
|
||||||
|
rot = rot or 0
|
||||||
|
smoother = smoother or camera.smooth.none() -- for locking, see below
|
||||||
|
return setmetatable({x = x, y = y, scale = zoom, rot = rot, smoother = smoother}, camera)
|
||||||
|
end
|
||||||
|
|
||||||
|
function camera:lookAt(x,y)
|
||||||
|
self.x, self.y = x, y
|
||||||
|
return self
|
||||||
|
end
|
||||||
|
|
||||||
|
function camera:move(dx,dy)
|
||||||
|
self.x, self.y = self.x + dx, self.y + dy
|
||||||
|
return self
|
||||||
|
end
|
||||||
|
|
||||||
|
function camera:position()
|
||||||
|
return self.x, self.y
|
||||||
|
end
|
||||||
|
|
||||||
|
function camera:rotate(phi)
|
||||||
|
self.rot = self.rot + phi
|
||||||
|
return self
|
||||||
|
end
|
||||||
|
|
||||||
|
function camera:rotateTo(phi)
|
||||||
|
self.rot = phi
|
||||||
|
return self
|
||||||
|
end
|
||||||
|
|
||||||
|
function camera:zoom(mul)
|
||||||
|
self.scale = self.scale * mul
|
||||||
|
return self
|
||||||
|
end
|
||||||
|
|
||||||
|
function camera:zoomTo(zoom)
|
||||||
|
self.scale = zoom
|
||||||
|
return self
|
||||||
|
end
|
||||||
|
|
||||||
|
local function rotate(phi, x,y)
|
||||||
|
local c, s = cos(phi), sin(phi)
|
||||||
|
return c*x - s*y, s*x + c*y
|
||||||
|
end
|
||||||
|
|
||||||
|
-- (dx, dy) = (((x,y) - center) * (1 / cam.scale - 1 / cam.new_scale)):rotated(-cam.rot)
|
||||||
|
function camera:zoomInto(zoom, tx, ty, x, y, w, h)
|
||||||
|
local oldScale = self.scale
|
||||||
|
self:zoom(zoom)
|
||||||
|
x, y = x or 0, y or 0
|
||||||
|
w, h = w or love.graphics.getWidth(), h or love.graphics.getHeight()
|
||||||
|
local cx, cy = x+w/2, y+h/2
|
||||||
|
local scalar = (1/oldScale - 1/self.scale)
|
||||||
|
local dx = (tx - cx) * scalar
|
||||||
|
local dy = (ty - cy) * scalar
|
||||||
|
|
||||||
|
self:move(rotate(-self.rot, dx, dy))
|
||||||
|
end
|
||||||
|
|
||||||
|
-- (dx, dy) = (((new_x, new_y) - (x, y)) / cam.scale):rotated(-cam.rot)
|
||||||
|
function camera:relativeMove(x, y)
|
||||||
|
local dx = x / self.scale
|
||||||
|
local dy = y / self.scale
|
||||||
|
|
||||||
|
self:move(rotate(-self.rot, dx, dy))
|
||||||
|
end
|
||||||
|
|
||||||
|
function camera:getViewport(x, y, w, h)
|
||||||
|
x,y = x or 0, y or 0
|
||||||
|
w,h = w or love.graphics.getWidth(), h or love.graphics.getHeight()
|
||||||
|
|
||||||
|
local corner1_x, corner1_y = self:worldCoords(0, 0, x, y, w, h)
|
||||||
|
local corner2_x, corner2_y = self:worldCoords(w, 0, x, y, w, h)
|
||||||
|
local corner3_x, corner3_y = self:worldCoords(0, h, x, y, w, h)
|
||||||
|
local corner4_x, corner4_y = self:worldCoords(w, h, x, y, w, h)
|
||||||
|
|
||||||
|
local left = math.min(corner1_x, corner2_x, corner3_x, corner4_x)
|
||||||
|
local top = math.min(corner1_y, corner2_y, corner3_y, corner4_y)
|
||||||
|
local right = math.max(corner1_x, corner2_x, corner3_x, corner4_x)
|
||||||
|
local bottom = math.max(corner1_y, corner2_y, corner3_y, corner4_y)
|
||||||
|
|
||||||
|
return left, top, right, bottom
|
||||||
|
end
|
||||||
|
|
||||||
|
function camera:attach(x,y,w,h, noclip)
|
||||||
|
x,y = x or 0, y or 0
|
||||||
|
w,h = w or love.graphics.getWidth(), h or love.graphics.getHeight()
|
||||||
|
|
||||||
|
self._sx,self._sy,self._sw,self._sh = love.graphics.getScissor()
|
||||||
|
if not noclip then
|
||||||
|
love.graphics.setScissor(x,y,w,h)
|
||||||
|
end
|
||||||
|
|
||||||
|
local cx,cy = x+w/2, y+h/2
|
||||||
|
love.graphics.push()
|
||||||
|
love.graphics.translate(cx, cy)
|
||||||
|
love.graphics.scale(self.scale)
|
||||||
|
love.graphics.rotate(self.rot)
|
||||||
|
love.graphics.translate(-self.x, -self.y)
|
||||||
|
end
|
||||||
|
|
||||||
|
function camera:detach()
|
||||||
|
love.graphics.pop()
|
||||||
|
love.graphics.setScissor(self._sx,self._sy,self._sw,self._sh)
|
||||||
|
end
|
||||||
|
|
||||||
|
function camera:draw(...)
|
||||||
|
local x,y,w,h,noclip,func
|
||||||
|
local nargs = select("#", ...)
|
||||||
|
if nargs == 1 then
|
||||||
|
func = ...
|
||||||
|
elseif nargs == 5 then
|
||||||
|
x,y,w,h,func = ...
|
||||||
|
elseif nargs == 6 then
|
||||||
|
x,y,w,h,noclip,func = ...
|
||||||
|
else
|
||||||
|
error("Invalid arguments to camera:draw()")
|
||||||
|
end
|
||||||
|
|
||||||
|
self:attach(x,y,w,h,noclip)
|
||||||
|
func()
|
||||||
|
self:detach()
|
||||||
|
end
|
||||||
|
|
||||||
|
-- world coordinates to camera coordinates
|
||||||
|
function camera:cameraCoords(x,y, ox,oy,w,h)
|
||||||
|
ox, oy = ox or 0, oy or 0
|
||||||
|
w,h = w or love.graphics.getWidth(), h or love.graphics.getHeight()
|
||||||
|
|
||||||
|
-- x,y = ((x,y) - (self.x, self.y)):rotated(self.rot) * self.scale + center
|
||||||
|
x, y = rotate(self.rot, x - self.x, y - self.y)
|
||||||
|
return x*self.scale + w/2 + ox, y*self.scale + h/2 + oy
|
||||||
|
end
|
||||||
|
|
||||||
|
-- camera coordinates to world coordinates
|
||||||
|
function camera:worldCoords(x,y, ox,oy,w,h)
|
||||||
|
ox, oy = ox or 0, oy or 0
|
||||||
|
w,h = w or love.graphics.getWidth(), h or love.graphics.getHeight()
|
||||||
|
|
||||||
|
-- x,y = (((x,y) - center) / self.scale):rotated(-self.rot) + (self.x,self.y)
|
||||||
|
x,y = (x - w/2 - ox) / self.scale, (y - h/2 - oy) / self.scale
|
||||||
|
x,y = rotate(-self.rot, x, y)
|
||||||
|
return x+self.x, y+self.y
|
||||||
|
end
|
||||||
|
|
||||||
|
function camera:mousePosition(ox,oy,w,h)
|
||||||
|
local mx,my = love.mouse.getPosition()
|
||||||
|
return self:worldCoords(mx,my, ox,oy,w,h)
|
||||||
|
end
|
||||||
|
|
||||||
|
-- camera scrolling utilities
|
||||||
|
function camera:lockX(x, smoother, ...)
|
||||||
|
local dx, dy = (smoother or self.smoother)(x - self.x, self.y, ...)
|
||||||
|
self.x = self.x + dx
|
||||||
|
return self
|
||||||
|
end
|
||||||
|
|
||||||
|
function camera:lockY(y, smoother, ...)
|
||||||
|
local dx, dy = (smoother or self.smoother)(self.x, y - self.y, ...)
|
||||||
|
self.y = self.y + dy
|
||||||
|
return self
|
||||||
|
end
|
||||||
|
|
||||||
|
function camera:lockPosition(x,y, smoother, ...)
|
||||||
|
return self:move((smoother or self.smoother)(x - self.x, y - self.y, ...))
|
||||||
|
end
|
||||||
|
|
||||||
|
function camera:lockWindow(x, y, x_min, x_max, y_min, y_max, smoother, ...)
|
||||||
|
-- figure out displacement in camera coordinates
|
||||||
|
x,y = self:cameraCoords(x,y)
|
||||||
|
local dx, dy = 0,0
|
||||||
|
if x < x_min then
|
||||||
|
dx = x - x_min
|
||||||
|
elseif x > x_max then
|
||||||
|
dx = x - x_max
|
||||||
|
end
|
||||||
|
if y < y_min then
|
||||||
|
dy = y - y_min
|
||||||
|
elseif y > y_max then
|
||||||
|
dy = y - y_max
|
||||||
|
end
|
||||||
|
|
||||||
|
-- transform displacement to movement in world coordinates
|
||||||
|
local c,s = cos(-self.rot), sin(-self.rot)
|
||||||
|
dx,dy = (c*dx - s*dy) / self.scale, (s*dx + c*dy) / self.scale
|
||||||
|
|
||||||
|
-- move
|
||||||
|
self:move((smoother or self.smoother)(dx,dy,...))
|
||||||
|
end
|
||||||
|
|
||||||
|
-- the module
|
||||||
|
return setmetatable({new = new, smooth = camera.smooth},
|
||||||
|
{__call = function(_, ...) return new(...) end})
|
98
src/lib/hump/class.lua
Normal file
98
src/lib/hump/class.lua
Normal file
@ -0,0 +1,98 @@
|
|||||||
|
--[[
|
||||||
|
Copyright (c) 2010-2013 Matthias Richter
|
||||||
|
|
||||||
|
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||||
|
of this software and associated documentation files (the "Software"), to deal
|
||||||
|
in the Software without restriction, including without limitation the rights
|
||||||
|
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||||
|
copies of the Software, and to permit persons to whom the Software is
|
||||||
|
furnished to do so, subject to the following conditions:
|
||||||
|
|
||||||
|
The above copyright notice and this permission notice shall be included in
|
||||||
|
all copies or substantial portions of the Software.
|
||||||
|
|
||||||
|
Except as contained in this notice, the name(s) of the above copyright holders
|
||||||
|
shall not be used in advertising or otherwise to promote the sale, use or
|
||||||
|
other dealings in this Software without prior written authorization.
|
||||||
|
|
||||||
|
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||||
|
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||||
|
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||||
|
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||||
|
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||||
|
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
||||||
|
THE SOFTWARE.
|
||||||
|
]]--
|
||||||
|
|
||||||
|
local function include_helper(to, from, seen)
|
||||||
|
if from == nil then
|
||||||
|
return to
|
||||||
|
elseif type(from) ~= 'table' then
|
||||||
|
return from
|
||||||
|
elseif seen[from] then
|
||||||
|
return seen[from]
|
||||||
|
end
|
||||||
|
|
||||||
|
seen[from] = to
|
||||||
|
for k,v in pairs(from) do
|
||||||
|
k = include_helper({}, k, seen) -- keys might also be tables
|
||||||
|
if to[k] == nil then
|
||||||
|
to[k] = include_helper({}, v, seen)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
return to
|
||||||
|
end
|
||||||
|
|
||||||
|
-- deeply copies `other' into `class'. keys in `other' that are already
|
||||||
|
-- defined in `class' are omitted
|
||||||
|
local function include(class, other)
|
||||||
|
return include_helper(class, other, {})
|
||||||
|
end
|
||||||
|
|
||||||
|
-- returns a deep copy of `other'
|
||||||
|
local function clone(other)
|
||||||
|
return setmetatable(include({}, other), getmetatable(other))
|
||||||
|
end
|
||||||
|
|
||||||
|
local function new(class)
|
||||||
|
-- mixins
|
||||||
|
class = class or {} -- class can be nil
|
||||||
|
local inc = class.__includes or {}
|
||||||
|
if getmetatable(inc) then inc = {inc} end
|
||||||
|
|
||||||
|
for _, other in ipairs(inc) do
|
||||||
|
if type(other) == "string" then
|
||||||
|
other = _G[other]
|
||||||
|
end
|
||||||
|
include(class, other)
|
||||||
|
end
|
||||||
|
|
||||||
|
-- class implementation
|
||||||
|
class.__index = class
|
||||||
|
class.init = class.init or class[1] or function() end
|
||||||
|
class.include = class.include or include
|
||||||
|
class.clone = class.clone or clone
|
||||||
|
|
||||||
|
-- constructor call
|
||||||
|
return setmetatable(class, {__call = function(c, ...)
|
||||||
|
local o = setmetatable({}, c)
|
||||||
|
o:init(...)
|
||||||
|
return o
|
||||||
|
end})
|
||||||
|
end
|
||||||
|
|
||||||
|
-- interface for cross class-system compatibility (see https://github.com/bartbes/Class-Commons).
|
||||||
|
if class_commons ~= false and not common then
|
||||||
|
common = {}
|
||||||
|
function common.class(name, prototype, parent)
|
||||||
|
return new{__includes = {prototype, parent}}
|
||||||
|
end
|
||||||
|
function common.instance(class, ...)
|
||||||
|
return class(...)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
|
||||||
|
-- the module
|
||||||
|
return setmetatable({new = new, include = include, clone = clone},
|
||||||
|
{__call = function(_,...) return new(...) end})
|
113
src/lib/hump/gamestate.lua
Normal file
113
src/lib/hump/gamestate.lua
Normal file
@ -0,0 +1,113 @@
|
|||||||
|
--[[
|
||||||
|
Copyright (c) 2010-2013 Matthias Richter
|
||||||
|
|
||||||
|
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||||
|
of this software and associated documentation files (the "Software"), to deal
|
||||||
|
in the Software without restriction, including without limitation the rights
|
||||||
|
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||||
|
copies of the Software, and to permit persons to whom the Software is
|
||||||
|
furnished to do so, subject to the following conditions:
|
||||||
|
|
||||||
|
The above copyright notice and this permission notice shall be included in
|
||||||
|
all copies or substantial portions of the Software.
|
||||||
|
|
||||||
|
Except as contained in this notice, the name(s) of the above copyright holders
|
||||||
|
shall not be used in advertising or otherwise to promote the sale, use or
|
||||||
|
other dealings in this Software without prior written authorization.
|
||||||
|
|
||||||
|
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||||
|
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||||
|
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||||
|
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||||
|
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||||
|
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
||||||
|
THE SOFTWARE.
|
||||||
|
]]--
|
||||||
|
|
||||||
|
local function __NULL__() end
|
||||||
|
|
||||||
|
-- default gamestate produces error on every callback
|
||||||
|
local state_init = setmetatable({leave = __NULL__},
|
||||||
|
{__index = function() error("Gamestate not initialized. Use Gamestate.switch()") end})
|
||||||
|
local stack = {state_init}
|
||||||
|
local initialized_states = setmetatable({}, {__mode = "k"})
|
||||||
|
local state_is_dirty = true
|
||||||
|
|
||||||
|
local GS = {}
|
||||||
|
function GS.new(t) return t or {} end -- constructor - deprecated!
|
||||||
|
|
||||||
|
local function change_state(stack_offset, to, ...)
|
||||||
|
local pre = stack[#stack]
|
||||||
|
|
||||||
|
-- initialize only on first call
|
||||||
|
;(initialized_states[to] or to.init or __NULL__)(to)
|
||||||
|
initialized_states[to] = __NULL__
|
||||||
|
|
||||||
|
stack[#stack+stack_offset] = to
|
||||||
|
state_is_dirty = true
|
||||||
|
return (to.enter or __NULL__)(to, pre, ...)
|
||||||
|
end
|
||||||
|
|
||||||
|
function GS.switch(to, ...)
|
||||||
|
assert(to, "Missing argument: Gamestate to switch to")
|
||||||
|
assert(to ~= GS, "Can't call switch with colon operator")
|
||||||
|
;(stack[#stack].leave or __NULL__)(stack[#stack])
|
||||||
|
return change_state(0, to, ...)
|
||||||
|
end
|
||||||
|
|
||||||
|
function GS.push(to, ...)
|
||||||
|
assert(to, "Missing argument: Gamestate to switch to")
|
||||||
|
assert(to ~= GS, "Can't call push with colon operator")
|
||||||
|
return change_state(1, to, ...)
|
||||||
|
end
|
||||||
|
|
||||||
|
function GS.pop(...)
|
||||||
|
assert(#stack > 1, "No more states to pop!")
|
||||||
|
local pre, to = stack[#stack], stack[#stack-1]
|
||||||
|
stack[#stack] = nil
|
||||||
|
;(pre.leave or __NULL__)(pre)
|
||||||
|
state_is_dirty = true
|
||||||
|
return (to.resume or __NULL__)(to, pre, ...)
|
||||||
|
end
|
||||||
|
|
||||||
|
function GS.current()
|
||||||
|
return stack[#stack]
|
||||||
|
end
|
||||||
|
|
||||||
|
-- XXX: don't overwrite love.errorhandler by default:
|
||||||
|
-- this callback is different than the other callbacks
|
||||||
|
-- (see http://love2d.org/wiki/love.errorhandler)
|
||||||
|
-- overwriting thi callback can result in random crashes (issue #95)
|
||||||
|
local all_callbacks = { 'draw', 'update' }
|
||||||
|
|
||||||
|
-- fetch event callbacks from love.handlers
|
||||||
|
for k in pairs(love.handlers) do
|
||||||
|
all_callbacks[#all_callbacks+1] = k
|
||||||
|
end
|
||||||
|
|
||||||
|
function GS.registerEvents(callbacks)
|
||||||
|
local registry = {}
|
||||||
|
callbacks = callbacks or all_callbacks
|
||||||
|
for _, f in ipairs(callbacks) do
|
||||||
|
registry[f] = love[f] or __NULL__
|
||||||
|
love[f] = function(...)
|
||||||
|
registry[f](...)
|
||||||
|
return GS[f](...)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
-- forward any undefined functions
|
||||||
|
setmetatable(GS, {__index = function(_, func)
|
||||||
|
-- call function only if at least one 'update' was called beforehand
|
||||||
|
-- (see issue #46)
|
||||||
|
if not state_is_dirty or func == 'update' then
|
||||||
|
state_is_dirty = false
|
||||||
|
return function(...)
|
||||||
|
return (stack[#stack][func] or __NULL__)(stack[#stack], ...)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
return __NULL__
|
||||||
|
end})
|
||||||
|
|
||||||
|
return GS
|
102
src/lib/hump/signal.lua
Normal file
102
src/lib/hump/signal.lua
Normal file
@ -0,0 +1,102 @@
|
|||||||
|
--[[
|
||||||
|
Copyright (c) 2012-2013 Matthias Richter
|
||||||
|
|
||||||
|
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||||
|
of this software and associated documentation files (the "Software"), to deal
|
||||||
|
in the Software without restriction, including without limitation the rights
|
||||||
|
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||||
|
copies of the Software, and to permit persons to whom the Software is
|
||||||
|
furnished to do so, subject to the following conditions:
|
||||||
|
|
||||||
|
The above copyright notice and this permission notice shall be included in
|
||||||
|
all copies or substantial portions of the Software.
|
||||||
|
|
||||||
|
Except as contained in this notice, the name(s) of the above copyright holders
|
||||||
|
shall not be used in advertising or otherwise to promote the sale, use or
|
||||||
|
other dealings in this Software without prior written authorization.
|
||||||
|
|
||||||
|
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||||
|
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||||
|
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||||
|
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||||
|
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||||
|
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
||||||
|
THE SOFTWARE.
|
||||||
|
]]--
|
||||||
|
|
||||||
|
local Registry = {}
|
||||||
|
Registry.__index = function(self, key)
|
||||||
|
return Registry[key] or (function()
|
||||||
|
local t = {}
|
||||||
|
rawset(self, key, t)
|
||||||
|
return t
|
||||||
|
end)()
|
||||||
|
end
|
||||||
|
|
||||||
|
function Registry:register(s, f)
|
||||||
|
self[s][f] = f
|
||||||
|
return f
|
||||||
|
end
|
||||||
|
|
||||||
|
function Registry:emit(s, ...)
|
||||||
|
for f in pairs(self[s]) do
|
||||||
|
f(...)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
function Registry:remove(s, ...)
|
||||||
|
local f = {...}
|
||||||
|
for i = 1,select('#', ...) do
|
||||||
|
self[s][f[i]] = nil
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
function Registry:clear(...)
|
||||||
|
local s = {...}
|
||||||
|
for i = 1,select('#', ...) do
|
||||||
|
self[s[i]] = {}
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
function Registry:emitPattern(p, ...)
|
||||||
|
for s in pairs(self) do
|
||||||
|
if s:match(p) then self:emit(s, ...) end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
function Registry:registerPattern(p, f)
|
||||||
|
for s in pairs(self) do
|
||||||
|
if s:match(p) then self:register(s, f) end
|
||||||
|
end
|
||||||
|
return f
|
||||||
|
end
|
||||||
|
|
||||||
|
function Registry:removePattern(p, ...)
|
||||||
|
for s in pairs(self) do
|
||||||
|
if s:match(p) then self:remove(s, ...) end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
function Registry:clearPattern(p)
|
||||||
|
for s in pairs(self) do
|
||||||
|
if s:match(p) then self[s] = {} end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
-- instancing
|
||||||
|
function Registry.new()
|
||||||
|
return setmetatable({}, Registry)
|
||||||
|
end
|
||||||
|
|
||||||
|
-- default instance
|
||||||
|
local default = Registry.new()
|
||||||
|
|
||||||
|
-- module forwards calls to default instance
|
||||||
|
local module = {}
|
||||||
|
for k in pairs(Registry) do
|
||||||
|
if k ~= "__index" then
|
||||||
|
module[k] = function(...) return default[k](default, ...) end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
return setmetatable(module, {__call = Registry.new})
|
215
src/lib/hump/timer.lua
Normal file
215
src/lib/hump/timer.lua
Normal file
@ -0,0 +1,215 @@
|
|||||||
|
--[[
|
||||||
|
Copyright (c) 2010-2013 Matthias Richter
|
||||||
|
|
||||||
|
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||||
|
of this software and associated documentation files (the "Software"), to deal
|
||||||
|
in the Software without restriction, including without limitation the rights
|
||||||
|
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||||
|
copies of the Software, and to permit persons to whom the Software is
|
||||||
|
furnished to do so, subject to the following conditions:
|
||||||
|
|
||||||
|
The above copyright notice and this permission notice shall be included in
|
||||||
|
all copies or substantial portions of the Software.
|
||||||
|
|
||||||
|
Except as contained in this notice, the name(s) of the above copyright holders
|
||||||
|
shall not be used in advertising or otherwise to promote the sale, use or
|
||||||
|
other dealings in this Software without prior written authorization.
|
||||||
|
|
||||||
|
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||||
|
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||||
|
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||||
|
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||||
|
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||||
|
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
||||||
|
THE SOFTWARE.
|
||||||
|
]]--
|
||||||
|
|
||||||
|
local Timer = {}
|
||||||
|
Timer.__index = Timer
|
||||||
|
|
||||||
|
local function _nothing_() end
|
||||||
|
|
||||||
|
local function updateTimerHandle(handle, dt)
|
||||||
|
-- handle: {
|
||||||
|
-- time = <number>,
|
||||||
|
-- after = <function>,
|
||||||
|
-- during = <function>,
|
||||||
|
-- limit = <number>,
|
||||||
|
-- count = <number>,
|
||||||
|
-- }
|
||||||
|
handle.time = handle.time + dt
|
||||||
|
handle.during(dt, math.max(handle.limit - handle.time, 0))
|
||||||
|
|
||||||
|
while handle.time >= handle.limit and handle.count > 0 do
|
||||||
|
if handle.after(handle.after) == false then
|
||||||
|
handle.count = 0
|
||||||
|
break
|
||||||
|
end
|
||||||
|
handle.time = handle.time - handle.limit
|
||||||
|
handle.count = handle.count - 1
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
function Timer:update(dt)
|
||||||
|
-- timers may create new timers, which leads to undefined behavior
|
||||||
|
-- in pairs() - so we need to put them in a different table first
|
||||||
|
local to_update = {}
|
||||||
|
for handle in pairs(self.functions) do
|
||||||
|
to_update[handle] = handle
|
||||||
|
end
|
||||||
|
|
||||||
|
for handle in pairs(to_update) do
|
||||||
|
if self.functions[handle] then
|
||||||
|
updateTimerHandle(handle, dt)
|
||||||
|
if handle.count == 0 then
|
||||||
|
self.functions[handle] = nil
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
function Timer:during(delay, during, after)
|
||||||
|
local handle = { time = 0, during = during, after = after or _nothing_, limit = delay, count = 1 }
|
||||||
|
self.functions[handle] = true
|
||||||
|
return handle
|
||||||
|
end
|
||||||
|
|
||||||
|
function Timer:after(delay, func)
|
||||||
|
return self:during(delay, _nothing_, func)
|
||||||
|
end
|
||||||
|
|
||||||
|
function Timer:every(delay, after, count)
|
||||||
|
local count = count or math.huge -- exploit below: math.huge - 1 = math.huge
|
||||||
|
local handle = { time = 0, during = _nothing_, after = after, limit = delay, count = count }
|
||||||
|
self.functions[handle] = true
|
||||||
|
return handle
|
||||||
|
end
|
||||||
|
|
||||||
|
function Timer:cancel(handle)
|
||||||
|
self.functions[handle] = nil
|
||||||
|
end
|
||||||
|
|
||||||
|
function Timer:clear()
|
||||||
|
self.functions = {}
|
||||||
|
end
|
||||||
|
|
||||||
|
function Timer:script(f)
|
||||||
|
local co = coroutine.wrap(f)
|
||||||
|
co(function(t)
|
||||||
|
self:after(t, co)
|
||||||
|
coroutine.yield()
|
||||||
|
end)
|
||||||
|
end
|
||||||
|
|
||||||
|
Timer.tween = setmetatable({
|
||||||
|
-- helper functions
|
||||||
|
out = function(f) -- 'rotates' a function
|
||||||
|
return function(s, ...) return 1 - f(1-s, ...) end
|
||||||
|
end,
|
||||||
|
chain = function(f1, f2) -- concatenates two functions
|
||||||
|
return function(s, ...) return (s < .5 and f1(2*s, ...) or 1 + f2(2*s-1, ...)) * .5 end
|
||||||
|
end,
|
||||||
|
|
||||||
|
-- useful tweening functions
|
||||||
|
linear = function(s) return s end,
|
||||||
|
quad = function(s) return s*s end,
|
||||||
|
cubic = function(s) return s*s*s end,
|
||||||
|
quart = function(s) return s*s*s*s end,
|
||||||
|
quint = function(s) return s*s*s*s*s end,
|
||||||
|
sine = function(s) return 1-math.cos(s*math.pi/2) end,
|
||||||
|
expo = function(s) return 2^(10*(s-1)) end,
|
||||||
|
circ = function(s) return 1 - math.sqrt(1-s*s) end,
|
||||||
|
|
||||||
|
back = function(s,bounciness)
|
||||||
|
bounciness = bounciness or 1.70158
|
||||||
|
return s*s*((bounciness+1)*s - bounciness)
|
||||||
|
end,
|
||||||
|
|
||||||
|
bounce = function(s) -- magic numbers ahead
|
||||||
|
local a,b = 7.5625, 1/2.75
|
||||||
|
return math.min(a*s^2, a*(s-1.5*b)^2 + .75, a*(s-2.25*b)^2 + .9375, a*(s-2.625*b)^2 + .984375)
|
||||||
|
end,
|
||||||
|
|
||||||
|
elastic = function(s, amp, period)
|
||||||
|
amp, period = amp and math.max(1, amp) or 1, period or .3
|
||||||
|
return (-amp * math.sin(2*math.pi/period * (s-1) - math.asin(1/amp))) * 2^(10*(s-1))
|
||||||
|
end,
|
||||||
|
}, {
|
||||||
|
|
||||||
|
-- register new tween
|
||||||
|
__call = function(tween, self, len, subject, target, method, after, ...)
|
||||||
|
-- recursively collects fields that are defined in both subject and target into a flat list
|
||||||
|
local function tween_collect_payload(subject, target, out)
|
||||||
|
for k,v in pairs(target) do
|
||||||
|
local ref = subject[k]
|
||||||
|
assert(type(v) == type(ref), 'Type mismatch in field "'..k..'".')
|
||||||
|
if type(v) == 'table' then
|
||||||
|
tween_collect_payload(ref, v, out)
|
||||||
|
else
|
||||||
|
local ok, delta = pcall(function() return (v-ref)*1 end)
|
||||||
|
assert(ok, 'Field "'..k..'" does not support arithmetic operations')
|
||||||
|
out[#out+1] = {subject, k, delta}
|
||||||
|
end
|
||||||
|
end
|
||||||
|
return out
|
||||||
|
end
|
||||||
|
|
||||||
|
method = tween[method or 'linear'] -- see __index
|
||||||
|
local payload, t, args = tween_collect_payload(subject, target, {}), 0, {...}
|
||||||
|
|
||||||
|
local last_s = 0
|
||||||
|
return self:during(len, function(dt)
|
||||||
|
t = t + dt
|
||||||
|
local s = method(math.min(1, t/len), unpack(args))
|
||||||
|
local ds = s - last_s
|
||||||
|
last_s = s
|
||||||
|
for _, info in ipairs(payload) do
|
||||||
|
local ref, key, delta = unpack(info)
|
||||||
|
ref[key] = ref[key] + delta * ds
|
||||||
|
end
|
||||||
|
end, after)
|
||||||
|
end,
|
||||||
|
|
||||||
|
-- fetches function and generated compositions for method `key`
|
||||||
|
__index = function(tweens, key)
|
||||||
|
if type(key) == 'function' then return key end
|
||||||
|
|
||||||
|
assert(type(key) == 'string', 'Method must be function or string.')
|
||||||
|
if rawget(tweens, key) then return rawget(tweens, key) end
|
||||||
|
|
||||||
|
local function construct(pattern, f)
|
||||||
|
local method = rawget(tweens, key:match(pattern))
|
||||||
|
if method then return f(method) end
|
||||||
|
return nil
|
||||||
|
end
|
||||||
|
|
||||||
|
local out, chain = rawget(tweens,'out'), rawget(tweens,'chain')
|
||||||
|
return construct('^in%-([^-]+)$', function(...) return ... end)
|
||||||
|
or construct('^out%-([^-]+)$', out)
|
||||||
|
or construct('^in%-out%-([^-]+)$', function(f) return chain(f, out(f)) end)
|
||||||
|
or construct('^out%-in%-([^-]+)$', function(f) return chain(out(f), f) end)
|
||||||
|
or error('Unknown interpolation method: ' .. key)
|
||||||
|
end})
|
||||||
|
|
||||||
|
-- Timer instancing
|
||||||
|
function Timer.new()
|
||||||
|
return setmetatable({functions = {}, tween = Timer.tween}, Timer)
|
||||||
|
end
|
||||||
|
|
||||||
|
-- default instance
|
||||||
|
local default = Timer.new()
|
||||||
|
|
||||||
|
-- module forwards calls to default instance
|
||||||
|
local module = {}
|
||||||
|
for k in pairs(Timer) do
|
||||||
|
if k ~= "__index" then
|
||||||
|
module[k] = function(...) return default[k](default, ...) end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
module.tween = setmetatable({}, {
|
||||||
|
__index = Timer.tween,
|
||||||
|
__newindex = function(k,v) Timer.tween[k] = v end,
|
||||||
|
__call = function(t, ...) return default:tween(...) end,
|
||||||
|
})
|
||||||
|
|
||||||
|
return setmetatable(module, {__call = Timer.new})
|
89
src/lib/log.lua
Normal file
89
src/lib/log.lua
Normal file
@ -0,0 +1,89 @@
|
|||||||
|
--
|
||||||
|
-- log.lua
|
||||||
|
--
|
||||||
|
-- Copyright (c) 2016 rxi
|
||||||
|
--
|
||||||
|
-- This library is free software; you can redistribute it and/or modify it
|
||||||
|
-- under the terms of the MIT license. See LICENSE for details.
|
||||||
|
--
|
||||||
|
|
||||||
|
local log = { _version = "0.1.0" }
|
||||||
|
|
||||||
|
log.usecolor = true
|
||||||
|
log.outfile = nil
|
||||||
|
log.level = "trace"
|
||||||
|
|
||||||
|
|
||||||
|
local modes = {
|
||||||
|
{ name = "trace", color = "\27[34m", },
|
||||||
|
{ name = "debug", color = "\27[36m", },
|
||||||
|
{ name = "info", color = "\27[32m", },
|
||||||
|
{ name = "warn", color = "\27[33m", },
|
||||||
|
{ name = "error", color = "\27[31m", },
|
||||||
|
{ name = "fatal", color = "\27[35m", },
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
local levels = {}
|
||||||
|
for i, v in ipairs(modes) do
|
||||||
|
levels[v.name] = i
|
||||||
|
end
|
||||||
|
|
||||||
|
|
||||||
|
local round = function(x, increment)
|
||||||
|
increment = increment or 1
|
||||||
|
x = x / increment
|
||||||
|
return (x > 0 and math.floor(x + .5) or math.ceil(x - .5)) * increment
|
||||||
|
end
|
||||||
|
|
||||||
|
|
||||||
|
local _tostring = tostring
|
||||||
|
|
||||||
|
local tostring = function(...)
|
||||||
|
local t = {}
|
||||||
|
for i = 1, select('#', ...) do
|
||||||
|
local x = select(i, ...)
|
||||||
|
if type(x) == "number" then
|
||||||
|
x = round(x, .01)
|
||||||
|
end
|
||||||
|
t[#t + 1] = _tostring(x)
|
||||||
|
end
|
||||||
|
return table.concat(t, " ")
|
||||||
|
end
|
||||||
|
|
||||||
|
local dateFormat = "%H:%M:%S %d/%m/%Y"
|
||||||
|
|
||||||
|
for i, x in ipairs(modes) do
|
||||||
|
local nameupper = x.name:upper()
|
||||||
|
log[x.name] = function(...)
|
||||||
|
|
||||||
|
-- Return early if we're below the log level
|
||||||
|
if i < levels[log.level] then
|
||||||
|
return
|
||||||
|
end
|
||||||
|
|
||||||
|
local msg = tostring(...)
|
||||||
|
-- local info = debug.getinfo(2, "Sl")
|
||||||
|
-- local lineinfo = info.short_src .. ":" .. info.currentline
|
||||||
|
|
||||||
|
-- Output to console
|
||||||
|
print(string.format("%s[%-6s%s]%s %s",
|
||||||
|
log.usecolor and x.color or "",
|
||||||
|
nameupper,
|
||||||
|
os.date(dateFormat),
|
||||||
|
log.usecolor and "\27[0m" or "",
|
||||||
|
msg))
|
||||||
|
|
||||||
|
-- Output to log file
|
||||||
|
if log.outfile then
|
||||||
|
local fp = io.open(log.outfile, "a")
|
||||||
|
local str = string.format("[%-6s%s] %s\n", nameupper, os.date(dateFormat), msg)
|
||||||
|
fp:write(str)
|
||||||
|
fp:close()
|
||||||
|
end
|
||||||
|
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
|
||||||
|
return log
|
688
src/lib/lume.lua
Normal file
688
src/lib/lume.lua
Normal file
@ -0,0 +1,688 @@
|
|||||||
|
--
|
||||||
|
-- lume
|
||||||
|
--
|
||||||
|
-- Copyright (c) 2018 rxi
|
||||||
|
--
|
||||||
|
-- Permission is hereby granted, free of charge, to any person obtaining a copy of
|
||||||
|
-- this software and associated documentation files (the "Software"), to deal in
|
||||||
|
-- the Software without restriction, including without limitation the rights to
|
||||||
|
-- use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies
|
||||||
|
-- of the Software, and to permit persons to whom the Software is furnished to do
|
||||||
|
-- so, subject to the following conditions:
|
||||||
|
--
|
||||||
|
-- The above copyright notice and this permission notice shall be included in all
|
||||||
|
-- copies or substantial portions of the Software.
|
||||||
|
--
|
||||||
|
-- THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||||
|
-- IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||||
|
-- FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||||
|
-- AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||||
|
-- LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||||
|
-- OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||||
|
-- SOFTWARE.
|
||||||
|
--
|
||||||
|
local lume = {_version = "2.3.0"}
|
||||||
|
|
||||||
|
local pairs, ipairs = pairs, ipairs
|
||||||
|
local type, assert, unpack = type, assert, unpack or table.unpack
|
||||||
|
local tostring, tonumber = tostring, tonumber
|
||||||
|
local math_floor = math.floor
|
||||||
|
local math_ceil = math.ceil
|
||||||
|
local math_atan2 = math.atan2 or math.atan
|
||||||
|
local math_sqrt = math.sqrt
|
||||||
|
local math_abs = math.abs
|
||||||
|
|
||||||
|
local noop = function()
|
||||||
|
end
|
||||||
|
|
||||||
|
local identity = function(x)
|
||||||
|
return x
|
||||||
|
end
|
||||||
|
|
||||||
|
local patternescape = function(str)
|
||||||
|
return str:gsub("[%(%)%.%%%+%-%*%?%[%]%^%$]", "%%%1")
|
||||||
|
end
|
||||||
|
|
||||||
|
local absindex = function(len, i)
|
||||||
|
return i < 0 and (len + i + 1) or i
|
||||||
|
end
|
||||||
|
|
||||||
|
local iscallable = function(x)
|
||||||
|
if type(x) == "function" then return true end
|
||||||
|
local mt = getmetatable(x)
|
||||||
|
return mt and mt.__call ~= nil
|
||||||
|
end
|
||||||
|
|
||||||
|
local getiter = function(x)
|
||||||
|
if lume.isarray(x) then
|
||||||
|
return ipairs
|
||||||
|
elseif type(x) == "table" then
|
||||||
|
return pairs
|
||||||
|
end
|
||||||
|
error("expected table", 3)
|
||||||
|
end
|
||||||
|
|
||||||
|
local iteratee = function(x)
|
||||||
|
if x == nil then return identity end
|
||||||
|
if iscallable(x) then return x end
|
||||||
|
if type(x) == "table" then
|
||||||
|
return function(z)
|
||||||
|
for k, v in pairs(x) do if z[k] ~= v then return false end end
|
||||||
|
return true
|
||||||
|
end
|
||||||
|
end
|
||||||
|
return function(z)
|
||||||
|
return z[x]
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
function lume.clamp(x, min, max)
|
||||||
|
return x < min and min or (x > max and max or x)
|
||||||
|
end
|
||||||
|
|
||||||
|
function lume.round(x, increment)
|
||||||
|
if increment then return lume.round(x / increment) * increment end
|
||||||
|
return x >= 0 and math_floor(x + .5) or math_ceil(x - .5)
|
||||||
|
end
|
||||||
|
|
||||||
|
function lume.sign(x)
|
||||||
|
return x < 0 and -1 or 1
|
||||||
|
end
|
||||||
|
|
||||||
|
function lume.lerp(a, b, amount)
|
||||||
|
return a + (b - a) * lume.clamp(amount, 0, 1)
|
||||||
|
end
|
||||||
|
|
||||||
|
function lume.smooth(a, b, amount)
|
||||||
|
local t = lume.clamp(amount, 0, 1)
|
||||||
|
local m = t * t * (3 - 2 * t)
|
||||||
|
return a + (b - a) * m
|
||||||
|
end
|
||||||
|
|
||||||
|
function lume.pingpong(x)
|
||||||
|
return 1 - math_abs(1 - x % 2)
|
||||||
|
end
|
||||||
|
|
||||||
|
function lume.distance(x1, y1, x2, y2, squared)
|
||||||
|
local dx = x1 - x2
|
||||||
|
local dy = y1 - y2
|
||||||
|
local s = dx * dx + dy * dy
|
||||||
|
return squared and s or math_sqrt(s)
|
||||||
|
end
|
||||||
|
|
||||||
|
function lume.angle(x1, y1, x2, y2)
|
||||||
|
return math_atan2(y2 - y1, x2 - x1)
|
||||||
|
end
|
||||||
|
|
||||||
|
function lume.vector(angle, magnitude)
|
||||||
|
return math.cos(angle) * magnitude, math.sin(angle) * magnitude
|
||||||
|
end
|
||||||
|
|
||||||
|
function lume.random(a, b)
|
||||||
|
if not a then a, b = 0, 1 end
|
||||||
|
if not b then b = 0 end
|
||||||
|
return a + math.random() * (b - a)
|
||||||
|
end
|
||||||
|
|
||||||
|
function lume.randomchoice(t)
|
||||||
|
return t[math.random(#t)]
|
||||||
|
end
|
||||||
|
|
||||||
|
function lume.weightedchoice(t)
|
||||||
|
local sum = 0
|
||||||
|
for _, v in pairs(t) do
|
||||||
|
assert(v >= 0, "weight value less than zero")
|
||||||
|
sum = sum + v
|
||||||
|
end
|
||||||
|
assert(sum ~= 0, "all weights are zero")
|
||||||
|
local rnd = lume.random(sum)
|
||||||
|
for k, v in pairs(t) do
|
||||||
|
if rnd < v then return k end
|
||||||
|
rnd = rnd - v
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
function lume.isarray(x)
|
||||||
|
return type(x) == "table" and x[1] ~= nil
|
||||||
|
end
|
||||||
|
|
||||||
|
function lume.push(t, ...)
|
||||||
|
local n = select("#", ...)
|
||||||
|
for i = 1, n do t[#t + 1] = select(i, ...) end
|
||||||
|
return ...
|
||||||
|
end
|
||||||
|
|
||||||
|
function lume.remove(t, x)
|
||||||
|
local iter = getiter(t)
|
||||||
|
for i, v in iter(t) do
|
||||||
|
if v == x then
|
||||||
|
if lume.isarray(t) then
|
||||||
|
table.remove(t, i)
|
||||||
|
break
|
||||||
|
else
|
||||||
|
t[i] = nil
|
||||||
|
break
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
return x
|
||||||
|
end
|
||||||
|
|
||||||
|
function lume.clear(t)
|
||||||
|
local iter = getiter(t)
|
||||||
|
for k in iter(t) do t[k] = nil end
|
||||||
|
return t
|
||||||
|
end
|
||||||
|
|
||||||
|
function lume.extend(t, ...)
|
||||||
|
for i = 1, select("#", ...) do
|
||||||
|
local x = select(i, ...)
|
||||||
|
if x then for k, v in pairs(x) do t[k] = v end end
|
||||||
|
end
|
||||||
|
return t
|
||||||
|
end
|
||||||
|
|
||||||
|
function lume.shuffle(t)
|
||||||
|
local rtn = {}
|
||||||
|
for i = 1, #t do
|
||||||
|
local r = math.random(i)
|
||||||
|
if r ~= i then rtn[i] = rtn[r] end
|
||||||
|
rtn[r] = t[i]
|
||||||
|
end
|
||||||
|
return rtn
|
||||||
|
end
|
||||||
|
|
||||||
|
function lume.sort(t, comp)
|
||||||
|
local rtn = lume.clone(t)
|
||||||
|
if comp then
|
||||||
|
if type(comp) == "string" then
|
||||||
|
table.sort(rtn, function(a, b)
|
||||||
|
return a[comp] < b[comp]
|
||||||
|
end)
|
||||||
|
else
|
||||||
|
table.sort(rtn, comp)
|
||||||
|
end
|
||||||
|
else
|
||||||
|
table.sort(rtn)
|
||||||
|
end
|
||||||
|
return rtn
|
||||||
|
end
|
||||||
|
|
||||||
|
function lume.array(...)
|
||||||
|
local t = {}
|
||||||
|
for x in ... do t[#t + 1] = x end
|
||||||
|
return t
|
||||||
|
end
|
||||||
|
|
||||||
|
function lume.each(t, fn, ...)
|
||||||
|
local iter = getiter(t)
|
||||||
|
if type(fn) == "string" then
|
||||||
|
for _, v in iter(t) do v[fn](v, ...) end
|
||||||
|
else
|
||||||
|
for _, v in iter(t) do fn(v, ...) end
|
||||||
|
end
|
||||||
|
return t
|
||||||
|
end
|
||||||
|
|
||||||
|
function lume.map(t, fn)
|
||||||
|
fn = iteratee(fn)
|
||||||
|
local iter = getiter(t)
|
||||||
|
local rtn = {}
|
||||||
|
for k, v in iter(t) do rtn[k] = fn(v) end
|
||||||
|
return rtn
|
||||||
|
end
|
||||||
|
|
||||||
|
function lume.all(t, fn)
|
||||||
|
fn = iteratee(fn)
|
||||||
|
local iter = getiter(t)
|
||||||
|
for _, v in iter(t) do if not fn(v) then return false end end
|
||||||
|
return true
|
||||||
|
end
|
||||||
|
|
||||||
|
function lume.any(t, fn)
|
||||||
|
fn = iteratee(fn)
|
||||||
|
local iter = getiter(t)
|
||||||
|
for _, v in iter(t) do if fn(v) then return true end end
|
||||||
|
return false
|
||||||
|
end
|
||||||
|
|
||||||
|
function lume.reduce(t, fn, first)
|
||||||
|
local acc = first
|
||||||
|
local started = first and true or false
|
||||||
|
local iter = getiter(t)
|
||||||
|
for _, v in iter(t) do
|
||||||
|
if started then
|
||||||
|
acc = fn(acc, v)
|
||||||
|
else
|
||||||
|
acc = v
|
||||||
|
started = true
|
||||||
|
end
|
||||||
|
end
|
||||||
|
assert(started, "reduce of an empty table with no first value")
|
||||||
|
return acc
|
||||||
|
end
|
||||||
|
|
||||||
|
function lume.unique(t)
|
||||||
|
local rtn = {}
|
||||||
|
for k in pairs(lume.invert(t)) do rtn[#rtn + 1] = k end
|
||||||
|
return rtn
|
||||||
|
end
|
||||||
|
|
||||||
|
function lume.filter(t, fn, retainkeys)
|
||||||
|
fn = iteratee(fn)
|
||||||
|
local iter = getiter(t)
|
||||||
|
local rtn = {}
|
||||||
|
if retainkeys then
|
||||||
|
for k, v in iter(t) do if fn(v) then rtn[k] = v end end
|
||||||
|
else
|
||||||
|
for _, v in iter(t) do if fn(v) then rtn[#rtn + 1] = v end end
|
||||||
|
end
|
||||||
|
return rtn
|
||||||
|
end
|
||||||
|
|
||||||
|
function lume.reject(t, fn, retainkeys)
|
||||||
|
fn = iteratee(fn)
|
||||||
|
local iter = getiter(t)
|
||||||
|
local rtn = {}
|
||||||
|
if retainkeys then
|
||||||
|
for k, v in iter(t) do if not fn(v) then rtn[k] = v end end
|
||||||
|
else
|
||||||
|
for _, v in iter(t) do if not fn(v) then rtn[#rtn + 1] = v end end
|
||||||
|
end
|
||||||
|
return rtn
|
||||||
|
end
|
||||||
|
|
||||||
|
function lume.merge(...)
|
||||||
|
local rtn = {}
|
||||||
|
for i = 1, select("#", ...) do
|
||||||
|
local t = select(i, ...)
|
||||||
|
local iter = getiter(t)
|
||||||
|
for k, v in iter(t) do rtn[k] = v end
|
||||||
|
end
|
||||||
|
return rtn
|
||||||
|
end
|
||||||
|
|
||||||
|
function lume.concat(...)
|
||||||
|
local rtn = {}
|
||||||
|
for i = 1, select("#", ...) do
|
||||||
|
local t = select(i, ...)
|
||||||
|
if t ~= nil then
|
||||||
|
local iter = getiter(t)
|
||||||
|
for _, v in iter(t) do rtn[#rtn + 1] = v end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
return rtn
|
||||||
|
end
|
||||||
|
|
||||||
|
function lume.find(t, value)
|
||||||
|
local iter = getiter(t)
|
||||||
|
for k, v in iter(t) do if v == value then return k end end
|
||||||
|
return nil
|
||||||
|
end
|
||||||
|
|
||||||
|
function lume.match(t, fn)
|
||||||
|
fn = iteratee(fn)
|
||||||
|
local iter = getiter(t)
|
||||||
|
for k, v in iter(t) do if fn(v) then return v, k end end
|
||||||
|
return nil
|
||||||
|
end
|
||||||
|
|
||||||
|
function lume.count(t, fn)
|
||||||
|
local count = 0
|
||||||
|
local iter = getiter(t)
|
||||||
|
if fn then
|
||||||
|
fn = iteratee(fn)
|
||||||
|
for _, v in iter(t) do if fn(v) then count = count + 1 end end
|
||||||
|
else
|
||||||
|
if lume.isarray(t) then return #t end
|
||||||
|
for _ in iter(t) do count = count + 1 end
|
||||||
|
end
|
||||||
|
return count
|
||||||
|
end
|
||||||
|
|
||||||
|
function lume.slice(t, i, j)
|
||||||
|
i = i and absindex(#t, i) or 1
|
||||||
|
j = j and absindex(#t, j) or #t
|
||||||
|
local rtn = {}
|
||||||
|
for x = i < 1 and 1 or i, j > #t and #t or j do rtn[#rtn + 1] = t[x] end
|
||||||
|
return rtn
|
||||||
|
end
|
||||||
|
|
||||||
|
function lume.first(t, n)
|
||||||
|
if not n then return t[1] end
|
||||||
|
return lume.slice(t, 1, n)
|
||||||
|
end
|
||||||
|
|
||||||
|
function lume.last(t, n)
|
||||||
|
if not n then return t[#t] end
|
||||||
|
return lume.slice(t, -n, -1)
|
||||||
|
end
|
||||||
|
|
||||||
|
function lume.invert(t)
|
||||||
|
local rtn = {}
|
||||||
|
for k, v in pairs(t) do rtn[v] = k end
|
||||||
|
return rtn
|
||||||
|
end
|
||||||
|
|
||||||
|
function lume.pick(t, ...)
|
||||||
|
local rtn = {}
|
||||||
|
for i = 1, select("#", ...) do
|
||||||
|
local k = select(i, ...)
|
||||||
|
rtn[k] = t[k]
|
||||||
|
end
|
||||||
|
return rtn
|
||||||
|
end
|
||||||
|
|
||||||
|
function lume.keys(t)
|
||||||
|
local rtn = {}
|
||||||
|
local iter = getiter(t)
|
||||||
|
for k in iter(t) do rtn[#rtn + 1] = k end
|
||||||
|
return rtn
|
||||||
|
end
|
||||||
|
|
||||||
|
function lume.clone(t)
|
||||||
|
local rtn = {}
|
||||||
|
for k, v in pairs(t) do rtn[k] = v end
|
||||||
|
return rtn
|
||||||
|
end
|
||||||
|
|
||||||
|
function lume.fn(fn, ...)
|
||||||
|
assert(iscallable(fn), "expected a function as the first argument")
|
||||||
|
local args = {...}
|
||||||
|
return function(...)
|
||||||
|
local a = lume.concat(args, {...})
|
||||||
|
return fn(unpack(a))
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
function lume.once(fn, ...)
|
||||||
|
local f = lume.fn(fn, ...)
|
||||||
|
local done = false
|
||||||
|
return function(...)
|
||||||
|
if done then return end
|
||||||
|
done = true
|
||||||
|
return f(...)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
local memoize_fnkey = {}
|
||||||
|
local memoize_nil = {}
|
||||||
|
|
||||||
|
function lume.memoize(fn)
|
||||||
|
local cache = {}
|
||||||
|
return function(...)
|
||||||
|
local c = cache
|
||||||
|
for i = 1, select("#", ...) do
|
||||||
|
local a = select(i, ...) or memoize_nil
|
||||||
|
c[a] = c[a] or {}
|
||||||
|
c = c[a]
|
||||||
|
end
|
||||||
|
c[memoize_fnkey] = c[memoize_fnkey] or {fn(...)}
|
||||||
|
return unpack(c[memoize_fnkey])
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
function lume.combine(...)
|
||||||
|
local n = select("#", ...)
|
||||||
|
if n == 0 then return noop end
|
||||||
|
if n == 1 then
|
||||||
|
local fn = select(1, ...)
|
||||||
|
if not fn then return noop end
|
||||||
|
assert(iscallable(fn), "expected a function or nil")
|
||||||
|
return fn
|
||||||
|
end
|
||||||
|
local funcs = {}
|
||||||
|
for i = 1, n do
|
||||||
|
local fn = select(i, ...)
|
||||||
|
if fn ~= nil then
|
||||||
|
assert(iscallable(fn), "expected a function or nil")
|
||||||
|
funcs[#funcs + 1] = fn
|
||||||
|
end
|
||||||
|
end
|
||||||
|
return function(...)
|
||||||
|
for _, f in ipairs(funcs) do f(...) end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
function lume.call(fn, ...)
|
||||||
|
if fn then return fn(...) end
|
||||||
|
end
|
||||||
|
|
||||||
|
function lume.time(fn, ...)
|
||||||
|
local start = os.clock()
|
||||||
|
local rtn = {fn(...)}
|
||||||
|
return (os.clock() - start), unpack(rtn)
|
||||||
|
end
|
||||||
|
|
||||||
|
local lambda_cache = {}
|
||||||
|
|
||||||
|
function lume.lambda(str)
|
||||||
|
if not lambda_cache[str] then
|
||||||
|
local args, body = str:match([[^([%w,_ ]-)%->(.-)$]])
|
||||||
|
assert(args and body, "bad string lambda")
|
||||||
|
local s = "return function(" .. args .. ")\nreturn " .. body .. "\nend"
|
||||||
|
lambda_cache[str] = lume.dostring(s)
|
||||||
|
end
|
||||||
|
return lambda_cache[str]
|
||||||
|
end
|
||||||
|
|
||||||
|
local serialize
|
||||||
|
|
||||||
|
local serialize_map = {
|
||||||
|
["boolean"] = tostring,
|
||||||
|
["nil"] = tostring,
|
||||||
|
["string"] = function(v)
|
||||||
|
return string.format("%q", v)
|
||||||
|
end,
|
||||||
|
["number"] = function(v)
|
||||||
|
if v ~= v then
|
||||||
|
return "0/0" -- nan
|
||||||
|
elseif v == 1 / 0 then
|
||||||
|
return "1/0" -- inf
|
||||||
|
elseif v == -1 / 0 then
|
||||||
|
return "-1/0"
|
||||||
|
end -- -inf
|
||||||
|
return tostring(v)
|
||||||
|
end,
|
||||||
|
["table"] = function(t, stk)
|
||||||
|
stk = stk or {}
|
||||||
|
if stk[t] then error("circular reference") end
|
||||||
|
local rtn = {}
|
||||||
|
stk[t] = true
|
||||||
|
for k, v in pairs(t) do rtn[#rtn + 1] = "[" .. serialize(k, stk) .. "]=" .. serialize(v, stk) end
|
||||||
|
stk[t] = nil
|
||||||
|
return "{" .. table.concat(rtn, ",") .. "}"
|
||||||
|
end
|
||||||
|
}
|
||||||
|
|
||||||
|
setmetatable(serialize_map, {
|
||||||
|
__index = function(_, k)
|
||||||
|
error("unsupported serialize type: " .. k)
|
||||||
|
end
|
||||||
|
})
|
||||||
|
|
||||||
|
serialize = function(x, stk)
|
||||||
|
return serialize_map[type(x)](x, stk)
|
||||||
|
end
|
||||||
|
|
||||||
|
function lume.serialize(x)
|
||||||
|
return serialize(x)
|
||||||
|
end
|
||||||
|
|
||||||
|
function lume.deserialize(str)
|
||||||
|
return lume.dostring("return " .. str)
|
||||||
|
end
|
||||||
|
|
||||||
|
function lume.split(str, sep)
|
||||||
|
if not sep then
|
||||||
|
return lume.array(str:gmatch("([%S]+)"))
|
||||||
|
else
|
||||||
|
assert(sep ~= "", "empty separator")
|
||||||
|
local psep = patternescape(sep)
|
||||||
|
return lume.array((str .. sep):gmatch("(.-)(" .. psep .. ")"))
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
function lume.trim(str, chars)
|
||||||
|
if not chars then return str:match("^[%s]*(.-)[%s]*$") end
|
||||||
|
chars = patternescape(chars)
|
||||||
|
return str:match("^[" .. chars .. "]*(.-)[" .. chars .. "]*$")
|
||||||
|
end
|
||||||
|
|
||||||
|
function lume.wordwrap(str, limit)
|
||||||
|
limit = limit or 72
|
||||||
|
local check
|
||||||
|
if type(limit) == "number" then
|
||||||
|
check = function(s)
|
||||||
|
return #s >= limit
|
||||||
|
end
|
||||||
|
else
|
||||||
|
check = limit
|
||||||
|
end
|
||||||
|
local rtn = {}
|
||||||
|
local line = ""
|
||||||
|
for word, spaces in str:gmatch("(%S+)(%s*)") do
|
||||||
|
local s = line .. word
|
||||||
|
if check(s) then
|
||||||
|
table.insert(rtn, line .. "\n")
|
||||||
|
line = word
|
||||||
|
else
|
||||||
|
line = s
|
||||||
|
end
|
||||||
|
for c in spaces:gmatch(".") do
|
||||||
|
if c == "\n" then
|
||||||
|
table.insert(rtn, line .. "\n")
|
||||||
|
line = ""
|
||||||
|
else
|
||||||
|
line = line .. c
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
table.insert(rtn, line)
|
||||||
|
return table.concat(rtn)
|
||||||
|
end
|
||||||
|
|
||||||
|
function lume.format(str, vars)
|
||||||
|
if not vars then return str end
|
||||||
|
local f = function(x)
|
||||||
|
return tostring(vars[x] or vars[tonumber(x)] or "{" .. x .. "}")
|
||||||
|
end
|
||||||
|
return (str:gsub("{(.-)}", f))
|
||||||
|
end
|
||||||
|
|
||||||
|
function lume.trace(...)
|
||||||
|
local info = debug.getinfo(2, "Sl")
|
||||||
|
local t = {info.short_src .. ":" .. info.currentline .. ":"}
|
||||||
|
for i = 1, select("#", ...) do
|
||||||
|
local x = select(i, ...)
|
||||||
|
if type(x) == "number" then x = string.format("%g", lume.round(x, .01)) end
|
||||||
|
t[#t + 1] = tostring(x)
|
||||||
|
end
|
||||||
|
print(table.concat(t, " "))
|
||||||
|
end
|
||||||
|
|
||||||
|
function lume.dostring(str)
|
||||||
|
return assert((loadstring or load)(str))()
|
||||||
|
end
|
||||||
|
|
||||||
|
function lume.uuid()
|
||||||
|
local fn = function(x)
|
||||||
|
local r = math.random(16) - 1
|
||||||
|
r = (x == "x") and (r + 1) or (r % 4) + 9
|
||||||
|
return ("0123456789abcdef"):sub(r, r)
|
||||||
|
end
|
||||||
|
return (("xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx"):gsub("[xy]", fn))
|
||||||
|
end
|
||||||
|
|
||||||
|
function lume.hotswap(modname)
|
||||||
|
local oldglobal = lume.clone(_G)
|
||||||
|
local updated = {}
|
||||||
|
local function update(old, new)
|
||||||
|
if updated[old] then return end
|
||||||
|
updated[old] = true
|
||||||
|
local oldmt, newmt = getmetatable(old), getmetatable(new)
|
||||||
|
if oldmt and newmt then update(oldmt, newmt) end
|
||||||
|
for k, v in pairs(new) do
|
||||||
|
if type(v) == "table" then
|
||||||
|
update(old[k], v)
|
||||||
|
else
|
||||||
|
old[k] = v
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
local err = nil
|
||||||
|
local function onerror(e)
|
||||||
|
for k in pairs(_G) do _G[k] = oldglobal[k] end
|
||||||
|
err = lume.trim(e)
|
||||||
|
end
|
||||||
|
local ok, oldmod = pcall(require, modname)
|
||||||
|
oldmod = ok and oldmod or nil
|
||||||
|
xpcall(function()
|
||||||
|
package.loaded[modname] = nil
|
||||||
|
local newmod = require(modname)
|
||||||
|
if type(oldmod) == "table" then update(oldmod, newmod) end
|
||||||
|
for k, v in pairs(oldglobal) do
|
||||||
|
if v ~= _G[k] and type(v) == "table" then
|
||||||
|
update(v, _G[k])
|
||||||
|
_G[k] = v
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end, onerror)
|
||||||
|
package.loaded[modname] = oldmod
|
||||||
|
if err then return nil, err end
|
||||||
|
return oldmod
|
||||||
|
end
|
||||||
|
|
||||||
|
local ripairs_iter = function(t, i)
|
||||||
|
i = i - 1
|
||||||
|
local v = t[i]
|
||||||
|
if v ~= nil then return i, v end
|
||||||
|
end
|
||||||
|
|
||||||
|
function lume.ripairs(t)
|
||||||
|
return ripairs_iter, t, (#t + 1)
|
||||||
|
end
|
||||||
|
|
||||||
|
function lume.color(str, mul)
|
||||||
|
mul = mul or 1
|
||||||
|
local r, g, b, a
|
||||||
|
r, g, b = str:match("#(%x%x)(%x%x)(%x%x)")
|
||||||
|
if r then
|
||||||
|
r = tonumber(r, 16) / 0xff
|
||||||
|
g = tonumber(g, 16) / 0xff
|
||||||
|
b = tonumber(b, 16) / 0xff
|
||||||
|
a = 1
|
||||||
|
elseif str:match("rgba?%s*%([%d%s%.,]+%)") then
|
||||||
|
local f = str:gmatch("[%d.]+")
|
||||||
|
r = (f() or 0) / 0xff
|
||||||
|
g = (f() or 0) / 0xff
|
||||||
|
b = (f() or 0) / 0xff
|
||||||
|
a = f() or 1
|
||||||
|
else
|
||||||
|
error(("bad color string '%s'"):format(str))
|
||||||
|
end
|
||||||
|
return r * mul, g * mul, b * mul, a * mul
|
||||||
|
end
|
||||||
|
|
||||||
|
local chain_mt = {}
|
||||||
|
chain_mt.__index = lume.map(lume.filter(lume, iscallable, true), function(fn)
|
||||||
|
return function(self, ...)
|
||||||
|
self._value = fn(self._value, ...)
|
||||||
|
return self
|
||||||
|
end
|
||||||
|
end)
|
||||||
|
chain_mt.__index.result = function(x)
|
||||||
|
return x._value
|
||||||
|
end
|
||||||
|
|
||||||
|
function lume.chain(value)
|
||||||
|
return setmetatable({_value = value}, chain_mt)
|
||||||
|
end
|
||||||
|
|
||||||
|
setmetatable(lume, {
|
||||||
|
__call = function(_, ...)
|
||||||
|
return lume.chain(...)
|
||||||
|
end
|
||||||
|
})
|
||||||
|
|
||||||
|
return lume
|
546
src/lib/nata.lua
Normal file
546
src/lib/nata.lua
Normal file
@ -0,0 +1,546 @@
|
|||||||
|
--- Entity management for Lua.
|
||||||
|
-- @module nata
|
||||||
|
|
||||||
|
local nata = {
|
||||||
|
_VERSION = 'Nata v0.3.3',
|
||||||
|
_DESCRIPTION = 'Entity management for Lua.',
|
||||||
|
_URL = 'https://github.com/tesselode/nata',
|
||||||
|
_LICENSE = [[
|
||||||
|
MIT License
|
||||||
|
|
||||||
|
Copyright (c) 2020 Andrew Minnich
|
||||||
|
|
||||||
|
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||||
|
of this software and associated documentation files (the "Software"), to deal
|
||||||
|
in the Software without restriction, including without limitation the rights
|
||||||
|
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||||
|
copies of the Software, and to permit persons to whom the Software is
|
||||||
|
furnished to do so, subject to the following conditions:
|
||||||
|
|
||||||
|
The above copyright notice and this permission notice shall be included in all
|
||||||
|
copies or substantial portions of the Software.
|
||||||
|
|
||||||
|
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||||
|
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||||
|
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||||
|
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||||
|
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||||
|
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||||
|
SOFTWARE.
|
||||||
|
]]
|
||||||
|
}
|
||||||
|
|
||||||
|
-- gets the error level needed to make an error appear
|
||||||
|
-- in the user's code, not the library code
|
||||||
|
local function getUserErrorLevel()
|
||||||
|
local source = debug.getinfo(1).source
|
||||||
|
local level = 1
|
||||||
|
while debug.getinfo(level).source == source do
|
||||||
|
level = level + 1
|
||||||
|
end
|
||||||
|
--[[
|
||||||
|
we return level - 1 here and not just level
|
||||||
|
because the level was calculated one function
|
||||||
|
deeper than the function that will actually
|
||||||
|
use this value. if we produced an error *inside*
|
||||||
|
this function, level would be correct, but
|
||||||
|
for the function calling this function, level - 1
|
||||||
|
is correct.
|
||||||
|
]]
|
||||||
|
return level - 1
|
||||||
|
end
|
||||||
|
|
||||||
|
-- gets the name of the function that the user called
|
||||||
|
-- that eventually caused an error
|
||||||
|
local function getUserCalledFunctionName()
|
||||||
|
return debug.getinfo(getUserErrorLevel() - 1).name
|
||||||
|
end
|
||||||
|
|
||||||
|
local function checkCondition(condition, message)
|
||||||
|
if condition then return end
|
||||||
|
error(message, getUserErrorLevel())
|
||||||
|
end
|
||||||
|
|
||||||
|
-- changes a list of types into a human-readable phrase
|
||||||
|
-- i.e. string, table, number -> "string, table, or number"
|
||||||
|
local function getAllowedTypesText(...)
|
||||||
|
local numberOfArguments = select('#', ...)
|
||||||
|
if numberOfArguments >= 3 then
|
||||||
|
local text = ''
|
||||||
|
for i = 1, numberOfArguments - 1 do
|
||||||
|
text = text .. string.format('%s, ', select(i, ...))
|
||||||
|
end
|
||||||
|
text = text .. string.format('or %s', select(numberOfArguments, ...))
|
||||||
|
return text
|
||||||
|
elseif numberOfArguments == 2 then
|
||||||
|
return string.format('%s or %s', select(1, ...), select(2, ...))
|
||||||
|
end
|
||||||
|
return select(1, ...)
|
||||||
|
end
|
||||||
|
|
||||||
|
-- checks if an argument is of the correct type, and if not,
|
||||||
|
-- throws a "bad argument" error consistent with the ones
|
||||||
|
-- lua and love produce
|
||||||
|
local function checkArgument(argumentIndex, argument, ...)
|
||||||
|
for i = 1, select('#', ...) do
|
||||||
|
-- allow tables with the __call metamethod to be treated like functions
|
||||||
|
if select(i, ...) == 'function' then
|
||||||
|
if type(argument) == 'table' and getmetatable(argument).__call then
|
||||||
|
return
|
||||||
|
end
|
||||||
|
end
|
||||||
|
if type(argument) == select(i, ...) then return end
|
||||||
|
end
|
||||||
|
error(
|
||||||
|
string.format(
|
||||||
|
"bad argument #%i to '%s' (expected %s, got %s)",
|
||||||
|
argumentIndex,
|
||||||
|
getUserCalledFunctionName(),
|
||||||
|
getAllowedTypesText(...),
|
||||||
|
type(argument)
|
||||||
|
),
|
||||||
|
getUserErrorLevel()
|
||||||
|
)
|
||||||
|
end
|
||||||
|
|
||||||
|
local function checkOptionalArgument(argumentIndex, argument, ...)
|
||||||
|
if argument == nil then return end
|
||||||
|
checkArgument(argumentIndex, argument, ...)
|
||||||
|
end
|
||||||
|
|
||||||
|
local function removeByValue(t, v)
|
||||||
|
for i = #t, 1, -1 do
|
||||||
|
if t[i] == v then table.remove(t, i) end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
local function entityHasKeys(entity, keys)
|
||||||
|
for _, key in ipairs(keys) do
|
||||||
|
if not entity[key] then return false end
|
||||||
|
end
|
||||||
|
return true
|
||||||
|
end
|
||||||
|
|
||||||
|
local function filterEntity(entity, filter)
|
||||||
|
if type(filter) == 'table' then
|
||||||
|
return entityHasKeys(entity, filter)
|
||||||
|
elseif type(filter) == 'function' then
|
||||||
|
return filter(entity)
|
||||||
|
end
|
||||||
|
return true
|
||||||
|
end
|
||||||
|
|
||||||
|
--- Defines the behaviors of a system.
|
||||||
|
--
|
||||||
|
-- There's no constructor for SystemDefinitions. Rather, you simply
|
||||||
|
-- define a table with functions that correspond to events. These
|
||||||
|
-- events can be named anything you like. Below are the built-in events
|
||||||
|
-- that the pool will automatically call.
|
||||||
|
-- @type SystemDefinition
|
||||||
|
|
||||||
|
--- Called when the pool is first created.
|
||||||
|
-- @function SystemDefinition:init
|
||||||
|
-- @param ... additional arguments that were passed to `nata.new`.
|
||||||
|
|
||||||
|
--- Called when an entity is added to the pool.
|
||||||
|
-- @function SystemDefinition:add
|
||||||
|
-- @tparam table e the entity that was added
|
||||||
|
|
||||||
|
--- Called when an entity is removed from the pool.
|
||||||
|
-- @function SystemDefinition:remove
|
||||||
|
-- @tparam table e the entity that was removed
|
||||||
|
|
||||||
|
--- Called when an entity is added to a group.
|
||||||
|
-- @function SystemDefinition:addToGroup
|
||||||
|
-- @string groupName the name of the group that the entity was added to
|
||||||
|
-- @tparam table e the entity that was added
|
||||||
|
|
||||||
|
--- Called when an entity is removed from a group.
|
||||||
|
-- @function SystemDefinition:removeFromGroup
|
||||||
|
-- @string groupName the name of the group that the entity was removed from
|
||||||
|
-- @tparam table e the entity that was removed
|
||||||
|
|
||||||
|
--- Responds to events in a pool.
|
||||||
|
--
|
||||||
|
-- Systems are not created directly. They're created by the @{Pool}
|
||||||
|
-- according to the @{SystemDefinition}s passed to `nata.new`.
|
||||||
|
-- Each system instance inherits all of the functions of its
|
||||||
|
-- @{SystemDefinition}.
|
||||||
|
-- @type System
|
||||||
|
|
||||||
|
--- The @{Pool} that this system is running on.
|
||||||
|
-- @tfield Pool pool
|
||||||
|
|
||||||
|
--- Manages a subset of entities.
|
||||||
|
-- @type Group
|
||||||
|
|
||||||
|
--- The filter that defines which entities are added to this group.
|
||||||
|
-- Can be either:
|
||||||
|
--
|
||||||
|
-- - A list of required keys
|
||||||
|
-- - A function that takes the entity as the first argument
|
||||||
|
-- and returns true if the entity should be added to the group
|
||||||
|
-- @tfield[opt] table|function filter
|
||||||
|
|
||||||
|
--- A function that specifies how the entities in this group should be sorted.
|
||||||
|
-- Has the same requirements as the function argument to Lua's built-in `table.sort`.
|
||||||
|
-- @tfield[opt] function sort
|
||||||
|
|
||||||
|
--- A list of all the entities in the group.
|
||||||
|
-- @tfield table entities
|
||||||
|
|
||||||
|
--- A set of all the entities in the group.
|
||||||
|
-- @tfield table hasEntity
|
||||||
|
-- @usage
|
||||||
|
-- print(pool.groups.physical.hasEntity[e]) -- prints "true" if the entity is in the "physical" group, or "nil" if not
|
||||||
|
|
||||||
|
--- Manages entities in a game world.
|
||||||
|
-- @type Pool
|
||||||
|
local Pool = {}
|
||||||
|
Pool.__index = Pool
|
||||||
|
|
||||||
|
--- A list of all the entities in the pool.
|
||||||
|
-- @tfield table entities
|
||||||
|
|
||||||
|
--- A set of all the entities in the pool.
|
||||||
|
-- @tfield table hasEntity
|
||||||
|
-- @usage
|
||||||
|
-- print(pool.hasEntity[e]) -- prints "true" if the entity is in the pool, or "nil" if not
|
||||||
|
|
||||||
|
--- A dictionary of the @{Group}s in the pool.
|
||||||
|
-- @tfield table groups
|
||||||
|
|
||||||
|
--- A field containing any data you want.
|
||||||
|
-- @field data
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
function Pool:_validateOptions(options)
|
||||||
|
checkOptionalArgument(1, options, 'table')
|
||||||
|
if not options then return end
|
||||||
|
if options.groups then
|
||||||
|
checkCondition(type(options.groups) == 'table', "groups must be a table")
|
||||||
|
for groupName, groupOptions in pairs(options.groups) do
|
||||||
|
checkCondition(type(groupOptions) == 'table',
|
||||||
|
string.format("options for group '$s' must be a table", groupName))
|
||||||
|
local filter = groupOptions.filter
|
||||||
|
if filter ~= nil then
|
||||||
|
checkCondition(type(filter) == 'table' or type(filter) == 'function',
|
||||||
|
string.format("filter for group '%s' must be a table or function", groupName))
|
||||||
|
end
|
||||||
|
local sort = groupOptions.sort
|
||||||
|
if sort ~= nil then
|
||||||
|
checkCondition(type(sort) == 'function',
|
||||||
|
string.format("sort for group '%s' must be a function", groupName))
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
if options.systems then
|
||||||
|
checkCondition(type(options.systems) == 'table', "systems must be a table")
|
||||||
|
for _, system in ipairs(options.systems) do
|
||||||
|
checkCondition(type(system) == 'table', "all systems must be tables")
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
function Pool:_init(options, ...)
|
||||||
|
self:_validateOptions(options)
|
||||||
|
options = options or {}
|
||||||
|
-- entities that will be added to the pool on the next flush
|
||||||
|
self._queue = {}
|
||||||
|
-- a temporary table for entities that will be added to the pool
|
||||||
|
-- on the current flush (see Pool.flush for more details)
|
||||||
|
self._entitiesToFlush = {}
|
||||||
|
self.entities = {}
|
||||||
|
self.hasEntity = {}
|
||||||
|
self.groups = {}
|
||||||
|
self._systems = {}
|
||||||
|
self._events = {}
|
||||||
|
self.data = options.data or {}
|
||||||
|
local groups = options.groups or {}
|
||||||
|
local systems = options.systems or {nata.oop()}
|
||||||
|
for groupName, groupOptions in pairs(groups) do
|
||||||
|
self.groups[groupName] = {
|
||||||
|
filter = groupOptions.filter,
|
||||||
|
sort = groupOptions.sort,
|
||||||
|
entities = {},
|
||||||
|
hasEntity = {},
|
||||||
|
}
|
||||||
|
end
|
||||||
|
for _, systemDefinition in ipairs(systems) do
|
||||||
|
local system = setmetatable({
|
||||||
|
pool = self,
|
||||||
|
}, {__index = systemDefinition})
|
||||||
|
table.insert(self._systems, system)
|
||||||
|
end
|
||||||
|
self:emit('init', ...)
|
||||||
|
end
|
||||||
|
|
||||||
|
--- Queues an entity to be added to the pool.
|
||||||
|
-- @tparam table entity the entity to add
|
||||||
|
-- @treturn table the queued entity
|
||||||
|
function Pool:queue(entity)
|
||||||
|
table.insert(self._queue, entity)
|
||||||
|
return entity
|
||||||
|
end
|
||||||
|
|
||||||
|
--- Adds the queued entities to the pool. Entities are added
|
||||||
|
-- in the order they were queued.
|
||||||
|
function Pool:flush()
|
||||||
|
--[[
|
||||||
|
Move the currently queued entities to a temporary
|
||||||
|
table. This way, if an add/addToGroup/removeToGroup
|
||||||
|
event emission leads to another entity being queued,
|
||||||
|
it will be saved for the next flush, rather than
|
||||||
|
adding entities to the table we're in the middle
|
||||||
|
of iterating over, which would lead to an array with
|
||||||
|
holes and screw everything up.
|
||||||
|
]]
|
||||||
|
for i = 1, #self._queue do
|
||||||
|
local entity = self._queue[i]
|
||||||
|
self._entitiesToFlush[i] = entity
|
||||||
|
self._queue[i] = nil
|
||||||
|
end
|
||||||
|
for i = 1, #self._entitiesToFlush do
|
||||||
|
local entity = self._entitiesToFlush[i]
|
||||||
|
-- check if the entity belongs in each group and
|
||||||
|
-- add it to/remove it from the group as needed
|
||||||
|
for groupName, group in pairs(self.groups) do
|
||||||
|
if filterEntity(entity, group.filter) then
|
||||||
|
if not group.hasEntity[entity] then
|
||||||
|
table.insert(group.entities, entity)
|
||||||
|
group.hasEntity[entity] = true
|
||||||
|
self:emit('addToGroup', groupName, entity)
|
||||||
|
end
|
||||||
|
if group.sort then group._needsResort = true end
|
||||||
|
elseif group.hasEntity[entity] then
|
||||||
|
removeByValue(group.entities, entity)
|
||||||
|
group.hasEntity[entity] = nil
|
||||||
|
self:emit('removeFromGroup', groupName, entity)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
-- add the entity to the pool if it hasn't been added already
|
||||||
|
if not self.hasEntity[entity] then
|
||||||
|
table.insert(self.entities, entity)
|
||||||
|
self.hasEntity[entity] = true
|
||||||
|
self:emit('add', entity)
|
||||||
|
end
|
||||||
|
self._entitiesToFlush[i] = nil
|
||||||
|
end
|
||||||
|
-- re-sort groups
|
||||||
|
for _, group in pairs(self.groups) do
|
||||||
|
if group._needsResort then
|
||||||
|
table.sort(group.entities, group.sort)
|
||||||
|
group._needsResort = nil
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
--- Removes entities from the pool.
|
||||||
|
-- @tparam function f the condition upon which an entity should be
|
||||||
|
-- removed. The function should take an entity as the first argument
|
||||||
|
-- and return `true` if the entity should be removed.
|
||||||
|
function Pool:remove(f)
|
||||||
|
checkArgument(1, f, 'function')
|
||||||
|
for groupName, group in pairs(self.groups) do
|
||||||
|
for i = #group.entities, 1, -1 do
|
||||||
|
local entity = group.entities[i]
|
||||||
|
if f(entity) then
|
||||||
|
self:emit('removeFromGroup', groupName, entity)
|
||||||
|
table.remove(group.entities, i)
|
||||||
|
group.hasEntity[entity] = nil
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
for i = #self.entities, 1, -1 do
|
||||||
|
local entity = self.entities[i]
|
||||||
|
if f(entity) then
|
||||||
|
self:emit('remove', entity)
|
||||||
|
table.remove(self.entities, i)
|
||||||
|
self.hasEntity[entity] = nil
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
--- Registers a function to be called when an event is emitted.
|
||||||
|
-- @string event the event to listen for
|
||||||
|
-- @tparam function f the function to call
|
||||||
|
-- @treturn function the function that was registered
|
||||||
|
function Pool:on(event, f)
|
||||||
|
checkCondition(event ~= nil, "event cannot be nil")
|
||||||
|
checkArgument(2, f, 'function')
|
||||||
|
self._events[event] = self._events[event] or {}
|
||||||
|
table.insert(self._events[event], f)
|
||||||
|
return f
|
||||||
|
end
|
||||||
|
|
||||||
|
--- Unregisters a function from an event.
|
||||||
|
-- @string event the event to unregister the function from
|
||||||
|
-- @tparam function f the function to unregister
|
||||||
|
function Pool:off(event, f)
|
||||||
|
checkCondition(event ~= nil, "event cannot be nil")
|
||||||
|
checkArgument(2, f, 'function')
|
||||||
|
if self._events[event] then
|
||||||
|
removeByValue(self._events[event], f)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
--- Emits an event. The `system[event]` function will be called
|
||||||
|
-- for each system that has it, and functions registered
|
||||||
|
-- to the event will be called as well.
|
||||||
|
-- @string event the event to emit
|
||||||
|
-- @param ... additional arguments to pass to the functions that are called
|
||||||
|
function Pool:emit(event, ...)
|
||||||
|
checkCondition(event ~= nil, "event cannot be nil")
|
||||||
|
for _, system in ipairs(self._systems) do
|
||||||
|
if type(system[event]) == 'function' then
|
||||||
|
system[event](system, ...)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
if self._events[event] then
|
||||||
|
for _, f in ipairs(self._events[event]) do
|
||||||
|
f(...)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
--- Gets this pool's instance of a system.
|
||||||
|
-- @tparam SystemDefinition systemDefinition the system class to get the instance of
|
||||||
|
-- @treturn System the instance of the system running in this pool
|
||||||
|
function Pool:getSystem(systemDefinition)
|
||||||
|
checkArgument(1, systemDefinition, 'table')
|
||||||
|
for _, system in ipairs(self._systems) do
|
||||||
|
if getmetatable(system).__index == systemDefinition then
|
||||||
|
return system
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
---
|
||||||
|
-- @section end
|
||||||
|
|
||||||
|
local function validateOopOptions(options)
|
||||||
|
checkOptionalArgument(1, options, 'table')
|
||||||
|
if not options then return end
|
||||||
|
if options.include then
|
||||||
|
checkCondition(type(options.include) == 'table', "include must be a table")
|
||||||
|
end
|
||||||
|
if options.exclude then
|
||||||
|
checkCondition(type(options.exclude) == 'table', "exclude must be a table")
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
--- Defines the behavior of an OOP system.
|
||||||
|
-- @type OopOptions
|
||||||
|
-- @see nata.oop
|
||||||
|
|
||||||
|
--- A list of events to forward to entities. If not defined,
|
||||||
|
-- the system will forward all events to entities (except
|
||||||
|
-- for the ones in the exclude list).
|
||||||
|
-- @tfield table include
|
||||||
|
|
||||||
|
--- A list of events *not* to forward to entities.
|
||||||
|
-- @tfield table exclude
|
||||||
|
|
||||||
|
--- The name of the group of entities to forward events to.
|
||||||
|
-- If not defined, the system will forward events to all entities.
|
||||||
|
-- @tfield string group
|
||||||
|
|
||||||
|
---
|
||||||
|
-- @section end
|
||||||
|
|
||||||
|
--- Creates a new OOP system definition.
|
||||||
|
-- An OOP system, upon receiving an event, will call
|
||||||
|
-- the function of the same name on each entity it monitors
|
||||||
|
-- (if it exists). This facilitates a more traditional, OOP-style
|
||||||
|
-- entity management, where you loop over a table of entities and
|
||||||
|
-- call update and draw functions on them.
|
||||||
|
-- @tparam[opt] OopOptions options how to set up the OOP system
|
||||||
|
-- @treturn SystemDefinition the new OOP system definition
|
||||||
|
function nata.oop(options)
|
||||||
|
validateOopOptions(options)
|
||||||
|
local group = options and options.group
|
||||||
|
local include, exclude
|
||||||
|
if options and options.include then
|
||||||
|
include = {}
|
||||||
|
for _, event in ipairs(options.include) do
|
||||||
|
include[event] = true
|
||||||
|
end
|
||||||
|
end
|
||||||
|
if options and options.exclude then
|
||||||
|
exclude = {}
|
||||||
|
for _, event in ipairs(options.exclude) do
|
||||||
|
exclude[event] = true
|
||||||
|
end
|
||||||
|
end
|
||||||
|
return setmetatable({_cache = {}}, {
|
||||||
|
__index = function(t, event)
|
||||||
|
t._cache[event] = t._cache[event] or function(self, ...)
|
||||||
|
local shouldCallEvent = true
|
||||||
|
if include and not include[event] then shouldCallEvent = false end
|
||||||
|
if exclude and exclude[event] then shouldCallEvent = false end
|
||||||
|
if shouldCallEvent then
|
||||||
|
local entities
|
||||||
|
-- not using ternary here because if the group doesn't exist,
|
||||||
|
-- i'd rather it cause an error than just silently falling back
|
||||||
|
-- to the main entity pool
|
||||||
|
if group then
|
||||||
|
entities = self.pool.groups[group].entities
|
||||||
|
else
|
||||||
|
entities = self.pool.entities
|
||||||
|
end
|
||||||
|
for _, entity in ipairs(entities) do
|
||||||
|
if type(entity[event]) == 'function' then
|
||||||
|
entity[event](entity, ...)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
return t._cache[event]
|
||||||
|
end
|
||||||
|
})
|
||||||
|
end
|
||||||
|
|
||||||
|
--- Defines the filter and sort function for a @{Group}.
|
||||||
|
-- @type GroupOptions
|
||||||
|
|
||||||
|
--- The filter that defines which entities are added to this group.
|
||||||
|
-- Can be either:
|
||||||
|
--
|
||||||
|
-- - A list of required keys
|
||||||
|
-- - A function that takes the entity as the first argument
|
||||||
|
-- and returns true if the entity should be added to the group
|
||||||
|
-- @tfield[opt] table|function filter
|
||||||
|
|
||||||
|
--- A function that specifies how the entities in this group should be sorted.
|
||||||
|
-- Has the same requirements as the function argument to Lua's built-in `table.sort`.
|
||||||
|
-- @tfield[opt] function sort
|
||||||
|
|
||||||
|
--- Defines the groups and systems for a @{Pool}.
|
||||||
|
-- @type PoolOptions
|
||||||
|
|
||||||
|
--- A dictionary of groups for the pool to have.
|
||||||
|
-- Each key is the name of the group, and each value
|
||||||
|
-- should be a @{GroupOptions} table.
|
||||||
|
-- @tfield[opt={}] table groups
|
||||||
|
|
||||||
|
--- A list of @{SystemDefinition}s for the pool to use.
|
||||||
|
-- @tfield[opt={nata.oop()}] table systems
|
||||||
|
|
||||||
|
--- An initial value to set @{Pool.data} to.
|
||||||
|
-- @field[opt={}] data
|
||||||
|
|
||||||
|
---
|
||||||
|
-- @section end
|
||||||
|
|
||||||
|
--- Creates a new @{Pool}.
|
||||||
|
-- @tparam[opt] PoolOptions options how to set up the pool
|
||||||
|
-- @param[opt] ... additional arguments to pass to the pool's init event
|
||||||
|
-- @treturn Pool the new pool
|
||||||
|
function nata.new(options, ...)
|
||||||
|
local pool = setmetatable({}, Pool)
|
||||||
|
pool:_init(options, ...)
|
||||||
|
return pool
|
||||||
|
end
|
||||||
|
|
||||||
|
return nata
|
475
src/lib/pprint.lua
Normal file
475
src/lib/pprint.lua
Normal file
@ -0,0 +1,475 @@
|
|||||||
|
local pprint = { VERSION = '0.1' }
|
||||||
|
|
||||||
|
local depth = 1
|
||||||
|
|
||||||
|
pprint.defaults = {
|
||||||
|
-- If set to number N, then limit table recursion to N deep.
|
||||||
|
depth_limit = false,
|
||||||
|
-- type display trigger, hide not useful datatypes by default
|
||||||
|
-- custom types are treated as table
|
||||||
|
show_nil = false,
|
||||||
|
show_boolean = true,
|
||||||
|
show_number = true,
|
||||||
|
show_string = true,
|
||||||
|
show_table = true,
|
||||||
|
show_function = false,
|
||||||
|
show_thread = false,
|
||||||
|
show_userdata = false,
|
||||||
|
-- additional display trigger
|
||||||
|
show_metatable = false, -- show metatable
|
||||||
|
show_all = false, -- override other show settings and show everything
|
||||||
|
use_tostring = true, -- use __tostring to print table if available
|
||||||
|
filter_function = nil, -- called like callback(value[,key, parent]), return truty value to hide
|
||||||
|
object_cache = 'local', -- cache blob and table to give it a id, 'local' cache per print, 'global' cache
|
||||||
|
-- per process, falsy value to disable (might cause infinite loop)
|
||||||
|
-- format settings
|
||||||
|
indent_size = 2, -- indent for each nested table level
|
||||||
|
level_width = 80, -- max width per indent level
|
||||||
|
wrap_string = true, -- wrap string when it's longer than level_width
|
||||||
|
wrap_array = false, -- wrap every array elements
|
||||||
|
sort_keys = true, -- sort table keys
|
||||||
|
}
|
||||||
|
|
||||||
|
local TYPES = {
|
||||||
|
['nil'] = 1, ['boolean'] = 2, ['number'] = 3, ['string'] = 4,
|
||||||
|
['table'] = 5, ['function'] = 6, ['thread'] = 7, ['userdata'] = 8
|
||||||
|
}
|
||||||
|
|
||||||
|
-- seems this is the only way to escape these, as lua don't know how to map char '\a' to 'a'
|
||||||
|
local ESCAPE_MAP = {
|
||||||
|
['\a'] = '\\a', ['\b'] = '\\b', ['\f'] = '\\f', ['\n'] = '\\n', ['\r'] = '\\r',
|
||||||
|
['\t'] = '\\t', ['\v'] = '\\v', ['\\'] = '\\\\',
|
||||||
|
}
|
||||||
|
|
||||||
|
-- generic utilities
|
||||||
|
local function escape(s)
|
||||||
|
s = s:gsub('([%c\\])', ESCAPE_MAP)
|
||||||
|
local dq = s:find('"')
|
||||||
|
local sq = s:find("'")
|
||||||
|
if dq and sq then
|
||||||
|
return s:gsub('"', '\\"'), '"'
|
||||||
|
elseif sq then
|
||||||
|
return s, '"'
|
||||||
|
else
|
||||||
|
return s, "'"
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
local function is_plain_key(key)
|
||||||
|
return type(key) == 'string' and key:match('^[%a_][%a%d_]*$')
|
||||||
|
end
|
||||||
|
|
||||||
|
local CACHE_TYPES = {
|
||||||
|
['table'] = true, ['function'] = true, ['thread'] = true, ['userdata'] = true
|
||||||
|
}
|
||||||
|
|
||||||
|
-- cache would be populated to be like:
|
||||||
|
-- {
|
||||||
|
-- function = { `fun1` = 1, _cnt = 1 }, -- object id
|
||||||
|
-- table = { `table1` = 1, `table2` = 2, _cnt = 2 },
|
||||||
|
-- visited_tables = { `table1` = 7, `table2` = 8 }, -- visit count
|
||||||
|
-- }
|
||||||
|
-- use weakrefs to avoid accidentall adding refcount
|
||||||
|
local function cache_apperance(obj, cache, option)
|
||||||
|
if not cache.visited_tables then
|
||||||
|
cache.visited_tables = setmetatable({}, {__mode = 'k'})
|
||||||
|
end
|
||||||
|
local t = type(obj)
|
||||||
|
|
||||||
|
-- TODO can't test filter_function here as we don't have the ix and key,
|
||||||
|
-- might cause different results?
|
||||||
|
-- respect show_xxx and filter_function to be consistent with print results
|
||||||
|
if (not TYPES[t] and not option.show_table)
|
||||||
|
or (TYPES[t] and not option['show_'..t]) then
|
||||||
|
return
|
||||||
|
end
|
||||||
|
|
||||||
|
if CACHE_TYPES[t] or TYPES[t] == nil then
|
||||||
|
if not cache[t] then
|
||||||
|
cache[t] = setmetatable({}, {__mode = 'k'})
|
||||||
|
cache[t]._cnt = 0
|
||||||
|
end
|
||||||
|
if not cache[t][obj] then
|
||||||
|
cache[t]._cnt = cache[t]._cnt + 1
|
||||||
|
cache[t][obj] = cache[t]._cnt
|
||||||
|
end
|
||||||
|
end
|
||||||
|
if t == 'table' or TYPES[t] == nil then
|
||||||
|
if cache.visited_tables[obj] == false then
|
||||||
|
-- already printed, no need to mark this and its children anymore
|
||||||
|
return
|
||||||
|
elseif cache.visited_tables[obj] == nil then
|
||||||
|
cache.visited_tables[obj] = 1
|
||||||
|
else
|
||||||
|
-- visited already, increment and continue
|
||||||
|
cache.visited_tables[obj] = cache.visited_tables[obj] + 1
|
||||||
|
return
|
||||||
|
end
|
||||||
|
for k, v in pairs(obj) do
|
||||||
|
cache_apperance(k, cache, option)
|
||||||
|
cache_apperance(v, cache, option)
|
||||||
|
end
|
||||||
|
local mt = getmetatable(obj)
|
||||||
|
if mt and option.show_metatable then
|
||||||
|
cache_apperance(mt, cache, option)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
-- makes 'foo2' < 'foo100000'. string.sub makes substring anyway, no need to use index based method
|
||||||
|
local function str_natural_cmp(lhs, rhs)
|
||||||
|
while #lhs > 0 and #rhs > 0 do
|
||||||
|
local lmid, lend = lhs:find('%d+')
|
||||||
|
local rmid, rend = rhs:find('%d+')
|
||||||
|
if not (lmid and rmid) then return lhs < rhs end
|
||||||
|
|
||||||
|
local lsub = lhs:sub(1, lmid-1)
|
||||||
|
local rsub = rhs:sub(1, rmid-1)
|
||||||
|
if lsub ~= rsub then
|
||||||
|
return lsub < rsub
|
||||||
|
end
|
||||||
|
|
||||||
|
local lnum = tonumber(lhs:sub(lmid, lend))
|
||||||
|
local rnum = tonumber(rhs:sub(rmid, rend))
|
||||||
|
if lnum ~= rnum then
|
||||||
|
return lnum < rnum
|
||||||
|
end
|
||||||
|
|
||||||
|
lhs = lhs:sub(lend+1)
|
||||||
|
rhs = rhs:sub(rend+1)
|
||||||
|
end
|
||||||
|
return lhs < rhs
|
||||||
|
end
|
||||||
|
|
||||||
|
local function cmp(lhs, rhs)
|
||||||
|
local tleft = type(lhs)
|
||||||
|
local tright = type(rhs)
|
||||||
|
if tleft == 'number' and tright == 'number' then return lhs < rhs end
|
||||||
|
if tleft == 'string' and tright == 'string' then return str_natural_cmp(lhs, rhs) end
|
||||||
|
if tleft == tright then return str_natural_cmp(tostring(lhs), tostring(rhs)) end
|
||||||
|
|
||||||
|
-- allow custom types
|
||||||
|
local oleft = TYPES[tleft] or 9
|
||||||
|
local oright = TYPES[tright] or 9
|
||||||
|
return oleft < oright
|
||||||
|
end
|
||||||
|
|
||||||
|
-- setup option with default
|
||||||
|
local function make_option(option)
|
||||||
|
if option == nil then
|
||||||
|
option = {}
|
||||||
|
end
|
||||||
|
for k, v in pairs(pprint.defaults) do
|
||||||
|
if option[k] == nil then
|
||||||
|
option[k] = v
|
||||||
|
end
|
||||||
|
if option.show_all then
|
||||||
|
for t, _ in pairs(TYPES) do
|
||||||
|
option['show_'..t] = true
|
||||||
|
end
|
||||||
|
option.show_metatable = true
|
||||||
|
end
|
||||||
|
end
|
||||||
|
return option
|
||||||
|
end
|
||||||
|
|
||||||
|
-- override defaults and take effects for all following calls
|
||||||
|
function pprint.setup(option)
|
||||||
|
pprint.defaults = make_option(option)
|
||||||
|
end
|
||||||
|
|
||||||
|
-- format lua object into a string
|
||||||
|
function pprint.pformat(obj, option, printer)
|
||||||
|
option = make_option(option)
|
||||||
|
local buf = {}
|
||||||
|
local function default_printer(s)
|
||||||
|
table.insert(buf, s)
|
||||||
|
end
|
||||||
|
printer = printer or default_printer
|
||||||
|
|
||||||
|
local cache
|
||||||
|
if option.object_cache == 'global' then
|
||||||
|
-- steal the cache into a local var so it's not visible from _G or anywhere
|
||||||
|
-- still can't avoid user explicitly referentce pprint._cache but it shouldn't happen anyway
|
||||||
|
cache = pprint._cache or {}
|
||||||
|
pprint._cache = nil
|
||||||
|
elseif option.object_cache == 'local' then
|
||||||
|
cache = {}
|
||||||
|
end
|
||||||
|
|
||||||
|
local last = '' -- used for look back and remove trailing comma
|
||||||
|
local status = {
|
||||||
|
indent = '', -- current indent
|
||||||
|
len = 0, -- current line length
|
||||||
|
}
|
||||||
|
|
||||||
|
local wrapped_printer = function(s)
|
||||||
|
printer(last)
|
||||||
|
last = s
|
||||||
|
end
|
||||||
|
|
||||||
|
local function _indent(d)
|
||||||
|
status.indent = string.rep(' ', d + #(status.indent))
|
||||||
|
end
|
||||||
|
|
||||||
|
local function _n(d)
|
||||||
|
wrapped_printer('\n')
|
||||||
|
wrapped_printer(status.indent)
|
||||||
|
if d then
|
||||||
|
_indent(d)
|
||||||
|
end
|
||||||
|
status.len = 0
|
||||||
|
return true -- used to close bracket correctly
|
||||||
|
end
|
||||||
|
|
||||||
|
local function _p(s, nowrap)
|
||||||
|
status.len = status.len + #s
|
||||||
|
if not nowrap and status.len > option.level_width then
|
||||||
|
_n()
|
||||||
|
wrapped_printer(s)
|
||||||
|
status.len = #s
|
||||||
|
else
|
||||||
|
wrapped_printer(s)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
local formatter = {}
|
||||||
|
local function format(v)
|
||||||
|
local f = formatter[type(v)]
|
||||||
|
f = f or formatter.table -- allow patched type()
|
||||||
|
if option.filter_function and option.filter_function(v, nil, nil) then
|
||||||
|
return ''
|
||||||
|
else
|
||||||
|
return f(v)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
local function tostring_formatter(v)
|
||||||
|
return tostring(v)
|
||||||
|
end
|
||||||
|
|
||||||
|
local function number_formatter(n)
|
||||||
|
return n == math.huge and '[[math.huge]]' or tostring(n)
|
||||||
|
end
|
||||||
|
|
||||||
|
local function nop_formatter(v)
|
||||||
|
return ''
|
||||||
|
end
|
||||||
|
|
||||||
|
local function make_fixed_formatter(t, has_cache)
|
||||||
|
if has_cache then
|
||||||
|
return function (v)
|
||||||
|
return string.format('[[%s %d]]', t, cache[t][v])
|
||||||
|
end
|
||||||
|
else
|
||||||
|
return function (v)
|
||||||
|
return '[['..t..']]'
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
local function string_formatter(s, force_long_quote)
|
||||||
|
local s, quote = escape(s)
|
||||||
|
local quote_len = force_long_quote and 4 or 2
|
||||||
|
if quote_len + #s + status.len > option.level_width then
|
||||||
|
_n()
|
||||||
|
-- only wrap string when is longer than level_width
|
||||||
|
if option.wrap_string and #s + quote_len > option.level_width then
|
||||||
|
-- keep the quotes together
|
||||||
|
_p('[[')
|
||||||
|
while #s + status.len >= option.level_width do
|
||||||
|
local seg = option.level_width - status.len
|
||||||
|
_p(string.sub(s, 1, seg), true)
|
||||||
|
_n()
|
||||||
|
s = string.sub(s, seg+1)
|
||||||
|
end
|
||||||
|
_p(s) -- print the remaining parts
|
||||||
|
return ']]'
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
return force_long_quote and '[['..s..']]' or quote..s..quote
|
||||||
|
end
|
||||||
|
|
||||||
|
local function table_formatter(t)
|
||||||
|
if option.use_tostring then
|
||||||
|
local mt = getmetatable(t)
|
||||||
|
if mt and mt.__tostring then
|
||||||
|
return string_formatter(tostring(t), true)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
local print_header_ix = nil
|
||||||
|
local ttype = type(t)
|
||||||
|
if option.object_cache then
|
||||||
|
local cache_state = cache.visited_tables[t]
|
||||||
|
local tix = cache[ttype][t]
|
||||||
|
-- FIXME should really handle `cache_state == nil`
|
||||||
|
-- as user might add things through filter_function
|
||||||
|
if cache_state == false then
|
||||||
|
-- already printed, just print the the number
|
||||||
|
return string_formatter(string.format('%s %d', ttype, tix), true)
|
||||||
|
elseif cache_state > 1 then
|
||||||
|
-- appeared more than once, print table header with number
|
||||||
|
print_header_ix = tix
|
||||||
|
cache.visited_tables[t] = false
|
||||||
|
else
|
||||||
|
-- appeared exactly once, print like a normal table
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
local limit = tonumber(option.depth_limit)
|
||||||
|
if limit and depth > limit then
|
||||||
|
if print_header_ix then
|
||||||
|
return string.format('[[%s %d]]...', ttype, print_header_ix)
|
||||||
|
end
|
||||||
|
return string_formatter(tostring(t), true)
|
||||||
|
end
|
||||||
|
|
||||||
|
local tlen = #t
|
||||||
|
local wrapped = false
|
||||||
|
_p('{')
|
||||||
|
_indent(option.indent_size)
|
||||||
|
_p(string.rep(' ', option.indent_size - 1))
|
||||||
|
if print_header_ix then
|
||||||
|
_p(string.format('--[[%s %d]] ', ttype, print_header_ix))
|
||||||
|
end
|
||||||
|
for ix = 1,tlen do
|
||||||
|
local v = t[ix]
|
||||||
|
if formatter[type(v)] == nop_formatter or
|
||||||
|
(option.filter_function and option.filter_function(v, ix, t)) then
|
||||||
|
-- pass
|
||||||
|
else
|
||||||
|
if option.wrap_array then
|
||||||
|
wrapped = _n()
|
||||||
|
end
|
||||||
|
depth = depth+1
|
||||||
|
_p(format(v)..', ')
|
||||||
|
depth = depth-1
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
-- hashmap part of the table, in contrast to array part
|
||||||
|
local function is_hash_key(k)
|
||||||
|
if type(k) ~= 'number' then
|
||||||
|
return true
|
||||||
|
end
|
||||||
|
|
||||||
|
local numkey = math.floor(tonumber(k))
|
||||||
|
if numkey ~= k or numkey > tlen or numkey <= 0 then
|
||||||
|
return true
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
local function print_kv(k, v, t)
|
||||||
|
-- can't use option.show_x as obj may contain custom type
|
||||||
|
if formatter[type(v)] == nop_formatter or
|
||||||
|
formatter[type(k)] == nop_formatter or
|
||||||
|
(option.filter_function and option.filter_function(v, k, t)) then
|
||||||
|
return
|
||||||
|
end
|
||||||
|
wrapped = _n()
|
||||||
|
if is_plain_key(k) then
|
||||||
|
_p(k, true)
|
||||||
|
else
|
||||||
|
_p('[')
|
||||||
|
-- [[]] type string in key is illegal, needs to add spaces inbetween
|
||||||
|
local k = format(k)
|
||||||
|
if string.match(k, '%[%[') then
|
||||||
|
_p(' '..k..' ', true)
|
||||||
|
else
|
||||||
|
_p(k, true)
|
||||||
|
end
|
||||||
|
_p(']')
|
||||||
|
end
|
||||||
|
_p(' = ', true)
|
||||||
|
depth = depth+1
|
||||||
|
_p(format(v), true)
|
||||||
|
depth = depth-1
|
||||||
|
_p(',', true)
|
||||||
|
end
|
||||||
|
|
||||||
|
if option.sort_keys then
|
||||||
|
local keys = {}
|
||||||
|
for k, _ in pairs(t) do
|
||||||
|
if is_hash_key(k) then
|
||||||
|
table.insert(keys, k)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
table.sort(keys, cmp)
|
||||||
|
for _, k in ipairs(keys) do
|
||||||
|
print_kv(k, t[k], t)
|
||||||
|
end
|
||||||
|
else
|
||||||
|
for k, v in pairs(t) do
|
||||||
|
if is_hash_key(k) then
|
||||||
|
print_kv(k, v, t)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
if option.show_metatable then
|
||||||
|
local mt = getmetatable(t)
|
||||||
|
if mt then
|
||||||
|
print_kv('__metatable', mt, t)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
_indent(-option.indent_size)
|
||||||
|
-- make { } into {}
|
||||||
|
last = string.gsub(last, '^ +$', '')
|
||||||
|
-- peek last to remove trailing comma
|
||||||
|
last = string.gsub(last, ',%s*$', ' ')
|
||||||
|
if wrapped then
|
||||||
|
_n()
|
||||||
|
end
|
||||||
|
_p('}')
|
||||||
|
|
||||||
|
return ''
|
||||||
|
end
|
||||||
|
|
||||||
|
-- set formatters
|
||||||
|
formatter['nil'] = option.show_nil and tostring_formatter or nop_formatter
|
||||||
|
formatter['boolean'] = option.show_boolean and tostring_formatter or nop_formatter
|
||||||
|
formatter['number'] = option.show_number and number_formatter or nop_formatter -- need to handle math.huge
|
||||||
|
formatter['function'] = option.show_function and make_fixed_formatter('function', option.object_cache) or nop_formatter
|
||||||
|
formatter['thread'] = option.show_thread and make_fixed_formatter('thread', option.object_cache) or nop_formatter
|
||||||
|
formatter['userdata'] = option.show_userdata and make_fixed_formatter('userdata', option.object_cache) or nop_formatter
|
||||||
|
formatter['string'] = option.show_string and string_formatter or nop_formatter
|
||||||
|
formatter['table'] = option.show_table and table_formatter or nop_formatter
|
||||||
|
|
||||||
|
if option.object_cache then
|
||||||
|
-- needs to visit the table before start printing
|
||||||
|
cache_apperance(obj, cache, option)
|
||||||
|
end
|
||||||
|
|
||||||
|
_p(format(obj))
|
||||||
|
printer(last) -- close the buffered one
|
||||||
|
|
||||||
|
-- put cache back if global
|
||||||
|
if option.object_cache == 'global' then
|
||||||
|
pprint._cache = cache
|
||||||
|
end
|
||||||
|
|
||||||
|
return table.concat(buf)
|
||||||
|
end
|
||||||
|
|
||||||
|
-- pprint all the arguments
|
||||||
|
function pprint.pprint( ... )
|
||||||
|
local args = {...}
|
||||||
|
-- select will get an accurate count of array len, counting trailing nils
|
||||||
|
local len = select('#', ...)
|
||||||
|
for ix = 1,len do
|
||||||
|
pprint.pformat(args[ix], nil, io.write)
|
||||||
|
io.write('\n')
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
setmetatable(pprint, {
|
||||||
|
__call = function (_, ...)
|
||||||
|
pprint.pprint(...)
|
||||||
|
end
|
||||||
|
})
|
||||||
|
|
||||||
|
return pprint
|
||||||
|
|
522
src/lib/ripple.lua
Normal file
522
src/lib/ripple.lua
Normal file
@ -0,0 +1,522 @@
|
|||||||
|
local ripple = {
|
||||||
|
_VERSION = 'Ripple',
|
||||||
|
_DESCRIPTION = 'Audio helpers for LÖVE.',
|
||||||
|
_URL = 'https://github.com/tesselode/ripple',
|
||||||
|
_LICENSE = [[
|
||||||
|
MIT License
|
||||||
|
|
||||||
|
Copyright (c) 2019 Andrew Minnich
|
||||||
|
|
||||||
|
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||||
|
of this software and associated documentation files (the "Software"), to deal
|
||||||
|
in the Software without restriction, including without limitation the rights
|
||||||
|
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||||
|
copies of the Software, and to permit persons to whom the Software is
|
||||||
|
furnished to do so, subject to the following conditions:
|
||||||
|
|
||||||
|
The above copyright notice and this permission notice shall be included in all
|
||||||
|
copies or substantial portions of the Software.
|
||||||
|
|
||||||
|
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||||
|
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||||
|
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||||
|
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||||
|
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||||
|
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||||
|
SOFTWARE.
|
||||||
|
]]
|
||||||
|
}
|
||||||
|
|
||||||
|
local unpack = unpack or table.unpack -- luacheck: ignore
|
||||||
|
|
||||||
|
--[[
|
||||||
|
Represents an object that:
|
||||||
|
- can have tags applied
|
||||||
|
- has a volume
|
||||||
|
- can have effects applied
|
||||||
|
|
||||||
|
Tags, instances, and sounds are all taggable.
|
||||||
|
|
||||||
|
Note that not all taggable objects have children - tags and sounds
|
||||||
|
do, but instances do not.
|
||||||
|
]]
|
||||||
|
local Taggable = {}
|
||||||
|
|
||||||
|
--[[
|
||||||
|
Gets the total volume of this object given its own volume
|
||||||
|
and the volume of each of its tags.
|
||||||
|
]]
|
||||||
|
function Taggable:_getTotalVolume()
|
||||||
|
local volume = self.volume
|
||||||
|
for tag, _ in pairs(self._tags) do
|
||||||
|
volume = volume * tag:_getTotalVolume()
|
||||||
|
end
|
||||||
|
return volume
|
||||||
|
end
|
||||||
|
|
||||||
|
--[[
|
||||||
|
Gets all the effects that should be applied to this object given
|
||||||
|
its own effects and the effects of each of its tags. The object's
|
||||||
|
own effects will override tag effects.
|
||||||
|
|
||||||
|
Note: currently, if multiple tags define settings for the same effect,
|
||||||
|
the final result is undefined, as taggable objects use pairs to iterate
|
||||||
|
through the tags, which iterates in an undefined order.
|
||||||
|
]]
|
||||||
|
function Taggable:_getAllEffects()
|
||||||
|
local effects = {}
|
||||||
|
for tag, _ in pairs(self._tags) do
|
||||||
|
for name, filterSettings in pairs(tag:_getAllEffects()) do
|
||||||
|
effects[name] = filterSettings
|
||||||
|
end
|
||||||
|
end
|
||||||
|
for name, filterSettings in pairs(self._effects) do
|
||||||
|
effects[name] = filterSettings
|
||||||
|
end
|
||||||
|
return effects
|
||||||
|
end
|
||||||
|
|
||||||
|
--[[
|
||||||
|
A callback that is called when anything happens that could
|
||||||
|
lead to a change in the object's total volume.
|
||||||
|
]]
|
||||||
|
function Taggable:_onChangeVolume() end
|
||||||
|
|
||||||
|
--[[
|
||||||
|
A callback that is called when anything happens that could
|
||||||
|
change which effects are applied to the object.
|
||||||
|
]]
|
||||||
|
function Taggable:_onChangeEffects() end
|
||||||
|
|
||||||
|
function Taggable:_setVolume(volume)
|
||||||
|
self._volume = volume
|
||||||
|
self:_onChangeVolume()
|
||||||
|
end
|
||||||
|
|
||||||
|
--[[
|
||||||
|
_tag, _untag, and _setEffect are analogous to the
|
||||||
|
similarly named public API functions (see below), but
|
||||||
|
they don't call the _onChangeVolume and _onChangeEffects
|
||||||
|
callbacks. This allows me to have finer control over when
|
||||||
|
to call those callbacks, so I can set multiple tags and
|
||||||
|
effects without needlessly calling the callbacks for each
|
||||||
|
one.
|
||||||
|
]]
|
||||||
|
|
||||||
|
function Taggable:_tag(tag)
|
||||||
|
self._tags[tag] = true
|
||||||
|
tag._children[self] = true
|
||||||
|
end
|
||||||
|
|
||||||
|
function Taggable:_untag(tag)
|
||||||
|
self._tags[tag] = nil
|
||||||
|
tag._children[self] = nil
|
||||||
|
end
|
||||||
|
|
||||||
|
function Taggable:_setEffect(name, filterSettings)
|
||||||
|
if filterSettings == nil then filterSettings = true end
|
||||||
|
self._effects[name] = filterSettings
|
||||||
|
end
|
||||||
|
|
||||||
|
--[[
|
||||||
|
Given an options table, initializes the object's volume,
|
||||||
|
tags, and effects.
|
||||||
|
]]
|
||||||
|
function Taggable:_setOptions(options)
|
||||||
|
self.volume = options and options.volume or 1
|
||||||
|
-- reset tags
|
||||||
|
for tag in pairs(self._tags) do
|
||||||
|
self:_untag(tag)
|
||||||
|
end
|
||||||
|
-- apply new tags
|
||||||
|
if options and options.tags then
|
||||||
|
for _, tag in ipairs(options.tags) do
|
||||||
|
self:_tag(tag)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
-- reset effects
|
||||||
|
for name in pairs(self._effects) do
|
||||||
|
self._effects[name] = nil
|
||||||
|
end
|
||||||
|
-- apply new effects
|
||||||
|
if options and options.effects then
|
||||||
|
for name, filterSettings in pairs(options.effects) do
|
||||||
|
self:_setEffect(name, filterSettings)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
-- update final volume and effects
|
||||||
|
self:_onChangeVolume()
|
||||||
|
self:_onChangeEffects()
|
||||||
|
end
|
||||||
|
|
||||||
|
function Taggable:tag(...)
|
||||||
|
for i = 1, select('#', ...) do
|
||||||
|
local tag = select(i, ...)
|
||||||
|
self:_tag(tag)
|
||||||
|
end
|
||||||
|
self:_onChangeVolume()
|
||||||
|
self:_onChangeEffects()
|
||||||
|
end
|
||||||
|
|
||||||
|
function Taggable:untag(...)
|
||||||
|
for i = 1, select('#', ...) do
|
||||||
|
local tag = select(i, ...)
|
||||||
|
self:_untag(tag)
|
||||||
|
end
|
||||||
|
self:_onChangeVolume()
|
||||||
|
self:_onChangeEffects()
|
||||||
|
end
|
||||||
|
|
||||||
|
--[[
|
||||||
|
Sets an effect for this object. filterSettings can be the following types:
|
||||||
|
- table - the effect will be enabled with the filter settings given in the table
|
||||||
|
- true/nil - the effect will be enabled with no filter
|
||||||
|
- false - the effect will be explicitly disabled, overriding effect settings
|
||||||
|
from a parent sound or tag
|
||||||
|
]]
|
||||||
|
function Taggable:setEffect(name, filterSettings)
|
||||||
|
self:_setEffect(name, filterSettings)
|
||||||
|
self:_onChangeEffects()
|
||||||
|
end
|
||||||
|
|
||||||
|
function Taggable:removeEffect(name)
|
||||||
|
self._effects[name] = nil
|
||||||
|
self:_onChangeEffects()
|
||||||
|
end
|
||||||
|
|
||||||
|
function Taggable:getEffect(name)
|
||||||
|
return self._effects[name]
|
||||||
|
end
|
||||||
|
|
||||||
|
function Taggable:__index(key)
|
||||||
|
if key == 'volume' then
|
||||||
|
return self._volume
|
||||||
|
end
|
||||||
|
return Taggable[key]
|
||||||
|
end
|
||||||
|
|
||||||
|
function Taggable:__newindex(key, value)
|
||||||
|
if key == 'volume' then
|
||||||
|
self:_setVolume(value)
|
||||||
|
else
|
||||||
|
rawset(self, key, value)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
--[[
|
||||||
|
Represents a tag that can be applied to sounds,
|
||||||
|
instances of sounds, or other tags.
|
||||||
|
]]
|
||||||
|
local Tag = {__newindex = Taggable.__newindex}
|
||||||
|
|
||||||
|
function Tag:__index(key)
|
||||||
|
if Tag[key] then return Tag[key] end
|
||||||
|
return Taggable.__index(self, key)
|
||||||
|
end
|
||||||
|
|
||||||
|
function Tag:_onChangeVolume()
|
||||||
|
-- tell objects using this tag about a potential
|
||||||
|
-- volume change
|
||||||
|
for child, _ in pairs(self._children) do
|
||||||
|
child:_onChangeVolume()
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
function Tag:_onChangeEffect()
|
||||||
|
-- tell objects using this tag about a potential
|
||||||
|
-- effect change
|
||||||
|
for child, _ in pairs(self._children) do
|
||||||
|
child:_onChangeEffect()
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
-- Pauses all the sounds and instances tagged with this tag.
|
||||||
|
function Tag:pause(fadeDuration)
|
||||||
|
for child, _ in pairs(self._children) do
|
||||||
|
child:pause(fadeDuration)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
-- Resumes all the sounds and instances tagged with this tag.
|
||||||
|
function Tag:resume(fadeDuration)
|
||||||
|
for child, _ in pairs(self._children) do
|
||||||
|
child:resume(fadeDuration)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
-- Stops all the sounds and instances tagged with this tag.
|
||||||
|
function Tag:stop(fadeDuration)
|
||||||
|
for child, _ in pairs(self._children) do
|
||||||
|
child:stop(fadeDuration)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
function ripple.newTag(options)
|
||||||
|
local tag = setmetatable({
|
||||||
|
_effects = {},
|
||||||
|
_tags = {},
|
||||||
|
_children = {},
|
||||||
|
}, Tag)
|
||||||
|
tag:_setOptions(options)
|
||||||
|
return tag
|
||||||
|
end
|
||||||
|
|
||||||
|
-- Represents a specific occurrence of a sound.
|
||||||
|
local Instance = {}
|
||||||
|
|
||||||
|
function Instance:__index(key)
|
||||||
|
if key == 'pitch' then
|
||||||
|
return self._source:getPitch()
|
||||||
|
elseif key == 'loop' then
|
||||||
|
return self._source:isLooping()
|
||||||
|
elseif Instance[key] then
|
||||||
|
return Instance[key]
|
||||||
|
end
|
||||||
|
return Taggable.__index(self, key)
|
||||||
|
end
|
||||||
|
|
||||||
|
function Instance:__newindex(key, value)
|
||||||
|
if key == 'pitch' then
|
||||||
|
self._source:setPitch(value)
|
||||||
|
elseif key == 'loop' then
|
||||||
|
self._source:setLooping(value)
|
||||||
|
else
|
||||||
|
Taggable.__newindex(self, key, value)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
function Instance:_getTotalVolume()
|
||||||
|
local volume = Taggable._getTotalVolume(self)
|
||||||
|
-- apply sound volume as well as tag/self volumes
|
||||||
|
volume = volume * self._sound:_getTotalVolume()
|
||||||
|
-- apply fade volume
|
||||||
|
volume = volume * self._fadeVolume
|
||||||
|
return volume
|
||||||
|
end
|
||||||
|
|
||||||
|
function Instance:_getAllEffects()
|
||||||
|
local effects = {}
|
||||||
|
for tag, _ in pairs(self._tags) do
|
||||||
|
for name, filterSettings in pairs(tag:_getAllEffects()) do
|
||||||
|
effects[name] = filterSettings
|
||||||
|
end
|
||||||
|
end
|
||||||
|
-- apply sound effects as well as tag/self effects
|
||||||
|
for name, filterSettings in pairs(self._sound:_getAllEffects()) do
|
||||||
|
effects[name] = filterSettings
|
||||||
|
end
|
||||||
|
for name, filterSettings in pairs(self._effects) do
|
||||||
|
effects[name] = filterSettings
|
||||||
|
end
|
||||||
|
return effects
|
||||||
|
end
|
||||||
|
|
||||||
|
function Instance:_onChangeVolume()
|
||||||
|
-- update the source's volume
|
||||||
|
self._source:setVolume(self:_getTotalVolume())
|
||||||
|
end
|
||||||
|
|
||||||
|
function Instance:_onChangeEffects()
|
||||||
|
-- get the list of effects that should be applied
|
||||||
|
local effects = self:_getAllEffects()
|
||||||
|
for name, filterSettings in pairs(effects) do
|
||||||
|
-- remember which effects are currently applied to the source
|
||||||
|
if filterSettings == false then
|
||||||
|
self._appliedEffects[name] = nil
|
||||||
|
else
|
||||||
|
self._appliedEffects[name] = true
|
||||||
|
end
|
||||||
|
if filterSettings == true then
|
||||||
|
self._source:setEffect(name)
|
||||||
|
else
|
||||||
|
self._source:setEffect(name, filterSettings)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
-- remove effects that are currently applied but shouldn't be anymore
|
||||||
|
for name in pairs(self._appliedEffects) do
|
||||||
|
if not effects[name] then
|
||||||
|
self._source:setEffect(name, false)
|
||||||
|
self._appliedEffects[name] = nil
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
function Instance:_play(options)
|
||||||
|
if options and options.fadeDuration then
|
||||||
|
self._fadeVolume = 0
|
||||||
|
self._fadeSpeed = 1 / options.fadeDuration
|
||||||
|
else
|
||||||
|
self._fadeVolume = 1
|
||||||
|
end
|
||||||
|
self._fadeDirection = 1
|
||||||
|
self._afterFadingOut = false
|
||||||
|
self._paused = false
|
||||||
|
self:_setOptions(options)
|
||||||
|
self.pitch = options and options.pitch or 1
|
||||||
|
if options and options.loop ~= nil then
|
||||||
|
self.loop = options.loop
|
||||||
|
end
|
||||||
|
self._source:seek(options and options.seek or 0)
|
||||||
|
self._source:play()
|
||||||
|
end
|
||||||
|
|
||||||
|
function Instance:_update(dt)
|
||||||
|
-- fade in
|
||||||
|
if self._fadeDirection == 1 and self._fadeVolume < 1 then
|
||||||
|
self._fadeVolume = self._fadeVolume + self._fadeSpeed * dt
|
||||||
|
if self._fadeVolume > 1 then self._fadeVolume = 1 end
|
||||||
|
self:_onChangeVolume()
|
||||||
|
-- fade out
|
||||||
|
elseif self._fadeDirection == -1 and self._fadeVolume > 0 then
|
||||||
|
self._fadeVolume = self._fadeVolume - self._fadeSpeed * dt
|
||||||
|
if self._fadeVolume < 0 then
|
||||||
|
self._fadeVolume = 0
|
||||||
|
-- pause or stop after fading out
|
||||||
|
if self._afterFadingOut == 'pause' then
|
||||||
|
self:pause()
|
||||||
|
elseif self._afterFadingOut == 'stop' then
|
||||||
|
self:stop()
|
||||||
|
end
|
||||||
|
end
|
||||||
|
self:_onChangeVolume()
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
function Instance:setPosition(x, y, z)
|
||||||
|
self._source:setPosition(x, y, z or 0)
|
||||||
|
end
|
||||||
|
|
||||||
|
function Instance:isStopped()
|
||||||
|
return (not self._source:isPlaying()) and (not self._paused)
|
||||||
|
end
|
||||||
|
|
||||||
|
function Instance:pause(fadeDuration)
|
||||||
|
if fadeDuration and not self._paused then
|
||||||
|
self._fadeDirection = -1
|
||||||
|
self._fadeSpeed = 1 / fadeDuration
|
||||||
|
self._afterFadingOut = 'pause'
|
||||||
|
else
|
||||||
|
self._source:pause()
|
||||||
|
self._paused = true
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
function Instance:resume(fadeDuration)
|
||||||
|
if fadeDuration then
|
||||||
|
if self._paused then
|
||||||
|
self._fadeVolume = 0
|
||||||
|
self:_onChangeVolume()
|
||||||
|
end
|
||||||
|
self._fadeDirection = 1
|
||||||
|
self._fadeSpeed = 1 / fadeDuration
|
||||||
|
end
|
||||||
|
self._source:play()
|
||||||
|
self._paused = false
|
||||||
|
end
|
||||||
|
|
||||||
|
function Instance:stop(fadeDuration)
|
||||||
|
if fadeDuration and not self._paused then
|
||||||
|
self._fadeDirection = -1
|
||||||
|
self._fadeSpeed = 1 / fadeDuration
|
||||||
|
self._afterFadingOut = 'stop'
|
||||||
|
else
|
||||||
|
self._source:stop()
|
||||||
|
self._paused = false
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
-- Represents a sound that can be played.
|
||||||
|
local Sound = {}
|
||||||
|
|
||||||
|
function Sound:__index(key)
|
||||||
|
if key == 'loop' then
|
||||||
|
return self._source:isLooping()
|
||||||
|
elseif Sound[key] then
|
||||||
|
return Sound[key]
|
||||||
|
end
|
||||||
|
return Taggable.__index(self, key)
|
||||||
|
end
|
||||||
|
|
||||||
|
function Sound:__newindex(key, value)
|
||||||
|
if key == 'loop' then
|
||||||
|
self._source:setLooping(value)
|
||||||
|
for _, instance in ipairs(self._instances) do
|
||||||
|
instance.loop = value
|
||||||
|
end
|
||||||
|
else
|
||||||
|
Taggable.__newindex(self, key, value)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
function Sound:_onChangeVolume()
|
||||||
|
-- tell instances about potential volume changes
|
||||||
|
for _, instance in ipairs(self._instances) do
|
||||||
|
instance:_onChangeVolume()
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
function Sound:_onChangeEffects()
|
||||||
|
-- tell instances about potential effect changes
|
||||||
|
for _, instance in ipairs(self._instances) do
|
||||||
|
instance:_onChangeEffects()
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
function Sound:play(options)
|
||||||
|
-- reuse a stopped instance if one is available
|
||||||
|
for _, instance in ipairs(self._instances) do
|
||||||
|
if instance:isStopped() then
|
||||||
|
instance:_play(options)
|
||||||
|
return instance
|
||||||
|
end
|
||||||
|
end
|
||||||
|
-- otherwise, create a brand new one
|
||||||
|
local instance = setmetatable({
|
||||||
|
_sound = self,
|
||||||
|
_source = self._source:clone(),
|
||||||
|
_effects = {},
|
||||||
|
_tags = {},
|
||||||
|
_appliedEffects = {},
|
||||||
|
}, Instance)
|
||||||
|
table.insert(self._instances, instance)
|
||||||
|
instance:_play(options)
|
||||||
|
return instance
|
||||||
|
end
|
||||||
|
|
||||||
|
function Sound:pause(fadeDuration)
|
||||||
|
for _, instance in ipairs(self._instances) do
|
||||||
|
instance:pause(fadeDuration)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
function Sound:resume(fadeDuration)
|
||||||
|
for _, instance in ipairs(self._instances) do
|
||||||
|
instance:resume(fadeDuration)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
function Sound:stop(fadeDuration)
|
||||||
|
for _, instance in ipairs(self._instances) do
|
||||||
|
instance:stop(fadeDuration)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
function Sound:update(dt)
|
||||||
|
for _, instance in ipairs(self._instances) do
|
||||||
|
instance:_update(dt)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
function ripple.newSound(source, options)
|
||||||
|
local sound = setmetatable({
|
||||||
|
_source = source,
|
||||||
|
_effects = {},
|
||||||
|
_tags = {},
|
||||||
|
_instances = {},
|
||||||
|
}, Sound)
|
||||||
|
sound:_setOptions(options)
|
||||||
|
if options and options.loop then sound.loop = true end
|
||||||
|
return sound
|
||||||
|
end
|
||||||
|
|
||||||
|
return ripple
|
503
src/lib/vivid.lua
Normal file
503
src/lib/vivid.lua
Normal file
@ -0,0 +1,503 @@
|
|||||||
|
-- The MIT License (MIT)
|
||||||
|
--
|
||||||
|
-- Copyright (c) 2015 WetDesertRock
|
||||||
|
--
|
||||||
|
-- Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||||
|
-- of this software and associated documentation files (the "Software"), to deal
|
||||||
|
-- in the Software without restriction, including without limitation the rights
|
||||||
|
-- to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||||
|
-- copies of the Software, and to permit persons to whom the Software is
|
||||||
|
-- furnished to do so, subject to the following conditions:
|
||||||
|
--
|
||||||
|
-- The above copyright notice and this permission notice shall be included in
|
||||||
|
-- all copies or substantial portions of the Software.
|
||||||
|
--
|
||||||
|
-- THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||||
|
-- IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||||
|
-- FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||||
|
-- AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||||
|
-- LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||||
|
-- OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
||||||
|
-- THE SOFTWARE.
|
||||||
|
|
||||||
|
local vivid = {}
|
||||||
|
|
||||||
|
local math_min = math.min
|
||||||
|
local math_max = math.max
|
||||||
|
local math_atan = math.atan
|
||||||
|
local math_pi = math.pi
|
||||||
|
local math_sqrt = math.sqrt
|
||||||
|
local math_atan2 = math.atan2
|
||||||
|
local math_cos = math.cos
|
||||||
|
local math_sin = math.sin
|
||||||
|
local math_rad = math.rad
|
||||||
|
local math_abs = math.abs
|
||||||
|
|
||||||
|
-- Helper function to mimic the LOVE API, allows you to pass a table or four arguments
|
||||||
|
local function getColorArgs(first, ...)
|
||||||
|
if type(first) == "table" then
|
||||||
|
return unpack(first)
|
||||||
|
end
|
||||||
|
return first, ...
|
||||||
|
end
|
||||||
|
|
||||||
|
--RGB to `colorspace`
|
||||||
|
function vivid.RGBtoHSL(...)
|
||||||
|
local r,g,b,a = getColorArgs(...)
|
||||||
|
|
||||||
|
local min = math_min(r,g,b)
|
||||||
|
local max = math_max(r,g,b)
|
||||||
|
local delta_max = max-min
|
||||||
|
|
||||||
|
local h,s,l
|
||||||
|
|
||||||
|
l = (max+min)/2
|
||||||
|
|
||||||
|
if (delta_max == 0) then
|
||||||
|
h,s = 0,0
|
||||||
|
else
|
||||||
|
if l < 0.5 then
|
||||||
|
s = delta_max / (max+min)
|
||||||
|
else
|
||||||
|
s = delta_max / (2-max-min)
|
||||||
|
end
|
||||||
|
|
||||||
|
local delta_r = (((max-r)/6) + (delta_max/2)) / delta_max
|
||||||
|
local delta_g = (((max-g)/6) + (delta_max/2)) / delta_max
|
||||||
|
local delta_b = (((max-b)/6) + (delta_max/2)) / delta_max
|
||||||
|
|
||||||
|
if r == max then
|
||||||
|
h = delta_b - delta_g
|
||||||
|
elseif g == max then
|
||||||
|
h = (1/3) + delta_r - delta_b
|
||||||
|
elseif b == max then
|
||||||
|
h = (2/3) + delta_b - delta_r
|
||||||
|
end
|
||||||
|
|
||||||
|
if h < 0 then h = h + 1 end
|
||||||
|
if h > 1 then h = h - 1 end
|
||||||
|
end
|
||||||
|
|
||||||
|
return h,s,l,a
|
||||||
|
end
|
||||||
|
|
||||||
|
function vivid.RGBtoHSV(...)
|
||||||
|
local r,g,b,a = getColorArgs(...)
|
||||||
|
|
||||||
|
local min = math_min(r,g,b)
|
||||||
|
local max = math_max(r,g,b)
|
||||||
|
local delta_max = max-min
|
||||||
|
|
||||||
|
local h,s,v
|
||||||
|
|
||||||
|
v = max
|
||||||
|
|
||||||
|
if (delta_max == 0) then
|
||||||
|
h,s = 0,0
|
||||||
|
else
|
||||||
|
s = delta_max / max
|
||||||
|
|
||||||
|
local delta_r = (((max-r)/6) + (delta_max/2)) / delta_max
|
||||||
|
local delta_g = (((max-g)/6) + (delta_max/2)) / delta_max
|
||||||
|
local delta_b = (((max-b)/6) + (delta_max/2)) / delta_max
|
||||||
|
|
||||||
|
if r == max then
|
||||||
|
h = delta_b - delta_g
|
||||||
|
elseif g == max then
|
||||||
|
h = (1/3) + delta_r - delta_b
|
||||||
|
elseif b == max then
|
||||||
|
h = (2/3) + delta_b - delta_r
|
||||||
|
end
|
||||||
|
|
||||||
|
if h < 0 then h = h + 1 end
|
||||||
|
if h > 1 then h = h - 1 end
|
||||||
|
end
|
||||||
|
|
||||||
|
return h,s,v,a
|
||||||
|
end
|
||||||
|
|
||||||
|
function vivid.RGBtoXYZ(...)
|
||||||
|
--(Observer = 2°, Illuminant = D65)
|
||||||
|
local r,g,b,a = getColorArgs(...)
|
||||||
|
|
||||||
|
|
||||||
|
if r > 0.04045 then
|
||||||
|
r = ((r+0.055)/1.055)^2.4
|
||||||
|
else
|
||||||
|
r = r/12.92
|
||||||
|
end
|
||||||
|
if g > 0.04045 then
|
||||||
|
g = ((g+0.055)/1.055)^2.4
|
||||||
|
else
|
||||||
|
g = g/12.92
|
||||||
|
end
|
||||||
|
if b > 0.04045 then
|
||||||
|
b = ((b+0.055)/1.055)^2.4
|
||||||
|
else
|
||||||
|
b = b/12.92
|
||||||
|
end
|
||||||
|
|
||||||
|
r = r*100
|
||||||
|
g = g*100
|
||||||
|
b = b*100
|
||||||
|
|
||||||
|
local x = r * 0.4124 + g * 0.3576 + b * 0.1805
|
||||||
|
local y = r * 0.2126 + g * 0.7152 + b * 0.0722
|
||||||
|
local z = r * 0.0193 + g * 0.1192 + b * 0.9505
|
||||||
|
|
||||||
|
return x,y,z,a
|
||||||
|
end
|
||||||
|
|
||||||
|
function vivid.RGBtoLab(...)
|
||||||
|
return vivid.XYZtoLab(vivid.RGBtoXYZ(...))
|
||||||
|
end
|
||||||
|
|
||||||
|
function vivid.RGBtoLCH(...)
|
||||||
|
return vivid.LabtoLCH(vivid.RGBtoLab(...))
|
||||||
|
end
|
||||||
|
|
||||||
|
function vivid.RGBtoLuv(...)
|
||||||
|
return vivid.XYZtoLuv(vivid.RGBtoXYZ(...))
|
||||||
|
end
|
||||||
|
|
||||||
|
--`colorspace` to RGB
|
||||||
|
function vivid.HSLtoRGB(...)
|
||||||
|
local h,s,l,a = getColorArgs(...)
|
||||||
|
h,s,l = h,s,l
|
||||||
|
local r,g,b
|
||||||
|
|
||||||
|
if s == 0 then
|
||||||
|
r = l
|
||||||
|
g = l
|
||||||
|
b = l
|
||||||
|
else
|
||||||
|
local var1,var2
|
||||||
|
|
||||||
|
if l < 0.5 then
|
||||||
|
var2 = l*(1+s)
|
||||||
|
else
|
||||||
|
var2 = (l+s) - (s*l)
|
||||||
|
end
|
||||||
|
|
||||||
|
var1 = 2*l-var2
|
||||||
|
|
||||||
|
local function huetorgb(v1,v2,vh)
|
||||||
|
if vh < 0 then vh = vh+1 end
|
||||||
|
if vh > 1 then vh = vh-1 end
|
||||||
|
if 6*vh < 1 then return v1 + (v2-v1) * 6 * vh end
|
||||||
|
if 2*vh < 1 then return v2 end
|
||||||
|
if 3*vh < 2 then return v1 + (v2-v1) * ((2/3)-vh) * 6 end
|
||||||
|
return v1
|
||||||
|
end
|
||||||
|
|
||||||
|
r = huetorgb(var1, var2, h + (1/3))
|
||||||
|
g = huetorgb(var1, var2, h)
|
||||||
|
b = huetorgb(var1, var2, h - (1/3))
|
||||||
|
end
|
||||||
|
|
||||||
|
return r,g,b,a
|
||||||
|
end
|
||||||
|
|
||||||
|
function vivid.HSVtoRGB(...)
|
||||||
|
local h,s,v,a = getColorArgs(...)
|
||||||
|
|
||||||
|
local r,g,b
|
||||||
|
|
||||||
|
if s == 0 then
|
||||||
|
r = v
|
||||||
|
g = v
|
||||||
|
b = v
|
||||||
|
else
|
||||||
|
local varh,vari,var1,var2,var3
|
||||||
|
varh = h*6
|
||||||
|
if varh == 6 then varh = 0 end
|
||||||
|
vari = math.floor(varh)
|
||||||
|
var1 = v*(1-s)
|
||||||
|
var2 = v*(1-s*(varh-vari))
|
||||||
|
var3 = v*(1-s*(1-(varh-vari)))
|
||||||
|
|
||||||
|
if vari == 0 then
|
||||||
|
r = v
|
||||||
|
g = var3
|
||||||
|
b = var1
|
||||||
|
elseif vari == 1 then
|
||||||
|
r = var2
|
||||||
|
g = v
|
||||||
|
b = var1
|
||||||
|
elseif vari == 2 then
|
||||||
|
r = var1
|
||||||
|
g = v
|
||||||
|
b = var3
|
||||||
|
elseif vari == 3 then
|
||||||
|
r = var1
|
||||||
|
g = var2
|
||||||
|
b = v
|
||||||
|
elseif vari == 4 then
|
||||||
|
r = var3
|
||||||
|
g = var1
|
||||||
|
b = v
|
||||||
|
else
|
||||||
|
r = v
|
||||||
|
g = var1
|
||||||
|
b = var2
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
return r,g,b,a
|
||||||
|
end
|
||||||
|
|
||||||
|
function vivid.XYZtoRGB(...)
|
||||||
|
--(Observer = 2°, Illuminant = D65)
|
||||||
|
local x,y,z,a = getColorArgs(...)
|
||||||
|
x,y,z = x/100,y/100,z/100
|
||||||
|
local r,g,b
|
||||||
|
|
||||||
|
r = x * 3.2406 + y * -1.5372 + z * -0.4986
|
||||||
|
g = x * -0.9689 + y * 1.8758 + z * 0.0415
|
||||||
|
b = x * 0.0557 + y * -0.2040 + z * 1.0570
|
||||||
|
|
||||||
|
if r > 0.0031308 then
|
||||||
|
r = 1.055*(r^(1/2.4))-0.055
|
||||||
|
else
|
||||||
|
r = r*12.92
|
||||||
|
end
|
||||||
|
if g > 0.0031308 then
|
||||||
|
g = 1.055*(g^(1/2.4))-0.055
|
||||||
|
else
|
||||||
|
g = g*12.92
|
||||||
|
end
|
||||||
|
if b > 0.0031308 then
|
||||||
|
b = 1.055*(b^(1/2.4))-0.055
|
||||||
|
else
|
||||||
|
b = b*12.92
|
||||||
|
end
|
||||||
|
|
||||||
|
return r,g,b,a
|
||||||
|
end
|
||||||
|
|
||||||
|
function vivid.LabtoRGB(...)
|
||||||
|
return vivid.XYZtoRGB(vivid.LabtoXYZ(...))
|
||||||
|
end
|
||||||
|
|
||||||
|
function vivid.LCHtoRGB(...)
|
||||||
|
return vivid.LabtoRGB(vivid.LCHtoLab(...))
|
||||||
|
end
|
||||||
|
|
||||||
|
function vivid.LuvtoRGB(...)
|
||||||
|
return vivid.XYZtoRGB(vivid.LuvtoXYZ(...))
|
||||||
|
end
|
||||||
|
|
||||||
|
--Other conversions
|
||||||
|
local refx,refy,refz = 95.047,100.000,108.883
|
||||||
|
local refu = (4 * refx) / (refx + (15 * refy) + (3 * refz))
|
||||||
|
local refv = (9 * refy) / (refx + (15 * refy) + (3 * refz))
|
||||||
|
|
||||||
|
function vivid.XYZtoLab(...)
|
||||||
|
local x,y,z,alpha = getColorArgs(...)
|
||||||
|
local L,a,b
|
||||||
|
x,y,z = x/refx,y/refy,z/refz
|
||||||
|
if x > 0.008856 then
|
||||||
|
x = x^(1/3)
|
||||||
|
else
|
||||||
|
x = (7.787*x) + (16/116)
|
||||||
|
end
|
||||||
|
if y > 0.008856 then
|
||||||
|
y = y^(1/3)
|
||||||
|
else
|
||||||
|
y = (7.787*y) + (16/116)
|
||||||
|
end
|
||||||
|
if z > 0.008856 then
|
||||||
|
z = z^(1/3)
|
||||||
|
else
|
||||||
|
z = (7.787*z) + (16/116)
|
||||||
|
end
|
||||||
|
|
||||||
|
L = (116*y) - 16
|
||||||
|
a = 500*(x-y)
|
||||||
|
b = 200*(y-z)
|
||||||
|
return L,a,b,alpha
|
||||||
|
end
|
||||||
|
|
||||||
|
function vivid.LabtoXYZ(...)
|
||||||
|
local L,a,b,alpha = getColorArgs(...)
|
||||||
|
|
||||||
|
local y = (L+16) / 116
|
||||||
|
local x = a / 500 + y
|
||||||
|
local z = y - b / 200
|
||||||
|
if x^3 > 0.008856 then
|
||||||
|
x = x^3
|
||||||
|
else
|
||||||
|
x = (x - 16 / 116) / 7.787
|
||||||
|
end
|
||||||
|
if y^3 > 0.008856 then
|
||||||
|
y = y^3
|
||||||
|
else
|
||||||
|
y = (y - 16 / 116) / 7.787
|
||||||
|
end
|
||||||
|
if z^3 > 0.008856 then
|
||||||
|
z = z^3
|
||||||
|
else
|
||||||
|
z = (z - 16 / 116) / 7.787
|
||||||
|
end
|
||||||
|
|
||||||
|
return refx*x,refy*y,refz*z,alpha
|
||||||
|
end
|
||||||
|
|
||||||
|
function vivid.LabtoLCH(...)
|
||||||
|
local L,a,b,alpha = getColorArgs(...)
|
||||||
|
local C,H
|
||||||
|
H = math_atan2(b,a)
|
||||||
|
|
||||||
|
if H > 0 then
|
||||||
|
H = (H / math_pi) * 180
|
||||||
|
else
|
||||||
|
H = 360 - ( math_abs(H) / math_pi) * 180
|
||||||
|
end
|
||||||
|
|
||||||
|
C = math_sqrt(a ^ 2 + b ^ 2)
|
||||||
|
|
||||||
|
return L,C,H
|
||||||
|
end
|
||||||
|
|
||||||
|
function vivid.LCHtoLab(...)
|
||||||
|
local L,C,H,alpha = getColorArgs(...)
|
||||||
|
|
||||||
|
return L,math_cos(math_rad(H))*C,math_sin(math_rad(H))*C
|
||||||
|
end
|
||||||
|
|
||||||
|
function vivid.XYZtoLuv(...)
|
||||||
|
local x,y,z,alpha = getColorArgs(...)
|
||||||
|
local L,u,v
|
||||||
|
u = (4 * x) / (x + (15 * y) + (3 * z))
|
||||||
|
v = (9 * y) / (x + (15 * y) + (3 * z))
|
||||||
|
y = y/100
|
||||||
|
|
||||||
|
if y > 0.008856 then
|
||||||
|
y = y ^ (1/3)
|
||||||
|
else
|
||||||
|
y = (7.787 * y) + (16 / 116)
|
||||||
|
end
|
||||||
|
|
||||||
|
L = (116 * y) - 16
|
||||||
|
u = 13 * L * (u - refu)
|
||||||
|
v = 13 * L * (v - refv)
|
||||||
|
|
||||||
|
return L,u,v
|
||||||
|
end
|
||||||
|
|
||||||
|
function vivid.LuvtoXYZ(...)
|
||||||
|
local L,u,v,alpha = getColorArgs(...)
|
||||||
|
local x,y,z
|
||||||
|
|
||||||
|
y = (L + 16) / 116
|
||||||
|
if y^3 > 0.008856 then
|
||||||
|
y = y^3
|
||||||
|
else
|
||||||
|
y = (y - 16 / 116) / 7.787
|
||||||
|
end
|
||||||
|
|
||||||
|
u = u / (13 * L) + refu
|
||||||
|
v = v / (13 * L) + refv
|
||||||
|
|
||||||
|
y = y*100
|
||||||
|
x = -(9 * y * u) / ((u - 4 ) * v - u * v)
|
||||||
|
z = (9 * y - (15 * v * y) - (v * x)) / (3 * v)
|
||||||
|
|
||||||
|
return x,y,z
|
||||||
|
end
|
||||||
|
|
||||||
|
--Manipulations:
|
||||||
|
function vivid.lighten(amount, ...)
|
||||||
|
local h,s,l,a = vivid.RGBtoHSL(getColorArgs(...))
|
||||||
|
return vivid.HSLtoRGB(h,s,l+amount,a)
|
||||||
|
end
|
||||||
|
function vivid.darken(amount, ...)
|
||||||
|
local h,s,l,a = vivid.RGBtoHSL(getColorArgs(...))
|
||||||
|
return vivid.HSLtoRGB(h,s,l-amount,a)
|
||||||
|
end
|
||||||
|
function vivid.saturate(amount, ...)
|
||||||
|
local h,s,v,a = vivid.RGBtoHSV(getColorArgs(...))
|
||||||
|
return vivid.HSVtoRGB(h,s+amount,v,a)
|
||||||
|
end
|
||||||
|
function vivid.desaturate(amount, ...)
|
||||||
|
local h,s,v,a = vivid.RGBtoHSV(getColorArgs(...))
|
||||||
|
return vivid.HSVtoRGB(h,s-amount,v,a)
|
||||||
|
end
|
||||||
|
function vivid.hue(hue, ...)
|
||||||
|
local h,s,l,a = vivid.RGBtoHSL(getColorArgs(...))
|
||||||
|
return vivid.HSLtoRGB(hue,s,l,a)
|
||||||
|
end
|
||||||
|
function vivid.invert(...)
|
||||||
|
local r,g,b,a = getColorArgs(...)
|
||||||
|
return 1-r, 1-g, 1-b, a
|
||||||
|
end
|
||||||
|
function vivid.invertHue(...)
|
||||||
|
local h,s,l,a = vivid.RGBtoHSL(getColorArgs(...))
|
||||||
|
return vivid.HSLtoRGB(1-h,s,l,a)
|
||||||
|
end
|
||||||
|
|
||||||
|
--Spread
|
||||||
|
function vivid.HSLSpread(count,hoffset,s,l,a)
|
||||||
|
local incval = 1/count
|
||||||
|
local colors = {}
|
||||||
|
|
||||||
|
for i=0,count-1 do
|
||||||
|
table.insert(colors,{vivid.HSLtoRGB((i*incval+hoffset)%1,s,l,a)})
|
||||||
|
end
|
||||||
|
|
||||||
|
return colors
|
||||||
|
end
|
||||||
|
function vivid.HSVSpread(count,hoffset,s,v,a)
|
||||||
|
local incval = 1/count
|
||||||
|
local colors = {}
|
||||||
|
|
||||||
|
for i=0,count-1 do
|
||||||
|
table.insert(colors,{vivid.HSVtoRGB((i*incval+hoffset)%1,s,v,a)})
|
||||||
|
end
|
||||||
|
|
||||||
|
return colors
|
||||||
|
end
|
||||||
|
function vivid.LCHSpread(count,l,c,hoffset,a)
|
||||||
|
local incval = 360/count
|
||||||
|
local colors = {}
|
||||||
|
|
||||||
|
for i=0,count-1 do
|
||||||
|
table.insert(colors,{vivid.LCHtoRGB(l,c,(i*incval+hoffset)%360,a)})
|
||||||
|
end
|
||||||
|
|
||||||
|
return colors
|
||||||
|
end
|
||||||
|
|
||||||
|
--Wrap functions:
|
||||||
|
function vivid.wrapHSV(fn)
|
||||||
|
return function(...)
|
||||||
|
return fn(vivid.RGBtoHSV(...))
|
||||||
|
end
|
||||||
|
end
|
||||||
|
function vivid.wrapHSL(fn)
|
||||||
|
return function(...)
|
||||||
|
return fn(vivid.RGBtoHSL(...))
|
||||||
|
end
|
||||||
|
end
|
||||||
|
function vivid.wrapXYZ(fn)
|
||||||
|
return function(...)
|
||||||
|
return fn(vivid.RGBtoXYZ(...))
|
||||||
|
end
|
||||||
|
end
|
||||||
|
function vivid.wrapLab(fn)
|
||||||
|
return function(...)
|
||||||
|
return fn(vivid.RGBtoLab(...))
|
||||||
|
end
|
||||||
|
end
|
||||||
|
function vivid.wrapLCH(fn)
|
||||||
|
return function(...)
|
||||||
|
return fn(vivid.RGBtoLCH(...))
|
||||||
|
end
|
||||||
|
end
|
||||||
|
function vivid.wrapLuv(fn)
|
||||||
|
return function(...)
|
||||||
|
return fn(vivid.RGBtoLuv(...))
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
return vivid
|
9
src/main.lua
Normal file
9
src/main.lua
Normal file
@ -0,0 +1,9 @@
|
|||||||
|
local Gamestate = require("lib.hump.gamestate")
|
||||||
|
|
||||||
|
function love.load()
|
||||||
|
love.math.setRandomSeed(love.timer.getTime())
|
||||||
|
math.randomseed(love.timer.getTime())
|
||||||
|
|
||||||
|
Gamestate.switch(require("states.main"))
|
||||||
|
Gamestate.registerEvents()
|
||||||
|
end
|
7
src/states/main.lua
Normal file
7
src/states/main.lua
Normal file
@ -0,0 +1,7 @@
|
|||||||
|
local MainState = {}
|
||||||
|
|
||||||
|
function MainState:draw()
|
||||||
|
love.graphics.rectangle("fill", 20, 20, 100, 100)
|
||||||
|
end
|
||||||
|
|
||||||
|
return MainState
|
Loading…
Reference in New Issue
Block a user