initial commit

This commit is contained in:
Sebastian Hugentobler 2014-04-03 09:15:25 +02:00
commit 47525f364d
28 changed files with 3181 additions and 0 deletions

2
.gitignore vendored Normal file
View File

@ -0,0 +1,2 @@
build/*
.DS_Store

20
Makefile Normal file
View File

@ -0,0 +1,20 @@
ZIP := zip
LOVE := love
NAME := ds_alpha
VERSION := 0.0.1
SRCDIR := src
BUILDDIR := build
.PHONY : all
all : love
love :
cd $(SRCDIR)/ && \
$(ZIP) -9 -r ../$(BUILDDIR)/$(NAME)_$(VERSION).love .
run :
$(LOVE) $(SRCDIR)/
clean :
rm -rf $(BUILDDIR)/*

30
src/30log.lua Executable file
View File

@ -0,0 +1,30 @@
local assert, pairs, type, tostring, setmetatable = assert, pairs, type, tostring, setmetatable
local baseMt, _instances, _classes, class = {}, setmetatable({},{__mode='k'}), setmetatable({},{__mode='k'})
local function deep_copy(t, dest, aType)
local t, r = t or {}, dest or {}
for k,v in pairs(t) do
if aType and type(v)==aType then r[k] = v elseif not aType then
if type(v) == 'table' and k ~= "__index" then r[k] = deep_copy(v) else r[k] = v end
end
end; return r
end
local function instantiate(self,...)
assert(_classes[self],'new() should be called from a class.')
local instance = deep_copy(self) ; _instances[instance] = tostring(instance); setmetatable(instance,self)
if self.__init then if type(self.__init) == 'table' then deep_copy(self.__init, instance) else self.__init(instance, ...) end; end; return instance
end
local function extends(self,extra_params)
local heir = {}; _classes[heir] = tostring(heir); deep_copy(extra_params, deep_copy(self, heir));
heir.__index, heir.super = heir, self; return setmetatable(heir,self)
end
baseMt = { __call = function (self,...) return self:new(...) end, __tostring = function(self,...)
if _instances[self] then return ('object(of %s):<%s>'):format((rawget(getmetatable(self),'__name') or '?'), _instances[self]) end
return _classes[self] and ('class(%s):<%s>'):format((rawget(self,'__name') or '?'),_classes[self]) or self
end}
class = function(attr)
local c = deep_copy(attr) ; _classes[c] = tostring(c);
c.include = function(self,include) assert(_classes[self], 'Mixins can only be used on classes.'); return deep_copy(include, self, 'function') end
c.new, c.extends, c.__index, c.__call, c.__tostring = instantiate, extends, c, baseMt.__call, baseMt.__tostring;
c.is = function(self, kind) local super; while true do super = getmetatable(super or self) ; if super == kind or super == nil then break end ; end;
return kind and (super == kind) end; return setmetatable(c,baseMt)
end; return class

Binary file not shown.

After

Width:  |  Height:  |  Size: 74 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 74 KiB

285
src/assets/map.lua Normal file
View File

@ -0,0 +1,285 @@
return {
version = "1.1",
luaversion = "5.1",
orientation = "orthogonal",
width = 20,
height = 20,
tilewidth = 32,
tileheight = 32,
properties = {},
tilesets = {
{
name = "atlas00",
firstgid = 1,
tilewidth = 32,
tileheight = 32,
spacing = 0,
margin = 0,
image = "tiles/atlas00.png",
imagewidth = 1024,
imageheight = 1024,
tileoffset = {
x = 0,
y = 0
},
properties = {},
tiles = {}
},
{
name = "atlas01",
firstgid = 1025,
tilewidth = 32,
tileheight = 32,
spacing = 0,
margin = 0,
image = "tiles/atlas01.png",
imagewidth = 1024,
imageheight = 1024,
tileoffset = {
x = 0,
y = 0
},
properties = {},
tiles = {}
},
{
name = "atlas03",
firstgid = 2049,
tilewidth = 32,
tileheight = 32,
spacing = 0,
margin = 0,
image = "tiles/atlas03.png",
imagewidth = 1024,
imageheight = 1024,
tileoffset = {
x = 0,
y = 0
},
properties = {},
tiles = {}
},
{
name = "atlas04",
firstgid = 3073,
tilewidth = 32,
tileheight = 32,
spacing = 0,
margin = 0,
image = "tiles/atlas04.png",
imagewidth = 1024,
imageheight = 1024,
tileoffset = {
x = 0,
y = 0
},
properties = {},
tiles = {}
}
},
layers = {
{
type = "tilelayer",
name = "background",
x = 0,
y = 0,
width = 20,
height = 20,
visible = true,
opacity = 1,
properties = {},
encoding = "lua",
data = {
3204, 3204, 3204, 3204, 3204, 3204, 3204, 3204, 3204, 3204, 3204, 3204, 3204, 3204, 3204, 3204, 3204, 3204, 3204, 3204,
3204, 3201, 3202, 3205, 3205, 3205, 3205, 3205, 3205, 3205, 3205, 3205, 3205, 3205, 3205, 3205, 3205, 3206, 3207, 3204,
3204, 3233, 3234, 3237, 3235, 3237, 3237, 3236, 3237, 3237, 3235, 3237, 3236, 3237, 3237, 3235, 3237, 3238, 3239, 3204,
3204, 3265, 3266, 3299, 3299, 3299, 3299, 3299, 3299, 3299, 3299, 3299, 3299, 3299, 3299, 3299, 3299, 3270, 3271, 3204,
3204, 3297, 3298, 3299, 3299, 3299, 3299, 3299, 3299, 3299, 3299, 3299, 3299, 3299, 3299, 3299, 3299, 3302, 3303, 3204,
3204, 3297, 3298, 3299, 3299, 3299, 3299, 3299, 3299, 3299, 3299, 3299, 3299, 3299, 3299, 3299, 3299, 3302, 3303, 3204,
3204, 3297, 3298, 3299, 3299, 3299, 3299, 3299, 3299, 3299, 3299, 3299, 3299, 3299, 3299, 3299, 3299, 3302, 3303, 3204,
3204, 3297, 3298, 3299, 3299, 3299, 3299, 3299, 3299, 3299, 3299, 3299, 3299, 3299, 3299, 3299, 3299, 3302, 3303, 3204,
3204, 3297, 3298, 3299, 3299, 3299, 3299, 3299, 3299, 3299, 3299, 3299, 3299, 3299, 3299, 3299, 3299, 3302, 3303, 3204,
3204, 3297, 3298, 3299, 3299, 3299, 3299, 3299, 3299, 3299, 3299, 3299, 3299, 3299, 3299, 3299, 3299, 564, 3303, 3204,
3204, 3297, 3298, 3299, 3299, 3299, 3299, 3299, 3299, 3299, 3299, 3299, 3299, 3299, 3299, 3299, 3299, 3302, 3303, 3204,
3204, 3297, 3298, 3299, 3299, 3299, 3299, 3299, 3299, 3299, 3299, 3299, 3299, 3299, 3299, 3299, 3299, 3302, 3303, 3204,
3204, 3297, 3298, 3299, 3299, 3299, 3299, 3299, 3299, 3299, 3299, 3299, 3299, 3299, 3299, 3299, 3299, 3302, 3303, 3204,
3204, 3297, 3298, 3299, 3299, 3299, 3299, 3299, 3299, 3299, 3299, 3299, 3299, 3299, 3299, 3299, 3299, 3302, 3303, 3204,
3204, 3297, 3298, 3299, 3299, 3299, 3299, 3299, 3299, 3299, 3299, 3299, 3299, 3299, 3299, 3299, 3299, 3302, 3303, 3204,
3204, 3297, 3298, 3299, 3299, 3299, 3299, 3299, 3299, 3299, 3299, 3299, 3299, 3299, 3299, 3299, 3299, 3302, 3303, 3204,
3204, 3329, 3330, 3331, 3333, 3333, 3331, 3332, 3331, 3331, 3331, 3331, 3332, 3331, 3331, 3333, 3331, 3334, 3335, 3204,
3204, 3361, 3362, 3363, 3363, 3363, 3363, 3363, 3363, 3363, 3363, 3363, 3363, 3363, 3363, 3363, 3363, 3366, 3367, 3204,
3204, 3393, 3394, 3395, 3395, 3395, 3395, 3395, 3395, 3395, 3395, 3395, 3395, 3395, 3395, 3395, 3396, 3398, 3399, 3204,
3204, 3204, 3204, 3204, 3204, 3204, 3204, 3204, 3204, 3204, 3204, 3204, 3204, 3204, 3204, 3204, 3204, 3204, 3204, 3204
}
},
{
type = "tilelayer",
name = "foreground00",
x = 0,
y = 0,
width = 20,
height = 20,
visible = true,
opacity = 1,
properties = {},
encoding = "lua",
data = {
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 500, 0, 0,
0, 0, 2574, 2575, 2576, 2577, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 437, 0, 0,
0, 0, 2606, 2607, 2608, 2609, 3426, 3427, 2460, 0, 0, 0, 0, 0, 0, 0, 0, 564, 0, 0,
0, 0, 2638, 2639, 2640, 2641, 0, 0, 2459, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
0, 0, 2670, 2671, 2672, 2673, 0, 0, 2458, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
0, 0, 2702, 2703, 2704, 2705, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
0, 0, 0, 0, 0, 0, 0, 0, 2460, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
0, 0, 2426, 2427, 2427, 2427, 2427, 2427, 2556, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
0, 0, 0, 1676, 1677, 1676, 1677, 1676, 1677, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
0, 0, 0, 1708, 1709, 1708, 1709, 1708, 1709, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
0, 0, 0, 1740, 1741, 1740, 1741, 1740, 1741, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
0, 0, 0, 1556, 1557, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 3556, 3557, 0, 0,
0, 0, 0, 1588, 1589, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 3588, 3589, 0, 0,
0, 0, 0, 0, 1520, 0, 0, 0, 0, 0, 0, 0, 0, 0, 3489, 3492, 3493, 3522, 0, 0,
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 3521, 3524, 3525, 0, 0, 0,
0, 0, 0, 0, 0, 3948, 3949, 3950, 3951, 3533, 3534, 3535, 3799, 3829, 0, 0, 0, 0, 0, 0,
0, 0, 0, 0, 0, 3980, 3981, 3982, 3983, 3565, 3566, 3567, 0, 0, 0, 0, 0, 0, 0, 0,
0, 0, 0, 0, 0, 0, 0, 0, 0, 3597, 3598, 3599, 0, 0, 0, 0, 0, 0, 0, 0
}
},
{
type = "tilelayer",
name = "foreground01",
x = 0,
y = 0,
width = 20,
height = 20,
visible = true,
opacity = 1,
properties = {},
encoding = "lua",
data = {
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
0, 0, 0, 0, 0, 0, 3669, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
0, 0, 0, 0, 0, 0, 3701, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
0, 0, 0, 1491, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
0, 0, 0, 1523, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
0, 0, 0, 0, 0, 0, 0, 0, 0, 2782, 0, 2782, 0, 0, 0, 0, 0, 0, 0, 0,
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0
}
},
{
type = "objectgroup",
name = "characters",
visible = true,
opacity = 1,
properties = {},
objects = {
{
name = "matty",
type = "",
shape = "rectangle",
x = 416,
y = 96,
width = 32,
height = 64,
rotation = 0,
visible = true,
properties = {}
},
{
name = "player",
type = "",
shape = "rectangle",
x = 320,
y = 480,
width = 32,
height = 64,
rotation = 0,
visible = true,
properties = {}
}
}
},
{
type = "tilelayer",
name = "overlay",
x = 0,
y = 0,
width = 20,
height = 20,
visible = true,
opacity = 1,
properties = {},
encoding = "lua",
data = {
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 500, 0, 0,
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 532, 0, 0,
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0
}
},
{
type = "tilelayer",
name = "collision",
x = 0,
y = 0,
width = 20,
height = 20,
visible = false,
opacity = 1,
properties = {},
encoding = "lua",
data = {
185, 185, 185, 185, 185, 185, 185, 185, 185, 185, 185, 185, 185, 185, 185, 185, 185, 185, 185, 185,
185, 185, 185, 185, 185, 185, 185, 185, 185, 185, 185, 185, 185, 185, 185, 185, 185, 185, 185, 185,
185, 185, 185, 185, 185, 185, 185, 185, 185, 0, 0, 0, 0, 0, 0, 0, 0, 185, 185, 185,
185, 185, 185, 185, 185, 185, 185, 0, 185, 0, 0, 0, 0, 0, 0, 0, 0, 0, 185, 185,
185, 185, 185, 185, 185, 185, 0, 0, 185, 0, 0, 0, 0, 0, 0, 0, 0, 0, 185, 185,
185, 185, 185, 185, 185, 185, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 185, 185,
185, 185, 0, 0, 0, 0, 0, 0, 185, 0, 0, 0, 0, 0, 0, 0, 0, 0, 185, 185,
185, 185, 185, 185, 185, 185, 185, 185, 185, 0, 0, 0, 0, 0, 0, 0, 0, 0, 185, 185,
185, 185, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 185, 185,
185, 185, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 365, 185, 185,
185, 185, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 185, 185,
185, 185, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 185, 185,
185, 185, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 185, 185,
185, 185, 0, 185, 185, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 185, 185, 185, 185,
185, 185, 0, 185, 185, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 185, 185, 185, 185,
185, 185, 0, 185, 185, 0, 0, 0, 0, 0, 0, 0, 0, 0, 185, 185, 185, 185, 185, 185,
185, 185, 0, 0, 0, 0, 0, 0, 0, 185, 0, 185, 0, 0, 185, 185, 185, 185, 185, 185,
185, 185, 185, 185, 185, 185, 185, 185, 185, 185, 0, 185, 185, 185, 185, 185, 185, 185, 185, 185,
185, 185, 185, 185, 185, 185, 185, 185, 185, 185, 0, 185, 185, 185, 185, 185, 185, 185, 185, 185,
185, 185, 185, 185, 185, 185, 185, 185, 185, 185, 185, 185, 185, 185, 185, 185, 185, 185, 185, 185
}
}
}
}

44
src/assets/map.tmx Normal file
View File

@ -0,0 +1,44 @@
<?xml version="1.0" encoding="UTF-8"?>
<map version="1.0" orientation="orthogonal" width="20" height="20" tilewidth="32" tileheight="32">
<tileset firstgid="1" name="atlas00" tilewidth="32" tileheight="32">
<image source="tiles/atlas00.png" width="1024" height="1024"/>
</tileset>
<tileset firstgid="1025" name="atlas01" tilewidth="32" tileheight="32">
<image source="tiles/atlas01.png" width="1024" height="1024"/>
</tileset>
<tileset firstgid="2049" name="atlas03" tilewidth="32" tileheight="32">
<image source="tiles/atlas03.png" width="1024" height="1024"/>
</tileset>
<tileset firstgid="3073" name="atlas04" tilewidth="32" tileheight="32">
<image source="tiles/atlas04.png" width="1024" height="1024"/>
</tileset>
<layer name="background" width="20" height="20">
<data encoding="base64" compression="zlib">
eJztzsENgkAQQNEVFC9ShJpAAzYAEq1LglqHoFKHHLQOMNE2/CRLnJOHdQ8cOLzMZLL52WymVGbZDin2hg44it4JOS4o9GydxV78uF1Rit4NFZ6G7niIXo3mj94L76HXm97K6ff/hp7d3shXyoGLidbuYz0leeveeZj6396cfYGloQCh6EXsMdYGEmywFT2bPspTJQc=
</data>
</layer>
<layer name="foreground00" width="20" height="20">
<data encoding="base64" compression="zlib">
eJzdk7tOAkEUhg8ENmGT3dVEX4NLAq/h5RW05drCKyy3FmxMoMOSB8BoJ1LyAlqqFNhR8G1HMrM6s1rxJ19OMpPz5ZxJRuTv2abUM98VCeAETl0731zjy+MoQBFKcOOJ3MJd7ndfOa2eXeC4hCu4PphvbODTpY6jAU1oHfhGCX09HH0YwNDy/Wxi8n66tOnraNgl9EUJHZGuo9akmdH74Kg1aV7oXTpqPdacsdu55X5v/Ml3T39XxlWx9GV8kawff7/JmnnumWkCU3iMmc8mCxxP8GzgqgYiNahDA5b0vMIK1uz2/cN+uoQ4utCDPnzg+YSvf9grisc8PgQGc+0BNfsyEw==
</data>
</layer>
<layer name="foreground01" width="20" height="20">
<data encoding="base64" compression="zlib">
eJxjYBiaIJSPuuaVUtm8UTAKRgEquMxKXfM+U9k8XOAeFyo9ChAAANQNBIc=
</data>
</layer>
<objectgroup name="characters">
<object name="matty" x="416" y="96" width="32" height="64"/>
<object name="player" x="320" y="480" width="32" height="64"/>
</objectgroup>
<layer name="overlay" width="20" height="20">
<data encoding="base64" compression="zlib">
eJxjYBgFo4C+4Asjdc0TYaKueaNgFIwC4gAA9U4BDA==
</data>
</layer>
<layer name="collision" width="20" height="20" visible="0">
<data encoding="base64" compression="zlib">
eJzbycDAsHOIY0KAFHOIMY8YM5HVUdM8YgGx+qnlPlLdSqn/aGleLuPgdh+9zCPHbEJ5a6DMoyTNYisbqFFmUWoGLc0jFgMAI62XeA==
</data>
</layer>
</map>

BIN
src/assets/tiles/atlas00.png Executable file

Binary file not shown.

After

Width:  |  Height:  |  Size: 328 KiB

BIN
src/assets/tiles/atlas01.png Executable file

Binary file not shown.

After

Width:  |  Height:  |  Size: 211 KiB

BIN
src/assets/tiles/atlas03.png Executable file

Binary file not shown.

After

Width:  |  Height:  |  Size: 181 KiB

BIN
src/assets/tiles/atlas04.png Executable file

Binary file not shown.

After

Width:  |  Height:  |  Size: 254 KiB

BIN
src/assets/ui/menu_back.jpg Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 91 KiB

58
src/character.lua Normal file
View File

@ -0,0 +1,58 @@
local character = {}
character.width = 64
character.height = 64
function character.sortNorthSouth(a, b)
return a.info.y < b.info.y
end
character.spellcast_up = {
offsetX = 0,
offsetY = 0,
count = 9
}
character.spellcast_left = {
offsetX = 0,
offsetY = 1,
count = 9
}
character.spellcast_down = {
offsetX = 0,
offsetY = 2,
count = 9
}
character.spellcast_right = {
offsetX = 0,
offsetY = 3,
count = 9
}
character.walking_up = {
offsetX = 0,
offsetY = 8,
count = 9
}
character.walking_left = {
offsetX = 0,
offsetY = 9,
count = 9
}
character.walking_down = {
offsetX = 0,
offsetY = 10,
count = 9
}
character.walking_right = {
offsetX = 0,
offsetY = 11,
count = 9
}
return character

8
src/characters/matty.lua Normal file
View File

@ -0,0 +1,8 @@
local character = require "../character"
local gridwalker = require "../controllers/gridwalker"
return {
spritesheet = "assets/characters/matty.png",
pose = character.walking_down,
controller = Gridwalker()
}

14
src/characters/player.lua Normal file
View File

@ -0,0 +1,14 @@
local character = require "../character"
local gridwalker = require "../controllers/gridwalker"
return {
spritesheet = "assets/characters/player.png",
pose = character.walking_up,
controller = Gridwalker(),
relevantKeys = {
"w", "up",
"s", "down",
"d", "right",
"a", "left"
}
}

32
src/conf.lua Normal file
View File

@ -0,0 +1,32 @@
function love.conf(t)
t.identity = nil -- The name of the save directory (string)
t.version = "0.9.0" -- The LÖVE version this game was made for (string)
t.window.title = "Die Streuner" -- The window title (string)
t.window.icon = nil -- Filepath to an image to use as the window's icon (string)
t.window.width = 640 -- The window width (number)
t.window.height = 640 -- The window height (number)
t.window.borderless = false -- Remove all border visuals from the window (boolean)
t.window.resizable = false -- Let the window be user-resizable (boolean)
t.window.fullscreen = false -- Enable fullscreen (boolean)
t.window.fullscreentype = "normal" -- Standard fullscreen or desktop fullscreen mode (string)
t.window.vsync = true -- Enable vertical sync (boolean)
t.window.fsaa = 0 -- The number of samples to use with multi-sampled antialiasing (number)
t.window.display = 1 -- Index of the monitor to show the window in (number)
t.window.highdpi = true -- Enable high-dpi mode for the window on a Retina display (boolean). Added in 0.9.1
t.window.srgb = false -- Enable sRGB gamma correction when drawing to the screen (boolean). Added in 0.9.1
t.modules.audio = true -- Enable the audio module (boolean)
t.modules.event = true -- Enable the event module (boolean)
t.modules.graphics = true -- Enable the graphics module (boolean)
t.modules.image = true -- Enable the image module (boolean)
t.modules.joystick = true -- Enable the joystick module (boolean)
t.modules.keyboard = true -- Enable the keyboard module (boolean)
t.modules.math = true -- Enable the math module (boolean)
t.modules.mouse = true -- Enable the mouse module (boolean)
t.modules.physics = true -- Enable the physics module (boolean)
t.modules.sound = true -- Enable the sound module (boolean)
t.modules.system = true -- Enable the system module (boolean)
t.modules.timer = true -- Enable the timer module (boolean)
t.modules.window = true -- Enable the window module (boolean)
end

View File

@ -0,0 +1,92 @@
local character = require "../character"
Gridwalker = class()
Gridwalker.speed = 2
Gridwalker.collisionTestSize = 12
Gridwalker.neighbourOffsetX = (character.width - Gridwalker.collisionTestSize) / 2
Gridwalker.neighbourOffsetY = character.width - Gridwalker.collisionTestSize * 1.5
Gridwalker.testShape = nil
Gridwalker.charinfo = nil
function Gridwalker:sendKey(key)
if key == "w" or key == "up" then
self:up()
end
if key == "s" or key == "down" then
self:down()
end
if key == "d" or key == "right" then
self:right()
end
if key == "a" or key == "left" then
self:left()
end
end
function Gridwalker:up()
local newX = self.charinfo.x
local newY = self.charinfo.y - self.speed
self:move(newX, newY, character.walking_up)
end
function Gridwalker:down()
local newX = self.charinfo.x
local newY = self.charinfo.y + self.speed
self:move(newX, newY, character.walking_down)
end
function Gridwalker:right()
local newX = self.charinfo.x + self.speed
local newY = self.charinfo.y
self:move(newX, newY, character.walking_right)
end
function Gridwalker:left()
local newX = self.charinfo.x - self.speed
local newY = self.charinfo.y
self:move(newX, newY, character.walking_left)
end
function Gridwalker:init()
self.testShape = collider:addRectangle(self.charinfo.x + self.neighbourOffsetX, self.charinfo.y + self.neighbourOffsetY, self.collisionTestSize, self.collisionTestSize)
collider:setPassive(self.testShape)
end
function Gridwalker:setTestShape(x, y)
local testX = x + self.neighbourOffsetX + self.collisionTestSize / 2
local testY = y + self.neighbourOffsetY + self.collisionTestSize / 2
self.testShape:moveTo(testX, testY)
return testX, testY
end
function Gridwalker:move(x, y, pose)
local noCollision = true
local testX, testY = self:setTestShape(x, y)
for other in pairs(collider:shapesInRange(testX, testY, testX + self.collisionTestSize, testY + self.collisionTestSize)) do
if self.testShape:collidesWith(other) then
noCollision = false
break
end
end
if noCollision then
self.charinfo.x = x
self.charinfo.y = y
else
self:setTestShape(self.charinfo.x, self.charinfo.y)
end
self.charinfo.pose = pose
end

4
src/hardoncollider/README Executable file
View File

@ -0,0 +1,4 @@
General Purpose 2D Collision Detection System
Documentation and examples here:
http://vrld.github.com/HardonCollider

99
src/hardoncollider/class.lua Executable file
View File

@ -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
src/hardoncollider/gjk.lua Executable file
View File

@ -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

310
src/hardoncollider/init.lua Executable file
View File

@ -0,0 +1,310 @@
--[[
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 function __NULL__() end
local HC = {}
function HC:init(cell_size, callback_collide, callback_stop)
self._active_shapes = {}
self._passive_shapes = {}
self._ghost_shapes = {}
self.groups = {}
self._colliding_only_last_frame = {}
self.on_collide = callback_collide or __NULL__
self.on_stop = callback_stop or __NULL__
self._hash = common_local.instance(Spatialhash, cell_size)
end
function HC:clear()
self._active_shapes = {}
self._passive_shapes = {}
self._ghost_shapes = {}
self.groups = {}
self._colliding_only_last_frame = {}
self._hash = common_local.instance(Spatialhash, self._hash.cell_size)
return self
end
function HC:setCallbacks(collide, stop)
if type(collide) == "table" and not (getmetatable(collide) or {}).__call then
stop = collide.stop
collide = collide.collide
end
if collide then
assert(type(collide) == "function" or (getmetatable(collide) or {}).__call,
"collision callback must be a function or callable table")
self.on_collide = collide
end
if stop then
assert(type(stop) == "function" or (getmetatable(stop) or {}).__call,
"stop callback must be a function or callable table")
self.on_stop = stop
end
return self
end
function HC:addShape(shape)
assert(shape.bbox and shape.collidesWith,
"Cannot add custom shape: Incompatible shape.")
self._active_shapes[shape] = shape
self._hash:insert(shape, shape:bbox())
shape._groups = {}
local hash = self._hash
local move, rotate,scale = shape.move, shape.rotate, shape.scale
for _, func in ipairs{'move', 'rotate', 'scale'} do
local old_func = shape[func]
shape[func] = function(self, ...)
local x1,y1,x2,y2 = self:bbox()
old_func(self, ...)
local x3,y3,x4,y4 = self:bbox()
hash:update(self, x1,y1, x2,y2, x3,y3, x4,y4)
end
end
function shape:neighbors()
local neighbors = hash:inRange(self:bbox())
rawset(neighbors, self, nil)
return neighbors
end
function shape:_removeFromHash()
return hash:remove(shape, self:bbox())
end
function shape:inGroup(group)
return self._groups[group]
end
return shape
end
function HC:activeShapes()
return pairs(self._active_shapes)
end
function HC:shapesInRange(x1,y1, x2,y2)
return self._hash:inRange(x1,y1, x2,y2)
end
function HC:addPolygon(...)
return self:addShape(newPolygonShape(...))
end
function HC:addRectangle(x,y,w,h)
return self:addPolygon(x,y, x+w,y, x+w,y+h, x,y+h)
end
function HC:addCircle(cx, cy, radius)
return self:addShape(newCircleShape(cx,cy, radius))
end
function HC:addPoint(x,y)
return self:addShape(newPointShape(x,y))
end
function HC:share_group(shape, other)
for name,group in pairs(shape._groups) do
if group[other] then return true end
end
return false
end
-- check for collisions
function HC:update(dt)
-- cache for tested/colliding shapes
local tested, colliding = {}, {}
local function may_skip_test(shape, other)
return (shape == other)
or (tested[other] and tested[other][shape])
or self._ghost_shapes[other]
or self:share_group(shape, other)
end
-- collect active shapes. necessary, because a callback might add shapes to
-- _active_shapes, which will lead to undefined behavior (=random crashes) in
-- next()
local active = {}
for shape in self:activeShapes() do
active[shape] = shape
end
local only_last_frame = self._colliding_only_last_frame
for shape in pairs(active) do
tested[shape] = {}
for other in self._hash:rangeIter(shape:bbox()) do
if not self._active_shapes[shape] then
-- break out of this loop is shape was removed in a callback
break
end
if not may_skip_test(shape, other) then
local collide, sx,sy = shape:collidesWith(other)
if collide then
if not colliding[shape] then colliding[shape] = {} end
colliding[shape][other] = {sx, sy}
-- flag shape colliding this frame and call collision callback
if only_last_frame[shape] then
only_last_frame[shape][other] = nil
end
self.on_collide(dt, shape, other, sx, sy)
end
tested[shape][other] = true
end
end
end
-- call stop callback on shapes that do not collide anymore
for a,reg in pairs(only_last_frame) do
for b, info in pairs(reg) do
self.on_stop(dt, a, b, info[1], info[2])
end
end
self._colliding_only_last_frame = colliding
end
-- get list of shapes at point (x,y)
function HC:shapesAt(x, y)
local shapes = {}
for s in pairs(self._hash:cellAt(x,y)) do
if s:contains(x,y) then
shapes[#shapes+1] = s
end
end
return shapes
end
-- remove shape from internal tables and the hash
function HC:remove(shape, ...)
if not shape then return end
self._active_shapes[shape] = nil
self._passive_shapes[shape] = nil
self._ghost_shapes[shape] = nil
for name, group in pairs(shape._groups) do
group[shape] = nil
end
shape:_removeFromHash()
return self:remove(...)
end
-- group support
function HC:addToGroup(group, shape, ...)
if not shape then return end
assert(self._active_shapes[shape] or self._passive_shapes[shape],
"Shape is not registered with HC")
if not self.groups[group] then self.groups[group] = {} end
self.groups[group][shape] = true
shape._groups[group] = self.groups[group]
return self:addToGroup(group, ...)
end
function HC:removeFromGroup(group, shape, ...)
if not shape or not self.groups[group] then return end
assert(self._active_shapes[shape] or self._passive_shapes[shape],
"Shape is not registered with HC")
self.groups[group][shape] = nil
shape._groups[group] = nil
return self:removeFromGroup(group, ...)
end
function HC:setPassive(shape, ...)
if not shape then return end
if not self._ghost_shapes[shape] then
assert(self._active_shapes[shape], "Shape is not active")
self._active_shapes[shape] = nil
self._passive_shapes[shape] = shape
end
return self:setPassive(...)
end
function HC:setActive(shape, ...)
if not shape then return end
if not self._ghost_shapes[shape] then
assert(self._passive_shapes[shape], "Shape is not passive")
self._active_shapes[shape] = shape
self._passive_shapes[shape] = nil
end
return self:setActive(...)
end
function HC:setGhost(shape, ...)
if not shape then return end
assert(self._active_shapes[shape] or self._passive_shapes[shape],
"Shape is not registered with HC")
self._active_shapes[shape] = nil
-- dont remove from passive shapes, see below
self._ghost_shapes[shape] = shape
return self:setGhost(...)
end
function HC:setSolid(shape, ...)
if not shape then return end
assert(self._ghost_shapes[shape], "Shape not a ghost")
-- re-register shape. passive shapes were not unregistered above, so if a shape
-- is not passive, it must be registered as active again.
if not self._passive_shapes[shape] then
self._active_shapes[shape] = shape
end
self._ghost_shapes[shape] = nil
return self:setSolid(...)
end
-- the module
HC = common_local.class("HardonCollider", HC)
local function new(...)
return common_local.instance(HC, ...)
end
return setmetatable({HardonCollider = HC, new = new},
{__call = function(_,...) return new(...) end})

474
src/hardoncollider/polygon.lua Executable file
View File

@ -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
src/hardoncollider/shapes.lua Executable file
View File

@ -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,
}

View File

@ -0,0 +1,159 @@
--[[
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
function Spatialhash:inRange(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:rangeIter(...)
return pairs(self:inRange(...))
end
function Spatialhash:insert(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)

View File

@ -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,
}

104
src/main.lua Normal file
View File

@ -0,0 +1,104 @@
class = require '30log'
local HC = require 'hardoncollider'
-- has to come before the sti initialization so I can add the collision tiles
collider = HC(100)
local sti = require "sti"
local character = require "character"
local ui = require "ui"
local characters = {}
function love.load()
windowWidth = love.graphics.getWidth()
windowHeight = love.graphics.getHeight()
map = sti.new("assets/map")
collision = map:getCollisionMap("collision")
initCharacters()
end
function love.update(dt)
for _, char in ipairs(characters) do
if char.relevantKeys then
for _, relevantKey in ipairs(char.relevantKeys) do
if love.keyboard.isDown(relevantKey) then
char.controller:sendKey(relevantKey)
end
end
end
end
collider:update(dt)
map:update(dt)
end
function love.draw()
-- Translation would normally be based on a player's x/y
local translateX = 0
local translateY = 0
-- Draw Range culls unnecessary tiles
map:setDrawRange(translateX, translateY, windowWidth, windowHeight)
map:draw()
--debugDrawing()
end
function debugDrawing()
-- draw collision shapes for debugging
for shape in pairs(collider:shapesInRange(0, 0, windowWidth, windowHeight)) do
shape:draw()
end
-- Draw Collision Map (useful for debugging)
map:drawCollisionMap(collision)
end
function initCharacters()
local collisionBoxOffset = 5
map:addCustomLayer("character layer", 4)
local characterLayer = map.layers["character layer"]
characterLayer.sprites = {}
for _, char in pairs(map.layers["characters"].objects) do
local filename = "characters/" .. char.name .. ".lua"
local charinfo = love.filesystem.load(filename)()
charinfo.x = char.x - character.width / 4
charinfo.y = char.y
charinfo.controller.charinfo = charinfo
charinfo.image = love.graphics.newImage(charinfo.spritesheet)
charinfo.controller:init()
table.insert(characterLayer.sprites, {info = charinfo})
table.insert(characters, charinfo)
end
-- Draw callback for Custom Layer
function characterLayer:draw()
table.sort(self.sprites, character.sortNorthSouth)
for _, sprite in pairs(self.sprites) do
local x = math.floor(sprite.info.x)
local y = math.floor(sprite.info.y)
local offsetX = sprite.info.pose.offsetX * character.width
local offsetY = sprite.info.pose.offsetY * character.height
local quad = love.graphics.newQuad(offsetX, offsetY, character.width, character.height, sprite.info.image:getWidth(), sprite.info.image:getHeight())
love.graphics.draw(sprite.info.image, quad, x, y)
end
end
map.layers["characters"].visible = false
end

645
src/sti.lua Normal file
View File

@ -0,0 +1,645 @@
--[[
------------------------------------------------------------------------------
Simple Tiled Implementation is licensed under the MIT Open Source License.
(http://www.opensource.org/licenses/mit-license.html)
------------------------------------------------------------------------------
Copyright (c) 2014 Landon Manning - LManning17@gmail.com - LandonManning.com
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.
]]--
-- Simple Tiled Implementation v0.6.16
local bit = require "bit"
local STI = {}
local Map = {}
function STI.new(map)
map = map .. ".lua"
-- Get path to map
local path = map:reverse():find("[/\\]") or ""
if path ~= "" then
path = map:sub(1, 1 + (#map - path))
end
-- Load map
map = assert(love.filesystem.load(map), "File not found: " .. map)
setfenv(map, {})
map = setmetatable(map(), {__index = Map})
map.tiles = {}
map.drawRange = {
sx = 1,
sy = 1,
ex = map.width,
ey = map.height,
}
-- Set tiles, images
local gid = 1
for i, tileset in ipairs(map.tilesets) do
local image = STI.formatPath(path..tileset.image)
tileset.image = love.graphics.newImage(image)
tileset.image:setFilter("nearest", "nearest")
gid = map:setTiles(i, tileset, gid)
end
-- Set layers
for i, layer in ipairs(map.layers) do
map:setLayer(layer, path)
end
return map
end
function STI.formatPath(path)
local str = string.split(path, "/")
for i, segment in pairs(str) do
if segment == ".." then
str[i] = nil
str[i-1] = nil
end
end
path = ""
for _, segment in pairs(str) do
path = path .. segment .. "/"
end
return string.sub(path, 1, path:len()-1)
end
function Map:setTiles(index, tileset, gid)
local function getTiles(i, t, m, s)
i = i - m
local n = 0
while i >= t do
i = i - t
if n ~= 0 then i = i - s end
if i >= 0 then n = n + 1 end
end
return n
end
local quad = love.graphics.newQuad
local mw = self.tilewidth
local iw = tileset.imagewidth
local ih = tileset.imageheight
local tw = tileset.tilewidth
local th = tileset.tileheight
local s = tileset.spacing
local m = tileset.margin
local w = getTiles(iw, tw, m, s)
local h = getTiles(ih, th, m, s)
for y = 1, h do
for x = 1, w do
local qx = (x - 1) * tw + m + (x - 1) * s
local qy = (y - 1) * th + m + (y - 1) * s
local properties
for _, tile in pairs(tileset.tiles) do
if tile.id == gid - tileset.firstgid + 1 then
properties = tile.properties
end
end
local tile = {
gid = gid,
tileset = index,
quad = quad(qx, qy, tw, th, iw, ih),
properties = properties,
sx = 1,
sy = 1,
r = 0,
offset = {
x = -mw,
y = -th,
},
}
if self.orientation == "isometric" then
tile.offset.x = -mw / 2
end
--[[ THIS IS A TEMPORARY FIX FOR 0.9.1 ]]--
if tileset.tileoffset then
tile.offset.x = tile.offset.x + tileset.tileoffset.x
tile.offset.y = tile.offset.y + tileset.tileoffset.y
end
self.tiles[gid] = tile
gid = gid + 1
end
end
return gid
end
function Map:setLayer(layer, path)
layer.x = layer.x or 0
layer.y = layer.y or 0
layer.update = function(dt) return end
if layer.type == "tilelayer" then
self:setTileData(layer)
self:setSpriteBatches(layer)
layer.draw = function() self:drawTileLayer(layer) end
elseif layer.type == "objectgroup" then
layer.draw = function() self:drawObjectLayer(layer) end
elseif layer.type == "imagelayer" then
layer.draw = function() self:drawImageLayer(layer) end
local image = STI.formatPath(path..layer.image)
if layer.image ~= "" then
layer.image = love.graphics.newImage(image)
end
end
self.layers[layer.name] = layer
end
function Map:setTileData(layer)
local i = 1
local map = {}
for y = 1, layer.height do
map[y] = {}
for x = 1, layer.width do
local gid = layer.data[i]
if gid > 0 then
local tile = self.tiles[gid]
if tile then
map[y][x] = tile
else
local flipX = bit.status(gid, 31)
local flipY = bit.status(gid, 30)
local flipD = bit.status(gid, 29)
local realgid = bit.band(gid, bit.bnot(bit.bor(2^31, 2^30, 2^29)))
local tile = self.tiles[realgid]
local data = {
gid = tile.gid,
tileset = tile.tileset,
offset = tile.offset,
quad = tile.quad,
properties = tile.properties,
sx = tile.sx,
sy = tile.sy,
r = tile.r,
}
if flipX then
if flipY then
data.sx = -1
data.sy = -1
elseif flipD then
data.r = math.rad(90)
else
data.sx = -1
end
elseif flipY then
if flipD then
data.r = math.rad(-90)
else
data.sy = -1
end
elseif flipD then
data.r = math.rad(90)
data.sy = -1
end
self.tiles[gid] = data
map[y][x] = self.tiles[gid]
end
end
i = i + 1
end
end
layer.data = map
end
function Map:setSpriteBatches(layer)
local newBatch = love.graphics.newSpriteBatch
local w = love.graphics.getWidth()
local h = love.graphics.getHeight()
local tw = self.tilewidth
local th = self.tileheight
local bw = math.ceil(w / tw)
local bh = math.ceil(h / th)
-- Minimum of 400 tiles per batch
if bw < 20 then bw = 20 end
if bh < 20 then bh = 20 end
local size = bw * bh
local batches = {
width = bw,
height = bh,
data = {},
}
for y = 1, layer.height do
local by = math.ceil(y / bh)
for x = 1, layer.width do
local tile = layer.data[y][x]
local bx = math.ceil(x / bw)
if tile then
local ts = tile.tileset
local image = self.tilesets[tile.tileset].image
batches.data[ts] = batches.data[ts] or {}
batches.data[ts][by] = batches.data[ts][by] or {}
batches.data[ts][by][bx] = batches.data[ts][by][bx] or newBatch(image, size)
local batch = batches.data[ts][by][bx]
local tx, ty
if self.orientation == "orthogonal" then
tx = x * tw + tile.offset.x
ty = y * th + tile.offset.y
-- Compensation for scale/rotation shift
if tile.sx < 0 then tx = tx + tw end
if tile.sy < 0 then ty = ty + th end
if tile.r > 0 then tx = tx + tw end
if tile.r < 0 then ty = ty + th end
elseif self.orientation == "isometric" then
tx = (x - y) * (tw / 2) + tile.offset.x
ty = (x + y) * (th / 2) + tile.offset.y
elseif self.orientation == "staggered" then
if y % 2 == 0 then
tx = x * tw + tw / 2 + tile.offset.x
else
tx = x * tw + tile.offset.x
end
ty = y * th / 2 + tile.offset.y
end
batch:add(tile.quad, tx, ty, tile.r, tile.sx, tile.sy)
end
end
end
layer.batches = batches
end
function Map:setDrawRange(tx, ty, w, h)
tx = -tx
ty = -ty
local tw = self.tilewidth
local th = self.tileheight
local sx, sy, ex, ey
if self.orientation == "orthogonal" then
sx = math.ceil(tx / tw)
sy = math.ceil(ty / th)
ex = math.ceil(sx + w / tw)
ey = math.ceil(sy + h / th)
elseif self.orientation == "isometric" then
sx = math.ceil(((ty / (th / 2)) + (tx / (tw / 2))) / 2)
sy = math.ceil(((ty / (th / 2)) - (tx / (tw / 2))) / 2 - h / th)
ex = math.ceil(sx + (h / th) + (w / tw))
ey = math.ceil(sy + (h / th) * 2 + (w / tw))
elseif self.orientation == "staggered" then
sx = math.ceil(tx / tw - 1)
sy = math.ceil(ty / th)
ex = math.ceil(sx + w / tw + 1)
ey = math.ceil(sy + h / th * 2)
end
self.drawRange = {
sx = sx,
sy = sy,
ex = ex,
ey = ey,
}
end
function Map:getCollisionMap(index)
local layer = assert(self.layers[index], "Layer not found: " .. index)
assert(layer.type == "tilelayer", "Invalid layer type: " .. layer.type .. ". Layer must be of type: tilelayer")
local w = self.width
local h = self.height
local map = {
type = layer.type,
orientation = layer.orientation,
collision = true,
opacity = 0.5,
data = {},
}
for y=1, h do
map.data[y] = {}
for x=1, w do
if layer.data[y][x] == nil then
map.data[y][x] = 0
else
map.data[y][x] = 1
ctile = collider:addRectangle((x - 1) * self.tilewidth, (y - 1) * self.tileheight, self.tilewidth, self.tileheight)
collider:setPassive(ctile)
end
end
end
return map
end
function Map:addCustomLayer(name, index)
local layer = {
type = "customlayer",
name = name,
visible = true,
opacity = 1,
properties = {},
}
function layer:draw() return end
function layer:update(dt) return end
table.insert(self.layers, index, layer)
self.layers[name] = self.layers[index]
end
function Map:convertToCustomLayer(index)
local layer = assert(self.layers[index], "Layer not found: " .. index)
layer.type = "customlayer"
layer.x = nil
layer.y = nil
layer.width = nil
layer.height = nil
layer.encoding = nil
layer.data = nil
layer.objects = nil
layer.image = nil
function layer:draw() return end
function layer:update(dt) return end
end
function Map:removeLayer(index)
local layer = assert(self.layers[index], "Layer not found: " .. index)
if type(index) == "string" then
for i, layer in ipairs(self.layers) do
if layer.name == index then
table.remove(self.layers, i)
table.remove(self.layers, index)
break
end
end
else
local name = self.layers[index].name
table.remove(self.layers, index)
table.remove(self.layers, name)
end
end
function Map:update(dt)
for _, layer in ipairs(self.layers) do
layer:update(dt)
end
end
function Map:draw()
for _, layer in ipairs(self.layers) do
if layer.visible and layer.opacity > 0 then
self:drawLayer(layer)
end
end
end
function Map:drawLayer(layer)
love.graphics.setColor(255, 255, 255, 255 * layer.opacity)
layer:draw()
love.graphics.setColor(255, 255, 255, 255)
end
function Map:drawTileLayer(layer)
assert(layer.type == "tilelayer", "Invalid layer type: " .. layer.type .. ". Layer must be of type: tilelayer")
local bw = layer.batches.width
local bh = layer.batches.height
local sx = math.ceil((self.drawRange.sx - layer.x / self.tilewidth - 1) / bw)
local sy = math.ceil((self.drawRange.sy - layer.y / self.tileheight - 1) / bh)
local ex = math.ceil((self.drawRange.ex - layer.x / self.tilewidth + 1) / bw)
local ey = math.ceil((self.drawRange.ey - layer.y / self.tileheight + 1) / bh)
local mx = math.ceil(self.width / bw)
local my = math.ceil(self.height / bh)
for by=sy, ey do
for bx=sx, ex do
if bx >= 1 and bx <= mx and by >= 1 and by <= my then
for _, batches in pairs(layer.batches.data) do
local batch = batches[by] and batches[by][bx]
if batch then
love.graphics.draw(batch, math.floor(layer.x), math.floor(layer.y))
end
end
end
end
end
end
function Map:drawObjectLayer(layer)
assert(layer.type == "objectgroup", "Invalid layer type: " .. layer.type .. ". Layer must be of type: objectgroup")
local line = { 160, 160, 160, 255 * layer.opacity }
local fill = { 160, 160, 160, 255 * layer.opacity * 0.2 }
local shadow = { 0, 0, 0, 255 * layer.opacity }
local function drawEllipse(mode, x, y, rx, ry)
local segments = 100
local vertices = {}
table.insert(vertices, x + rx / 2)
table.insert(vertices, y + ry / 2)
for i=0, segments do
local angle = (i / segments) * math.pi * 2
local px = x + rx / 2 + math.cos(angle) * rx / 2
local py = y + ry / 2 + math.sin(angle) * ry / 2
table.insert(vertices, px)
table.insert(vertices, py)
end
love.graphics.polygon(mode, vertices)
end
for _, object in ipairs(layer.objects) do
local x = layer.x + object.x
local y = layer.y + object.y
if object.shape == "rectangle" then
love.graphics.setColor(fill)
love.graphics.rectangle("fill", x, y, object.width, object.height)
love.graphics.setColor(shadow)
love.graphics.rectangle("line", x+1, y+1, object.width, object.height)
love.graphics.setColor(line)
love.graphics.rectangle("line", x, y, object.width, object.height)
elseif object.shape == "ellipse" then
love.graphics.setColor(fill)
drawEllipse("fill", x, y, object.width, object.height)
love.graphics.setColor(shadow)
drawEllipse("line", x+1, y+1, object.width, object.height)
love.graphics.setColor(line)
drawEllipse("line", x, y, object.width, object.height)
elseif object.shape == "polygon" then
local points = {{},{}}
for _, point in ipairs(object.polygon) do
table.insert(points[1], x + point.x)
table.insert(points[1], y + point.y)
table.insert(points[2], x + point.x+1)
table.insert(points[2], y + point.y+1)
end
love.graphics.setColor(fill)
if not love.math.isConvex(points[1]) then
local triangles = love.math.triangulate(points[1])
for _, triangle in ipairs(triangles) do
love.graphics.polygon("fill", triangle)
end
else
love.graphics.polygon("fill", points[1])
end
love.graphics.setColor(shadow)
love.graphics.polygon("line", points[2])
love.graphics.setColor(line)
love.graphics.polygon("line", points[1])
elseif object.shape == "polyline" then
local points = {{},{}}
for _, point in ipairs(object.polyline) do
table.insert(points[1], x + point.x)
table.insert(points[1], y + point.y)
table.insert(points[2], x + point.x+1)
table.insert(points[2], y + point.y+1)
end
love.graphics.setColor(shadow)
love.graphics.line(points[2])
love.graphics.setColor(line)
love.graphics.line(points[1])
end
end
end
function Map:drawImageLayer(layer)
assert(layer.type == "imagelayer", "Invalid layer type: " .. layer.type .. ". Layer must be of type: imagelayer")
if layer.image ~= "" then
love.graphics.draw(layer.image, layer.x, layer.y)
end
end
function Map:drawCollisionMap(layer)
assert(layer.type == "tilelayer", "Invalid layer type: " .. layer.type .. ". Layer must be of type: tilelayer")
assert(layer.collision, "This is not a collision layer")
local tw = self.tilewidth
local th = self.tileheight
love.graphics.setColor(255, 255, 255, 255 * layer.opacity)
for y=1, self.height do
for x=1, self.width do
local tx, ty
if self.orientation == "orthogonal" then
tx = (x - 1) * tw
ty = (y - 1) * th
elseif self.orientation == "isometric" then
tx = (x - y) * (tw / 2) - self.tilewidth / 2
ty = (x + y) * (th / 2) - self.tileheight
elseif self.orientation == "staggered" then
if y % 2 == 0 then
tx = x * tw + tw / 2 - self.tilewidth
else
tx = x * tw - self.tilewidth
end
ty = y * th / 2 - self.tileheight
end
if layer.data[y][x] == 1 then
love.graphics.rectangle("fill", tx, ty, tw, th)
else
love.graphics.rectangle("line", tx, ty, tw, th)
end
end
end
love.graphics.setColor(255, 255, 255, 255)
end
-- http://wiki.interfaceware.com/534.html
function string.split(s, d)
local t = {}
local i = 0
local f
local match = '(.-)' .. d .. '()'
if string.find(s, d) == nil then
return {s}
end
for sub, j in string.gmatch(s, match) do
i = i + 1
t[i] = sub
f = j
end
if i ~= 0 then
t[i+1] = string.sub(s, f)
end
return t
end
function bit.status(num, digit)
return bit.band(num, bit.lshift(1, digit)) ~= 0
end
return STI

4
src/ui.lua Normal file
View File

@ -0,0 +1,4 @@
local ui = {}
return ui