Pārlūkot izejas kodu

Add submodules;

bjorn 9 gadi atpakaļ
vecāks
revīzija
689273e612

+ 99 - 0
lib/deps/hardon/class.lua

@@ -0,0 +1,99 @@
+--[[
+Copyright (c) 2010-2011 Matthias Richter
+
+Permission is hereby granted, free of charge, to any person obtaining a copy
+of this software and associated documentation files (the "Software"), to deal
+in the Software without restriction, including without limitation the rights
+to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+copies of the Software, and to permit persons to whom the Software is
+furnished to do so, subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in
+all copies or substantial portions of the Software.
+
+Except as contained in this notice, the name(s) of the above copyright holders
+shall not be used in advertising or otherwise to promote the sale, use or
+other dealings in this Software without prior written authorization.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+THE SOFTWARE.
+]]--
+
+local function __NULL__() end
+
+-- class "inheritance" by copying functions
+local function inherit(class, interface, ...)
+	if not interface then return end
+	assert(type(interface) == "table", "Can only inherit from other classes.")
+
+	-- __index and construct are not overwritten as for them class[name] is defined
+	for name, func in pairs(interface) do
+		if not class[name] then
+			class[name] = func
+		end
+	end
+	for super in pairs(interface.__is_a or {}) do
+		class.__is_a[super] = true
+	end
+
+	return inherit(class, ...)
+end
+
+-- class builder
+local function new(args)
+	local super = {}
+	local name = '<unnamed class>'
+	local constructor = args or __NULL__
+	if type(args) == "table" then
+		-- nasty hack to check if args.inherits is a table of classes or a class or nil
+		super = (args.inherits or {}).__is_a and {args.inherits} or args.inherits or {}
+		name = args.name or name
+		constructor = args[1] or __NULL__
+	end
+	assert(type(constructor) == "function", 'constructor has to be nil or a function')
+
+	-- build class
+	local class = {}
+	class.__index = class
+	class.__tostring = function() return ("<instance of %s>"):format(tostring(class)) end
+	class.construct = constructor or __NULL__
+	class.inherit = inherit
+	class.__is_a = {[class] = true}
+	class.is_a = function(self, other) return not not self.__is_a[other] end
+
+	-- inherit superclasses (see above)
+	inherit(class, unpack(super))
+
+	-- syntactic sugar
+	local meta = {
+		__call = function(self, ...)
+			local obj = {}
+			setmetatable(obj, self)
+			self.construct(obj, ...)
+			return obj
+		end,
+		__tostring = function() return name end
+	}
+	return setmetatable(class, meta)
+end
+
+-- interface for cross class-system compatibility (see https://github.com/bartbes/Class-Commons).
+if common_class ~= false and not common then
+	common = {}
+	function common.class(name, prototype, parent)
+		local init = prototype.init or (parent or {}).init
+		return new{name = name, inherits = {prototype, parent}, init}
+	end
+	function common.instance(class, ...)
+		return class(...)
+	end
+end
+
+-- the module
+return setmetatable({new = new, inherit = inherit},
+	{__call = function(_,...) return new(...) end})

+ 193 - 0
lib/deps/hardon/gjk.lua

@@ -0,0 +1,193 @@
+--[[
+Copyright (c) 2012 Matthias Richter
+
+Permission is hereby granted, free of charge, to any person obtaining a copy
+of this software and associated documentation files (the "Software"), to deal
+in the Software without restriction, including without limitation the rights
+to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+copies of the Software, and to permit persons to whom the Software is
+furnished to do so, subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in
+all copies or substantial portions of the Software.
+
+Except as contained in this notice, the name(s) of the above copyright holders
+shall not be used in advertising or otherwise to promote the sale, use or
+other dealings in this Software without prior written authorization.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+THE SOFTWARE.
+]]--
+
+local _PACKAGE = (...):match("^(.+)%.[^%.]+")
+local vector  = require(_PACKAGE .. '.vector-light')
+local huge, abs = math.huge, math.abs
+
+local function support(shape_a, shape_b, dx, dy)
+	local x,y = shape_a:support(dx,dy)
+	return vector.sub(x,y, shape_b:support(-dx, -dy))
+end
+
+-- returns closest edge to the origin
+local function closest_edge(simplex)
+	local e = {dist = huge}
+
+	local i = #simplex-1
+	for k = 1,#simplex-1,2 do
+		local ax,ay = simplex[i], simplex[i+1]
+		local bx,by = simplex[k], simplex[k+1]
+		i = k
+
+		local ex,ey = vector.perpendicular(bx-ax, by-ay)
+		local nx,ny = vector.normalize(ex,ey)
+		local d = vector.dot(ax,ay, nx,ny)
+
+		if d < e.dist then
+			e.dist = d
+			e.nx, e.ny = nx, ny
+			e.i = k
+		end
+	end
+
+	return e
+end
+
+local function EPA(shape_a, shape_b, simplex)
+	-- make sure simplex is oriented counter clockwise
+	local cx,cy, bx,by, ax,ay = unpack(simplex)
+	if vector.dot(ax-bx,ay-by, cx-bx,cy-by) < 0 then
+		simplex[1],simplex[2] = ax,ay
+		simplex[5],simplex[6] = cx,cy
+	end
+
+	-- the expanding polytype algorithm
+	local is_either_circle = shape_a._center or shape_b._center
+	local last_diff_dist = huge
+	while true do
+		local e = closest_edge(simplex)
+		local px,py = support(shape_a, shape_b, e.nx, e.ny)
+		local d = vector.dot(px,py, e.nx, e.ny)
+
+		local diff_dist = d - e.dist
+		if diff_dist < 1e-6 or (is_either_circle and abs(last_diff_dist - diff_dist) < 1e-10) then
+			return -d*e.nx, -d*e.ny
+		end
+		last_diff_dist = diff_dist
+
+		-- simplex = {..., simplex[e.i-1], px, py, simplex[e.i]
+		table.insert(simplex, e.i, py)
+		table.insert(simplex, e.i, px)
+	end
+end
+
+--   :      :     origin must be in plane between A and B
+-- B o------o A   since A is the furthest point on the MD
+--   :      :     in direction of the origin.
+local function do_line(simplex)
+	local bx,by, ax,ay = unpack(simplex)
+
+	local abx,aby = bx-ax, by-ay
+
+	local dx,dy = vector.perpendicular(abx,aby)
+
+	if vector.dot(dx,dy, -ax,-ay) < 0 then
+		dx,dy = -dx,-dy
+	end
+	return simplex, dx,dy
+end
+
+-- B .'
+--  o-._  1
+--  |   `-. .'     The origin can only be in regions 1, 3 or 4:
+--  |  4   o A 2   A lies on the edge of the MD and we came
+--  |  _.-' '.     from left of BC.
+--  o-'  3
+-- C '.
+local function do_triangle(simplex)
+	local cx,cy, bx,by, ax,ay = unpack(simplex)
+	local aox,aoy = -ax,-ay
+	local abx,aby = bx-ax, by-ay
+	local acx,acy = cx-ax, cy-ay
+
+	-- test region 1
+	local dx,dy = vector.perpendicular(abx,aby)
+	if vector.dot(dx,dy, acx,acy) > 0 then
+		dx,dy = -dx,-dy
+	end
+	if vector.dot(dx,dy, aox,aoy) > 0 then
+		-- simplex = {bx,by, ax,ay}
+		simplex[1], simplex[2] = bx,by
+		simplex[3], simplex[4] = ax,ay
+		simplex[5], simplex[6] = nil, nil
+		return simplex, dx,dy
+	end
+
+	-- test region 3
+	dx,dy = vector.perpendicular(acx,acy)
+	if vector.dot(dx,dy, abx,aby) > 0 then
+		dx,dy = -dx,-dy
+	end
+	if vector.dot(dx,dy, aox, aoy) > 0 then
+		-- simplex = {cx,cy, ax,ay}
+		simplex[3], simplex[4] = ax,ay
+		simplex[5], simplex[6] = nil, nil
+		return simplex, dx,dy
+	end
+
+	-- must be in region 4
+	return simplex
+end
+
+local function GJK(shape_a, shape_b)
+	local ax,ay = support(shape_a, shape_b, 1,0)
+	if ax == 0 and ay == 0 then
+		-- only true if shape_a and shape_b are touching in a vertex, e.g.
+		--  .---                .---.
+		--  | A |           .-. | B |   support(A, 1,0)  = x
+		--  '---x---.  or  : A :x---'   support(B, -1,0) = x
+		--      | B |       `-'         => support(A,B,1,0) = x - x = 0
+		--      '---'
+		-- Since CircleShape:support(dx,dy) normalizes dx,dy we have to opt
+		-- out or the algorithm blows up. In accordance to the cases below
+		-- choose to judge this situation as not colliding.
+		return false
+	end
+
+	local simplex = {ax,ay}
+	local n = 2
+	local dx,dy = -ax,-ay
+
+	-- first iteration: line case
+	ax,ay = support(shape_a, shape_b, dx,dy)
+	if vector.dot(ax,ay, dx,dy) <= 0 then
+		return false
+	end
+
+	simplex[n+1], simplex[n+2] = ax,ay
+	simplex, dx, dy = do_line(simplex, dx, dy)
+	n = 4
+
+	-- all other iterations must be the triangle case
+	while true do
+		ax,ay = support(shape_a, shape_b, dx,dy)
+
+		if vector.dot(ax,ay, dx,dy) <= 0 then
+			return false
+		end
+
+		simplex[n+1], simplex[n+2] = ax,ay
+		simplex, dx, dy = do_triangle(simplex, dx,dy)
+		n = #simplex
+
+		if n == 6 then
+			return true, EPA(shape_a, shape_b, simplex)
+		end
+	end
+end
+
+return GJK

+ 142 - 0
lib/deps/hardon/init.lua

@@ -0,0 +1,142 @@
+--[[
+Copyright (c) 2011 Matthias Richter
+
+Permission is hereby granted, free of charge, to any person obtaining a copy
+of this software and associated documentation files (the "Software"), to deal
+in the Software without restriction, including without limitation the rights
+to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+copies of the Software, and to permit persons to whom the Software is
+furnished to do so, subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in
+all copies or substantial portions of the Software.
+
+Except as contained in this notice, the name(s) of the above copyright holders
+shall not be used in advertising or otherwise to promote the sale, use or
+other dealings in this Software without prior written authorization.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+THE SOFTWARE.
+]]--
+
+local _NAME, common_local = ..., common
+if not (type(common) == 'table' and common.class and common.instance) then
+	assert(common_class ~= false, 'No class commons specification available.')
+	require(_NAME .. '.class')
+end
+local Shapes      = require(_NAME .. '.shapes')
+local Spatialhash = require(_NAME .. '.spatialhash')
+
+-- reset global table `common' (required by class commons)
+if common_local ~= common then
+	common_local, common = common, common_local
+end
+
+local newPolygonShape = Shapes.newPolygonShape
+local newCircleShape  = Shapes.newCircleShape
+local newPointShape   = Shapes.newPointShape
+
+local HC = {}
+function HC:init(cell_size)
+	self.hash = common_local.instance(Spatialhash, cell_size or 100)
+end
+
+-- spatial hash management
+function HC:resetHash(cell_size)
+	local hash = self.hash
+	self.hash = common_local.instance(Spatialhash, cell_size or 100)
+	for shape in pairs(hash:shapes()) do
+		self.hash:register(shape, shape:bbox())
+	end
+	return self
+end
+
+function HC:register(shape)
+	self.hash:register(shape, shape:bbox())
+
+	-- keep track of where/how big the shape is
+	for _, f in ipairs({'move', 'rotate', 'scale'}) do
+		local old_function = shape[f]
+		shape[f] = function(this, ...)
+			local x1,y1,x2,y2 = this:bbox()
+			old_function(this, ...)
+			self.hash:update(this, x1,y1,x2,y2, this:bbox())
+			return this
+		end
+	end
+
+	return shape
+end
+
+function HC:remove(shape)
+	self.hash:remove(shape, shape:bbox())
+	for _, f in ipairs({'move', 'rotate', 'scale'}) do
+		shape[f] = function()
+			error(f.."() called on a removed shape")
+		end
+	end
+	return self
+end
+
+-- shape constructors
+function HC:polygon(...)
+	return self:register(newPolygonShape(...))
+end
+
+function HC:rectangle(x,y,w,h)
+	return self:polygon(x,y, x+w,y, x+w,y+h, x,y+h)
+end
+
+function HC:circle(x,y,r)
+	return self:register(newCircleShape(x,y,r))
+end
+
+function HC:point(x,y)
+	return self:register(newPointShape(x,y))
+end
+
+-- collision detection
+function HC:neighbors(shape)
+	local neighbors = self.hash:inSameCells(shape:bbox())
+	rawset(neighbors, shape, nil)
+	return neighbors
+end
+
+function HC:collisions(shape)
+	local candidates = self:neighbors(shape)
+	for other in pairs(candidates) do
+		local collides, dx, dy = shape:collidesWith(other)
+		if collides then
+			rawset(candidates, other, {dx,dy, x=dx, y=dy})
+		else
+			rawset(candidates, other, nil)
+		end
+	end
+	return candidates
+end
+
+-- the class and the instance
+HC = common_local.class('HardonCollider', HC)
+local instance = common_local.instance(HC)
+
+-- the module
+return setmetatable({
+	new       = function(...) return common_local.instance(HC, ...) end,
+	resetHash = function(...) return instance:resetHash(...) end,
+	register  = function(...) return instance:register(...) end,
+	remove    = function(...) return instance:remove(...) end,
+
+	polygon   = function(...) return instance:polygon(...) end,
+	rectangle = function(...) return instance:rectangle(...) end,
+	circle    = function(...) return instance:circle(...) end,
+	point     = function(...) return instance:point(...) end,
+
+	neighbors  = function(...) return instance:neighbors(...) end,
+	collisions = function(...) return instance:collisions(...) end,
+	hash       = function() return instance.hash end,
+}, {__call = function(_, ...) return common_local.instance(HC, ...) end})

+ 474 - 0
lib/deps/hardon/polygon.lua

@@ -0,0 +1,474 @@
+--[[
+Copyright (c) 2011 Matthias Richter
+
+Permission is hereby granted, free of charge, to any person obtaining a copy
+of this software and associated documentation files (the "Software"), to deal
+in the Software without restriction, including without limitation the rights
+to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+copies of the Software, and to permit persons to whom the Software is
+furnished to do so, subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in
+all copies or substantial portions of the Software.
+
+Except as contained in this notice, the name(s) of the above copyright holders
+shall not be used in advertising or otherwise to promote the sale, use or
+other dealings in this Software without prior written authorization.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+THE SOFTWARE.
+]]--
+
+local _PACKAGE, common_local = (...):match("^(.+)%.[^%.]+"), common
+if not (type(common) == 'table' and common.class and common.instance) then
+	assert(common_class ~= false, 'No class commons specification available.')
+	require(_PACKAGE .. '.class')
+	common_local, common = common, common_local
+end
+local vector = require(_PACKAGE .. '.vector-light')
+
+----------------------------
+-- Private helper functions
+--
+-- create vertex list of coordinate pairs
+local function toVertexList(vertices, x,y, ...)
+	if not (x and y) then return vertices end -- no more arguments
+
+	vertices[#vertices + 1] = {x = x, y = y}   -- set vertex
+	return toVertexList(vertices, ...)         -- recurse
+end
+
+-- returns true if three vertices lie on a line
+local function areCollinear(p, q, r, eps)
+	return math.abs(vector.det(q.x-p.x, q.y-p.y,  r.x-p.x,r.y-p.y)) <= (eps or 1e-32)
+end
+-- remove vertices that lie on a line
+local function removeCollinear(vertices)
+	local ret = {}
+	local i,k = #vertices - 1, #vertices
+	for l=1,#vertices do
+		if not areCollinear(vertices[i], vertices[k], vertices[l]) then
+			ret[#ret+1] = vertices[k]
+		end
+		i,k = k,l
+	end
+	return ret
+end
+
+-- get index of rightmost vertex (for testing orientation)
+local function getIndexOfleftmost(vertices)
+	local idx = 1
+	for i = 2,#vertices do
+		if vertices[i].x < vertices[idx].x then
+			idx = i
+		end
+	end
+	return idx
+end
+
+-- returns true if three points make a counter clockwise turn
+local function ccw(p, q, r)
+	return vector.det(q.x-p.x, q.y-p.y,  r.x-p.x, r.y-p.y) >= 0
+end
+
+-- test wether a and b lie on the same side of the line c->d
+local function onSameSide(a,b, c,d)
+	local px, py = d.x-c.x, d.y-c.y
+	local l = vector.det(px,py,  a.x-c.x, a.y-c.y)
+	local m = vector.det(px,py,  b.x-c.x, b.y-c.y)
+	return l*m >= 0
+end
+
+local function pointInTriangle(p, a,b,c)
+	return onSameSide(p,a, b,c) and onSameSide(p,b, a,c) and onSameSide(p,c, a,b)
+end
+
+-- test whether any point in vertices (but pqr) lies in the triangle pqr
+-- note: vertices is *set*, not a list!
+local function anyPointInTriangle(vertices, p,q,r)
+	for v in pairs(vertices) do
+		if v ~= p and v ~= q and v ~= r and pointInTriangle(v, p,q,r) then
+			return true
+		end
+	end
+	return false
+end
+
+-- test is the triangle pqr is an "ear" of the polygon
+-- note: vertices is *set*, not a list!
+local function isEar(p,q,r, vertices)
+	return ccw(p,q,r) and not anyPointInTriangle(vertices, p,q,r)
+end
+
+local function segmentsInterset(a,b, p,q)
+	return not (onSameSide(a,b, p,q) or onSameSide(p,q, a,b))
+end
+
+-- returns starting/ending indices of shared edge, i.e. if p and q share the
+-- edge with indices p1,p2 of p and q1,q2 of q, the return value is p1,q2
+local function getSharedEdge(p,q)
+	local pindex = setmetatable({}, {__index = function(t,k)
+		local s = {}
+		t[k] = s
+		return s
+	end})
+
+	-- record indices of vertices in p by their coordinates
+	for i = 1,#p do
+		pindex[p[i].x][p[i].y] = i
+	end
+
+	-- iterate over all edges in q. if both endpoints of that
+	-- edge are in p as well, return the indices of the starting
+	-- vertex
+	local i,k = #q,1
+	for k = 1,#q do
+		local v,w = q[i], q[k]
+		if pindex[v.x][v.y] and pindex[w.x][w.y] then
+			return pindex[w.x][w.y], k
+		end
+		i = k
+	end
+end
+
+-----------------
+-- Polygon class
+--
+local Polygon = {}
+function Polygon:init(...)
+	local vertices = removeCollinear( toVertexList({}, ...) )
+	assert(#vertices >= 3, "Need at least 3 non collinear points to build polygon (got "..#vertices..")")
+
+	-- assert polygon is oriented counter clockwise
+	local r = getIndexOfleftmost(vertices)
+	local q = r > 1 and r - 1 or #vertices
+	local s = r < #vertices and r + 1 or 1
+	if not ccw(vertices[q], vertices[r], vertices[s]) then -- reverse order if polygon is not ccw
+		local tmp = {}
+		for i=#vertices,1,-1 do
+			tmp[#tmp + 1] = vertices[i]
+		end
+		vertices = tmp
+	end
+
+	-- assert polygon is not self-intersecting
+	-- outer: only need to check segments #vert;1, 1;2, ..., #vert-3;#vert-2
+	-- inner: only need to check unconnected segments
+	local q,p = vertices[#vertices]
+	for i = 1,#vertices-2 do
+		p, q = q, vertices[i]
+		for k = i+1,#vertices-1 do
+			local a,b = vertices[k], vertices[k+1]
+			assert(not segmentsInterset(p,q, a,b), 'Polygon may not intersect itself')
+		end
+	end
+
+	self.vertices = vertices
+	-- make vertices immutable
+	setmetatable(self.vertices, {__newindex = function() error("Thou shall not change a polygon's vertices!") end})
+
+	-- compute polygon area and centroid
+	local p,q = vertices[#vertices], vertices[1]
+	local det = vector.det(p.x,p.y, q.x,q.y) -- also used below
+	self.area = det
+	for i = 2,#vertices do
+		p,q = q,vertices[i]
+		self.area = self.area + vector.det(p.x,p.y, q.x,q.y)
+	end
+	self.area = self.area / 2
+
+	p,q = vertices[#vertices], vertices[1]
+	self.centroid = {x = (p.x+q.x)*det, y = (p.y+q.y)*det}
+	for i = 2,#vertices do
+		p,q = q,vertices[i]
+		det = vector.det(p.x,p.y, q.x,q.y)
+		self.centroid.x = self.centroid.x + (p.x+q.x) * det
+		self.centroid.y = self.centroid.y + (p.y+q.y) * det
+	end
+	self.centroid.x = self.centroid.x / (6 * self.area)
+	self.centroid.y = self.centroid.y / (6 * self.area)
+
+	-- get outcircle
+	self._radius = 0
+	for i = 1,#vertices do
+		self._radius = math.max(self._radius,
+			vector.dist(vertices[i].x,vertices[i].y, self.centroid.x,self.centroid.y))
+	end
+end
+local newPolygon
+
+
+-- return vertices as x1,y1,x2,y2, ..., xn,yn
+function Polygon:unpack()
+	local v = {}
+	for i = 1,#self.vertices do
+		v[2*i-1] = self.vertices[i].x
+		v[2*i]   = self.vertices[i].y
+	end
+	return unpack(v)
+end
+
+-- deep copy of the polygon
+function Polygon:clone()
+	return Polygon( self:unpack() )
+end
+
+-- get bounding box
+function Polygon:bbox()
+	local ulx,uly = self.vertices[1].x, self.vertices[1].y
+	local lrx,lry = ulx,uly
+	for i=2,#self.vertices do
+		local p = self.vertices[i]
+		if ulx > p.x then ulx = p.x end
+		if uly > p.y then uly = p.y end
+
+		if lrx < p.x then lrx = p.x end
+		if lry < p.y then lry = p.y end
+	end
+
+	return ulx,uly, lrx,lry
+end
+
+-- a polygon is convex if all edges are oriented ccw
+function Polygon:isConvex()
+	local function isConvex()
+		local v = self.vertices
+		if #v == 3 then return true end
+
+		if not ccw(v[#v], v[1], v[2]) then
+			return false
+		end
+		for i = 2,#v-1 do
+			if not ccw(v[i-1], v[i], v[i+1]) then
+				return false
+			end
+		end
+		if not ccw(v[#v-1], v[#v], v[1]) then
+			return false
+		end
+		return true
+	end
+
+	-- replace function so that this will only be computed once
+	local status = isConvex()
+	self.isConvex = function() return status end
+	return status
+end
+
+function Polygon:move(dx, dy)
+	if not dy then
+		dx, dy = dx:unpack()
+	end
+	for i,v in ipairs(self.vertices) do
+		v.x = v.x + dx
+		v.y = v.y + dy
+	end
+	self.centroid.x = self.centroid.x + dx
+	self.centroid.y = self.centroid.y + dy
+end
+
+function Polygon:rotate(angle, cx, cy)
+	if not (cx and cy) then
+		cx,cy = self.centroid.x, self.centroid.y
+	end
+	for i,v in ipairs(self.vertices) do
+		-- v = (v - center):rotate(angle) + center
+		v.x,v.y = vector.add(cx,cy, vector.rotate(angle, v.x-cx, v.y-cy))
+	end
+	local v = self.centroid
+	v.x,v.y = vector.add(cx,cy, vector.rotate(angle, v.x-cx, v.y-cy))
+end
+
+function Polygon:scale(s, cx,cy)
+	if not (cx and cy) then
+		cx,cy = self.centroid.x, self.centroid.y
+	end
+	for i,v in ipairs(self.vertices) do
+		-- v = (v - center) * s + center
+		v.x,v.y = vector.add(cx,cy, vector.mul(s, v.x-cx, v.y-cy))
+	end
+	self._radius = self._radius * s
+end
+
+-- triangulation by the method of kong
+function Polygon:triangulate()
+	if #self.vertices == 3 then return {self:clone()} end
+
+	local vertices = self.vertices
+
+	local next_idx, prev_idx = {}, {}
+	for i = 1,#vertices do
+		next_idx[i], prev_idx[i] = i+1,i-1
+	end
+	next_idx[#next_idx], prev_idx[1] = 1, #prev_idx
+
+	local concave = {}
+	for i, v in ipairs(vertices) do
+		if not ccw(vertices[prev_idx[i]], v, vertices[next_idx[i]]) then
+			concave[v] = true
+		end
+	end
+
+	local triangles = {}
+	local n_vert, current, skipped, next, prev = #vertices, 1, 0
+	while n_vert > 3 do
+		next, prev = next_idx[current], prev_idx[current]
+		local p,q,r = vertices[prev], vertices[current], vertices[next]
+		if isEar(p,q,r, concave) then
+			triangles[#triangles+1] = newPolygon(p.x,p.y, q.x,q.y, r.x,r.y)
+			next_idx[prev], prev_idx[next] = next, prev
+			concave[q] = nil
+			n_vert, skipped = n_vert - 1, 0
+		else
+			skipped = skipped + 1
+			assert(skipped <= n_vert, "Cannot triangulate polygon")
+		end
+		current = next
+	end
+
+	next, prev = next_idx[current], prev_idx[current]
+	local p,q,r = vertices[prev], vertices[current], vertices[next]
+	triangles[#triangles+1] = newPolygon(p.x,p.y, q.x,q.y, r.x,r.y)
+	return triangles
+end
+
+-- return merged polygon if possible or nil otherwise
+function Polygon:mergedWith(other)
+	local p,q = getSharedEdge(self.vertices, other.vertices)
+	assert(p and q, "Polygons do not share an edge")
+
+	local ret = {}
+	for i = 1,p-1 do
+		ret[#ret+1] = self.vertices[i].x
+		ret[#ret+1] = self.vertices[i].y
+	end
+
+	for i = 0,#other.vertices-2 do
+		i = ((i-1 + q) % #other.vertices) + 1
+		ret[#ret+1] = other.vertices[i].x
+		ret[#ret+1] = other.vertices[i].y
+	end
+
+	for i = p+1,#self.vertices do
+		ret[#ret+1] = self.vertices[i].x
+		ret[#ret+1] = self.vertices[i].y
+	end
+
+	return newPolygon(unpack(ret))
+end
+
+-- split polygon into convex polygons.
+-- note that this won't be the optimal split in most cases, as
+-- finding the optimal split is a really hard problem.
+-- the method is to first triangulate and then greedily merge
+-- the triangles.
+function Polygon:splitConvex()
+	-- edge case: polygon is a triangle or already convex
+	if #self.vertices <= 3 or self:isConvex() then return {self:clone()} end
+
+	local convex = self:triangulate()
+	local i = 1
+	repeat
+		local p = convex[i]
+		local k = i + 1
+		while k <= #convex do
+			local success, merged = pcall(function() return p:mergedWith(convex[k]) end)
+			if success and merged:isConvex() then
+				convex[i] = merged
+				p = convex[i]
+				table.remove(convex, k)
+			else
+				k = k + 1
+			end
+		end
+		i = i + 1
+	until i >= #convex
+	
+	return convex
+end
+
+function Polygon:contains(x,y)
+	-- test if an edge cuts the ray
+	local function cut_ray(p,q)
+		return ((p.y > y and q.y < y) or (p.y < y and q.y > y)) -- possible cut
+			and (x - p.x < (y - p.y) * (q.x - p.x) / (q.y - p.y)) -- x < cut.x
+	end
+
+	-- test if the ray crosses boundary from interior to exterior.
+	-- this is needed due to edge cases, when the ray passes through
+	-- polygon corners
+	local function cross_boundary(p,q)
+		return (p.y == y and p.x > x and q.y < y)
+			or (q.y == y and q.x > x and p.y < y)
+	end
+
+	local v = self.vertices
+	local in_polygon = false
+	local p,q = v[#v],v[#v]
+	for i = 1, #v do
+		p,q = q,v[i]
+		if cut_ray(p,q) or cross_boundary(p,q) then
+			in_polygon = not in_polygon
+		end
+	end
+	return in_polygon
+end
+
+function Polygon:intersectionsWithRay(x,y, dx,dy)
+	local nx,ny = vector.perpendicular(dx,dy)
+	local wx,xy,det
+
+	local ts = {} -- ray parameters of each intersection
+	local q1,q2 = nil, self.vertices[#self.vertices]
+	for i = 1, #self.vertices do
+		q1,q2 = q2,self.vertices[i]
+		wx,wy = q2.x - q1.x, q2.y - q1.y
+		det = vector.det(dx,dy, wx,wy)
+
+		if det ~= 0 then
+			-- there is an intersection point. check if it lies on both
+			-- the ray and the segment.
+			local rx,ry = q2.x - x, q2.y - y
+			local l = vector.det(rx,ry, wx,wy) / det
+			local m = vector.det(dx,dy, rx,ry) / det
+			if m >= 0 and m <= 1 then
+				-- we cannot jump out early here (i.e. when l > tmin) because
+				-- the polygon might be concave
+				ts[#ts+1] = l
+			end
+		else
+			-- lines parralel or incident. get distance of line to
+			-- anchor point. if they are incident, check if an endpoint
+			-- lies on the ray
+			local dist = vector.dot(q1.x-x,q1.y-y, nx,ny)
+			if dist == 0 then
+				local l = vector.dot(dx,dy, q1.x-x,q1.y-y)
+				local m = vector.dot(dx,dy, q2.x-x,q2.y-y)
+				if l >= m then
+					ts[#ts+1] = l
+				else
+					ts[#ts+1] = m
+				end
+			end
+		end
+	end
+
+	return ts
+end
+
+function Polygon:intersectsRay(x,y, dx,dy)
+	local tmin = math.huge
+	for _, t in ipairs(self:intersectionsWithRay(x,y,dx,dy)) do
+		tmin = math.min(tmin, t)
+	end
+	return tmin ~= math.huge, tmin
+end
+
+Polygon = common_local.class('Polygon', Polygon)
+newPolygon = function(...) return common_local.instance(Polygon, ...) end
+return Polygon

+ 466 - 0
lib/deps/hardon/shapes.lua

@@ -0,0 +1,466 @@
+--[[
+Copyright (c) 2011 Matthias Richter
+
+Permission is hereby granted, free of charge, to any person obtaining a copy
+of this software and associated documentation files (the "Software"), to deal
+in the Software without restriction, including without limitation the rights
+to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+copies of the Software, and to permit persons to whom the Software is
+furnished to do so, subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in
+all copies or substantial portions of the Software.
+
+Except as contained in this notice, the name(s) of the above copyright holders
+shall not be used in advertising or otherwise to promote the sale, use or
+other dealings in this Software without prior written authorization.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+THE SOFTWARE.
+]]--
+
+local math_min, math_sqrt, math_huge = math.min, math.sqrt, math.huge
+
+local _PACKAGE, common_local = (...):match("^(.+)%.[^%.]+"), common
+if not (type(common) == 'table' and common.class and common.instance) then
+	assert(common_class ~= false, 'No class commons specification available.')
+	require(_PACKAGE .. '.class')
+end
+local vector  = require(_PACKAGE .. '.vector-light')
+local Polygon = require(_PACKAGE .. '.polygon')
+local GJK     = require(_PACKAGE .. '.gjk') -- actual collision detection
+
+-- reset global table `common' (required by class commons)
+if common_local ~= common then
+	common_local, common = common, common_local
+end
+
+--
+-- base class
+--
+local Shape = {}
+function Shape:init(t)
+	self._type = t
+	self._rotation = 0
+end
+
+function Shape:moveTo(x,y)
+	local cx,cy = self:center()
+	self:move(x - cx, y - cy)
+end
+
+function Shape:rotation()
+	return self._rotation
+end
+
+function Shape:rotate(angle)
+	self._rotation = self._rotation + angle
+end
+
+function Shape:setRotation(angle, x,y)
+	return self:rotate(angle - self._rotation, x,y)
+end
+
+--
+-- class definitions
+--
+local ConvexPolygonShape = {}
+function ConvexPolygonShape:init(polygon)
+	Shape.init(self, 'polygon')
+	assert(polygon:isConvex(), "Polygon is not convex.")
+	self._polygon = polygon
+end
+
+local ConcavePolygonShape = {}
+function ConcavePolygonShape:init(poly)
+	Shape.init(self, 'compound')
+	self._polygon = poly
+	self._shapes = poly:splitConvex()
+	for i,s in ipairs(self._shapes) do
+		self._shapes[i] = common_local.instance(ConvexPolygonShape, s)
+	end
+end
+
+local CircleShape = {}
+function CircleShape:init(cx,cy, radius)
+	Shape.init(self, 'circle')
+	self._center = {x = cx, y = cy}
+	self._radius = radius
+end
+
+local PointShape = {}
+function PointShape:init(x,y)
+	Shape.init(self, 'point')
+	self._pos = {x = x, y = y}
+end
+
+--
+-- collision functions
+--
+function ConvexPolygonShape:support(dx,dy)
+	local v = self._polygon.vertices
+	local max, vmax = -math_huge
+	for i = 1,#v do
+		local d = vector.dot(v[i].x,v[i].y, dx,dy)
+		if d > max then
+			max, vmax = d, v[i]
+		end
+	end
+	return vmax.x, vmax.y
+end
+
+function CircleShape:support(dx,dy)
+	return vector.add(self._center.x, self._center.y,
+		vector.mul(self._radius, vector.normalize(dx,dy)))
+end
+
+-- collision dispatching:
+-- let circle shape or compund shape handle the collision
+function ConvexPolygonShape:collidesWith(other)
+	if self == other then return false end
+	if other._type ~= 'polygon' then
+		local collide, sx,sy = other:collidesWith(self)
+		return collide, sx and -sx, sy and -sy
+	end
+
+	-- else: type is POLYGON
+	return GJK(self, other)
+end
+
+function ConcavePolygonShape:collidesWith(other)
+	if self == other then return false end
+	if other._type == 'point' then
+		return other:collidesWith(self)
+	end
+
+	-- TODO: better way of doing this. report all the separations?
+	local collide,dx,dy = false,0,0
+	for _,s in ipairs(self._shapes) do
+		local status, sx,sy = s:collidesWith(other)
+		collide = collide or status
+		if status then
+			if math.abs(dx) < math.abs(sx) then
+				dx = sx
+			end
+			if math.abs(dy) < math.abs(sy) then
+				dy = sy
+			end
+		end
+	end
+	return collide, dx, dy
+end
+
+function CircleShape:collidesWith(other)
+	if self == other then return false end
+	if other._type == 'circle' then
+		local px,py = self._center.x-other._center.x, self._center.y-other._center.y
+		local d = vector.len2(px,py)
+		local radii = self._radius + other._radius
+		if d < radii*radii then
+			-- if circles overlap, push it out upwards
+			if d == 0 then return true, 0,radii end
+			-- otherwise push out in best direction
+			return true, vector.mul(radii - math_sqrt(d), vector.normalize(px,py))
+		end
+		return false
+	elseif other._type == 'polygon' then
+		return GJK(self, other)
+	end
+
+	-- else: let the other shape decide
+	local collide, sx,sy = other:collidesWith(self)
+	return collide, sx and -sx, sy and -sy
+end
+
+function PointShape:collidesWith(other)
+	if self == other then return false end
+	if other._type == 'point' then
+		return (self._pos == other._pos), 0,0
+	end
+	return other:contains(self._pos.x, self._pos.y), 0,0
+end
+
+--
+-- point location/ray intersection
+--
+function ConvexPolygonShape:contains(x,y)
+	return self._polygon:contains(x,y)
+end
+
+function ConcavePolygonShape:contains(x,y)
+	return self._polygon:contains(x,y)
+end
+
+function CircleShape:contains(x,y)
+	return vector.len2(x-self._center.x, y-self._center.y) < self._radius * self._radius
+end
+
+function PointShape:contains(x,y)
+	return x == self._pos.x and y == self._pos.y
+end
+
+
+function ConcavePolygonShape:intersectsRay(x,y, dx,dy)
+	return self._polygon:intersectsRay(x,y, dx,dy)
+end
+
+function ConvexPolygonShape:intersectsRay(x,y, dx,dy)
+	return self._polygon:intersectsRay(x,y, dx,dy)
+end
+
+function ConcavePolygonShape:intersectionsWithRay(x,y, dx,dy)
+	return self._polygon:intersectionsWithRay(x,y, dx,dy)
+end
+
+function ConvexPolygonShape:intersectionsWithRay(x,y, dx,dy)
+	return self._polygon:intersectionsWithRay(x,y, dx,dy)
+end
+
+-- circle intersection if distance of ray/center is smaller
+-- than radius.
+-- with r(s) = p + d*s = (x,y) + (dx,dy) * s defining the ray and
+-- (x - cx)^2 + (y - cy)^2 = r^2, this problem is eqivalent to
+-- solving [with c = (cx,cy)]:
+--
+--     d*d s^2 + 2 d*(p-c) s + (p-c)*(p-c)-r^2 = 0
+function CircleShape:intersectionsWithRay(x,y, dx,dy)
+	local pcx,pcy = x-self._center.x, y-self._center.y
+
+	local a = vector.len2(dx,dy)
+	local b = 2 * vector.dot(dx,dy, pcx,pcy)
+	local c = vector.len2(pcx,pcy) - self._radius * self._radius
+	local discr = b*b - 4*a*c
+
+	if discr < 0 then return {} end
+
+	discr = math_sqrt(discr)
+	local ts, t1, t2 = {}, discr-b, -discr-b
+	if t1 >= 0 then ts[#ts+1] = t1/(2*a) end
+	if t2 >= 0 then ts[#ts+1] = t2/(2*a) end
+	return ts
+end
+
+function CircleShape:intersectsRay(x,y, dx,dy)
+	local tmin = math_huge
+	for _, t in ipairs(self:intersectionsWithRay(x,y,dx,dy)) do
+		tmin = math_min(t, tmin)
+	end
+	return tmin ~= math_huge, tmin
+end
+
+-- point shape intersects ray if it lies on the ray
+function PointShape:intersectsRay(x,y, dx,dy)
+	local px,py = self._pos.x-x, self._pos.y-y
+	local t = vector.dot(px,py, dx,dy) / vector.len2(dx,dy)
+	return t >= 0, t
+end
+
+function PointShape:intersectionsWithRay(x,y, dx,dy)
+	local intersects, t = self:intersectsRay(x,y, dx,dy)
+	return intersects and {t} or {}
+end
+
+--
+-- auxiliary
+--
+function ConvexPolygonShape:center()
+	return self._polygon.centroid.x, self._polygon.centroid.y
+end
+
+function ConcavePolygonShape:center()
+	return self._polygon.centroid.x, self._polygon.centroid.y
+end
+
+function CircleShape:center()
+	return self._center.x, self._center.y
+end
+
+function PointShape:center()
+	return self._pos.x, self._pos.y
+end
+
+function ConvexPolygonShape:outcircle()
+	local cx,cy = self:center()
+	return cx,cy, self._polygon._radius
+end
+
+function ConcavePolygonShape:outcircle()
+	local cx,cy = self:center()
+	return cx,cy, self._polygon._radius
+end
+
+function CircleShape:outcircle()
+	local cx,cy = self:center()
+	return cx,cy, self._radius
+end
+
+function PointShape:outcircle()
+	return self._pos.x, self._pos.y, 0
+end
+
+function ConvexPolygonShape:bbox()
+	return self._polygon:bbox()
+end
+
+function ConcavePolygonShape:bbox()
+	return self._polygon:bbox()
+end
+
+function CircleShape:bbox()
+	local cx,cy = self:center()
+	local r = self._radius
+	return cx-r,cy-r, cx+r,cy+r
+end
+
+function PointShape:bbox()
+	local x,y = self:center()
+	return x,y,x,y
+end
+
+
+function ConvexPolygonShape:move(x,y)
+	self._polygon:move(x,y)
+end
+
+function ConcavePolygonShape:move(x,y)
+	self._polygon:move(x,y)
+	for _,p in ipairs(self._shapes) do
+		p:move(x,y)
+	end
+end
+
+function CircleShape:move(x,y)
+	self._center.x = self._center.x + x
+	self._center.y = self._center.y + y
+end
+
+function PointShape:move(x,y)
+	self._pos.x = self._pos.x + x
+	self._pos.y = self._pos.y + y
+end
+
+
+function ConcavePolygonShape:rotate(angle,cx,cy)
+	Shape.rotate(self, angle)
+	if not (cx and cy) then
+		cx,cy = self:center()
+	end
+	self._polygon:rotate(angle,cx,cy)
+	for _,p in ipairs(self._shapes) do
+		p:rotate(angle, cx,cy)
+	end
+end
+
+function ConvexPolygonShape:rotate(angle, cx,cy)
+	Shape.rotate(self, angle)
+	self._polygon:rotate(angle, cx, cy)
+end
+
+function CircleShape:rotate(angle, cx,cy)
+	Shape.rotate(self, angle)
+	if not (cx and cy) then return end
+	self._center.x,self._center.y = vector.add(cx,cy, vector.rotate(angle, self._center.x-cx, self._center.y-cy))
+end
+
+function PointShape:rotate(angle, cx,cy)
+	Shape.rotate(self, angle)
+	if not (cx and cy) then return end
+	self._pos.x,self._pos.y = vector.add(cx,cy, vector.rotate(angle, self._pos.x-cx, self._pos.y-cy))
+end
+
+
+function ConcavePolygonShape:scale(s)
+	assert(type(s) == "number" and s > 0, "Invalid argument. Scale must be greater than 0")
+	local cx,cy = self:center()
+	self._polygon:scale(s, cx,cy)
+	for _, p in ipairs(self._shapes) do
+		local dx,dy = vector.sub(cx,cy, p:center())
+		p:scale(s)
+		p:moveTo(cx-dx*s, cy-dy*s)
+	end
+end
+
+function ConvexPolygonShape:scale(s)
+	assert(type(s) == "number" and s > 0, "Invalid argument. Scale must be greater than 0")
+	self._polygon:scale(s, self:center())
+end
+
+function CircleShape:scale(s)
+	assert(type(s) == "number" and s > 0, "Invalid argument. Scale must be greater than 0")
+	self._radius = self._radius * s
+end
+
+function PointShape:scale()
+	-- nothing
+end
+
+
+function ConvexPolygonShape:draw(mode)
+	mode = mode or 'line'
+	love.graphics.polygon(mode, self._polygon:unpack())
+end
+
+function ConcavePolygonShape:draw(mode, wireframe)
+	local mode = mode or 'line'
+	if mode == 'line' then
+		love.graphics.polygon('line', self._polygon:unpack())
+		if not wireframe then return end
+	end
+	for _,p in ipairs(self._shapes) do
+		love.graphics.polygon(mode, p._polygon:unpack())
+	end
+end
+
+function CircleShape:draw(mode, segments)
+	love.graphics.circle(mode or 'line', self:outcircle())
+end
+
+function PointShape:draw()
+	love.graphics.point(self:center())
+end
+
+
+Shape = common_local.class('Shape', Shape)
+ConvexPolygonShape  = common_local.class('ConvexPolygonShape',  ConvexPolygonShape,  Shape)
+ConcavePolygonShape = common_local.class('ConcavePolygonShape', ConcavePolygonShape, Shape)
+CircleShape         = common_local.class('CircleShape',         CircleShape,         Shape)
+PointShape          = common_local.class('PointShape',          PointShape,          Shape)
+
+local function newPolygonShape(polygon, ...)
+	-- create from coordinates if needed
+	if type(polygon) == "number" then
+		polygon = common_local.instance(Polygon, polygon, ...)
+	else
+		polygon = polygon:clone()
+	end
+
+	if polygon:isConvex() then
+		return common_local.instance(ConvexPolygonShape, polygon)
+	end
+
+	return common_local.instance(ConcavePolygonShape, polygon)
+end
+
+local function newCircleShape(...)
+	return common_local.instance(CircleShape, ...)
+end
+
+local function newPointShape(...)
+	return common_local.instance(PointShape, ...)
+end
+
+return {
+	ConcavePolygonShape = ConcavePolygonShape,
+	ConvexPolygonShape  = ConvexPolygonShape,
+	CircleShape         = CircleShape,
+	PointShape          = PointShape,
+	newPolygonShape     = newPolygonShape,
+	newCircleShape      = newCircleShape,
+	newPointShape       = newPointShape,
+}
+

+ 169 - 0
lib/deps/hardon/spatialhash.lua

@@ -0,0 +1,169 @@
+--[[
+Copyright (c) 2011 Matthias Richter
+
+Permission is hereby granted, free of charge, to any person obtaining a copy
+of this software and associated documentation files (the "Software"), to deal
+in the Software without restriction, including without limitation the rights
+to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+copies of the Software, and to permit persons to whom the Software is
+furnished to do so, subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in
+all copies or substantial portions of the Software.
+
+Except as contained in this notice, the name(s) of the above copyright holders
+shall not be used in advertising or otherwise to promote the sale, use or
+other dealings in this Software without prior written authorization.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+THE SOFTWARE.
+]]--
+
+local floor = math.floor
+local min, max = math.min, math.max
+
+local _PACKAGE, common_local = (...):match("^(.+)%.[^%.]+"), common
+if not (type(common) == 'table' and common.class and common.instance) then
+	assert(common_class ~= false, 'No class commons specification available.')
+	require(_PACKAGE .. '.class')
+	common_local, common = common, common_local
+end
+
+local Spatialhash = {}
+function Spatialhash:init(cell_size)
+	self.cell_size = cell_size or 100
+	self.cells = {}
+end
+
+function Spatialhash:cellCoords(x,y)
+	return floor(x / self.cell_size), floor(y / self.cell_size)
+end
+
+function Spatialhash:cell(i,k)
+	local row = rawget(self.cells, i)
+	if not row then
+		row = {}
+		rawset(self.cells, i, row)
+	end
+
+	local cell = rawget(row, k)
+	if not cell then
+		cell = setmetatable({}, {__mode = "kv"})
+		rawset(row, k, cell)
+	end
+
+	return cell
+end
+
+function Spatialhash:cellAt(x,y)
+	return self:cell(self:cellCoords(x,y))
+end
+
+ -- get all shapes
+function Spatialhash:shapes()
+	local set = {}
+	for i,row in pairs(self.cells) do
+		for k,cell in pairs(row) do
+			for obj in pairs(cell) do
+				rawset(set, obj, obj)
+			end
+		end
+	end
+	return set
+end
+
+-- get all shapes that are in the same cells as the bbox x1,y1 '--. x2,y2
+function Spatialhash:inSameCells(x1,y1, x2,y2)
+	local set = {}
+	x1, y1 = self:cellCoords(x1, y1)
+	x2, y2 = self:cellCoords(x2, y2)
+	for i = x1,x2 do
+		for k = y1,y2 do
+			for obj in pairs(self:cell(i,k)) do
+				rawset(set, obj, obj)
+			end
+		end
+	end
+	return set
+end
+
+function Spatialhash:register(obj, x1, y1, x2, y2)
+	x1, y1 = self:cellCoords(x1, y1)
+	x2, y2 = self:cellCoords(x2, y2)
+	for i = x1,x2 do
+		for k = y1,y2 do
+			rawset(self:cell(i,k), obj, obj)
+		end
+	end
+end
+
+function Spatialhash:remove(obj, x1, y1, x2,y2)
+	-- no bbox given. => must check all cells
+	if not (x1 and y1 and x2 and y2) then
+		for _,row in pairs(self.cells) do
+			for _,cell in pairs(row) do
+				rawset(cell, obj, nil)
+			end
+		end
+		return
+	end
+
+	-- else: remove only from bbox
+	x1,y1 = self:cellCoords(x1,y1)
+	x2,y2 = self:cellCoords(x2,y2)
+	for i = x1,x2 do
+		for k = y1,y2 do
+			rawset(self:cell(i,k), obj, nil)
+		end
+	end
+end
+
+-- update an objects position
+function Spatialhash:update(obj, old_x1,old_y1, old_x2,old_y2, new_x1,new_y1, new_x2,new_y2)
+	old_x1, old_y1 = self:cellCoords(old_x1, old_y1)
+	old_x2, old_y2 = self:cellCoords(old_x2, old_y2)
+
+	new_x1, new_y1 = self:cellCoords(new_x1, new_y1)
+	new_x2, new_y2 = self:cellCoords(new_x2, new_y2)
+
+	if old_x1 == new_x1 and old_y1 == new_y1 and
+	   old_x2 == new_x2 and old_y2 == new_y2 then
+		return
+	end
+
+	for i = old_x1,old_x2 do
+		for k = old_y1,old_y2 do
+			rawset(self:cell(i,k), obj, nil)
+		end
+	end
+	for i = new_x1,new_x2 do
+		for k = new_y1,new_y2 do
+			rawset(self:cell(i,k), obj, obj)
+		end
+	end
+end
+
+function Spatialhash:draw(how, show_empty, print_key)
+	if show_empty == nil then show_empty = true end
+	for k1,v in pairs(self.cells) do
+		for k2,cell in pairs(v) do
+			local is_empty = (next(cell) == nil)
+			if show_empty or not is_empty then
+				local x = k1 * self.cell_size
+				local y = k2 * self.cell_size
+				love.graphics.rectangle(how or 'line', x,y, self.cell_size, self.cell_size)
+
+				if print_key then
+					love.graphics.print(("%d:%d"):format(k1,k2), x+3,y+3)
+				end
+			end
+		end
+	end
+end
+
+return common_local.class('Spatialhash', Spatialhash)

+ 138 - 0
lib/deps/hardon/vector-light.lua

@@ -0,0 +1,138 @@
+--[[
+Copyright (c) 2012 Matthias Richter
+
+Permission is hereby granted, free of charge, to any person obtaining a copy
+of this software and associated documentation files (the "Software"), to deal
+in the Software without restriction, including without limitation the rights
+to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+copies of the Software, and to permit persons to whom the Software is
+furnished to do so, subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in
+all copies or substantial portions of the Software.
+
+Except as contained in this notice, the name(s) of the above copyright holders
+shall not be used in advertising or otherwise to promote the sale, use or
+other dealings in this Software without prior written authorization.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+THE SOFTWARE.
+]]--
+
+local sqrt, cos, sin = math.sqrt, math.cos, math.sin
+
+local function str(x,y)
+	return "("..tonumber(x)..","..tonumber(y)..")"
+end
+
+local function mul(s, x,y)
+	return s*x, s*y
+end
+
+local function div(s, x,y)
+	return x/s, y/s
+end
+
+local function add(x1,y1, x2,y2)
+	return x1+x2, y1+y2
+end
+
+local function sub(x1,y1, x2,y2)
+	return x1-x2, y1-y2
+end
+
+local function permul(x1,y1, x2,y2)
+	return x1*x2, y1*y2
+end
+
+local function dot(x1,y1, x2,y2)
+	return x1*x2 + y1*y2
+end
+
+local function det(x1,y1, x2,y2)
+	return x1*y2 - y1*x2
+end
+
+local function eq(x1,y1, x2,y2)
+	return x1 == x2 and y1 == y2
+end
+
+local function lt(x1,y1, x2,y2)
+	return x1 < x2 or (x1 == x2 and y1 < y2)
+end
+
+local function le(x1,y1, x2,y2)
+	return x1 <= x2 and y1 <= y2
+end
+
+local function len2(x,y)
+	return x*x + y*y
+end
+
+local function len(x,y)
+	return sqrt(x*x + y*y)
+end
+
+local function dist(x1,y1, x2,y2)
+	return len(x1-x2, y1-y2)
+end
+
+local function normalize(x,y)
+	local l = len(x,y)
+	return x/l, y/l
+end
+
+local function rotate(phi, x,y)
+	local c, s = cos(phi), sin(phi)
+	return c*x - s*y, s*x + c*y
+end
+
+local function perpendicular(x,y)
+	return -y, x
+end
+
+local function project(x,y, u,v)
+	local s = (x*u + y*v) / (u*u + v*v)
+	return s*u, s*v
+end
+
+local function mirror(x,y, u,v)
+	local s = 2 * (x*u + y*v) / (u*u + v*v)
+	return s*u - x, s*v - y
+end
+
+
+-- the module
+return {
+	str = str,
+
+	-- arithmetic
+	mul    = mul,
+	div    = div,
+	add    = add,
+	sub    = sub,
+	permul = permul,
+	dot    = dot,
+	det    = det,
+	cross  = det,
+
+	-- relation
+	eq = eq,
+	lt = lt,
+	le = le,
+
+	-- misc operations
+	len2          = len2,
+	len           = len,
+	dist          = dist,
+	normalize     = normalize,
+	rotate        = rotate,
+	perpendicular = perpendicular,
+	project       = project,
+	mirror        = mirror,
+}

+ 211 - 0
lib/deps/lutil/util.lua

@@ -0,0 +1,211 @@
+----------------
+-- Class
+----------------
+function new(x, ...)
+  local t = extend(x)
+  if t.init then
+    t:init(...)
+  end
+  return t
+end
+
+function extend(x)
+  local t = {}
+  setmetatable(t, {__index = x, __call = new})
+  return t
+end
+
+function class() return extend() end
+
+
+----------------
+-- Math
+----------------
+function math.sign(x) return x > 0 and 1 or x < 0 and -1 or 0 end
+function math.round(x) return math.sign(x) >= 0 and math.floor(x + .5) or math.ceil(x - .5) end
+function math.clamp(x, l, h) return math.min(math.max(x, l), h) end
+function math.lerp(x1, x2, z) return x1 + (x2 - x1) * z end
+function math.anglerp(d1, d2, z) return d1 + (math.anglediff(d1, d2) * z) end
+function math.dx(len, dir) return len * math.cos(dir) end
+function math.dy(len, dir) return len * math.sin(dir) end
+function math.distance(x1, y1, x2, y2) return ((x2 - x1) ^ 2 + (y2 - y1) ^ 2) ^ .5 end
+function math.direction(x1, y1, x2, y2) return math.atan2(y2 - y1, x2 - x1) end
+function math.vector(...) return math.distance(...), math.direction(...) end
+function math.inside(px, py, rx, ry, rw, rh) return px >= rx and px <= rx + rw and py >= ry and py <= ry + rh end
+function math.anglediff(d1, d2) return math.rad((((math.deg(d2) - math.deg(d1) % 360) + 540) % 360) - 180) end
+
+
+----------------
+-- Table
+----------------
+all = pairs
+function table.eq(t1, t2)
+  if type(t1) ~= type(t2) then return false end
+  if type(t1) ~= 'table' then return t1 == t2 end
+  if #t1 ~= #t2 then return false end
+  for k, _ in pairs(t1) do
+    if not table.eq(t1[k], t2[k]) then return false end
+  end
+  return true
+end
+
+function table.copy(x)
+  local t = type(x)
+  if t ~= 'table' then return x end
+  local y = {}
+  for k, v in next, x, nil do y[k] = table.copy(v) end
+  setmetatable(y, getmetatable(x))
+  return y
+end
+
+function table.has(t, x, deep)
+  local f = deep and table.eq or rawequal
+  for _, v in pairs(t) do if f(v, x) then return true end end
+  return false
+end
+
+function table.only(t, ks)
+  local res = {}
+  for _, k in pairs(ks) do res[k] = t[k] end
+  return res
+end
+
+function table.except(t, ks)
+  local res = table.copy(t)
+  for _, k in pairs(ks) do res[k] = nil end
+  return res
+end
+
+function table.keys(t)
+  local res = {}
+  table.each(t, function(_, k) table.insert(res, k) end)
+  return res
+end
+
+function table.values(t)
+  local res = {}
+  table.each(t, function(v) table.insert(res, v) end)
+  return res
+end
+
+function table.take(t, n)
+  local res = {}
+  for i = 1, n do res[i] = t[i] end
+  return res
+end
+
+function table.drop(t, n)
+  local res = table.copy(t)
+  for i = 1, n do table.remove(t, 1) end
+  return res
+end
+
+function table.each(t, f)
+  if not t then return end
+  for k, v in pairs(t) do if f(v, k) then break end end
+end
+
+function table.with(t, k, ...)
+  return table.each(t, f.egoexe(k, ...))
+end
+
+function table.map(t, f)
+  if not t then return end
+  local res = {}
+  table.each(t, function(v, k) res[k] = f(v, k) end)
+  return res
+end
+
+function table.filter(t, f)
+  return table.map(t, function(v, k) return f(v, k) and v or nil end)
+end
+
+function table.clear(t, v)
+  table.each(t, function(_, k) t[k] = v end)
+end
+
+function table.merge(t1, t2, shallow)
+  t1, t2 = t1 or {}, t2 or {}
+  for k, v in pairs(t1) do t2[k] = shallow and v or table.copy(v) end
+  return t2
+end
+
+function table.deltas(t1, t2)
+  local res = {}
+  for k, v in pairs(t1) do
+    if type(t1[k]) ~= type(t2[k]) then
+      res[k] = type(t2[k]) == 'table' and table.copy(t2[k]) or t2[k]
+    elseif type(t2[k]) == 'table' then
+      res[k] = table.deltas(t1[k], t2[k])
+    elseif t1[k] ~= t2[k] then
+      res[k] = t2[k]
+    end
+  end
+  
+  return res
+end
+
+function table.interpolate(t1, t2, z)
+  local interp = table.copy(t1)
+  for k, v in pairs(interp) do
+    if t2[k] then
+      if type(v) == 'table' then interp[k] = table.interpolate(t1[k], t2[k], z)
+      elseif type(v) == 'number' then
+        if k == 'angle' then interp[k] = math.anglerp(t1[k], t2[k], z)
+        else interp[k] = math.lerp(t1[k], t2[k], z) end
+      end
+    end
+  end
+  return interp
+end
+
+function table.shuffle(t)
+  for i = 1, #t do
+    local a, b = math.random(#t), math.random(#t)
+    t[a], t[b] = t[b], t[a]
+  end
+end
+
+function table.count(t)
+  local ct = 0
+  table.each(t, function() ct = ct + 1 end)
+  return ct
+end
+
+function table.print(t, n)
+  n = n or 0
+  if n > 10 then return end
+  if t == nil then print('nil') end
+  if type(t) ~= 'table' then io.write(tostring(t)) io.write('\n')
+  else
+    local empty = true
+    for k, v in pairs(t) do
+      empty = false
+      io.write(string.rep('\t', n))
+      io.write(k)
+      if type(v) == 'table' then io.write('\n')
+      else io.write('\t') end
+      table.print(v, n + 1)
+    end
+    if empty then io.write(string.rep('\t', n) .. '{}\n') end
+  end
+end
+
+
+----------------
+-- Functions
+----------------
+f = {}
+f.empty = function() end
+f.exe = function(x, ...) if type(x) == 'function' then return x(...) end return x end
+f.ego = function(f, ...) local a = {...} return function(x) x[f](x, unpack(a)) end end
+f.egoexe = function(f, ...) local a = {...} return function(x) if x[f] then x[f](x, unpack(a)) end end end
+f.val = function(x) return type(x) == 'function' and x or function() return x end end
+f.cur = function(fn, x) return function(y) return fn(x, y) end end
+f.wrap = function(fn, ...) local a = {...} return function() fn(unpack(a)) end end
+
+
+----------------
+-- String
+----------------
+string.capitalize = function(s) s = ' ' .. s return s:gsub('(%s%l)', string.upper):sub(2) end

+ 312 - 0
lib/deps/richtext/richtext.lua

@@ -0,0 +1,312 @@
+-- richtext library
+
+--[[
+Copyright (c) 2010 Robin Wellner
+Copyright (c) 2014 Florian Fischer (class changes, initial color, ...)
+
+This software is provided 'as-is', without any express or implied
+warranty. In no event will the authors be held liable for any damages
+arising from the use of this software.
+
+Permission is granted to anyone to use this software for any purpose,
+including commercial applications, and to alter it and redistribute it
+freely, subject to the following restrictions:
+
+   1. The origin of this software must not be misrepresented; you must not
+   claim that you wrote the original software. If you use this software
+   in a product, an acknowledgment in the product documentation would be
+   appreciated but is not required.
+
+   2. Altered source versions must be plainly marked as such, and must not be
+   misrepresented as being the original software.
+
+   3. This notice may not be removed or altered from any source
+   distribution.
+]]
+
+-- issues/bugs:
+--  * still under-tested
+--  * word wrapping might not be optimal
+--  * words keep their final space in wrapping, which may cause words to be wrapped too soon
+
+local rich = {}
+rich.__index = rich
+
+function rich:new(t, stdcolor) -- syntax: rt = rich.new{text, width, resource1 = ..., ...}
+	local obj = setmetatable({parsedtext = {}, resources = {}}, rich)
+	obj.width = t[2]
+	obj.hardwrap = false
+	obj:extract(t)
+	obj:parse(t)
+	-- set text standard color
+	if stdcolor and type(stdcolor) =='table' then love.graphics.setColor( unpack(stdcolor) ) end
+	if love.graphics.isSupported and love.graphics.isSupported('canvas') then
+		obj:render()
+		obj:render(true)
+	end
+	return obj
+end
+
+function rich:draw(x, y)
+	local firstR, firstG, firstB, firstA = love.graphics.getColor()
+	love.graphics.setColor(255, 255, 255, 255)
+	local prevMode = love.graphics.getBlendMode()
+	if self.framebuffer then
+		love.graphics.setBlendMode("premultiplied")
+		love.graphics.draw(self.framebuffer, x, y)
+		love.graphics.setBlendMode(prevMode)
+	else
+		love.graphics.push()
+		love.graphics.translate(x, y)
+		self:render()
+		love.graphics.pop()
+	end
+	love.graphics.setColor(firstR, firstG, firstB, firstA)
+end
+
+function rich:extract(t)
+	if t[3] and type(t[3]) == 'table' then
+		for key,value in pairs(t[3]) do
+			local meta = type(value) == 'table' and value or {value}
+			self.resources[key] = self:initmeta(meta) -- sets default values, does a PO2 fix...
+		end
+	else
+		for key,value in pairs(t) do
+			if type(key) == 'string' then
+				local meta = type(value) == 'table' and value or {value}
+				self.resources[key] = self:initmeta(meta) -- sets default values, does a PO2 fix...
+			end
+		end
+	end
+end
+
+local function parsefragment(parsedtext, textfragment)
+	-- break up fragments with newlines
+	local n = textfragment:find('\n', 1, true)
+	while n do
+		table.insert(parsedtext, textfragment:sub(1, n-1))
+		table.insert(parsedtext, {type='nl'})
+		textfragment = textfragment:sub(n + 1)
+		n = textfragment:find('\n', 1, true)
+	end
+	table.insert(parsedtext, textfragment)
+end
+
+function rich:parse(t)
+	local text = t[1]
+	if string.len(text) > 0 then 
+		-- look for {tags} or [tags]
+		for textfragment, foundtag in text:gmatch'([^{]*){(.-)}' do
+			parsefragment(self.parsedtext, textfragment)
+			table.insert(self.parsedtext, self.resources[foundtag] or foundtag)
+		end
+		parsefragment(self.parsedtext, text:match('[^}]+$'))
+	end
+end
+
+-- [[ since 0.8.0, no autopadding needed any more
+local log2 = 1/math.log(2)
+local function nextpo2(n)
+	return math.pow(2, math.ceil(math.log(n)*log2))
+end
+
+local metainit = {}
+function metainit.Image(res, meta)
+	meta.type = 'img'
+	local w, h = res:getWidth(), res:getHeight()
+	--[[ since 0.8.0, no autopadding needed any more
+	if not rich.nopo2 then
+		local neww = nextpo2(w)
+		local newh = nextpo2(h)
+		if neww ~= w or newh ~= h then
+			local padded = love.image.newImageData(wp, hp)
+			padded:paste(love.image.newImageData(res), 0, 0)
+			meta[1] = love.graphics.newImage(padded)
+		end
+	end
+	]]
+	meta.width = meta.width or w
+	meta.height = meta.height or h
+end
+function metainit.Font(res, meta)
+	meta.type = 'font'
+end
+function metainit.number(res, meta)
+	meta.type = 'color'
+end
+
+function rich:initmeta(meta)
+	local res = meta[1]
+	local type = (type(res) == 'userdata') and res:type() or type(res)
+	if metainit[type] then
+		metainit[type](res, meta)
+	else
+		error("Unsupported type")
+	end
+	return meta
+end
+
+local function wrapText(parsedtext, fragment, lines, maxheight, x, width, i, fnt, hardwrap)
+	if not hardwrap or (hardwrap and x > 0) then
+		-- find first space, split again later if necessary
+		local n = fragment:find(' ', 1, true)
+		local lastn = n
+		while n do
+			local newx = x + fnt:getWidth(fragment:sub(1, n-1))
+			if newx > width then
+				break
+			end
+			lastn = n
+			n = fragment:find(' ', n + 1, true)
+		end
+		n = lastn or (#fragment + 1)
+		-- wrapping
+		parsedtext[i] = fragment:sub(1, n-1)
+		table.insert(parsedtext, i+1, fragment:sub((fragment:find('[^ ]', n) or (n+1)) - 1))
+		lines[#lines].height = maxheight
+		maxheight = 0
+		x = 0
+		table.insert(lines, {})
+	end
+	
+	return maxheight, 0
+end
+
+local function renderText(parsedtext, fragment, lines, maxheight, x, width, i, hardwrap)
+	local fnt = love.graphics.getFont() or love.graphics.newFont(12)
+	if x + fnt:getWidth(fragment) > width then -- oh oh! split the text
+		maxheight, x = wrapText(parsedtext, fragment, lines, maxheight, x, width, i, fnt, hardwrap)
+	end
+
+	-- hardwrap long words
+	if hardwrap and x + fnt:getWidth(parsedtext[i]) > width then
+		local n = #parsedtext[i]
+		while x + fnt:getWidth(parsedtext[i]:sub(1, n)) > width do
+			n = n - 1
+		end
+		local p1, p2 = parsedtext[i]:sub(1, n - 1), parsedtext[i]:sub(n)
+		parsedtext[i] = p1
+		if not parsedtext[i + 1] then
+			parsedtext[i + 1] = p2
+		elseif type(parsedtext[i + 1]) == 'string' then
+			parsedtext[i + 1] = p2 .. parsedtext[i + 1]
+		elseif type(parsedtext[i + 1]) == 'table' then
+			table.insert(parsedtext, i + 2, p2)
+			table.insert(parsedtext, i + 3, {type='nl'})
+		end
+		lines[#lines].height = maxheight
+		maxheight = 0
+		x = 0
+		table.insert(lines, {})
+	end
+
+	local h = math.floor(fnt:getHeight(parsedtext[i]) * fnt:getLineHeight())
+	maxheight = math.max(maxheight, h)
+	return maxheight, x + fnt:getWidth(parsedtext[i]), {parsedtext[i], x = x > 0 and x or 0, type = 'string', height = h, width = fnt:getWidth(parsedtext[i])}
+end
+
+local function renderImage(fragment, lines, maxheight, x, width)
+	local newx = x + fragment.width
+	if newx > width and x > 0 then -- wrapping
+		lines[#lines].height = maxheight
+		maxheight = 0
+		x = 0
+		table.insert(lines, {})
+	end
+	maxheight = math.max(maxheight, fragment.height)
+	return maxheight, newx, {fragment, x = x, type = 'img'}
+end
+
+local function doRender(parsedtext, width, hardwrap)
+	local x = 0
+	local lines = {{}}
+	local maxheight = 0
+	for i, fragment in ipairs(parsedtext) do -- prepare rendering
+		if type(fragment) == 'string' then
+			maxheight, x, fragment = renderText(parsedtext, fragment, lines, maxheight, x, width, i, hardwrap)
+		elseif fragment.type == 'img' then
+			maxheight, x, fragment = renderImage(fragment, lines, maxheight, x, width)
+		elseif fragment.type == 'font' then
+			love.graphics.setFont(fragment[1])
+		elseif fragment.type == 'nl' then
+			-- move onto next line, reset x and maxheight
+			lines[#lines].height = maxheight
+			maxheight = 0
+			x = 0
+			table.insert(lines, {})
+			-- don't want nl inserted into line
+			fragment = ''
+		end
+		table.insert(lines[#lines], fragment)
+	end
+--~	 for i,f in ipairs(parsedtext) do
+--~		 print(f)
+--~	 end
+	lines[#lines].height = maxheight
+	return lines
+end
+
+local function doDraw(lines)
+	local y = 0
+	local colorr,colorg,colorb,colora = love.graphics.getColor()
+	for i, line in ipairs(lines) do -- do the actual rendering
+		y = y + line.height
+		for j, fragment in ipairs(line) do
+			if fragment.type == 'string' then
+				-- remove leading spaces, but only at the begin of a new line
+				-- Note: the check for fragment 2 (j==2) is to avoid a sub for leading line space
+				if j==2 and string.sub(fragment[1], 1, 1) == ' ' then
+					fragment[1] = string.sub(fragment[1], 2)
+				end
+				love.graphics.print(fragment[1], fragment.x, y - fragment.height)
+				if rich.debug then
+					love.graphics.rectangle('line', fragment.x, y - fragment.height, fragment.width, fragment.height)
+				end
+			elseif fragment.type == 'img' then
+				love.graphics.setColor(255,255,255)
+				love.graphics.draw(fragment[1][1], fragment.x, y - fragment[1].height)
+				if rich.debug then
+					love.graphics.rectangle('line', fragment.x, y - fragment[1].height, fragment[1].width, fragment[1].height)
+				end
+				love.graphics.setColor(colorr,colorg,colorb,colora)
+			elseif fragment.type == 'font' then
+				love.graphics.setFont(fragment[1])
+			elseif fragment.type == 'color' then
+				love.graphics.setColor(unpack(fragment))
+				colorr,colorg,colorb,colora = love.graphics.getColor()
+			end
+		end
+
+	end
+end
+
+function rich:calcHeight(lines)
+	local h = 0
+	for _, line in ipairs(lines) do
+		h = h + line.height
+	end
+	return h
+end
+
+function rich:render(usefb)
+	local renderWidth = self.width or math.huge -- if not given, use no wrapping
+	local firstFont = love.graphics.getFont() or love.graphics.newFont(12)
+	local firstR, firstG, firstB, firstA = love.graphics.getColor()
+	local lines = doRender(self.parsedtext, renderWidth, self.hardwrap)
+	-- dirty hack, add half height of last line to bottom of height to ensure tails of y's and g's, etc fit in properly.
+	self.height = self:calcHeight(lines) + math.floor((lines[#lines].height / 2) + 0.5)
+	local fbWidth = math.max(nextpo2(math.max(love.graphics.getWidth(), self.width or 0)), nextpo2(math.max(love.graphics.getHeight(), self.height)))
+	local fbHeight = fbWidth
+	love.graphics.setFont(firstFont)
+	if usefb then
+		self.framebuffer = love.graphics.newCanvas(fbWidth, fbHeight)
+		self.framebuffer:setFilter( 'nearest', 'nearest' )
+		self.framebuffer:renderTo(function () doDraw(lines) end)
+	else
+		self.height = doDraw(lines)
+	end
+	love.graphics.setFont(firstFont)
+	love.graphics.setColor(firstR, firstG, firstB, firstA)
+end
+
+return rich

+ 188 - 0
lib/deps/slam/slam.lua

@@ -0,0 +1,188 @@
+-- Simple LÖVE Audio Manager
+--
+-- Copyright (c) 2011 Matthias Richter
+-- Permission is hereby granted, free of charge, to any person obtaining a copy
+-- of this software and associated documentation files (the "Software"), to deal
+-- in the Software without restriction, including without limitation the rights
+-- to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+-- copies of the Software, and to permit persons to whom the Software is
+-- furnished to do so, subject to the following conditions:
+--
+-- The above copyright notice and this permission notice shall be included in
+-- all copies or substantial portions of the Software.
+--
+-- Except as contained in this notice, the name(s) of the above copyright holders
+-- shall not be used in advertising or otherwise to promote the sale, use or
+-- other dealings in this Software without prior written authorization.
+--
+-- THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+-- IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+-- FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+-- AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+-- LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+-- OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+-- THE SOFTWARE.
+--
+
+local newInstance = love.audio.newSource
+local stop = love.audio.stop
+
+------------------
+-- source class --
+------------------
+local Source = {}
+Source.__index = Source
+Source.__newindex = function(_,k) error(('Cannot write key %s'):format(tostring(k))) end
+
+local function remove_stopped(sources)
+	local remove = {}
+	for s in pairs(sources) do
+		remove[s] = true
+	end
+	for s in pairs(remove) do
+		sources[s] = nil
+	end
+end
+
+local function get_target(target)
+	if type(target) == 'table' then
+		return target[math.random(1,#target)]
+	end
+	return target
+end
+
+local play_instance, stop_instance
+function Source:play()
+	remove_stopped(self.instances)
+	if self._paused then self:stop() end
+	local instance = newInstance(get_target(self.target), self.how)
+
+	-- overwrite instance:stop() and instance:play()
+	if not (play_instance and stop_instance) then
+		play_instance = getmetatable(instance).play
+		getmetatable(instance).play = error
+
+		stop_instance = getmetatable(instance).stop
+		getmetatable(instance).stop = function(this)
+			stop_instance(this)
+			self.instances[this] = nil
+		end
+	end
+
+	instance:setLooping(self.looping)
+	instance:setPitch(self.pitch)
+	instance:setVolume(self.volume)
+
+	self.instances[instance] = instance
+	play_instance(instance)
+	return instance
+end
+
+function Source:stop()
+	for s in pairs(self.instances) do
+		s:stop()
+	end
+	self._paused = false
+	self.instances = {}
+end
+
+function Source:pause()
+	if self._paused then return end
+	for s in pairs(self.instances) do
+		s:pause()
+	end
+	self._paused = true
+end
+
+function Source:resume()
+	if not self._paused then return end
+	for s in pairs(self.instances) do
+		s:resume()
+	end
+	self._paused = false
+end
+
+function Source:addTags(tag, ...)
+	if not tag then return end
+	love.audio.tags[tag][self] = self
+	return Source.addTags(self, ...)
+end
+
+function Source:removeTags(tag, ...)
+	if not tag then return end
+	love.audio.tags[tag][self] = nil
+	return Source.removeTags(self, ...)
+end
+
+function Source:isStatic()
+	return self.how ~= "stream"
+end
+
+-- getter/setter for looping, pitch and volume
+for _, property in ipairs{'looping', 'pitch', 'volume'} do
+	local name = property:sub(1,1):upper() .. property:sub(2)
+	Source['get' .. name] = function(self)
+		return self[property]
+	end
+
+	Source['set' .. name] = function(self, val)
+		self[property] = val
+		for s in pairs(self.instances) do
+			s['set' .. name](s, val)
+		end
+	end
+end
+Source.isLooping = Source.getLooping
+
+--------------------------
+-- love.audio interface --
+--------------------------
+function love.audio.newSource(target, how)
+	local s = {
+		_paused   = false,
+		target    = target,
+		how       = how,
+		instances = {},
+		looping   = false,
+		pitch     = 1,
+		volume    = 1,
+	}
+	if how == 'static' and type(target) == 'string' then
+		s.target = love.sound.newSoundData(target)
+	end
+	love.audio.tags.all[s] = s
+	return setmetatable(s, Source)
+end
+
+function love.audio.play(source)
+	assert(source and source.instances, "Can only play source objects.")
+	return source:play()
+end
+
+function love.audio.stop(source)
+	if source and source.stop then return source:stop() end
+	stop()
+end
+
+----------
+-- tags --
+----------
+local Tag = { __mode = "kv" }
+function Tag:__index(func)
+	-- calls a function on all tagged sources
+	return function(...)
+		for s in pairs(self) do
+			assert(type(s[func]) == "function", ("`%s' does not name a function."):format(func))
+			s[func](s, ...)
+		end
+	end
+end
+
+love.audio.tags = setmetatable({}, {
+	__newindex = error,
+	__index = function(t,k)
+		local tag = setmetatable({}, Tag)
+		rawset(t, k, tag)
+		return tag
+	end,
+})

+ 0 - 2
require.lua

@@ -17,5 +17,3 @@ load 'lib/core'
 load 'lib'
 load 'lib/editor'
 load 'lib/goregous'
-
-require 'test/runner'