modules/world: add needed libs
This commit is contained in:
parent
836fd77435
commit
696a568d0c
8 changed files with 4116 additions and 0 deletions
947
gamecore/modules/world/libs/bump-3dpd.lua
Normal file
947
gamecore/modules/world/libs/bump-3dpd.lua
Normal file
|
@ -0,0 +1,947 @@
|
||||||
|
local bump = {
|
||||||
|
_VERSION = 'bump-3dpd v0.2.0',
|
||||||
|
_URL = 'https://github.com/oniietzschan/bump-3dpd',
|
||||||
|
_DESCRIPTION = 'A 3D 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 assertIsCube(x,y,z,w,h,d)
|
||||||
|
assertType('number', x, 'x')
|
||||||
|
assertType('number', y, 'y')
|
||||||
|
assertType('number', z, 'z')
|
||||||
|
assertIsPositiveNumber(w, 'w')
|
||||||
|
assertIsPositiveNumber(h, 'h')
|
||||||
|
assertIsPositiveNumber(d, 'd')
|
||||||
|
end
|
||||||
|
|
||||||
|
local defaultFilter = function()
|
||||||
|
return 'slide'
|
||||||
|
end
|
||||||
|
|
||||||
|
------------------------------------------
|
||||||
|
-- Cube functions
|
||||||
|
------------------------------------------
|
||||||
|
|
||||||
|
local function cube_getNearestCorner(x,y,z,w,h,d, px, py, pz)
|
||||||
|
return nearest(px, x, x + w),
|
||||||
|
nearest(py, y, y + h),
|
||||||
|
nearest(pz, z, z + d)
|
||||||
|
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 cube
|
||||||
|
-- Notice that normals are only guaranteed to be accurate when initially ti1, ti2 == -math.huge, math.huge
|
||||||
|
local function cube_getSegmentIntersectionIndices(x,y,z,w,h,d, x1,y1,z1,x2,y2,z2, ti1,ti2)
|
||||||
|
ti1, ti2 = ti1 or 0, ti2 or 1
|
||||||
|
local dx = x2 - x1
|
||||||
|
local dy = y2 - y1
|
||||||
|
local dz = z2 - z1
|
||||||
|
local nx, ny, nz
|
||||||
|
local nx1, ny1, nz1, nx2, ny2, nz2 = 0,0,0,0,0,0
|
||||||
|
local p, q, r
|
||||||
|
|
||||||
|
for side = 1,6 do
|
||||||
|
if side == 1 then -- Left
|
||||||
|
nx,ny,nz,p,q = -1, 0, 0, -dx, x1 - x
|
||||||
|
elseif side == 2 then -- Right
|
||||||
|
nx,ny,nz,p,q = 1, 0, 0, dx, x + w - x1
|
||||||
|
elseif side == 3 then -- Top
|
||||||
|
nx,ny,nz,p,q = 0, -1, 0, -dy, y1 - y
|
||||||
|
elseif side == 4 then -- Bottom
|
||||||
|
nx,ny,nz,p,q = 0, 1, 0, dy, y + h - y1
|
||||||
|
elseif side == 5 then -- Front
|
||||||
|
nx,ny,nz,p,q = 0, 0, -1, -dz, z1 - z
|
||||||
|
else -- Back
|
||||||
|
nx,ny,nz,p,q = 0, 0, 1, dz, z + d - z1
|
||||||
|
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,nz1 = r, nx,ny,nz
|
||||||
|
end
|
||||||
|
else -- p > 0
|
||||||
|
if r < ti1 then
|
||||||
|
return nil
|
||||||
|
elseif r < ti2 then
|
||||||
|
ti2, nx2,ny2,nz2 = r,nx,ny,nz
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
return ti1,ti2, nx1,ny1,nz1, nx2,ny2,nz2
|
||||||
|
end
|
||||||
|
|
||||||
|
-- Calculates the minkowsky difference between 2 cubes, which is another cube
|
||||||
|
local function cube_getDiff(x1,y1,z1,w1,h1,d1, x2,y2,z2,w2,h2,d2)
|
||||||
|
return x2 - x1 - w1,
|
||||||
|
y2 - y1 - h1,
|
||||||
|
z2 - z1 - d1,
|
||||||
|
w1 + w2,
|
||||||
|
h1 + h2,
|
||||||
|
d1 + d2
|
||||||
|
end
|
||||||
|
|
||||||
|
local function cube_containsPoint(x,y,z,w,h,d, px,py,pz)
|
||||||
|
return px - x > DELTA
|
||||||
|
and py - y > DELTA
|
||||||
|
and pz - z > DELTA
|
||||||
|
and x + w - px > DELTA
|
||||||
|
and y + h - py > DELTA
|
||||||
|
and z + d - pz > DELTA
|
||||||
|
end
|
||||||
|
|
||||||
|
local function cube_isIntersecting(x1,y1,z1,w1,h1,d1, x2,y2,z2,w2,h2,d2)
|
||||||
|
return x1 < x2 + w2 and x2 < x1 + w1 and
|
||||||
|
y1 < y2 + h2 and y2 < y1 + h1 and
|
||||||
|
z1 < z2 + d2 and z2 < z1 + d1
|
||||||
|
end
|
||||||
|
|
||||||
|
local function cube_getCubeDistance(x1,y1,z1,w1,h1,d1, x2,y2,z2,w2,h2,d2)
|
||||||
|
local dx = x1 - x2 + (w1 - w2)/2
|
||||||
|
local dy = y1 - y2 + (h1 - h2)/2
|
||||||
|
local dz = z1 - z2 + (d1 - d2)/2
|
||||||
|
return (dx * dx) + (dy * dy) + (dz * dz)
|
||||||
|
end
|
||||||
|
|
||||||
|
local function cube_detectCollision(x1,y1,z1,w1,h1,d1, x2,y2,z2,w2,h2,d2, goalX, goalY, goalZ)
|
||||||
|
goalX = goalX or x1
|
||||||
|
goalY = goalY or y1
|
||||||
|
goalZ = goalZ or z1
|
||||||
|
|
||||||
|
local dx = goalX - x1
|
||||||
|
local dy = goalY - y1
|
||||||
|
local dz = goalZ - z1
|
||||||
|
local x,y,z,w,h,d = cube_getDiff(x1,y1,z1,w1,h1,d1, x2,y2,z2,w2,h2,d2)
|
||||||
|
|
||||||
|
local overlaps, ti, nx, ny, nz
|
||||||
|
|
||||||
|
if cube_containsPoint(x,y,z,w,h,d, 0,0,0) then -- item was intersecting other
|
||||||
|
local px, py, pz = cube_getNearestCorner(x,y,z,w,h,d, 0,0,0)
|
||||||
|
-- Volume of intersection:
|
||||||
|
local wi = min(w1, abs(px))
|
||||||
|
local hi = min(h1, abs(py))
|
||||||
|
local di = min(d1, abs(pz))
|
||||||
|
ti = wi * hi * di * -1 -- ti is the negative volume of intersection
|
||||||
|
overlaps = true
|
||||||
|
else
|
||||||
|
local ti1,ti2,nx1,ny1,nz1 = cube_getSegmentIntersectionIndices(x,y,z,w,h,d, 0,0,0,dx,dy,dz, -math.huge, math.huge)
|
||||||
|
|
||||||
|
-- item tunnels into other
|
||||||
|
if ti1
|
||||||
|
and ti1 < 1
|
||||||
|
and (abs(ti1 - ti2) >= DELTA) -- special case for cube going through another cube's corner
|
||||||
|
and (0 < ti1 + DELTA
|
||||||
|
or 0 == ti1 and ti2 > 0)
|
||||||
|
then
|
||||||
|
ti, nx, ny, nz = ti1, nx1, ny1, nz1
|
||||||
|
overlaps = false
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
if not ti then
|
||||||
|
return
|
||||||
|
end
|
||||||
|
|
||||||
|
local tx, ty, tz
|
||||||
|
|
||||||
|
if overlaps then
|
||||||
|
if dx == 0 and dy == 0 and dz == 0 then
|
||||||
|
-- intersecting and not moving - use minimum displacement vector
|
||||||
|
local px, py, pz = cube_getNearestCorner(x,y,z,w,h,d, 0,0,0)
|
||||||
|
if abs(px) <= abs(py) and abs(px) <= abs(pz) then
|
||||||
|
-- X axis has minimum displacement
|
||||||
|
py, pz = 0, 0
|
||||||
|
elseif abs(py) <= abs(pz) then
|
||||||
|
-- Y axis has minimum displacement
|
||||||
|
px, pz = 0, 0
|
||||||
|
else
|
||||||
|
-- Z axis has minimum displacement
|
||||||
|
px, py = 0, 0
|
||||||
|
end
|
||||||
|
nx, ny, nz = sign(px), sign(py), sign(pz)
|
||||||
|
tx = x1 + px
|
||||||
|
ty = y1 + py
|
||||||
|
tz = z1 + pz
|
||||||
|
else
|
||||||
|
-- intersecting and moving - move in the opposite direction
|
||||||
|
local ti1, _
|
||||||
|
ti1,_,nx,ny,nz = cube_getSegmentIntersectionIndices(x,y,z,w,h,d, 0,0,0,dx,dy,dz, -math.huge, 1)
|
||||||
|
if not ti1 then
|
||||||
|
return
|
||||||
|
end
|
||||||
|
tx = x1 + dx * ti1
|
||||||
|
ty = y1 + dy * ti1
|
||||||
|
tz = z1 + dz * ti1
|
||||||
|
end
|
||||||
|
else -- tunnel
|
||||||
|
tx = x1 + dx * ti
|
||||||
|
ty = y1 + dy * ti
|
||||||
|
tz = z1 + dz * ti
|
||||||
|
end
|
||||||
|
|
||||||
|
return {
|
||||||
|
overlaps = overlaps,
|
||||||
|
ti = ti,
|
||||||
|
move = {x = dx, y = dy, z = dz},
|
||||||
|
normal = {x = nx, y = ny, z = nz},
|
||||||
|
touch = {x = tx, y = ty, z = tz},
|
||||||
|
itemCube = {x = x1, y = y1, z = z1, w = w1, h = h1, d = d1},
|
||||||
|
otherCube = {x = x2, y = y2, z = z2, w = w2, h = h2, d = d2},
|
||||||
|
}
|
||||||
|
end
|
||||||
|
|
||||||
|
------------------------------------------
|
||||||
|
-- Grid functions
|
||||||
|
------------------------------------------
|
||||||
|
|
||||||
|
local function grid_toWorld(cellSize, cx, cy, cz)
|
||||||
|
return (cx - 1) * cellSize,
|
||||||
|
(cy - 1) * cellSize,
|
||||||
|
(cz - 1) * cellSize
|
||||||
|
end
|
||||||
|
|
||||||
|
local function grid_toCell(cellSize, x, y, z)
|
||||||
|
return floor(x / cellSize) + 1,
|
||||||
|
floor(y / cellSize) + 1,
|
||||||
|
floor(z / 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,z1,x2,y2,z2, f)
|
||||||
|
local cx1, cy1, cz1 = grid_toCell(cellSize, x1, y1, z1)
|
||||||
|
local cx2, cy2, cz2 = grid_toCell(cellSize, x2, y2, z2)
|
||||||
|
local stepX, dx, tx = grid_traverse_initStep(cellSize, cx1, x1, x2)
|
||||||
|
local stepY, dy, ty = grid_traverse_initStep(cellSize, cy1, y1, y2)
|
||||||
|
local stepZ, dz, tz = grid_traverse_initStep(cellSize, cz1, z1, z2)
|
||||||
|
local cx, cy, cz = cx1, cy1, cz1
|
||||||
|
|
||||||
|
f(cx, cy, cz)
|
||||||
|
|
||||||
|
-- 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) + abs(cz - cz2) > 1 do
|
||||||
|
if tx < ty and tx < tz then -- tx is smallest
|
||||||
|
tx = tx + dx
|
||||||
|
cx = cx + stepX
|
||||||
|
f(cx, cy, cz)
|
||||||
|
elseif ty < tz then -- ty is smallest
|
||||||
|
-- Addition: include both cells when going through corners
|
||||||
|
if tx == ty then
|
||||||
|
f(cx + stepX, cy, cz)
|
||||||
|
end
|
||||||
|
ty = ty + dy
|
||||||
|
cy = cy + stepY
|
||||||
|
f(cx, cy, cz)
|
||||||
|
else -- tz is smallest
|
||||||
|
-- Addition: include both cells when going through corners
|
||||||
|
if tx == tz then
|
||||||
|
f(cx + stepX, cy, cz)
|
||||||
|
end
|
||||||
|
if ty == tz then
|
||||||
|
f(cx, cy + stepY, cz)
|
||||||
|
end
|
||||||
|
tz = tz + dz
|
||||||
|
cz = cz + stepZ
|
||||||
|
f(cx, cy, cz)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
-- If we have not arrived to the last cell, use it
|
||||||
|
if cx ~= cx2 or cy ~= cy2 or cz ~= cz2 then
|
||||||
|
f(cx2, cy2, cz2)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
local function grid_toCellCube(cellSize, x,y,z,w,h,d)
|
||||||
|
local cx,cy,cz = grid_toCell(cellSize, x, y, z)
|
||||||
|
local cx2 = ceil((x + w) / cellSize)
|
||||||
|
local cy2 = ceil((y + h) / cellSize)
|
||||||
|
local cz2 = ceil((z + d) / cellSize)
|
||||||
|
|
||||||
|
return cx,
|
||||||
|
cy,
|
||||||
|
cz,
|
||||||
|
cx2 - cx + 1,
|
||||||
|
cy2 - cy + 1,
|
||||||
|
cz2 - cz + 1
|
||||||
|
end
|
||||||
|
|
||||||
|
------------------------------------------
|
||||||
|
-- Responses
|
||||||
|
------------------------------------------
|
||||||
|
|
||||||
|
local touch = function(world, col, x,y,z,w,h,d, goalX, goalY, goalZ, filter)
|
||||||
|
return col.touch.x, col.touch.y, col.touch.z, {}, 0
|
||||||
|
end
|
||||||
|
|
||||||
|
local cross = function(world, col, x,y,z,w,h,d, goalX, goalY, goalZ, filter)
|
||||||
|
local cols, len = world:project(col.item, x,y,z,w,h,d, goalX, goalY, goalZ, filter)
|
||||||
|
|
||||||
|
return goalX, goalY, goalZ, cols, len
|
||||||
|
end
|
||||||
|
|
||||||
|
local slide = function(world, col, x,y,z,w,h,d, goalX, goalY, goalZ, filter)
|
||||||
|
goalX = goalX or x
|
||||||
|
goalY = goalY or y
|
||||||
|
goalZ = goalZ or z
|
||||||
|
|
||||||
|
local tch, move = col.touch, col.move
|
||||||
|
if move.x ~= 0 or move.y ~= 0 or move.z ~= 0 then
|
||||||
|
if col.normal.x ~= 0 then
|
||||||
|
goalX = tch.x
|
||||||
|
end
|
||||||
|
if col.normal.y ~= 0 then
|
||||||
|
goalY = tch.y
|
||||||
|
end
|
||||||
|
if col.normal.z ~= 0 then
|
||||||
|
goalZ = tch.z
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
col.slide = {x = goalX, y = goalY, z = goalZ}
|
||||||
|
|
||||||
|
x, y, z = tch.x, tch.y, tch.z
|
||||||
|
local cols, len = world:project(col.item, x,y,z,w,h,d, goalX, goalY, goalZ, filter)
|
||||||
|
|
||||||
|
return goalX, goalY, goalZ, cols, len
|
||||||
|
end
|
||||||
|
|
||||||
|
local bounce = function(world, col, x,y,z,w,h,d, goalX, goalY, goalZ, filter)
|
||||||
|
goalX = goalX or x
|
||||||
|
goalY = goalY or y
|
||||||
|
goalZ = goalZ or z
|
||||||
|
|
||||||
|
local tch, move = col.touch, col.move
|
||||||
|
local tx, ty, tz = tch.x, tch.y, tch.z
|
||||||
|
local bx, by, bz = tx, ty, tz
|
||||||
|
|
||||||
|
if move.x ~= 0 or move.y ~= 0 or move.z ~= 0 then
|
||||||
|
local bnx = goalX - tx
|
||||||
|
local bny = goalY - ty
|
||||||
|
local bnz = goalZ - tz
|
||||||
|
|
||||||
|
if col.normal.x ~= 0 then
|
||||||
|
bnx = -bnx
|
||||||
|
end
|
||||||
|
if col.normal.y ~= 0 then
|
||||||
|
bny = -bny
|
||||||
|
end
|
||||||
|
if col.normal.z ~= 0 then
|
||||||
|
bnz = -bnz
|
||||||
|
end
|
||||||
|
|
||||||
|
bx = tx + bnx
|
||||||
|
by = ty + bny
|
||||||
|
bz = tz + bnz
|
||||||
|
end
|
||||||
|
|
||||||
|
col.bounce = {x = bx, y = by, z = bz}
|
||||||
|
x, y, z = tch.x, tch.y, tch.z
|
||||||
|
goalX, goalY, goalZ = bx, by, bz
|
||||||
|
|
||||||
|
local cols, len = world:project(col.item, x,y,z,w,h,d, goalX, goalY, goalZ, filter)
|
||||||
|
|
||||||
|
return goalX, goalY, goalZ, 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.itemCube, a.otherCube, b.otherCube
|
||||||
|
local ad = cube_getCubeDistance(ir.x,ir.y,ir.z,ir.w,ir.h,ir.d, ar.x,ar.y,ar.z,ar.w,ar.h,ar.d)
|
||||||
|
local bd = cube_getCubeDistance(ir.x,ir.y,ir.z,ir.w,ir.h,ir.d, br.x,br.y,br.z,br.w,br.h,br.d)
|
||||||
|
return ad < bd
|
||||||
|
end
|
||||||
|
return a.ti < b.ti
|
||||||
|
end
|
||||||
|
|
||||||
|
local function addItemToCell(self, item, cx, cy, cz)
|
||||||
|
self.cells[cz] = self.cells[cz] or {}
|
||||||
|
self.cells[cz][cy] = self.cells[cz][cy] or setmetatable({}, {__mode = 'v'})
|
||||||
|
if self.cells[cz][cy][cx] == nil then
|
||||||
|
self.cells[cz][cy][cx] = {
|
||||||
|
itemCount = 0,
|
||||||
|
x = cx,
|
||||||
|
y = cy,
|
||||||
|
z = cz,
|
||||||
|
items = setmetatable({}, {__mode = 'k'})
|
||||||
|
}
|
||||||
|
end
|
||||||
|
|
||||||
|
local cell = self.cells[cz][cy][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, cz)
|
||||||
|
if not self.cells[cz]
|
||||||
|
or not self.cells[cz][cy]
|
||||||
|
or not self.cells[cz][cy][cx]
|
||||||
|
or not self.cells[cz][cy][cx].items[item]
|
||||||
|
then
|
||||||
|
return false
|
||||||
|
end
|
||||||
|
|
||||||
|
local cell = self.cells[cz][cy][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 getDictItemsInCellCube(self, cx,cy,cz, cw,ch,cd)
|
||||||
|
local items_dict = {}
|
||||||
|
|
||||||
|
for z = cz, cz + cd - 1 do
|
||||||
|
local plane = self.cells[z]
|
||||||
|
if plane then
|
||||||
|
for y = cy, cy + ch - 1 do
|
||||||
|
local row = plane[y]
|
||||||
|
if row then
|
||||||
|
for x = cx, cx + cw - 1 do
|
||||||
|
local cell = row[x]
|
||||||
|
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
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
return items_dict
|
||||||
|
end
|
||||||
|
|
||||||
|
local function getCellsTouchedBySegment(self, x1,y1,z1,x2,y2,z2)
|
||||||
|
local cells, cellsLen, visited = {}, 0, {}
|
||||||
|
|
||||||
|
grid_traverse(self.cellSize, x1,y1,z1,x2,y2,z2, function(cx, cy, cz)
|
||||||
|
local plane = self.cells[cz]
|
||||||
|
if not plane then
|
||||||
|
return
|
||||||
|
end
|
||||||
|
|
||||||
|
local row = plane[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,z1, x2,y2,z2, filter)
|
||||||
|
local cells, len = getCellsTouchedBySegment(self, x1,y1,z1,x2,y2,z2)
|
||||||
|
local cell, cube, x,y,z,w,h,d, 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
|
||||||
|
cube = self.cubes[item]
|
||||||
|
x, y, z, w, h, d = cube.x, cube.y, cube.z, cube.w, cube.h, cube.d
|
||||||
|
|
||||||
|
ti1, ti2 = cube_getSegmentIntersectionIndices(x,y,z,w,h,d, x1,y1,z1, x2,y2,z2, 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 = cube_getSegmentIntersectionIndices(x,y,z,w,h,d, x1,y1,z1, x2,y2,z2, -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:projectMove(item, x,y,z,w,h,d, goalX,goalY,goalZ, filter)
|
||||||
|
local cols, len = {}, 0
|
||||||
|
|
||||||
|
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 projected_cols, projected_len = self:project(item, x,y,z,w,h,d, goalX,goalY,goalZ, 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, goalZ, projected_cols, projected_len = response(
|
||||||
|
self,
|
||||||
|
col,
|
||||||
|
x, y, z, w, h, d,
|
||||||
|
goalX, goalY, goalZ,
|
||||||
|
visitedFilter
|
||||||
|
)
|
||||||
|
end
|
||||||
|
|
||||||
|
return goalX, goalY, goalZ, cols, len
|
||||||
|
end
|
||||||
|
|
||||||
|
function World:project(item, x,y,z,w,h,d, goalX,goalY,goalZ, filter)
|
||||||
|
assertIsCube(x, y, z, w, h, d)
|
||||||
|
|
||||||
|
goalX = goalX or x
|
||||||
|
goalY = goalY or y
|
||||||
|
goalZ = goalZ or z
|
||||||
|
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 cube of the whole movement. Conditional to building a queryPolygon method
|
||||||
|
local tx = min(goalX, x)
|
||||||
|
local ty = min(goalY, y)
|
||||||
|
local tz = min(goalZ, z)
|
||||||
|
local tx2 = max(goalX + w, x + w)
|
||||||
|
local ty2 = max(goalY + h, y + h)
|
||||||
|
local tz2 = max(goalZ + d, z + d)
|
||||||
|
local tw = tx2 - tx
|
||||||
|
local th = ty2 - ty
|
||||||
|
local td = tz2 - tz
|
||||||
|
|
||||||
|
local cx,cy,cz,cw,ch,cd = grid_toCellCube(self.cellSize, tx,ty,tz, tw,th,td)
|
||||||
|
|
||||||
|
local dictItemsInCellCube = getDictItemsInCellCube(self, cx,cy,cz,cw,ch,cd)
|
||||||
|
|
||||||
|
for other,_ in pairs(dictItemsInCellCube) do
|
||||||
|
if not visited[other] then
|
||||||
|
visited[other] = true
|
||||||
|
|
||||||
|
local responseName = filter(item, other)
|
||||||
|
if responseName then
|
||||||
|
local ox,oy,oz,ow,oh,od = self:getCube(other)
|
||||||
|
local col = cube_detectCollision(x,y,z,w,h,d, ox,oy,oz,ow,oh,od, goalX, goalY, goalZ)
|
||||||
|
|
||||||
|
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 _, plane in pairs(self.cells) do
|
||||||
|
for _, row in pairs(plane) do
|
||||||
|
for _,_ in pairs(row) do
|
||||||
|
count = count + 1
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
return count
|
||||||
|
end
|
||||||
|
|
||||||
|
function World:hasItem(item)
|
||||||
|
return not not self.cubes[item]
|
||||||
|
end
|
||||||
|
|
||||||
|
function World:getItems()
|
||||||
|
local items, len = {}, 0
|
||||||
|
for item,_ in pairs(self.cubes) do
|
||||||
|
len = len + 1
|
||||||
|
items[len] = item
|
||||||
|
end
|
||||||
|
return items, len
|
||||||
|
end
|
||||||
|
|
||||||
|
function World:countItems()
|
||||||
|
local len = 0
|
||||||
|
for _ in pairs(self.cubes) do len = len + 1 end
|
||||||
|
return len
|
||||||
|
end
|
||||||
|
|
||||||
|
function World:getCube(item)
|
||||||
|
local cube = self.cubes[item]
|
||||||
|
if not cube then
|
||||||
|
error('Item ' .. tostring(item) .. ' must be added to the world before getting its cube. Use world:add(item, x,y,z,w,h,d) to add it first.')
|
||||||
|
end
|
||||||
|
|
||||||
|
return cube.x, cube.y, cube.z, cube.w, cube.h, cube.d
|
||||||
|
end
|
||||||
|
|
||||||
|
function World:toWorld(cx, cy, cz)
|
||||||
|
return grid_toWorld(self.cellSize, cx, cy, cz)
|
||||||
|
end
|
||||||
|
|
||||||
|
function World:toCell(x,y,z)
|
||||||
|
return grid_toCell(self.cellSize, x, y, z)
|
||||||
|
end
|
||||||
|
|
||||||
|
|
||||||
|
-- Query methods
|
||||||
|
|
||||||
|
function World:queryCube(x,y,z,w,h,d, filter)
|
||||||
|
assertIsCube(x,y,z,w,h,d)
|
||||||
|
|
||||||
|
local cx,cy,cz,cw,ch,cd = grid_toCellCube(self.cellSize, x,y,z,w,h,d)
|
||||||
|
local dictItemsInCellCube = getDictItemsInCellCube(self, cx,cy,cz,cw,ch,cd)
|
||||||
|
|
||||||
|
local items, len = {}, 0
|
||||||
|
|
||||||
|
local cube
|
||||||
|
for item,_ in pairs(dictItemsInCellCube) do
|
||||||
|
cube = self.cubes[item]
|
||||||
|
if (not filter or filter(item))
|
||||||
|
and cube_isIntersecting(x,y,z,w,h,d, cube.x, cube.y, cube.z, cube.w, cube.h, cube.d)
|
||||||
|
then
|
||||||
|
len = len + 1
|
||||||
|
items[len] = item
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
return items, len
|
||||||
|
end
|
||||||
|
|
||||||
|
function World:queryPoint(x,y,z, filter)
|
||||||
|
local cx,cy,cz = self:toCell(x,y,z)
|
||||||
|
local dictItemsInCellCube = getDictItemsInCellCube(self, cx,cy,cz, 1,1,1)
|
||||||
|
|
||||||
|
local items, len = {}, 0
|
||||||
|
|
||||||
|
local cube
|
||||||
|
for item,_ in pairs(dictItemsInCellCube) do
|
||||||
|
cube = self.cubes[item]
|
||||||
|
if (not filter or filter(item))
|
||||||
|
and cube_containsPoint(cube.x, cube.y, cube.z, cube.w, cube.h, cube.d, x, y, z)
|
||||||
|
then
|
||||||
|
len = len + 1
|
||||||
|
items[len] = item
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
return items, len
|
||||||
|
end
|
||||||
|
|
||||||
|
function World:querySegment(x1, y1, z1, x2, y2, z2, filter)
|
||||||
|
local itemInfo, len = getInfoAboutItemsTouchedBySegment(self, x1, y1, z1, x2, y2, z2, filter)
|
||||||
|
local items = {}
|
||||||
|
for i = 1, len do
|
||||||
|
items[i] = itemInfo[i].item
|
||||||
|
end
|
||||||
|
|
||||||
|
return items, len
|
||||||
|
end
|
||||||
|
|
||||||
|
-- function World:querySegmentWithCoords(x1, y1, z1, x2, y2, z2, filter)
|
||||||
|
-- local itemInfo, len = getInfoAboutItemsTouchedBySegment(self, x1, y1, z1, x2, y2, z2, filter)
|
||||||
|
-- local dx, dy, dz = x2 - x1, y2 - y1, z2 - z1
|
||||||
|
-- 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,z,w,h,d)
|
||||||
|
local cube = self.cubes[item]
|
||||||
|
if cube then
|
||||||
|
error('Item ' .. tostring(item) .. ' added to the world twice.')
|
||||||
|
end
|
||||||
|
assertIsCube(x,y,z,w,h,d)
|
||||||
|
|
||||||
|
self.cubes[item] = {x=x,y=y,z=z,w=w,h=h,d=d}
|
||||||
|
|
||||||
|
local cl,ct,cs,cw,ch,cd = grid_toCellCube(self.cellSize, x,y,z,w,h,d)
|
||||||
|
for cz = cs, cs + cd - 1 do
|
||||||
|
for cy = ct, ct + ch - 1 do
|
||||||
|
for cx = cl, cl + cw - 1 do
|
||||||
|
addItemToCell(self, item, cx, cy, cz)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
return item
|
||||||
|
end
|
||||||
|
|
||||||
|
function World:remove(item)
|
||||||
|
local x,y,z,w,h,d = self:getCube(item)
|
||||||
|
|
||||||
|
self.cubes[item] = nil
|
||||||
|
local cl,ct,cs,cw,ch,cd = grid_toCellCube(self.cellSize, x,y,z,w,h,d)
|
||||||
|
for cz = cs, cs + cd - 1 do
|
||||||
|
for cy = ct, ct + ch - 1 do
|
||||||
|
for cx = cl, cl + cw - 1 do
|
||||||
|
removeItemFromCell(self, item, cx, cy, cz)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
function World:update(item, x2,y2,z2,w2,h2,d2)
|
||||||
|
local x1,y1,z1, w1,h1,d1 = self:getCube(item)
|
||||||
|
w2 = w2 or w1
|
||||||
|
h2 = h2 or h1
|
||||||
|
d2 = d2 or d1
|
||||||
|
assertIsCube(x2,y2,z2,w2,h2,d2)
|
||||||
|
|
||||||
|
if x1 == x2 and y1 == y2 and z1 == z2 and w1 == w2 and h1 == h2 and d1 == d2 then
|
||||||
|
return
|
||||||
|
end
|
||||||
|
|
||||||
|
local cl1,ct1,cs1,cw1,ch1,cd1 = grid_toCellCube(self.cellSize, x1,y1,z1, w1,h1,d1)
|
||||||
|
local cl2,ct2,cs2,cw2,ch2,cd2 = grid_toCellCube(self.cellSize, x2,y2,z2, w2,h2,d2)
|
||||||
|
|
||||||
|
if cl1 ~= cl2 or ct1 ~= ct2 or cs1 ~= cs2 or cw1 ~= cw2 or ch1 ~= ch2 or cd1 ~= cd2 then
|
||||||
|
local cr1 = cl1 + cw1 - 1
|
||||||
|
local cr2 = cl2 + cw2 - 1
|
||||||
|
local cb1 = ct1 + ch1 - 1
|
||||||
|
local cb2 = ct2 + ch2 - 1
|
||||||
|
local css1 = cs1 + cd1 - 1
|
||||||
|
local css2 = cs2 + cd2 - 1
|
||||||
|
local cyOut, czOut
|
||||||
|
|
||||||
|
for cz = cs1, css1 do
|
||||||
|
czOut = cz < cs2 or cz > css2
|
||||||
|
for cy = ct1, cb1 do
|
||||||
|
cyOut = cy < ct2 or cy > cb2
|
||||||
|
for cx = cl1, cr1 do
|
||||||
|
if czOut or cyOut or cx < cl2 or cx > cr2 then
|
||||||
|
removeItemFromCell(self, item, cx, cy, cz)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
for cz = cs2, css2 do
|
||||||
|
czOut = cz < cs1 or cz > css1
|
||||||
|
for cy = ct2, cb2 do
|
||||||
|
cyOut = cy < ct1 or cy > cb1
|
||||||
|
for cx = cl2, cr2 do
|
||||||
|
if czOut or cyOut or cx < cl1 or cx > cr1 then
|
||||||
|
addItemToCell(self, item, cx, cy, cz)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
local cube = self.cubes[item]
|
||||||
|
cube.x, cube.y, cube.z, cube.w, cube.h, cube.d = x2, y2, z2, w2, h2, d2
|
||||||
|
end
|
||||||
|
|
||||||
|
function World:move(item, goalX, goalY, goalZ, filter)
|
||||||
|
local actualX, actualY, actualZ, cols, len = self:check(item, goalX, goalY, goalZ, filter)
|
||||||
|
|
||||||
|
self:update(item, actualX, actualY, actualZ)
|
||||||
|
|
||||||
|
return actualX, actualY, actualZ, cols, len
|
||||||
|
end
|
||||||
|
|
||||||
|
function World:check(item, goalX, goalY, goalZ, filter)
|
||||||
|
local x,y,z,w,h,d = self:getCube(item)
|
||||||
|
|
||||||
|
return self:projectMove(item, x,y,z,w,h,d, goalX,goalY,goalZ, filter)
|
||||||
|
end
|
||||||
|
|
||||||
|
|
||||||
|
-- Public library functions
|
||||||
|
|
||||||
|
bump.newWorld = function(cellSize)
|
||||||
|
cellSize = cellSize or 64
|
||||||
|
assertIsPositiveNumber(cellSize, 'cellSize')
|
||||||
|
local world = setmetatable({
|
||||||
|
cellSize = cellSize,
|
||||||
|
cubes = {},
|
||||||
|
cells = {},
|
||||||
|
nonEmptyCells = {},
|
||||||
|
responses = {},
|
||||||
|
}, World_mt)
|
||||||
|
|
||||||
|
world:addResponse('touch', touch)
|
||||||
|
world:addResponse('cross', cross)
|
||||||
|
world:addResponse('slide', slide)
|
||||||
|
world:addResponse('bounce', bounce)
|
||||||
|
|
||||||
|
return world
|
||||||
|
end
|
||||||
|
|
||||||
|
bump.cube = {
|
||||||
|
getNearestCorner = cube_getNearestCorner,
|
||||||
|
getSegmentIntersectionIndices = cube_getSegmentIntersectionIndices,
|
||||||
|
getDiff = cube_getDiff,
|
||||||
|
containsPoint = cube_containsPoint,
|
||||||
|
isIntersecting = cube_isIntersecting,
|
||||||
|
getCubeDistance = cube_getCubeDistance,
|
||||||
|
detectCollision = cube_detectCollision
|
||||||
|
}
|
||||||
|
|
||||||
|
bump.responses = {
|
||||||
|
touch = touch,
|
||||||
|
cross = cross,
|
||||||
|
slide = slide,
|
||||||
|
bounce = bounce
|
||||||
|
}
|
||||||
|
|
||||||
|
return bump
|
769
gamecore/modules/world/libs/bump.lua
Normal file
769
gamecore/modules/world/libs/bump.lua
Normal file
|
@ -0,0 +1,769 @@
|
||||||
|
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
|
128
gamecore/modules/world/libs/sti/graphics.lua
Normal file
128
gamecore/modules/world/libs/sti/graphics.lua
Normal file
|
@ -0,0 +1,128 @@
|
||||||
|
local lg = love.graphics
|
||||||
|
local graphics = { isCreated = lg and true or false }
|
||||||
|
|
||||||
|
function graphics.newSpriteBatch(...)
|
||||||
|
if graphics.isCreated then
|
||||||
|
return lg.newSpriteBatch(...)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
function graphics.newCanvas(...)
|
||||||
|
if graphics.isCreated then
|
||||||
|
return lg.newCanvas(...)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
function graphics.newImage(...)
|
||||||
|
if graphics.isCreated then
|
||||||
|
return lg.newImage(...)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
function graphics.newQuad(...)
|
||||||
|
if graphics.isCreated then
|
||||||
|
return lg.newQuad(...)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
function graphics.getCanvas(...)
|
||||||
|
if graphics.isCreated then
|
||||||
|
return lg.getCanvas(...)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
function graphics.setCanvas(...)
|
||||||
|
if graphics.isCreated then
|
||||||
|
return lg.setCanvas(...)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
function graphics.clear(...)
|
||||||
|
if graphics.isCreated then
|
||||||
|
return lg.clear(...)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
function graphics.push(...)
|
||||||
|
if graphics.isCreated then
|
||||||
|
return lg.push(...)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
function graphics.origin(...)
|
||||||
|
if graphics.isCreated then
|
||||||
|
return lg.origin(...)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
function graphics.scale(...)
|
||||||
|
if graphics.isCreated then
|
||||||
|
return lg.scale(...)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
function graphics.translate(...)
|
||||||
|
if graphics.isCreated then
|
||||||
|
return lg.translate(...)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
function graphics.pop(...)
|
||||||
|
if graphics.isCreated then
|
||||||
|
return lg.pop(...)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
function graphics.draw(...)
|
||||||
|
if graphics.isCreated then
|
||||||
|
return lg.draw(...)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
function graphics.rectangle(...)
|
||||||
|
if graphics.isCreated then
|
||||||
|
return lg.rectangle(...)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
function graphics.getColor(...)
|
||||||
|
if graphics.isCreated then
|
||||||
|
return lg.getColor(...)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
function graphics.setColor(...)
|
||||||
|
if graphics.isCreated then
|
||||||
|
return lg.setColor(...)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
function graphics.line(...)
|
||||||
|
if graphics.isCreated then
|
||||||
|
return lg.line(...)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
function graphics.polygon(...)
|
||||||
|
if graphics.isCreated then
|
||||||
|
return lg.polygon(...)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
function graphics.getWidth()
|
||||||
|
if graphics.isCreated then
|
||||||
|
return lg.getWidth()
|
||||||
|
end
|
||||||
|
|
||||||
|
return 0
|
||||||
|
end
|
||||||
|
|
||||||
|
function graphics.getHeight()
|
||||||
|
if graphics.isCreated then
|
||||||
|
return lg.getHeight()
|
||||||
|
end
|
||||||
|
|
||||||
|
return 0
|
||||||
|
end
|
||||||
|
|
||||||
|
return graphics
|
1485
gamecore/modules/world/libs/sti/init.lua
Normal file
1485
gamecore/modules/world/libs/sti/init.lua
Normal file
File diff suppressed because it is too large
Load diff
303
gamecore/modules/world/libs/sti/plugins/box2d.lua
Normal file
303
gamecore/modules/world/libs/sti/plugins/box2d.lua
Normal file
|
@ -0,0 +1,303 @@
|
||||||
|
--- Box2D plugin for STI
|
||||||
|
-- @module box2d
|
||||||
|
-- @author Landon Manning
|
||||||
|
-- @copyright 2017
|
||||||
|
-- @license MIT/X11
|
||||||
|
|
||||||
|
local utils = require((...):gsub('plugins.box2d', 'utils'))
|
||||||
|
local lg = require((...):gsub('plugins.box2d', 'graphics'))
|
||||||
|
|
||||||
|
return {
|
||||||
|
box2d_LICENSE = "MIT/X11",
|
||||||
|
box2d_URL = "https://github.com/karai17/Simple-Tiled-Implementation",
|
||||||
|
box2d_VERSION = "2.3.2.6",
|
||||||
|
box2d_DESCRIPTION = "Box2D hooks for STI.",
|
||||||
|
|
||||||
|
--- Initialize Box2D physics world.
|
||||||
|
-- @param world The Box2D world to add objects to.
|
||||||
|
box2d_init = function(map, world)
|
||||||
|
assert(love.physics, "To use the Box2D plugin, please enable the love.physics module.")
|
||||||
|
|
||||||
|
local body = love.physics.newBody(world, map.offsetx, map.offsety)
|
||||||
|
local collision = {
|
||||||
|
body = body,
|
||||||
|
}
|
||||||
|
|
||||||
|
local function addObjectToWorld(objshape, vertices, userdata, object)
|
||||||
|
local shape
|
||||||
|
|
||||||
|
if objshape == "polyline" then
|
||||||
|
if #vertices == 4 then
|
||||||
|
shape = love.physics.newEdgeShape(unpack(vertices))
|
||||||
|
else
|
||||||
|
shape = love.physics.newChainShape(false, unpack(vertices))
|
||||||
|
end
|
||||||
|
else
|
||||||
|
shape = love.physics.newPolygonShape(unpack(vertices))
|
||||||
|
end
|
||||||
|
|
||||||
|
local currentBody = body
|
||||||
|
|
||||||
|
if userdata.properties.dynamic == true then
|
||||||
|
currentBody = love.physics.newBody(world, map.offsetx, map.offsety, 'dynamic')
|
||||||
|
end
|
||||||
|
|
||||||
|
local fixture = love.physics.newFixture(currentBody, shape)
|
||||||
|
|
||||||
|
fixture:setUserData(userdata)
|
||||||
|
|
||||||
|
-- Set some custom properties from userdata (or use default set by box2d)
|
||||||
|
fixture:setFriction(userdata.properties.friction or 0.2)
|
||||||
|
fixture:setRestitution(userdata.properties.restitution or 0.0)
|
||||||
|
fixture:setSensor(userdata.properties.sensor or false)
|
||||||
|
fixture:setFilterData(userdata.properties.categories or 1,
|
||||||
|
userdata.properties.mask or 65535,
|
||||||
|
userdata.properties.group or 0)
|
||||||
|
|
||||||
|
local obj = {
|
||||||
|
object = object,
|
||||||
|
body = currentBody,
|
||||||
|
shape = shape,
|
||||||
|
fixture = fixture,
|
||||||
|
}
|
||||||
|
|
||||||
|
table.insert(collision, obj)
|
||||||
|
end
|
||||||
|
|
||||||
|
local function getPolygonVertices(object)
|
||||||
|
local vertices = {}
|
||||||
|
for _, vertex in ipairs(object.polygon) do
|
||||||
|
table.insert(vertices, vertex.x)
|
||||||
|
table.insert(vertices, vertex.y)
|
||||||
|
end
|
||||||
|
|
||||||
|
return vertices
|
||||||
|
end
|
||||||
|
|
||||||
|
local function calculateObjectPosition(object, tile)
|
||||||
|
local o = {
|
||||||
|
shape = object.shape,
|
||||||
|
x = (object.dx or object.x) + map.offsetx,
|
||||||
|
y = (object.dy or object.y) + map.offsety,
|
||||||
|
w = object.width,
|
||||||
|
h = object.height,
|
||||||
|
polygon = object.polygon or object.polyline or object.ellipse or object.rectangle
|
||||||
|
}
|
||||||
|
|
||||||
|
local userdata = {
|
||||||
|
object = o,
|
||||||
|
properties = object.properties
|
||||||
|
}
|
||||||
|
|
||||||
|
if o.shape == "rectangle" then
|
||||||
|
o.r = object.rotation or 0
|
||||||
|
local cos = math.cos(math.rad(o.r))
|
||||||
|
local sin = math.sin(math.rad(o.r))
|
||||||
|
local oy = 0
|
||||||
|
|
||||||
|
if object.gid then
|
||||||
|
local tileset = map.tilesets[map.tiles[object.gid].tileset]
|
||||||
|
local lid = object.gid - tileset.firstgid
|
||||||
|
local t = {}
|
||||||
|
|
||||||
|
-- This fixes a height issue
|
||||||
|
o.y = o.y + map.tiles[object.gid].offset.y
|
||||||
|
oy = tileset.tileheight
|
||||||
|
|
||||||
|
for _, tt in ipairs(tileset.tiles) do
|
||||||
|
if tt.id == lid then
|
||||||
|
t = tt
|
||||||
|
break
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
if t.objectGroup then
|
||||||
|
for _, obj in ipairs(t.objectGroup.objects) do
|
||||||
|
-- Every object in the tile
|
||||||
|
calculateObjectPosition(obj, object)
|
||||||
|
end
|
||||||
|
|
||||||
|
return
|
||||||
|
else
|
||||||
|
o.w = map.tiles[object.gid].width
|
||||||
|
o.h = map.tiles[object.gid].height
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
o.polygon = {
|
||||||
|
{ x=o.x+0, y=o.y+0 },
|
||||||
|
{ x=o.x+o.w, y=o.y+0 },
|
||||||
|
{ x=o.x+o.w, y=o.y+o.h },
|
||||||
|
{ x=o.x+0, y=o.y+o.h }
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, vertex in ipairs(o.polygon) do
|
||||||
|
vertex.x, vertex.y = utils.rotate_vertex(map, vertex, o.x, o.y, cos, sin, oy)
|
||||||
|
end
|
||||||
|
|
||||||
|
local vertices = getPolygonVertices(o)
|
||||||
|
addObjectToWorld(o.shape, vertices, userdata, tile or object)
|
||||||
|
elseif o.shape == "ellipse" then
|
||||||
|
if not o.polygon then
|
||||||
|
o.polygon = utils.convert_ellipse_to_polygon(o.x, o.y, o.w, o.h)
|
||||||
|
end
|
||||||
|
local vertices = getPolygonVertices(o)
|
||||||
|
local triangles = love.math.triangulate(vertices)
|
||||||
|
|
||||||
|
for _, triangle in ipairs(triangles) do
|
||||||
|
addObjectToWorld(o.shape, triangle, userdata, tile or object)
|
||||||
|
end
|
||||||
|
elseif o.shape == "polygon" then
|
||||||
|
local vertices = getPolygonVertices(o)
|
||||||
|
local triangles = love.math.triangulate(vertices)
|
||||||
|
|
||||||
|
for _, triangle in ipairs(triangles) do
|
||||||
|
addObjectToWorld(o.shape, triangle, userdata, tile or object)
|
||||||
|
end
|
||||||
|
elseif o.shape == "polyline" then
|
||||||
|
local vertices = getPolygonVertices(o)
|
||||||
|
addObjectToWorld(o.shape, vertices, userdata, tile or object)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
for _, tile in pairs(map.tiles) do
|
||||||
|
if map.tileInstances[tile.gid] then
|
||||||
|
for _, instance in ipairs(map.tileInstances[tile.gid]) do
|
||||||
|
-- Every object in every instance of a tile
|
||||||
|
if tile.objectGroup then
|
||||||
|
for _, object in ipairs(tile.objectGroup.objects) do
|
||||||
|
if object.properties.collidable == true then
|
||||||
|
object.dx = instance.x + object.x
|
||||||
|
object.dy = instance.y + object.y
|
||||||
|
calculateObjectPosition(object, instance)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
-- Every instance of a tile
|
||||||
|
if tile.properties.collidable == true then
|
||||||
|
local object = {
|
||||||
|
shape = "rectangle",
|
||||||
|
x = instance.x,
|
||||||
|
y = instance.y,
|
||||||
|
width = map.tilewidth,
|
||||||
|
height = map.tileheight,
|
||||||
|
properties = tile.properties
|
||||||
|
}
|
||||||
|
|
||||||
|
calculateObjectPosition(object, instance)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
for _, layer in ipairs(map.layers) do
|
||||||
|
-- Entire layer
|
||||||
|
if layer.properties.collidable == true then
|
||||||
|
if layer.type == "tilelayer" then
|
||||||
|
for gid, tiles in pairs(map.tileInstances) do
|
||||||
|
local tile = map.tiles[gid]
|
||||||
|
local tileset = map.tilesets[tile.tileset]
|
||||||
|
|
||||||
|
for _, instance in ipairs(tiles) do
|
||||||
|
if instance.layer == layer then
|
||||||
|
local object = {
|
||||||
|
shape = "rectangle",
|
||||||
|
x = instance.x,
|
||||||
|
y = instance.y,
|
||||||
|
width = tileset.tilewidth,
|
||||||
|
height = tileset.tileheight,
|
||||||
|
properties = tile.properties
|
||||||
|
}
|
||||||
|
|
||||||
|
calculateObjectPosition(object, instance)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
elseif layer.type == "objectgroup" then
|
||||||
|
for _, object in ipairs(layer.objects) do
|
||||||
|
calculateObjectPosition(object)
|
||||||
|
end
|
||||||
|
elseif layer.type == "imagelayer" then
|
||||||
|
local object = {
|
||||||
|
shape = "rectangle",
|
||||||
|
x = layer.x or 0,
|
||||||
|
y = layer.y or 0,
|
||||||
|
width = layer.width,
|
||||||
|
height = layer.height,
|
||||||
|
properties = layer.properties
|
||||||
|
}
|
||||||
|
|
||||||
|
calculateObjectPosition(object)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
-- Individual objects
|
||||||
|
if layer.type == "objectgroup" then
|
||||||
|
for _, object in ipairs(layer.objects) do
|
||||||
|
if object.properties.collidable == true then
|
||||||
|
calculateObjectPosition(object)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
map.box2d_collision = collision
|
||||||
|
end,
|
||||||
|
|
||||||
|
--- Remove Box2D fixtures and shapes from world.
|
||||||
|
-- @param index The index or name of the layer being removed
|
||||||
|
box2d_removeLayer = function(map, index)
|
||||||
|
local layer = assert(map.layers[index], "Layer not found: " .. index)
|
||||||
|
local collision = map.box2d_collision
|
||||||
|
|
||||||
|
-- Remove collision objects
|
||||||
|
for i = #collision, 1, -1 do
|
||||||
|
local obj = collision[i]
|
||||||
|
|
||||||
|
if obj.object.layer == layer then
|
||||||
|
obj.fixture:destroy()
|
||||||
|
table.remove(collision, i)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end,
|
||||||
|
|
||||||
|
--- Draw Box2D physics world.
|
||||||
|
-- @param tx Translate on X
|
||||||
|
-- @param ty Translate on Y
|
||||||
|
-- @param sx Scale on X
|
||||||
|
-- @param sy Scale on Y
|
||||||
|
box2d_draw = function(map, tx, ty, sx, sy)
|
||||||
|
local collision = map.box2d_collision
|
||||||
|
|
||||||
|
lg.push()
|
||||||
|
lg.scale(sx or 1, sy or sx or 1)
|
||||||
|
lg.translate(math.floor(tx or 0), math.floor(ty or 0))
|
||||||
|
|
||||||
|
for _, obj in ipairs(collision) do
|
||||||
|
local points = {obj.body:getWorldPoints(obj.shape:getPoints())}
|
||||||
|
local shape_type = obj.shape:getType()
|
||||||
|
|
||||||
|
if shape_type == "edge" or shape_type == "chain" then
|
||||||
|
love.graphics.line(points)
|
||||||
|
elseif shape_type == "polygon" then
|
||||||
|
love.graphics.polygon("line", points)
|
||||||
|
else
|
||||||
|
error("sti box2d plugin does not support "..shape_type.." shapes")
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
lg.pop()
|
||||||
|
end,
|
||||||
|
}
|
||||||
|
|
||||||
|
--- Custom Properties in Tiled are used to tell this plugin what to do.
|
||||||
|
-- @table Properties
|
||||||
|
-- @field collidable set to true, can be used on any Layer, Tile, or Object
|
||||||
|
-- @field sensor set to true, can be used on any Tile or Object that is also collidable
|
||||||
|
-- @field dynamic set to true, can be used on any Tile or Object
|
||||||
|
-- @field friction can be used to define the friction of any Object
|
||||||
|
-- @field restitution can be used to define the restitution of any Object
|
||||||
|
-- @field categories can be used to set the filter Category of any Object
|
||||||
|
-- @field mask can be used to set the filter Mask of any Object
|
||||||
|
-- @field group can be used to set the filter Group of any Object
|
194
gamecore/modules/world/libs/sti/plugins/bump.lua
Normal file
194
gamecore/modules/world/libs/sti/plugins/bump.lua
Normal file
|
@ -0,0 +1,194 @@
|
||||||
|
--- Bump.lua plugin for STI
|
||||||
|
-- @module bump.lua
|
||||||
|
-- @author David Serrano (BobbyJones|FrenchFryLord)
|
||||||
|
-- @copyright 2016
|
||||||
|
-- @license MIT/X11
|
||||||
|
|
||||||
|
local lg = require((...):gsub('plugins.bump', 'graphics'))
|
||||||
|
|
||||||
|
return {
|
||||||
|
bump_LICENSE = "MIT/X11",
|
||||||
|
bump_URL = "https://github.com/karai17/Simple-Tiled-Implementation",
|
||||||
|
bump_VERSION = "3.1.6.1",
|
||||||
|
bump_DESCRIPTION = "Bump hooks for STI.",
|
||||||
|
|
||||||
|
--- Adds each collidable tile to the Bump world.
|
||||||
|
-- @param world The Bump world to add objects to.
|
||||||
|
-- @return collidables table containing the handles to the objects in the Bump world.
|
||||||
|
bump_init = function(map, world)
|
||||||
|
local collidables = {}
|
||||||
|
|
||||||
|
for _, tileset in ipairs(map.tilesets) do
|
||||||
|
for _, tile in ipairs(tileset.tiles) do
|
||||||
|
local gid = tileset.firstgid + tile.id
|
||||||
|
|
||||||
|
if map.tileInstances[gid] then
|
||||||
|
for _, instance in ipairs(map.tileInstances[gid]) do
|
||||||
|
-- Every object in every instance of a tile
|
||||||
|
if tile.objectGroup then
|
||||||
|
for _, object in ipairs(tile.objectGroup.objects) do
|
||||||
|
if object.properties.collidable == true then
|
||||||
|
local t = {
|
||||||
|
name = object.name,
|
||||||
|
type = object.type,
|
||||||
|
x = instance.x + map.offsetx + object.x,
|
||||||
|
y = instance.y + map.offsety + object.y,
|
||||||
|
width = object.width,
|
||||||
|
height = object.height,
|
||||||
|
layer = instance.layer,
|
||||||
|
properties = object.properties
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
world:add(t, t.x, t.y, t.width, t.height)
|
||||||
|
table.insert(collidables, t)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
-- Every instance of a tile
|
||||||
|
if tile.properties and tile.properties.collidable == true then
|
||||||
|
local t = {
|
||||||
|
x = instance.x + map.offsetx,
|
||||||
|
y = instance.y + map.offsety,
|
||||||
|
width = map.tilewidth,
|
||||||
|
height = map.tileheight,
|
||||||
|
layer = instance.layer,
|
||||||
|
properties = tile.properties
|
||||||
|
}
|
||||||
|
|
||||||
|
world:add(t, t.x, t.y, t.width, t.height)
|
||||||
|
table.insert(collidables, t)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
for _, layer in ipairs(map.layers) do
|
||||||
|
-- Entire layer
|
||||||
|
if layer.properties.collidable == true then
|
||||||
|
if layer.type == "tilelayer" then
|
||||||
|
for y, tiles in ipairs(layer.data) do
|
||||||
|
for x, tile in pairs(tiles) do
|
||||||
|
|
||||||
|
if tile.objectGroup then
|
||||||
|
for _, object in ipairs(tile.objectGroup.objects) do
|
||||||
|
if object.properties.collidable == true then
|
||||||
|
local t = {
|
||||||
|
name = object.name,
|
||||||
|
type = object.type,
|
||||||
|
x = ((x-1) * map.tilewidth + tile.offset.x + map.offsetx) + object.x,
|
||||||
|
y = ((y-1) * map.tileheight + tile.offset.y + map.offsety) + object.y,
|
||||||
|
width = object.width,
|
||||||
|
height = object.height,
|
||||||
|
layer = layer,
|
||||||
|
properties = object.properties
|
||||||
|
}
|
||||||
|
|
||||||
|
world:add(t, t.x, t.y, t.width, t.height)
|
||||||
|
table.insert(collidables, t)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
|
||||||
|
local t = {
|
||||||
|
x = (x-1) * map.tilewidth + tile.offset.x + map.offsetx,
|
||||||
|
y = (y-1) * map.tileheight + tile.offset.y + map.offsety,
|
||||||
|
width = tile.width,
|
||||||
|
height = tile.height,
|
||||||
|
layer = layer,
|
||||||
|
properties = tile.properties
|
||||||
|
}
|
||||||
|
|
||||||
|
world:add(t, t.x, t.y, t.width, t.height)
|
||||||
|
table.insert(collidables, t)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
elseif layer.type == "imagelayer" then
|
||||||
|
world:add(layer, layer.x, layer.y, layer.width, layer.height)
|
||||||
|
table.insert(collidables, layer)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
-- individual collidable objects in a layer that is not "collidable"
|
||||||
|
-- or whole collidable objects layer
|
||||||
|
if layer.type == "objectgroup" then
|
||||||
|
for _, obj in ipairs(layer.objects) do
|
||||||
|
if layer.properties.collidable == true or obj.properties.collidable == true then
|
||||||
|
if obj.shape == "rectangle" then
|
||||||
|
local t = {
|
||||||
|
name = obj.name,
|
||||||
|
type = obj.type,
|
||||||
|
x = obj.x + map.offsetx,
|
||||||
|
y = obj.y + map.offsety,
|
||||||
|
width = obj.width,
|
||||||
|
height = obj.height,
|
||||||
|
layer = layer,
|
||||||
|
properties = obj.properties
|
||||||
|
}
|
||||||
|
|
||||||
|
if obj.gid then
|
||||||
|
t.y = t.y - obj.height
|
||||||
|
end
|
||||||
|
|
||||||
|
world:add(t, t.x, t.y, t.width, t.height)
|
||||||
|
table.insert(collidables, t)
|
||||||
|
end -- TODO implement other object shapes?
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
end
|
||||||
|
map.bump_collidables = collidables
|
||||||
|
end,
|
||||||
|
|
||||||
|
--- Remove layer
|
||||||
|
-- @param index to layer to be removed
|
||||||
|
-- @param world bump world the holds the tiles
|
||||||
|
-- @param tx Translate on X
|
||||||
|
-- @param ty Translate on Y
|
||||||
|
-- @param sx Scale on X
|
||||||
|
-- @param sy Scale on Y
|
||||||
|
bump_removeLayer = function(map, index, world)
|
||||||
|
local layer = assert(map.layers[index], "Layer not found: " .. index)
|
||||||
|
local collidables = map.bump_collidables
|
||||||
|
|
||||||
|
-- Remove collision objects
|
||||||
|
for i = #collidables, 1, -1 do
|
||||||
|
local obj = collidables[i]
|
||||||
|
|
||||||
|
if obj.layer == layer
|
||||||
|
and (
|
||||||
|
layer.properties.collidable == true
|
||||||
|
or obj.properties.collidable == true
|
||||||
|
) then
|
||||||
|
world:remove(obj)
|
||||||
|
table.remove(collidables, i)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end,
|
||||||
|
|
||||||
|
--- Draw bump collisions world.
|
||||||
|
-- @param world bump world holding the tiles geometry
|
||||||
|
-- @param tx Translate on X
|
||||||
|
-- @param ty Translate on Y
|
||||||
|
-- @param sx Scale on X
|
||||||
|
-- @param sy Scale on Y
|
||||||
|
bump_draw = function(map, world, tx, ty, sx, sy)
|
||||||
|
lg.push()
|
||||||
|
lg.scale(sx or 1, sy or sx or 1)
|
||||||
|
lg.translate(math.floor(tx or 0), math.floor(ty or 0))
|
||||||
|
|
||||||
|
for _, collidable in pairs(map.bump_collidables) do
|
||||||
|
lg.rectangle("line", world:getRect(collidable))
|
||||||
|
end
|
||||||
|
|
||||||
|
lg.pop()
|
||||||
|
end
|
||||||
|
}
|
||||||
|
|
||||||
|
--- Custom Properties in Tiled are used to tell this plugin what to do.
|
||||||
|
-- @table Properties
|
||||||
|
-- @field collidable set to true, can be used on any Layer, Tile, or Object
|
206
gamecore/modules/world/libs/sti/utils.lua
Normal file
206
gamecore/modules/world/libs/sti/utils.lua
Normal file
|
@ -0,0 +1,206 @@
|
||||||
|
-- Some utility functions that shouldn't be exposed.
|
||||||
|
local utils = {}
|
||||||
|
|
||||||
|
-- https://github.com/stevedonovan/Penlight/blob/master/lua/pl/path.lua#L286
|
||||||
|
function utils.format_path(path)
|
||||||
|
local np_gen1,np_gen2 = '[^SEP]+SEP%.%.SEP?','SEP+%.?SEP'
|
||||||
|
local np_pat1, np_pat2 = np_gen1:gsub('SEP','/'), np_gen2:gsub('SEP','/')
|
||||||
|
local k
|
||||||
|
|
||||||
|
repeat -- /./ -> /
|
||||||
|
path,k = path:gsub(np_pat2,'/')
|
||||||
|
until k == 0
|
||||||
|
|
||||||
|
repeat -- A/../ -> (empty)
|
||||||
|
path,k = path:gsub(np_pat1,'')
|
||||||
|
until k == 0
|
||||||
|
|
||||||
|
if path == '' then path = '.' end
|
||||||
|
|
||||||
|
return path
|
||||||
|
end
|
||||||
|
|
||||||
|
-- Compensation for scale/rotation shift
|
||||||
|
function utils.compensate(tile, tileX, tileY, tileW, tileH)
|
||||||
|
local compx = 0
|
||||||
|
local compy = 0
|
||||||
|
|
||||||
|
if tile.sx < 0 then compx = tileW end
|
||||||
|
if tile.sy < 0 then compy = tileH end
|
||||||
|
|
||||||
|
if tile.r > 0 then
|
||||||
|
tileX = tileX + tileH - compy
|
||||||
|
tileY = tileY + tileH + compx - tileW
|
||||||
|
elseif tile.r < 0 then
|
||||||
|
tileX = tileX + compy
|
||||||
|
tileY = tileY - compx + tileH
|
||||||
|
else
|
||||||
|
tileX = tileX + compx
|
||||||
|
tileY = tileY + compy
|
||||||
|
end
|
||||||
|
|
||||||
|
return tileX, tileY
|
||||||
|
end
|
||||||
|
|
||||||
|
-- Cache images in main STI module
|
||||||
|
function utils.cache_image(sti, path, image)
|
||||||
|
image = image or love.graphics.newImage(path)
|
||||||
|
image:setFilter("nearest", "nearest")
|
||||||
|
sti.cache[path] = image
|
||||||
|
end
|
||||||
|
|
||||||
|
-- We just don't know.
|
||||||
|
function utils.get_tiles(imageW, tileW, margin, spacing)
|
||||||
|
imageW = imageW - margin
|
||||||
|
local n = 0
|
||||||
|
|
||||||
|
while imageW >= tileW do
|
||||||
|
imageW = imageW - tileW
|
||||||
|
if n ~= 0 then imageW = imageW - spacing end
|
||||||
|
if imageW >= 0 then n = n + 1 end
|
||||||
|
end
|
||||||
|
|
||||||
|
return n
|
||||||
|
end
|
||||||
|
|
||||||
|
-- Decompress tile layer data
|
||||||
|
function utils.get_decompressed_data(data)
|
||||||
|
local ffi = require "ffi"
|
||||||
|
local d = {}
|
||||||
|
local decoded = ffi.cast("uint32_t*", data)
|
||||||
|
|
||||||
|
for i = 0, data:len() / ffi.sizeof("uint32_t") do
|
||||||
|
table.insert(d, tonumber(decoded[i]))
|
||||||
|
end
|
||||||
|
|
||||||
|
return d
|
||||||
|
end
|
||||||
|
|
||||||
|
-- Convert a Tiled ellipse object to a LOVE polygon
|
||||||
|
function utils.convert_ellipse_to_polygon(x, y, w, h, max_segments)
|
||||||
|
local ceil = math.ceil
|
||||||
|
local cos = math.cos
|
||||||
|
local sin = math.sin
|
||||||
|
|
||||||
|
local function calc_segments(segments)
|
||||||
|
local function vdist(a, b)
|
||||||
|
local c = {
|
||||||
|
x = a.x - b.x,
|
||||||
|
y = a.y - b.y,
|
||||||
|
}
|
||||||
|
|
||||||
|
return c.x * c.x + c.y * c.y
|
||||||
|
end
|
||||||
|
|
||||||
|
segments = segments or 64
|
||||||
|
local vertices = {}
|
||||||
|
|
||||||
|
local v = { 1, 2, ceil(segments/4-1), ceil(segments/4) }
|
||||||
|
|
||||||
|
local m
|
||||||
|
if love and love.physics then
|
||||||
|
m = love.physics.getMeter()
|
||||||
|
else
|
||||||
|
m = 32
|
||||||
|
end
|
||||||
|
|
||||||
|
for _, i in ipairs(v) do
|
||||||
|
local angle = (i / segments) * math.pi * 2
|
||||||
|
local px = x + w / 2 + cos(angle) * w / 2
|
||||||
|
local py = y + h / 2 + sin(angle) * h / 2
|
||||||
|
|
||||||
|
table.insert(vertices, { x = px / m, y = py / m })
|
||||||
|
end
|
||||||
|
|
||||||
|
local dist1 = vdist(vertices[1], vertices[2])
|
||||||
|
local dist2 = vdist(vertices[3], vertices[4])
|
||||||
|
|
||||||
|
-- Box2D threshold
|
||||||
|
if dist1 < 0.0025 or dist2 < 0.0025 then
|
||||||
|
return calc_segments(segments-2)
|
||||||
|
end
|
||||||
|
|
||||||
|
return segments
|
||||||
|
end
|
||||||
|
|
||||||
|
local segments = calc_segments(max_segments)
|
||||||
|
local vertices = {}
|
||||||
|
|
||||||
|
table.insert(vertices, { x = x + w / 2, y = y + h / 2 })
|
||||||
|
|
||||||
|
for i = 0, segments do
|
||||||
|
local angle = (i / segments) * math.pi * 2
|
||||||
|
local px = x + w / 2 + cos(angle) * w / 2
|
||||||
|
local py = y + h / 2 + sin(angle) * h / 2
|
||||||
|
|
||||||
|
table.insert(vertices, { x = px, y = py })
|
||||||
|
end
|
||||||
|
|
||||||
|
return vertices
|
||||||
|
end
|
||||||
|
|
||||||
|
function utils.rotate_vertex(map, vertex, x, y, cos, sin)
|
||||||
|
if map.orientation == "isometric" then
|
||||||
|
x, y = utils.convert_isometric_to_screen(map, x, y)
|
||||||
|
vertex.x, vertex.y = utils.convert_isometric_to_screen(map, vertex.x, vertex.y)
|
||||||
|
end
|
||||||
|
|
||||||
|
vertex.x = vertex.x - x
|
||||||
|
vertex.y = vertex.y - y
|
||||||
|
|
||||||
|
return
|
||||||
|
x + cos * vertex.x - sin * vertex.y,
|
||||||
|
y + sin * vertex.x + cos * vertex.y
|
||||||
|
end
|
||||||
|
|
||||||
|
--- Project isometric position to cartesian position
|
||||||
|
function utils.convert_isometric_to_screen(map, x, y)
|
||||||
|
local mapH = map.height
|
||||||
|
local tileW = map.tilewidth
|
||||||
|
local tileH = map.tileheight
|
||||||
|
local tileX = x / tileH
|
||||||
|
local tileY = y / tileH
|
||||||
|
local offsetX = mapH * tileW / 2
|
||||||
|
|
||||||
|
return
|
||||||
|
(tileX - tileY) * tileW / 2 + offsetX,
|
||||||
|
(tileX + tileY) * tileH / 2
|
||||||
|
end
|
||||||
|
|
||||||
|
function utils.hex_to_color(hex)
|
||||||
|
if hex:sub(1, 1) == "#" then
|
||||||
|
hex = hex:sub(2)
|
||||||
|
end
|
||||||
|
|
||||||
|
return {
|
||||||
|
r = tonumber(hex:sub(1, 2), 16) / 255,
|
||||||
|
g = tonumber(hex:sub(3, 4), 16) / 255,
|
||||||
|
b = tonumber(hex:sub(5, 6), 16) / 255
|
||||||
|
}
|
||||||
|
end
|
||||||
|
|
||||||
|
function utils.pixel_function(_, _, r, g, b, a)
|
||||||
|
local mask = utils._TC
|
||||||
|
|
||||||
|
if r == mask.r and
|
||||||
|
g == mask.g and
|
||||||
|
b == mask.b then
|
||||||
|
return r, g, b, 0
|
||||||
|
end
|
||||||
|
|
||||||
|
return r, g, b, a
|
||||||
|
end
|
||||||
|
|
||||||
|
function utils.fix_transparent_color(tileset, path)
|
||||||
|
local image_data = love.image.newImageData(path)
|
||||||
|
tileset.image = love.graphics.newImage(image_data)
|
||||||
|
|
||||||
|
if tileset.transparentcolor then
|
||||||
|
utils._TC = utils.hex_to_color(tileset.transparentcolor)
|
||||||
|
|
||||||
|
image_data:mapPixel(utils.pixel_function)
|
||||||
|
tileset.image = love.graphics.newImage(image_data)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
return utils
|
84
gamecore/modules/world/libs/tsort.lua
Normal file
84
gamecore/modules/world/libs/tsort.lua
Normal file
|
@ -0,0 +1,84 @@
|
||||||
|
-- See: https://github.com/bungle/lua-resty-tsort
|
||||||
|
--
|
||||||
|
-- Copyright (c) 2016, Aapo Talvensaari
|
||||||
|
-- All rights reserved.
|
||||||
|
--
|
||||||
|
-- Redistribution and use in source and binary forms, with or without modification,
|
||||||
|
-- are permitted provided that the following conditions are met:
|
||||||
|
--
|
||||||
|
-- * Redistributions of source code must retain the above copyright notice, this
|
||||||
|
-- list of conditions and the following disclaimer.
|
||||||
|
--
|
||||||
|
-- * Redistributions in binary form must reproduce the above copyright notice, this
|
||||||
|
-- list of conditions and the following disclaimer in the documentation and/or
|
||||||
|
-- other materials provided with the distribution.
|
||||||
|
--
|
||||||
|
-- THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
|
||||||
|
-- ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
|
||||||
|
-- WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
|
||||||
|
-- DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR
|
||||||
|
-- ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
|
||||||
|
-- (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
|
||||||
|
-- LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON
|
||||||
|
-- ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
|
||||||
|
-- (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
|
||||||
|
-- SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
||||||
|
|
||||||
|
local setmetatable = setmetatable
|
||||||
|
local pairs = pairs
|
||||||
|
local type = type
|
||||||
|
local function visit(k, n, m, s)
|
||||||
|
if m[k] == 0 then return 1 end
|
||||||
|
if m[k] == 1 then return end
|
||||||
|
m[k] = 0
|
||||||
|
local f = n[k]
|
||||||
|
for i=1, #f do
|
||||||
|
if visit(f[i], n, m, s) then return 1 end
|
||||||
|
end
|
||||||
|
m[k] = 1
|
||||||
|
s[#s+1] = k
|
||||||
|
end
|
||||||
|
local tsort = {}
|
||||||
|
tsort.__index = tsort
|
||||||
|
function tsort.new()
|
||||||
|
return setmetatable({ n = {} }, tsort)
|
||||||
|
end
|
||||||
|
function tsort:add(...)
|
||||||
|
local p = { ... }
|
||||||
|
local c = #p
|
||||||
|
if c == 0 then return self end
|
||||||
|
if c == 1 then
|
||||||
|
p = p[1]
|
||||||
|
if type(p) == "table" then
|
||||||
|
c = #p
|
||||||
|
else
|
||||||
|
p = { p }
|
||||||
|
end
|
||||||
|
end
|
||||||
|
local n = self.n
|
||||||
|
for i=1, c do
|
||||||
|
local f = p[i]
|
||||||
|
if n[f] == nil then n[f] = {} end
|
||||||
|
end
|
||||||
|
for i=2, c, 1 do
|
||||||
|
local f = p[i]
|
||||||
|
local t = p[i-1]
|
||||||
|
local o = n[f]
|
||||||
|
o[#o+1] = t
|
||||||
|
end
|
||||||
|
return self
|
||||||
|
end
|
||||||
|
function tsort:sort()
|
||||||
|
local n = self.n
|
||||||
|
local s = {}
|
||||||
|
local m = {}
|
||||||
|
for k in pairs(n) do
|
||||||
|
if m[k] == nil then
|
||||||
|
if visit(k, n, m, s) then
|
||||||
|
return nil, "There is a circular dependency in the graph. It is not possible to derive a topological sort."
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
return s
|
||||||
|
end
|
||||||
|
return tsort
|
Loading…
Reference in a new issue