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