update to love 0.10.1

This commit is contained in:
Sebastian Hugentobler 2016-03-15 13:41:39 +01:00
parent 714a188e47
commit 814fc96669
34 changed files with 3305 additions and 2987 deletions

View File

@ -30,7 +30,7 @@ In the meantime take a moment to listen to the music of *Die Streuner*:
make run
```
Use arrow keys for movement and enter to get rid of dialogs.
Use arrow keys for movement and enter to get rid of dialogs (for movement w, a, s, d & h, j, k, l works too).
# Licensation
All original code is licensed under the [MPL 2.0](https://www.mozilla.org/MPL/2.0/index.txt).
@ -38,7 +38,7 @@ All original code is licensed under the [MPL 2.0](https://www.mozilla.org/MPL/2.
## Libraries
- [30log](https://github.com/Yonaba/30log): [MIT](http://opensource.org/licenses/mit-license.php)
- [anim8](https://github.com/kikito/anim8): [MIT](http://opensource.org/licenses/mit-license.php)
- [hardoncollider](http://vrld.github.io/HardonCollider/): [MIT](http://opensource.org/licenses/mit-license.php)
- [bump](https://github.com/kikito/bump.lua): [MIT](http://opensource.org/licenses/mit-license.php)
- [sti](https://github.com/karai17/Simple-Tiled-Implementation): [MIT](http://opensource.org/licenses/mit-license.php)
The tiles and character images are from the [liberated pixel cup](http://lpc.opengameart.org/) and are under a [cc-by.sa 3.0](http://creativecommons.org/licenses/by-sa/3.0/) license.

View File

@ -1,32 +1,42 @@
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.identity = "vanwa_streuner" -- The name of the save directory (string)
t.version = "0.10.1" -- The LÖVE version this game was made for (string)
t.console = false -- Attach a console (boolean, Windows only)
t.accelerometerjoystick = true -- Enable the accelerometer on iOS and Android by exposing it as a Joystick (boolean)
t.externalstorage = false -- True to save files (and read from the save directory) in external storage on Android (boolean)
t.gammacorrect = true -- Enable gamma-correct rendering, when supported by the system (boolean)
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.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.minwidth = 1 -- Minimum window width if the window is resizable (number)
t.window.minheight = 1 -- Minimum window height if the window is resizable (number)
t.window.fullscreen = false -- Enable fullscreen (boolean)
t.window.fullscreentype = "desktop" -- Choose between "desktop" fullscreen or "exclusive" fullscreen mode (string)
t.window.vsync = true -- Enable vertical sync (boolean)
t.window.msaa = 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 = false -- Enable high-dpi mode for the window on a Retina display (boolean)
t.window.x = nil -- The x-coordinate of the window's position in the specified display (number)
t.window.y = nil -- The y-coordinate of the window's position in the specified display (number)
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)
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), Disabling it will result 0 delta time in love.update
t.modules.touch = true -- Enable the touch module (boolean)
t.modules.video = true -- Enable the video module (boolean)
t.modules.window = true -- Enable the window module (boolean)
t.modules.thread = true -- Enable the thread module (boolean)
end

View File

@ -1,10 +1,9 @@
local anim8 = require 'engine/libs/anim8'
local object = require 'engine/object'
Animator = class()
Animator = class('Animator')
function Animator:__init(fullWidth, fullHeight)
self.animationGrid = anim8.newGrid(object.width, object.height, fullWidth, fullHeight)
function Animator:init(fullWidth, fullHeight)
self.animationGrid = anim8.newGrid(Global.tilewidth, Global.tileheight, fullWidth, fullHeight)
self.walk_up = anim8.newAnimation(self.animationGrid('1-9', 1), 0.1)
self.walk_left = anim8.newAnimation(self.animationGrid('1-9', 2), 0.1)
@ -13,3 +12,4 @@ function Animator:__init(fullWidth, fullHeight)
self.play = anim8.newAnimation(self.animationGrid('1-9', 5), 0.1)
end
return Animator

View File

@ -1,50 +1,21 @@
local object = require '../engine/object'
local animator = require '../engine/animator'
Gridwalker = class()
Gridwalker = class('Gridwalker')
Gridwalker.speed = 2
Gridwalker.collisionTestSize = 13
Gridwalker.neighbourOffsetX = (object.width - Gridwalker.collisionTestSize) / 2
Gridwalker.neighbourOffsetY = object.width - Gridwalker.collisionTestSize * 1.9
Gridwalker.testShape = nil
Gridwalker.objectinfo = nil
Gridwalker.animation = 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
function Gridwalker:init(objectinfo, world)
self.info = objectinfo
self.world = world
self.animator = Animator(self.info.image:getWidth(), self.info.image:getHeight())
self.animation = self.animator[self.info.pose]
self.animation:pauseAtStart()
end
function Gridwalker:findAnimation(animationName)
local foundAnimation = nil
if animationName == 'up' then
foundAnimation = self.animator.walk_up
elseif animationName == 'down' then
foundAnimation = self.animator.walk_down
elseif animationName == 'right' then
foundAnimation = self.animator.walk_right
elseif animationName == 'left' then
foundAnimation = self.animator.walk_left
elseif animationName == 'play' then
foundAnimation = self.animator.play
end
return foundAnimation
function Gridwalker:sendMovement(key)
if self[key] ~= nil then
self[key](self)
end
end
function Gridwalker:stopAnimation()
@ -52,83 +23,50 @@ function Gridwalker:stopAnimation()
end
function Gridwalker:startAnimation(animationName)
self.animation = self:findAnimation(animationName)
self.animation = self.animator[animationName]
self.animation:resume()
end
function Gridwalker:up()
local newX = self.objectinfo.x
local newY = self.objectinfo.y - self.speed
self.animation = self.animator.walk_up
self:move(newX, newY)
self:move(0, - self.speed)
end
function Gridwalker:down()
local newX = self.objectinfo.x
local newY = self.objectinfo.y + self.speed
self.animation = self.animator.walk_down
self:move(newX, newY)
self:move(0, self.speed)
end
function Gridwalker:right()
local newX = self.objectinfo.x + self.speed
local newY = self.objectinfo.y
self.animation = self.animator.walk_right
self:move(newX, newY)
self:move(self.speed, 0)
end
function Gridwalker:left()
local newX = self.objectinfo.x - self.speed
local newY = self.objectinfo.y
self.animation = self.animator.walk_left
self:move(newX, newY)
end
function Gridwalker:init()
self.testShape = collider:addRectangle(self.objectinfo.x + self.neighbourOffsetX, self.objectinfo.y + self.neighbourOffsetY, self.collisionTestSize, self.collisionTestSize)
collider:setPassive(self.testShape)
self.animator = Animator:new(self.objectinfo.image:getWidth(), self.objectinfo.image:getHeight())
self.animation = self:findAnimation(self.objectinfo.pose)
self.animation:pauseAtStart()
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
self:move(- self.speed, 0)
end
function Gridwalker:move(x, y)
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.animation:resume()
self.objectinfo.x = x
self.objectinfo.y = y
else
self:setTestShape(self.objectinfo.x, self.objectinfo.y)
end
local goalX, goalY = self.info.collision.x + x, self.info.collision.y + y
local actualX, actualY, cols, len = self.world:move(self.info.collision, goalX, goalY)
if len == 0 then
self.info.x = self.info.x + x
self.info.y = self.info.y + y
if self.info.collision then
self.info.collision.x = self.info.collision.x + x
self.info.collision.y = self.info.collision.y + y
self.world:update(self.info.collision, self.info.collision.x, self.info.collision.y)
end
end
end
function Gridwalker:draw()
self.animation:draw(self.info.image, self.info.x, self.info.y)
end
return Gridwalker

6
src/engine/global.lua Normal file
View File

@ -0,0 +1,6 @@
local Global = {}
Global.tilewidth = 64
Global.tileheight = 64
return Global

View File

@ -1,162 +1,86 @@
class = require 'engine/libs/30log'
Global = require 'engine/global'
local HC = require 'engine/libs/hardoncollider'
-- has to come before the sti initialization so I can add the collision tiles
collider = HC(100)
local sti = require 'engine/libs/sti'
local object = require 'engine/object'
local ui = require 'engine/ui'
local Gridwalker = require 'engine/controllers/gridwalker'
local Level = require 'engine/level'
local sound = require 'engine/sound'
local story = require 'story'
Engine = class()
Engine = class('Engine')
Engine.objects = {}
Engine.inputMap = {}
Engine.inputMap['w'] = 'up'
Engine.inputMap['s'] = 'down'
Engine.inputMap['a'] = 'left'
Engine.inputMap['d'] = 'right'
Engine.inputMap['up'] = 'up'
Engine.inputMap['down'] = 'down'
Engine.inputMap['left'] = 'left'
Engine.inputMap['right'] = 'right'
Engine.inputMap['k'] = 'up'
Engine.inputMap['j'] = 'down'
Engine.inputMap['h'] = 'left'
Engine.inputMap['l'] = 'right'
Engine.inputMap['return'] = 'enter'
function Engine:__init()
self.windowWidth = love.graphics.getWidth()
self.windowHeight = love.graphics.getHeight()
story:start(self)
Engine.controllers = {gridwalker = Gridwalker}
function Engine:init()
story:start(self)
end
function Engine:load(level)
self.level = Level(level)
end
function Engine:update(dt)
self.level:update(dt)
love.audio.update()
local input = Engine:getInput()
if input ~= nil then
self.level:checkObjectKeys(dt, input)
end
end
function Engine:draw(dt)
self.level:draw()
end
function Engine:checkObjectAnimation(key)
local input = Engine.inputMap[key]
if input ~= nil then
self.level:checkObjectAnimation(input)
end
end
function Engine:showMessage(message)
self.level:showMessage(message)
end
function Engine:animate(objectName, animationName)
self.level:startAnimate(objectName, animationName)
end
function Engine:stopAnimate(objectName)
self.level:stopAnimate(objectName)
end
function Engine:playSound(soundName, loop, finishedFunc)
return love.audio.play('assets/sound/' .. soundName .. '.ogg', 'stream', loop, finishedFunc)
end
function Engine:checkObjectAnimation(key)
for _, object in ipairs(self.objects) do
if object.relevantKeys then
for _, relevantKey in ipairs(object.relevantKeys) do
if key == relevantKey then
object.controller:stopAnimation()
function Engine:getInput()
local foundInput = nil
for key, input in pairs(Engine.inputMap) do
if love.keyboard.isDown(key) then
foundInput = input
break
end
end
end
end
return foundInput
end
function Engine:checkObjectKeys(dt)
for _, object in ipairs(self.objects) do
object.controller.animation:update(dt)
end
if not ui.active then
for _, object in ipairs(self.objects) do
if object.relevantKeys then
for _, relevantKey in ipairs(object.relevantKeys) do
if love.keyboard.isDown(relevantKey) then
object.controller:sendKey(relevantKey)
end
end
end
end
else
for _, relevantKey in ipairs(ui.relevantKeys) do
if love.keyboard.isDown(relevantKey) then
ui:sendKey(relevantKey)
end
end
end
end
function Engine:update(dt)
self:checkObjectKeys(dt)
collider:update(dt)
self.map:update(dt)
love.audio.update()
end
function Engine:draw()
local translateX = 0
local translateY = 0
-- Draw Range culls unnecessary tiles
self.map:setDrawRange(translateX, translateY, self.windowWidth, self.windowHeight)
self.map:draw()
ui:draw()
--self:debugDrawing()
end
function Engine:initObjects()
self.objects = {}
self.map:addCustomLayer("object layer", 5)
local objectLayer = self.map.layers["object layer"]
objectLayer.sprites = {}
for _, obj in pairs(self.map.layers["objects"].objects) do
local filename = "objects/" .. obj.name .. ".lua"
local objectinfo = love.filesystem.load(filename)()
objectinfo.x = obj.x - object.width / 4
objectinfo.y = obj.y
objectinfo.name = obj.name
objectinfo.controller.objectinfo = objectinfo
objectinfo.image = love.graphics.newImage(objectinfo.spritesheet)
objectinfo.controller:init()
table.insert(objectLayer.sprites, {info = objectinfo})
table.insert(self.objects, objectinfo)
end
-- Draw callback for Custom Layer
function objectLayer:draw()
table.sort(self.sprites, object.sortNorthSouth)
for _, sprite in pairs(self.sprites) do
local x = math.floor(sprite.info.x)
local y = math.floor(sprite.info.y)
sprite.info.controller.animation:draw(sprite.info.image, x, y)
end
end
self.map.layers["objects"].visible = false
end
function Engine:debugDrawing()
-- draw collision shapes for debugging
for shape in pairs(collider:shapesInRange(0, 0, self.windowWidth, self.windowHeight)) do
shape:draw()
end
-- Draw Collision Map (useful for debugging)
self.map:drawCollisionMap(collision)
end
function Engine:loadLevel(name)
local mapName = 'levels/' .. name
self.map = sti.new(mapName)
self.collision = self.map:getCollisionMap("collision")
self:initObjects()
end
function Engine:showMessage(message)
ui:showMessage(message)
end
function Engine:animate(objectName, animationName)
for _, object in ipairs(self.objects) do
if object.name == objectName then
object.controller:startAnimation(animationName)
break
end
end
end
function Engine:stopAnimate(objectName)
for _, object in ipairs(self.objects) do
if object.name == objectName then
object.controller:stopAnimation()
break
end
end
end
return Engine

150
src/engine/level.lua Normal file
View File

@ -0,0 +1,150 @@
local sti = require 'engine/libs/sti'
local ui = require 'engine/ui'
local bump = require 'engine/libs/bump'
Level = class('Level')
Level.controllers = {}
function Level:init(name)
Level:load(name)
end
function Level:update(dt)
self.map:update(dt)
for _, controller in ipairs(self.controllers) do
controller.animation:update(dt)
end
end
function Level:draw(dt)
local translateX = 0
local translateY = 0
-- Draw Range culls unnecessary tiles
self.map:setDrawRange(
translateX,
translateY,
love.graphics.getWidth(),
love.graphics.getHeight())
self.map:draw()
ui:draw()
-- debugging
-- self.map:bump_draw(self.bumpworld)
end
function Level:showMessage(message)
ui:showMessage(message)
end
function Level:checkObjectAnimation(input)
for _, controller in ipairs(self.controllers) do
if controller.info.relevantInputs then
for _, relevantInput in ipairs(controller.info.relevantInputs) do
if relevantInput == input then
controller:stopAnimation()
end
end
end
end
end
function Level:checkObjectKeys(dt, input)
if not ui.active then
for _, controller in ipairs(self.controllers) do
if controller.info.relevantInputs then
for _, relevantInput in ipairs(controller.info.relevantInputs) do
if relevantInput == input then
controller:sendMovement(input)
break
end
end
end
end
else
for _, relevantInput in ipairs(ui.relevantInputs) do
if relevantInput == input then
ui:sendInput(input)
end
end
end
end
function Level:load(name)
local mapName = 'levels/' .. name
self.map = sti.new(mapName, { 'bump' })
self.bumpworld = bump.newWorld()
self.map:bump_init(self.bumpworld)
Level:initObjects()
end
function Level:startAnimate(objectName, animationName)
for _, controller in ipairs(self.controllers) do
if controller.info.name == objectName then
controller:startAnimation(animationName)
break
end
end
end
function Level:stopAnimate(objectName)
for _, controller in ipairs(self.controllers) do
if controller.info.name == objectName then
controller:stopAnimation()
break
end
end
end
function Level:initObjects()
self.controllers = {}
self.map:addCustomLayer('object layer', 5)
local objectLayer = self.map.layers['object layer']
objectLayer.controllers = {}
for _, obj in pairs(self.map.layers['objects'].objects) do
if obj.properties.has_controller then
local filename = 'objects/' .. obj.name .. '.lua'
local objectinfo = love.filesystem.load(filename)()
objectinfo.x = obj.x - Global.tilewidth / 4
objectinfo.y = obj.y
objectinfo.name = obj.name
objectinfo.image = love.graphics.newImage(objectinfo.spritesheet)
if obj.properties.collision then
for _, collisionObj in pairs(self.map.bump_collidables) do
if collisionObj.name == obj.properties.collision then
objectinfo.collision = collisionObj
break
end
end
end
local controller = Engine.controllers[objectinfo.controller](objectinfo, self.bumpworld)
table.insert(self.controllers, controller)
table.insert(objectLayer.controllers, controller)
end
end
-- Draw callback for Custom Layer
function objectLayer:draw()
table.sort(self.controllers, function(a, b) return a.info.y < b.info.y end)
for _, controller in pairs(self.controllers) do
controller:draw()
end
end
self.map.layers['objects'].visible = false
end
return Level

View File

@ -1,7 +1,8 @@
-- 1.0.0
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 {}
local baseMt, _instances, _classes, _class = {}, setmetatable({},{__mode='k'}), setmetatable({},{__mode='k'})
local function assert_class(class, method) assert(_classes[class], ('Wrong method call. Expected class:%s.'):format(method)) end
local function deep_copy(t, dest, aType) t = t or {}; local r = 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
@ -9,22 +10,22 @@ local function deep_copy(t, dest, aType)
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
assert_class(self, 'new(...) or class(...)'); local instance = {class = 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)
local function extend(self, name, extra_params)
assert_class(self, 'extend(...)'); local heir = {}; _classes[heir] = tostring(heir); deep_copy(extra_params, deep_copy(self, heir));
heir.name, heir.__index, heir.super = extra_params and extra_params.name or name, 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
if _instances[self] then return ("instance of '%s' (%s)"):format(rawget(self.class,'name') or '?', _instances[self]) end
return _classes[self] and ("class '%s' (%s)"):format(rawget(self,'name') or '?',_classes[self]) or self
end}; _classes[baseMt] = tostring(baseMt); setmetatable(baseMt, {__tostring = baseMt.__tostring})
local class = {isClass = function(class, ofsuper) local isclass = not not _classes[class]; if ofsuper then return isclass and (class.super == ofsuper) end; return isclass end, isInstance = function(instance, ofclass)
local isinstance = not not _instances[instance]; if ofclass then return isinstance and (instance.class == ofclass) end; return isinstance end}; _class = function(name, attr)
local c = deep_copy(attr); c.mixins=setmetatable({},{__mode='k'}); _classes[c] = tostring(c); c.name, c.__tostring, c.__call = name or c.name, baseMt.__tostring, baseMt.__call
c.include = function(self,mixin) assert_class(self, 'include(mixin)'); self.mixins[mixin] = true; return deep_copy(mixin, self, 'function') end
c.new, c.extend, c.__index, c.includes = instantiate, extend, c, function(self,mixin) assert_class(self,'includes(mixin)') return not not (self.mixins[mixin] or (self.super and self.super:includes(mixin))) end
c.extends = function(self, class) assert_class(self, 'extends(class)') local super = self; repeat super = super.super until (super == class or super == nil); return class and (super == class) end
return setmetatable(c, baseMt) end; class._DESCRIPTION = '30 lines library for object orientation in Lua'; class._VERSION = '30log v1.0.0'; class._URL = 'http://github.com/Yonaba/30log'; class._LICENSE = 'MIT LICENSE <http://www.opensource.org/licenses/mit-license.php>'
return setmetatable(class,{__call = function(_,...) return _class(...) end })

View File

@ -1,5 +1,5 @@
local anim8 = {
_VERSION = 'anim8 v2.1.0',
_VERSION = 'anim8 v2.3.0',
_DESCRIPTION = 'An animation library for LÖVE',
_URL = 'https://github.com/kikito/anim8',
_LICENSE = [[
@ -262,22 +262,36 @@ function Animation:resume()
self.status = "playing"
end
function Animation:draw(image, x, y, r, sx, sy, ox, oy, ...)
function Animation:draw(image, x, y, r, sx, sy, ox, oy, kx, ky)
love.graphics.draw(image, self:getFrameInfo(x, y, r, sx, sy, ox, oy, kx, ky))
end
function Animation:getFrameInfo(x, y, r, sx, sy, ox, oy, kx, ky)
local frame = self.frames[self.position]
if self.flippedH or self.flippedV then
r,sx,sy,ox,oy = r or 0, sx or 1, sy or 1, ox or 0, oy or 0
r,sx,sy,ox,oy,kx,ky = r or 0, sx or 1, sy or 1, ox or 0, oy or 0, kx or 0, ky or 0
local _,_,w,h = frame:getViewport()
if self.flippedH then
sx = sx * -1
ox = w - ox
kx = kx * -1
ky = ky * -1
end
if self.flippedV then
sy = sy * -1
oy = h - oy
kx = kx * -1
ky = ky * -1
end
end
love.graphics.draw(image, frame, x, y, r, sx, sy, ox, oy, ...)
return frame, x, y, r, sx, sy, ox, oy, kx, ky
end
function Animation:getDimensions()
local _,_,w,h = self.frames[self.position]:getViewport()
return w,h
end
-----------------------------------------------------------

768
src/engine/libs/bump.lua Normal file
View File

@ -0,0 +1,768 @@
local bump = {
_VERSION = 'bump v3.1.5',
_URL = 'https://github.com/kikito/bump.lua',
_DESCRIPTION = 'A collision detection library for Lua',
_LICENSE = [[
MIT LICENSE
Copyright (c) 2014 Enrique García Cota
Permission is hereby granted, free of charge, to any person obtaining a
copy of this software and associated documentation files (the
"Software"), to deal in the Software without restriction, including
without limitation the rights to use, copy, modify, merge, publish,
distribute, sublicense, and/or sell copies of the Software, and to
permit persons to whom the Software is furnished to do so, subject to
the following conditions:
The above copyright notice and this permission notice shall be included
in all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS
OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY
CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT,
TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
]]
}
------------------------------------------
-- Auxiliary functions
------------------------------------------
local DELTA = 1e-10 -- floating-point margin of error
local abs, floor, ceil, min, max = math.abs, math.floor, math.ceil, math.min, math.max
local function sign(x)
if x > 0 then return 1 end
if x == 0 then return 0 end
return -1
end
local function nearest(x, a, b)
if abs(a - x) < abs(b - x) then return a else return b end
end
local function assertType(desiredType, value, name)
if type(value) ~= desiredType then
error(name .. ' must be a ' .. desiredType .. ', but was ' .. tostring(value) .. '(a ' .. type(value) .. ')')
end
end
local function assertIsPositiveNumber(value, name)
if type(value) ~= 'number' or value <= 0 then
error(name .. ' must be a positive integer, but was ' .. tostring(value) .. '(' .. type(value) .. ')')
end
end
local function assertIsRect(x,y,w,h)
assertType('number', x, 'x')
assertType('number', y, 'y')
assertIsPositiveNumber(w, 'w')
assertIsPositiveNumber(h, 'h')
end
local defaultFilter = function()
return 'slide'
end
------------------------------------------
-- Rectangle functions
------------------------------------------
local function rect_getNearestCorner(x,y,w,h, px, py)
return nearest(px, x, x+w), nearest(py, y, y+h)
end
-- This is a generalized implementation of the liang-barsky algorithm, which also returns
-- the normals of the sides where the segment intersects.
-- Returns nil if the segment never touches the rect
-- Notice that normals are only guaranteed to be accurate when initially ti1, ti2 == -math.huge, math.huge
local function rect_getSegmentIntersectionIndices(x,y,w,h, x1,y1,x2,y2, ti1,ti2)
ti1, ti2 = ti1 or 0, ti2 or 1
local dx, dy = x2-x1, y2-y1
local nx, ny
local nx1, ny1, nx2, ny2 = 0,0,0,0
local p, q, r
for side = 1,4 do
if side == 1 then nx,ny,p,q = -1, 0, -dx, x1 - x -- left
elseif side == 2 then nx,ny,p,q = 1, 0, dx, x + w - x1 -- right
elseif side == 3 then nx,ny,p,q = 0, -1, -dy, y1 - y -- top
else nx,ny,p,q = 0, 1, dy, y + h - y1 -- bottom
end
if p == 0 then
if q <= 0 then return nil end
else
r = q / p
if p < 0 then
if r > ti2 then return nil
elseif r > ti1 then ti1,nx1,ny1 = r,nx,ny
end
else -- p > 0
if r < ti1 then return nil
elseif r < ti2 then ti2,nx2,ny2 = r,nx,ny
end
end
end
end
return ti1,ti2, nx1,ny1, nx2,ny2
end
-- Calculates the minkowsky difference between 2 rects, which is another rect
local function rect_getDiff(x1,y1,w1,h1, x2,y2,w2,h2)
return x2 - x1 - w1,
y2 - y1 - h1,
w1 + w2,
h1 + h2
end
local function rect_containsPoint(x,y,w,h, px,py)
return px - x > DELTA and py - y > DELTA and
x + w - px > DELTA and y + h - py > DELTA
end
local function rect_isIntersecting(x1,y1,w1,h1, x2,y2,w2,h2)
return x1 < x2+w2 and x2 < x1+w1 and
y1 < y2+h2 and y2 < y1+h1
end
local function rect_getSquareDistance(x1,y1,w1,h1, x2,y2,w2,h2)
local dx = x1 - x2 + (w1 - w2)/2
local dy = y1 - y2 + (h1 - h2)/2
return dx*dx + dy*dy
end
local function rect_detectCollision(x1,y1,w1,h1, x2,y2,w2,h2, goalX, goalY)
goalX = goalX or x1
goalY = goalY or y1
local dx, dy = goalX - x1, goalY - y1
local x,y,w,h = rect_getDiff(x1,y1,w1,h1, x2,y2,w2,h2)
local overlaps, ti, nx, ny
if rect_containsPoint(x,y,w,h, 0,0) then -- item was intersecting other
local px, py = rect_getNearestCorner(x,y,w,h, 0, 0)
local wi, hi = min(w1, abs(px)), min(h1, abs(py)) -- area of intersection
ti = -wi * hi -- ti is the negative area of intersection
overlaps = true
else
local ti1,ti2,nx1,ny1 = rect_getSegmentIntersectionIndices(x,y,w,h, 0,0,dx,dy, -math.huge, math.huge)
-- item tunnels into other
if ti1 and ti1 < 1 and (0 < ti1 + DELTA or 0 == ti1 and ti2 > 0) then
ti, nx, ny = ti1, nx1, ny1
overlaps = false
end
end
if not ti then return end
local tx, ty
if overlaps then
if dx == 0 and dy == 0 then
-- intersecting and not moving - use minimum displacement vector
local px, py = rect_getNearestCorner(x,y,w,h, 0,0)
if abs(px) < abs(py) then py = 0 else px = 0 end
nx, ny = sign(px), sign(py)
tx, ty = x1 + px, y1 + py
else
-- intersecting and moving - move in the opposite direction
local ti1, _
ti1,_,nx,ny = rect_getSegmentIntersectionIndices(x,y,w,h, 0,0,dx,dy, -math.huge, 1)
if not ti1 then return end
tx, ty = x1 + dx * ti1, y1 + dy * ti1
end
else -- tunnel
tx, ty = x1 + dx * ti, y1 + dy * ti
end
return {
overlaps = overlaps,
ti = ti,
move = {x = dx, y = dy},
normal = {x = nx, y = ny},
touch = {x = tx, y = ty},
itemRect = {x = x1, y = y1, w = w1, h = h1},
otherRect = {x = x2, y = y2, w = w2, h = h2}
}
end
------------------------------------------
-- Grid functions
------------------------------------------
local function grid_toWorld(cellSize, cx, cy)
return (cx - 1)*cellSize, (cy-1)*cellSize
end
local function grid_toCell(cellSize, x, y)
return floor(x / cellSize) + 1, floor(y / cellSize) + 1
end
-- grid_traverse* functions are based on "A Fast Voxel Traversal Algorithm for Ray Tracing",
-- by John Amanides and Andrew Woo - http://www.cse.yorku.ca/~amana/research/grid.pdf
-- It has been modified to include both cells when the ray "touches a grid corner",
-- and with a different exit condition
local function grid_traverse_initStep(cellSize, ct, t1, t2)
local v = t2 - t1
if v > 0 then
return 1, cellSize / v, ((ct + v) * cellSize - t1) / v
elseif v < 0 then
return -1, -cellSize / v, ((ct + v - 1) * cellSize - t1) / v
else
return 0, math.huge, math.huge
end
end
local function grid_traverse(cellSize, x1,y1,x2,y2, f)
local cx1,cy1 = grid_toCell(cellSize, x1,y1)
local cx2,cy2 = grid_toCell(cellSize, x2,y2)
local stepX, dx, tx = grid_traverse_initStep(cellSize, cx1, x1, x2)
local stepY, dy, ty = grid_traverse_initStep(cellSize, cy1, y1, y2)
local cx,cy = cx1,cy1
f(cx, cy)
-- The default implementation had an infinite loop problem when
-- approaching the last cell in some occassions. We finish iterating
-- when we are *next* to the last cell
while abs(cx - cx2) + abs(cy - cy2) > 1 do
if tx < ty then
tx, cx = tx + dx, cx + stepX
f(cx, cy)
else
-- Addition: include both cells when going through corners
if tx == ty then f(cx + stepX, cy) end
ty, cy = ty + dy, cy + stepY
f(cx, cy)
end
end
-- If we have not arrived to the last cell, use it
if cx ~= cx2 or cy ~= cy2 then f(cx2, cy2) end
end
local function grid_toCellRect(cellSize, x,y,w,h)
local cx,cy = grid_toCell(cellSize, x, y)
local cr,cb = ceil((x+w) / cellSize), ceil((y+h) / cellSize)
return cx, cy, cr - cx + 1, cb - cy + 1
end
------------------------------------------
-- Responses
------------------------------------------
local touch = function(world, col, x,y,w,h, goalX, goalY, filter)
return col.touch.x, col.touch.y, {}, 0
end
local cross = function(world, col, x,y,w,h, goalX, goalY, filter)
local cols, len = world:project(col.item, x,y,w,h, goalX, goalY, filter)
return goalX, goalY, cols, len
end
local slide = function(world, col, x,y,w,h, goalX, goalY, filter)
goalX = goalX or x
goalY = goalY or y
local tch, move = col.touch, col.move
local sx, sy = tch.x, tch.y
if move.x ~= 0 or move.y ~= 0 then
if col.normal.x == 0 then
sx = goalX
else
sy = goalY
end
end
col.slide = {x = sx, y = sy}
x,y = tch.x, tch.y
goalX, goalY = sx, sy
local cols, len = world:project(col.item, x,y,w,h, goalX, goalY, filter)
return goalX, goalY, cols, len
end
local bounce = function(world, col, x,y,w,h, goalX, goalY, filter)
goalX = goalX or x
goalY = goalY or y
local tch, move = col.touch, col.move
local tx, ty = tch.x, tch.y
local bx, by = tx, ty
if move.x ~= 0 or move.y ~= 0 then
local bnx, bny = goalX - tx, goalY - ty
if col.normal.x == 0 then bny = -bny else bnx = -bnx end
bx, by = tx + bnx, ty + bny
end
col.bounce = {x = bx, y = by}
x,y = tch.x, tch.y
goalX, goalY = bx, by
local cols, len = world:project(col.item, x,y,w,h, goalX, goalY, filter)
return goalX, goalY, cols, len
end
------------------------------------------
-- World
------------------------------------------
local World = {}
local World_mt = {__index = World}
-- Private functions and methods
local function sortByWeight(a,b) return a.weight < b.weight end
local function sortByTiAndDistance(a,b)
if a.ti == b.ti then
local ir, ar, br = a.itemRect, a.otherRect, b.otherRect
local ad = rect_getSquareDistance(ir.x,ir.y,ir.w,ir.h, ar.x,ar.y,ar.w,ar.h)
local bd = rect_getSquareDistance(ir.x,ir.y,ir.w,ir.h, br.x,br.y,br.w,br.h)
return ad < bd
end
return a.ti < b.ti
end
local function addItemToCell(self, item, cx, cy)
self.rows[cy] = self.rows[cy] or setmetatable({}, {__mode = 'v'})
local row = self.rows[cy]
row[cx] = row[cx] or {itemCount = 0, x = cx, y = cy, items = setmetatable({}, {__mode = 'k'})}
local cell = row[cx]
self.nonEmptyCells[cell] = true
if not cell.items[item] then
cell.items[item] = true
cell.itemCount = cell.itemCount + 1
end
end
local function removeItemFromCell(self, item, cx, cy)
local row = self.rows[cy]
if not row or not row[cx] or not row[cx].items[item] then return false end
local cell = row[cx]
cell.items[item] = nil
cell.itemCount = cell.itemCount - 1
if cell.itemCount == 0 then
self.nonEmptyCells[cell] = nil
end
return true
end
local function getDictItemsInCellRect(self, cl,ct,cw,ch)
local items_dict = {}
for cy=ct,ct+ch-1 do
local row = self.rows[cy]
if row then
for cx=cl,cl+cw-1 do
local cell = row[cx]
if cell and cell.itemCount > 0 then -- no cell.itemCount > 1 because tunneling
for item,_ in pairs(cell.items) do
items_dict[item] = true
end
end
end
end
end
return items_dict
end
local function getCellsTouchedBySegment(self, x1,y1,x2,y2)
local cells, cellsLen, visited = {}, 0, {}
grid_traverse(self.cellSize, x1,y1,x2,y2, function(cx, cy)
local row = self.rows[cy]
if not row then return end
local cell = row[cx]
if not cell or visited[cell] then return end
visited[cell] = true
cellsLen = cellsLen + 1
cells[cellsLen] = cell
end)
return cells, cellsLen
end
local function getInfoAboutItemsTouchedBySegment(self, x1,y1, x2,y2, filter)
local cells, len = getCellsTouchedBySegment(self, x1,y1,x2,y2)
local cell, rect, l,t,w,h, ti1,ti2, tii0,tii1
local visited, itemInfo, itemInfoLen = {},{},0
for i=1,len do
cell = cells[i]
for item in pairs(cell.items) do
if not visited[item] then
visited[item] = true
if (not filter or filter(item)) then
rect = self.rects[item]
l,t,w,h = rect.x,rect.y,rect.w,rect.h
ti1,ti2 = rect_getSegmentIntersectionIndices(l,t,w,h, x1,y1, x2,y2, 0, 1)
if ti1 and ((0 < ti1 and ti1 < 1) or (0 < ti2 and ti2 < 1)) then
-- the sorting is according to the t of an infinite line, not the segment
tii0,tii1 = rect_getSegmentIntersectionIndices(l,t,w,h, x1,y1, x2,y2, -math.huge, math.huge)
itemInfoLen = itemInfoLen + 1
itemInfo[itemInfoLen] = {item = item, ti1 = ti1, ti2 = ti2, weight = min(tii0,tii1)}
end
end
end
end
end
table.sort(itemInfo, sortByWeight)
return itemInfo, itemInfoLen
end
local function getResponseByName(self, name)
local response = self.responses[name]
if not response then
error(('Unknown collision type: %s (%s)'):format(name, type(name)))
end
return response
end
-- Misc Public Methods
function World:addResponse(name, response)
self.responses[name] = response
end
function World:project(item, x,y,w,h, goalX, goalY, filter)
assertIsRect(x,y,w,h)
goalX = goalX or x
goalY = goalY or y
filter = filter or defaultFilter
local collisions, len = {}, 0
local visited = {}
if item ~= nil then visited[item] = true end
-- This could probably be done with less cells using a polygon raster over the cells instead of a
-- bounding rect of the whole movement. Conditional to building a queryPolygon method
local tl, tt = min(goalX, x), min(goalY, y)
local tr, tb = max(goalX + w, x+w), max(goalY + h, y+h)
local tw, th = tr-tl, tb-tt
local cl,ct,cw,ch = grid_toCellRect(self.cellSize, tl,tt,tw,th)
local dictItemsInCellRect = getDictItemsInCellRect(self, cl,ct,cw,ch)
for other,_ in pairs(dictItemsInCellRect) do
if not visited[other] then
visited[other] = true
local responseName = filter(item, other)
if responseName then
local ox,oy,ow,oh = self:getRect(other)
local col = rect_detectCollision(x,y,w,h, ox,oy,ow,oh, goalX, goalY)
if col then
col.other = other
col.item = item
col.type = responseName
len = len + 1
collisions[len] = col
end
end
end
end
table.sort(collisions, sortByTiAndDistance)
return collisions, len
end
function World:countCells()
local count = 0
for _,row in pairs(self.rows) do
for _,_ in pairs(row) do
count = count + 1
end
end
return count
end
function World:hasItem(item)
return not not self.rects[item]
end
function World:getItems()
local items, len = {}, 0
for item,_ in pairs(self.rects) do
len = len + 1
items[len] = item
end
return items, len
end
function World:countItems()
local len = 0
for _ in pairs(self.rects) do len = len + 1 end
return len
end
function World:getRect(item)
local rect = self.rects[item]
if not rect then
error('Item ' .. tostring(item) .. ' must be added to the world before getting its rect. Use world:add(item, x,y,w,h) to add it first.')
end
return rect.x, rect.y, rect.w, rect.h
end
function World:toWorld(cx, cy)
return grid_toWorld(self.cellSize, cx, cy)
end
function World:toCell(x,y)
return grid_toCell(self.cellSize, x, y)
end
--- Query methods
function World:queryRect(x,y,w,h, filter)
local cl,ct,cw,ch = grid_toCellRect(self.cellSize, x,y,w,h)
local dictItemsInCellRect = getDictItemsInCellRect(self, cl,ct,cw,ch)
local items, len = {}, 0
local rect
for item,_ in pairs(dictItemsInCellRect) do
rect = self.rects[item]
if (not filter or filter(item))
and rect_isIntersecting(x,y,w,h, rect.x, rect.y, rect.w, rect.h)
then
len = len + 1
items[len] = item
end
end
return items, len
end
function World:queryPoint(x,y, filter)
local cx,cy = self:toCell(x,y)
local dictItemsInCellRect = getDictItemsInCellRect(self, cx,cy,1,1)
local items, len = {}, 0
local rect
for item,_ in pairs(dictItemsInCellRect) do
rect = self.rects[item]
if (not filter or filter(item))
and rect_containsPoint(rect.x, rect.y, rect.w, rect.h, x, y)
then
len = len + 1
items[len] = item
end
end
return items, len
end
function World:querySegment(x1, y1, x2, y2, filter)
local itemInfo, len = getInfoAboutItemsTouchedBySegment(self, x1, y1, x2, y2, filter)
local items = {}
for i=1, len do
items[i] = itemInfo[i].item
end
return items, len
end
function World:querySegmentWithCoords(x1, y1, x2, y2, filter)
local itemInfo, len = getInfoAboutItemsTouchedBySegment(self, x1, y1, x2, y2, filter)
local dx, dy = x2-x1, y2-y1
local info, ti1, ti2
for i=1, len do
info = itemInfo[i]
ti1 = info.ti1
ti2 = info.ti2
info.weight = nil
info.x1 = x1 + dx * ti1
info.y1 = y1 + dy * ti1
info.x2 = x1 + dx * ti2
info.y2 = y1 + dy * ti2
end
return itemInfo, len
end
--- Main methods
function World:add(item, x,y,w,h)
local rect = self.rects[item]
if rect then
error('Item ' .. tostring(item) .. ' added to the world twice.')
end
assertIsRect(x,y,w,h)
self.rects[item] = {x=x,y=y,w=w,h=h}
local cl,ct,cw,ch = grid_toCellRect(self.cellSize, x,y,w,h)
for cy = ct, ct+ch-1 do
for cx = cl, cl+cw-1 do
addItemToCell(self, item, cx, cy)
end
end
return item
end
function World:remove(item)
local x,y,w,h = self:getRect(item)
self.rects[item] = nil
local cl,ct,cw,ch = grid_toCellRect(self.cellSize, x,y,w,h)
for cy = ct, ct+ch-1 do
for cx = cl, cl+cw-1 do
removeItemFromCell(self, item, cx, cy)
end
end
end
function World:update(item, x2,y2,w2,h2)
local x1,y1,w1,h1 = self:getRect(item)
w2,h2 = w2 or w1, h2 or h1
assertIsRect(x2,y2,w2,h2)
if x1 ~= x2 or y1 ~= y2 or w1 ~= w2 or h1 ~= h2 then
local cellSize = self.cellSize
local cl1,ct1,cw1,ch1 = grid_toCellRect(cellSize, x1,y1,w1,h1)
local cl2,ct2,cw2,ch2 = grid_toCellRect(cellSize, x2,y2,w2,h2)
if cl1 ~= cl2 or ct1 ~= ct2 or cw1 ~= cw2 or ch1 ~= ch2 then
local cr1, cb1 = cl1+cw1-1, ct1+ch1-1
local cr2, cb2 = cl2+cw2-1, ct2+ch2-1
local cyOut
for cy = ct1, cb1 do
cyOut = cy < ct2 or cy > cb2
for cx = cl1, cr1 do
if cyOut or cx < cl2 or cx > cr2 then
removeItemFromCell(self, item, cx, cy)
end
end
end
for cy = ct2, cb2 do
cyOut = cy < ct1 or cy > cb1
for cx = cl2, cr2 do
if cyOut or cx < cl1 or cx > cr1 then
addItemToCell(self, item, cx, cy)
end
end
end
end
local rect = self.rects[item]
rect.x, rect.y, rect.w, rect.h = x2,y2,w2,h2
end
end
function World:move(item, goalX, goalY, filter)
local actualX, actualY, cols, len = self:check(item, goalX, goalY, filter)
self:update(item, actualX, actualY)
return actualX, actualY, cols, len
end
function World:check(item, goalX, goalY, filter)
filter = filter or defaultFilter
local visited = {[item] = true}
local visitedFilter = function(itm, other)
if visited[other] then return false end
return filter(itm, other)
end
local cols, len = {}, 0
local x,y,w,h = self:getRect(item)
local projected_cols, projected_len = self:project(item, x,y,w,h, goalX,goalY, visitedFilter)
while projected_len > 0 do
local col = projected_cols[1]
len = len + 1
cols[len] = col
visited[col.other] = true
local response = getResponseByName(self, col.type)
goalX, goalY, projected_cols, projected_len = response(
self,
col,
x, y, w, h,
goalX, goalY,
visitedFilter
)
end
return goalX, goalY, cols, len
end
-- Public library functions
bump.newWorld = function(cellSize)
cellSize = cellSize or 64
assertIsPositiveNumber(cellSize, 'cellSize')
local world = setmetatable({
cellSize = cellSize,
rects = {},
rows = {},
nonEmptyCells = {},
responses = {}
}, World_mt)
world:addResponse('touch', touch)
world:addResponse('cross', cross)
world:addResponse('slide', slide)
world:addResponse('bounce', bounce)
return world
end
bump.rect = {
getNearestCorner = rect_getNearestCorner,
getSegmentIntersectionIndices = rect_getSegmentIntersectionIndices,
getDiff = rect_getDiff,
containsPoint = rect_containsPoint,
isIntersecting = rect_isIntersecting,
getSquareDistance = rect_getSquareDistance,
detectCollision = rect_detectCollision
}
bump.responses = {
touch = touch,
cross = cross,
slide = slide,
bounce = bounce
}
return bump

View File

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

View File

@ -1,99 +0,0 @@
--[[
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})

View File

@ -1,193 +0,0 @@
--[[
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

View File

@ -1,310 +0,0 @@
--[[
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})

View File

@ -1,474 +0,0 @@
--[[
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

View File

@ -1,466 +0,0 @@
--[[
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

@ -1,159 +0,0 @@
--[[
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

@ -1,138 +0,0 @@
--[[
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,
}

View File

@ -1,645 +0,0 @@
--[[
------------------------------------------------------------------------------
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

26
src/engine/libs/sti/LICENSE.md Executable file
View File

@ -0,0 +1,26 @@
# Simple Tiled Implementation
This code is licensed under the [**MIT/X11 Open Source License**][MIT].
Copyright (c) 2014 Landon Manning - LManning17@gmail.com - [LandonManning.com][LM]
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.
[MIT]: http://www.opensource.org/licenses/mit-license.html
[LM]: http://LandonManning.com

96
src/engine/libs/sti/README.md Executable file
View File

@ -0,0 +1,96 @@
# Simple Tiled Implementation
Simple Tiled Implementation is a [**Tiled**][Tiled] map loader and renderer designed for the **\*awesome\*** [**LÖVE**][LOVE] framework. Please read the [**documentation**][dox] to learn how it works!
## Quick Example
```lua
-- This example uses the default Box2D (love.physics) plugin!!
local sti = require "sti"
function love.load()
-- Grab window size
windowWidth = love.graphics.getWidth()
windowHeight = love.graphics.getHeight()
-- Set world meter size (in pixels)
love.physics.setMeter(32)
-- Load a map exported to Lua from Tiled
map = sti.new("assets/maps/map01.lua", { "box2d" })
-- Prepare physics world with horizontal and vertical gravity
world = love.physics.newWorld(0, 0)
-- Prepare collision objects
map:box2d_init(world)
-- Create a Custom Layer
map:addCustomLayer("Sprite Layer", 3)
-- Add data to Custom Layer
local spriteLayer = map.layers["Sprite Layer"]
spriteLayer.sprites = {
player = {
image = love.graphics.newImage("assets/sprites/player.png"),
x = 64,
y = 64,
r = 0,
}
}
-- Update callback for Custom Layer
function spriteLayer:update(dt)
for _, sprite in pairs(self.sprites) do
sprite.r = sprite.r + math.rad(90 * dt)
end
end
-- Draw callback for Custom Layer
function spriteLayer:draw()
for _, sprite in pairs(self.sprites) do
local x = math.floor(sprite.x)
local y = math.floor(sprite.y)
local r = sprite.r
love.graphics.draw(sprite.image, x, y, r)
end
end
end
function love.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)
-- Draw the map and all objects within
map:draw()
-- Draw Collision Map (useful for debugging)
love.graphics.setColor(255, 0, 0, 255)
map:box2d_draw()
-- Reset color
love.graphics.setColor(255, 255, 255, 255)
end
```
## Requirements
This library recommends LÖVE 0.9.2 or 0.10.0 and Tiled 0.14.1. If you are updating from an older version of Tiled, please re-export your Lua map files.
## License
This code is licensed under the [**MIT/X11 Open Source License**][MIT]. Check out the LICENSE file for more information.
[Tiled]: http://www.mapeditor.org/
[LOVE]: https://www.love2d.org/
[dox]: http://karai17.github.io/Simple-Tiled-Implementation/
[MIT]: http://www.opensource.org/licenses/mit-license.html

53
src/engine/libs/sti/init.lua Executable file
View File

@ -0,0 +1,53 @@
--- Simple and fast Tiled map loader and renderer.
-- @module sti
-- @author Landon Manning
-- @copyright 2015
-- @license MIT/X11
local STI = {
_LICENSE = "MIT/X11",
_URL = "https://github.com/karai17/Simple-Tiled-Implementation",
_VERSION = "0.14.1.12",
_DESCRIPTION = "Simple Tiled Implementation is a Tiled Map Editor library designed for the *awesome* LÖVE framework.",
cache = {}
}
local path = (...):gsub('%.init$', '') .. "."
local Map = require(path .. "map")
--- Instance a new map.
-- @param path Path to the map file.
-- @param plugins A list of plugins to load.
-- @param ox Offset of map on the X axis (in pixels)
-- @param oy Offset of map on the Y axis (in pixels)
-- @return table The loaded Map.
function STI.new(map, plugins, ox, oy)
-- Check for valid map type
local ext = map:sub(-4, -1)
assert(ext == ".lua", string.format(
"Invalid file type: %s. File must be of type: lua.",
ext
))
-- Get path to map
local path = map:reverse():find("[/\\]") or ""
if path ~= "" then
path = map:sub(1, 1 + (#map - path))
end
-- Load map
map = love.filesystem.load(map)
setfenv(map, {})
map = setmetatable(map(), {__index = Map})
map:init(STI, path, plugins, ox, oy)
return map
end
--- Flush image cache.
function STI:flush()
self.cache = {}
end
return STI

1388
src/engine/libs/sti/map.lua Executable file

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,349 @@
--- Box2D plugin for STI
-- @module box2d
-- @author Landon Manning
-- @copyright 2015
-- @license MIT/X11
return {
box2d_LICENSE = "MIT/X11",
box2d_URL = "https://github.com/karai17/Simple-Tiled-Implementation",
box2d_VERSION = "2.3.2.1",
box2d_DESCRIPTION = "Box2D hooks for STI.",
--- Initialize Box2D physics world.
-- @param world The Box2D world to add objects to.
-- @return nil
box2d_init = function(map, world)
assert(love.physics, "To use the Box2D plugin, please enable the love.physics module.")
local body = love.physics.newBody(world, map.offsetx, map.offsety)
local collision = {
body = body,
}
local function convertEllipseToPolygon(x, y, w, h, max_segments)
local function calc_segments(segments)
local function vdist(a, b)
local c = {
x = a.x - b.x,
y = a.y - b.y,
}
return c.x * c.x + c.y * c.y
end
segments = segments or 64
local vertices = {}
local v = { 1, 2, math.ceil(segments/4-1), math.ceil(segments/4) }
local m
if love.physics then
m = love.physics.getMeter()
else
m = 32
end
for _, i in ipairs(v) do
local angle = (i / segments) * math.pi * 2
local px = x + w / 2 + math.cos(angle) * w / 2
local py = y + h / 2 + math.sin(angle) * h / 2
table.insert(vertices, { x = px / m, y = py / m })
end
local dist1 = vdist(vertices[1], vertices[2])
local dist2 = vdist(vertices[3], vertices[4])
-- Box2D threshold
if dist1 < 0.0025 or dist2 < 0.0025 then
return calc_segments(segments-2)
end
return segments
end
local segments = calc_segments(max_segments)
local vertices = {}
table.insert(vertices, { x = x + w / 2, y = y + h / 2 })
for i=0, segments do
local angle = (i / segments) * math.pi * 2
local px = x + w / 2 + math.cos(angle) * w / 2
local py = y + h / 2 + math.sin(angle) * h / 2
table.insert(vertices, { x = px, y = py })
end
return vertices
end
local function rotateVertex(v, x, y, cos, sin, oy)
oy = oy or 0
local vertex = {
x = v.x,
y = v.y - oy,
}
vertex.x = vertex.x - x
vertex.y = vertex.y - y
local vx = cos * vertex.x - sin * vertex.y
local vy = sin * vertex.x + cos * vertex.y
return vx + x, vy + y + oy
end
local function addObjectToWorld(objshape, vertices, userdata, object)
local shape
if objshape == "polyline" then
shape = love.physics.newChainShape(false, unpack(vertices))
else
shape = love.physics.newPolygonShape(unpack(vertices))
end
local fixture = love.physics.newFixture(body, shape)
fixture:setUserData(userdata)
if userdata.properties.sensor == "true" then
fixture:setSensor(true)
end
local obj = {
object = object,
shape = shape,
fixture = fixture,
}
table.insert(collision, obj)
end
local function getPolygonVertices(object)
local vertices = {}
for _, vertex in ipairs(object.polygon) do
table.insert(vertices, vertex.x + object.x)
table.insert(vertices, vertex.y + object.y)
end
return vertices
end
local function calculateObjectPosition(object, tile)
local o = {
shape = object.shape,
x = object.dx or object.x,
y = object.dy or object.y,
w = object.width,
h = object.height,
polygon = object.polygon or object.polyline or object.ellipse or object.rectangle
}
local userdata = {
object = o,
properties = object.properties
}
if o.shape == "rectangle" then
o.r = object.rotation or 0
local cos = math.cos(math.rad(o.r))
local sin = math.sin(math.rad(o.r))
local oy = 0
if object.gid then
local tileset = map.tilesets[map.tiles[object.gid].tileset]
local lid = object.gid - tileset.firstgid
local tile = {}
-- This fixes a height issue
o.y = o.y + map.tiles[object.gid].offset.y
oy = tileset.tileheight
for _, t in ipairs(tileset.tiles) do
if t.id == lid then
tile = t
break
end
end
if tile.objectGroup then
for _, obj in ipairs(tile.objectGroup.objects) do
-- Every object in the tile
calculateObjectPosition(obj, object)
end
return
else
o.w = map.tiles[object.gid].width
o.h = map.tiles[object.gid].height
end
end
o.polygon = {
{ x=0, y=0 },
{ x=o.w, y=0 },
{ x=o.w, y=o.h },
{ x=0, y=o.h },
}
for _, vertex in ipairs(o.polygon) do
if map.orientation == "isometric" then
vertex.x, vertex.y = map:convertIsometricToScreen(vertex.x, vertex.y)
end
vertex.x, vertex.y = rotateVertex(vertex, o.x, o.y, cos, sin, oy)
end
local vertices = getPolygonVertices(o)
addObjectToWorld(o.shape, vertices, userdata, tile or object)
elseif o.shape == "ellipse" then
if not o.polygon then
o.polygon = convertEllipseToPolygon(o.x, o.y, o.w, o.h)
end
local vertices = getPolygonVertices(o)
local triangles = love.math.triangulate(vertices)
for _, triangle in ipairs(triangles) do
addObjectToWorld(o.shape, triangle, userdata, tile or object)
end
elseif o.shape == "polygon" then
local vertices = getPolygonVertices(o)
local triangles = love.math.triangulate(vertices)
for _, triangle in ipairs(triangles) do
addObjectToWorld(o.shape, triangle, userdata, tile or object)
end
elseif o.shape == "polyline" then
local vertices = getPolygonVertices(o)
addObjectToWorld(o.shape, vertices, userdata, tile or object)
end
end
for _, tile in pairs(map.tiles) do
local tileset = map.tilesets[tile.tileset]
-- Every object in every instance of a tile
if tile.objectGroup then
if map.tileInstances[tile.gid] then
for _, instance in ipairs(map.tileInstances[tile.gid]) do
for _, object in ipairs(tile.objectGroup.objects) do
object.dx = object.x + instance.x
object.dy = object.y + instance.y
calculateObjectPosition(object, instance)
end
end
end
-- Every instance of a tile
elseif tile.properties and tile.properties.collidable == "true" and map.tileInstances[tile.gid] then
for _, instance in ipairs(map.tileInstances[tile.gid]) do
local object = {
shape = "rectangle",
x = instance.x,
y = instance.y,
width = tileset.tilewidth,
height = tileset.tileheight,
properties = tile.properties
}
calculateObjectPosition(object, instance)
end
end
end
for _, layer in ipairs(map.layers) do
-- Entire layer
if layer.properties.collidable == "true" then
if layer.type == "tilelayer" then
for gid, tiles in pairs(map.tileInstances) do
local tile = map.tiles[gid]
local tileset = map.tilesets[tile.tileset]
for _, instance in ipairs(tiles) do
if instance.layer == layer then
local object = {
shape = "rectangle",
x = instance.x,
y = instance.y,
width = tileset.tilewidth,
height = tileset.tileheight,
properties = tile.properties
}
calculateObjectPosition(object, instance)
end
end
end
elseif layer.type == "objectgroup" then
for _, object in ipairs(layer.objects) do
calculateObjectPosition(object)
end
elseif layer.type == "imagelayer" then
local object = {
shape = "rectangle",
x = layer.x or 0,
y = layer.y or 0,
width = layer.width,
height = layer.height,
properties = layer.properties
}
calculateObjectPosition(object)
end
end
-- Individual objects
if layer.type == "objectgroup" then
for _, object in ipairs(layer.objects) do
if object.properties.collidable == "true" then
calculateObjectPosition(object)
end
end
end
end
map.box2d_collision = collision
end,
--- Remove Box2D fixtures and shapes from world.
-- @param index The index or name of the layer being removed
-- @return nil
box2d_removeLayer = function(map, index)
local layer = assert(map.layers[index], "Layer not found: " .. index)
local collision = map.box2d_collision
-- Remove collision objects
for i=#collision, 1, -1 do
local obj = collision[i]
if obj.object.layer == layer then
obj.fixture:destroy()
table.remove(collision, i)
end
end
end,
--- Draw Box2D physics world.
-- @return nil
box2d_draw = function(map)
local collision = map.box2d_collision
for _, obj in ipairs(collision) do
local points = {collision.body:getWorldPoints(obj.shape:getPoints())}
if #points == 4 then
love.graphics.line(points)
else
love.graphics.polygon("line", points)
end
end
end,
}
--- Custom Properties in Tiled are used to tell this plugin what to do.
-- @table Properties
-- @field collidable set to "true", can be used on any Layer, Tile, or Object
-- @field sensor set to "true", can be used on any Tile or Object that is also collidable

View File

@ -0,0 +1,103 @@
--- Bump.lua plugin for STI
-- @module bump.lua
-- @author David Serrano (BobbyJones|FrenchFryLord)
-- @copyright 2016
-- @license MIT/X11
return {
bump_LICENSE = "MIT/X11",
bump_URL = "https://github.com/karai17/Simple-Tiled-Implementation",
bump_VERSION = "3.1.5.2",
bump_DESCRIPTION = "Bump hooks for STI.",
--- Adds each collidable tile to the Bump world.
-- @param world The Bump world to add objects to.
-- @return collidables table containing the handles to the objects in the Bump world.
bump_init = function(map, world)
local collidables = {}
for _, tileset in ipairs(map.tilesets) do
for _, tile in ipairs(tileset.tiles) do
local gid = tileset.firstgid + tile.id
-- Every object in every instance of a tile
if tile.properties and tile.properties.collidable == "true" and map.tileInstances[gid] then
for _, instance in ipairs(map.tileInstances[gid]) do
local t = {properties = tile.properties, x = instance.x + map.offsetx, y = instance.y + map.offsety, width = map.tilewidth, height = map.tileheight, layer = instance.layer }
world:add(t, t.x,t.y, t.width,t.height)
table.insert(collidables,t)
end
end
end
end
for _, layer in ipairs(map.layers) do
-- Entire layer
if layer.properties.collidable == "true" then
if layer.type == "tilelayer" then
for y, tiles in ipairs(layer.data) do
for x, tile in pairs(tiles) do
local t = {properties = tile.properties, x = x * map.tilewidth + tile.offset.x + map.offsetx, y = y * map.tileheight + tile.offset.y + map.offsety, width = tile.width, height = tile.height, layer = layer }
world:add(t, t.x,t.y, t.width,t.height )
table.insert(collidables,t)
end
end
elseif layer.type == "imagelayer" then
world:add(layer, layer.x,layer.y, layer.width,layer.height)
table.insert(collidables,layer)
end
end
-- individual collidable objects in a layer that is not "collidable"
-- or whole collidable objects layer
if layer.type == "objectgroup" then
for _, obj in ipairs(layer.objects) do
if (layer.properties and layer.properties.collidable == "true")
or (obj.properties and obj.properties.collidable == "true") then
if obj.shape == "rectangle" then
local t = {properties = obj.properties, x = obj.x, y = obj.y, width = obj.width, height = obj.height, type = obj.type, name = obj.name, id = obj.id, gid = obj.gid, layer = layer }
if obj.gid then t.y = t.y - obj.height end
world:add(t, t.x,t.y, t.width,t.height )
table.insert(collidables,t)
end -- TODO implement other object shapes?
end
end
end
end
map.bump_collidables = collidables
end,
--- Remove layer
-- @params index to layer to be removed
-- @params world bump world the holds the tiles
-- @return nil
bump_removeLayer = function(map, index, world)
local layer = assert(map.layers[index], "Layer not found: " .. index)
local collidables = map.bump_collidables
-- Remove collision objects
for i=#collidables, 1, -1 do
local obj = collidables[i]
if obj.layer == layer
and (
layer.properties.collidable == "true"
or obj.properties.collidable == "true"
) then
world:remove(obj)
table.remove(collidables, i)
end
end
end,
--- Draw bump collisions world.
-- @params world bump world holding the tiles geometry
-- @return nil
bump_draw = function(map, world)
for k,collidable in pairs(map.bump_collidables) do
love.graphics.rectangle("line",world:getRect(collidable))
end
end
}

View File

@ -1,10 +0,0 @@
local object = {}
object.width = 64
object.height = 64
function object.sortNorthSouth(a, b)
return a.info.y < b.info.y
end
return object

View File

@ -1,14 +1,12 @@
--will hold the currently playing sources
local sources = {}
-- check for sources that finished playing and remove them
-- add to love.update
function love.audio.update()
local remove = {}
for _,s in pairs(sources) do
if s.audio:isStopped() then
if s.finishedFunc then s.finishedFunc() end
remove[#remove + 1] = s
end
end
@ -22,7 +20,7 @@ end
local play = love.audio.play
function love.audio.play(what, how, loop, finishedFunc)
local src = {}
if type(what) ~= "userdata" or not what:typeOf("Source") then
if type(what) ~= 'userdata' or not what:typeOf('Source') then
src.audio = love.audio.newSource(what, how)
src.finishedFunc = finishedFunc
src.audio:setLooping(loop or false)
@ -30,11 +28,10 @@ function love.audio.play(what, how, loop, finishedFunc)
play(src.audio)
sources[src] = src
return src
end
-- stops a source
local stop = love.audio.stop
function love.audio.stop(src)
if not src then return end

View File

@ -1,6 +1,6 @@
local ui = {}
ui = class('ui')
ui.relevantKeys = { "return", "up", "down" }
ui.relevantInputs = { 'enter', 'up', 'down' }
ui.height = 240
ui.fullMessage = ""
@ -12,7 +12,7 @@ ui.textLines = {}
ui.startLine = 1
ui.endLine = 1
local font = love.graphics.newFont("assets/ui/font.ttf", 24)
local font = love.graphics.newFont('assets/ui/font.ttf', 24)
love.graphics.setFont(font)
ui.windowWidth = love.graphics.getWidth()
@ -23,12 +23,12 @@ function ui:showMessage(message)
local maxLines = math.floor((ui.height - ui.border) / font:getHeight())
local fullLines = font:getWrap(ui.fullMessage, ui.windowWidth - ui.border)
ui.textLines = {}
local fullIndex = 1
local messageTail = message
while not endsWith(messageTail, ui.textLines[#ui.textLines]) do
fullIndex, line, messageTail = ui:getMaxString(string.sub(messageTail, fullIndex))
fullIndex = fullIndex + 1 -- account for the following space
@ -37,13 +37,13 @@ function ui:showMessage(message)
end
ui.endLine = maxLines
ui:setLineRange(ui.startLine, ui.endLine)
end
function ui:setLineRange(startLine, endLine)
ui.fullMessage = ""
if endLine > #ui.textLines then endLine = #ui.textLines end
for index = startLine, endLine, 1 do
@ -61,16 +61,16 @@ function ui:getMaxString(stringTail)
local width = font:getWidth(string.sub(stringTail, 1, index))
local needsCutting = width > ui.windowWidth - ui.border
while width > ui.windowWidth - ui.border do
width = font:getWidth(string.sub(stringTail, 1, index))
index = index - 1
end
if needsCutting and
string.sub(stringTail, index + 1, index + 1) ~= " " then
local cursor = "!"
while cursor ~= " " and cursor ~= "" do
if needsCutting and
string.sub(stringTail, index + 1, index + 1) ~= ' ' then
local cursor = '!'
while cursor ~= ' ' and cursor ~= '' do
index = index - 1
cursor = string.sub(stringTail, index, index)
end
@ -82,35 +82,40 @@ end
function ui:draw()
if ui.active then
love.graphics.setColor(255, 255, 255, 150)
love.graphics.rectangle("fill", 0, ui.windowHeight - ui.height, ui.windowWidth, ui.height)
love.graphics.rectangle('fill', 0, ui.windowHeight - ui.height, ui.windowWidth, ui.height)
love.graphics.setColor(55, 60, 60, 255)
love.graphics.printf(ui.fullMessage, ui.border, ui.windowHeight - ui.height + ui.border, ui.windowWidth - ui.border)
end
end
function ui:sendKey(key)
if key == "up" then
function ui:up()
if ui.startLine > 1 then
ui.startLine = ui.startLine - 1
ui.endLine = ui.endLine - 1
ui:setLineRange(ui.startLine, ui.endLine)
end
end
if key == "down" then
end
function ui:down()
if ui.endLine < #ui.textLines then
ui.startLine = ui.startLine + 1
ui.endLine = ui.endLine + 1
ui:setLineRange(ui.startLine, ui.endLine)
end
end
if key == "return" then
end
function ui:enter()
ui.active = false
end
end
function ui:sendInput(input)
local uiFunc = self[input]
if uiFunc ~= nil then
uiFunc(self)
end
end
return ui

View File

@ -1,11 +1,14 @@
return {
version = "1.1",
luaversion = "5.1",
tiledversion = "v0.15.1-76-g0cd1368",
orientation = "orthogonal",
renderorder = "right-down",
width = 20,
height = 20,
tilewidth = 32,
tileheight = 32,
nextobjectid = 7,
properties = {},
tilesets = {
{
@ -23,7 +26,16 @@ return {
y = 0
},
properties = {},
tiles = {}
terrains = {},
tilecount = 1024,
tiles = {
{
id = 184,
properties = {
["collidable"] = true
}
}
}
},
{
name = "atlas01",
@ -40,6 +52,8 @@ return {
y = 0
},
properties = {},
terrains = {},
tilecount = 1024,
tiles = {}
},
{
@ -57,6 +71,8 @@ return {
y = 0
},
properties = {},
terrains = {},
tilecount = 1024,
tiles = {}
},
{
@ -74,6 +90,8 @@ return {
y = 0
},
properties = {},
terrains = {},
tilecount = 1024,
tiles = {}
}
},
@ -87,30 +105,12 @@ return {
height = 20,
visible = true,
opacity = 1,
offsetx = 0,
offsety = 0,
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
}
encoding = "base64",
compression = "zlib",
data = "eJztzsENgkAQQNEVFC9ShJpAAzYAEq1LglqHoFKHHLQOMNE2/CRLnJOHdQ8cOLzMZLL52WymVGbZDin2hg44it4JOS4o9GydxV78uF1Rit4NFZ6G7niIXo3mj94L76HXm97K6ff/hp7d3shXyoGLidbuYz0leeveeZj6396cfYGloQCh6EXsMdYGEmywFT2bPspTJQc="
},
{
type = "tilelayer",
@ -121,30 +121,12 @@ return {
height = 20,
visible = true,
opacity = 1,
offsetx = 0,
offsety = 0,
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
}
encoding = "base64",
compression = "zlib",
data = "eJztk71OAkEURq+E3QSS3cUX4SdZXgPlFaRVflp4hQW0BRoT7bTkATTSgZS8gJYKhXQWHCpIZoCZhZIvOZlkJvfk3puMyPH5u1Dv/LRIABm4TNv5hhpfFkcO8lCAG0+kAoPUYV+YUO9KOK7gGspb/fUNfLpUcdSgDo0tXy+mr4OjC/fwYLk/m5jsT5cmdS0N/zF960SuSNtVz7h5ofbVVc+4GVM7cdXznE2++JPfnv4tZFdFy30lfRHH3/2+cMw8j/T0BM/wvqM/m7zh+ICRges2ELmDKtRgQs0nTGHGbMs98+kS4WhDB7rwg+cX5ieYax2PfnwIDPpaAcTwMd4="
},
{
type = "tilelayer",
@ -155,39 +137,24 @@ return {
height = 20,
visible = true,
opacity = 1,
offsetx = 0,
offsety = 0,
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
}
encoding = "base64",
compression = "zlib",
data = "eJxjYBiaIJSPuuaVUtm8UTAKRgEquMxKXfM+U9k8XOAeFyo9ChAAANQNBIc="
},
{
type = "objectgroup",
name = "objects",
visible = true,
opacity = 1,
offsetx = 0,
offsety = 0,
properties = {},
objects = {
{
id = 1,
name = "player",
type = "",
shape = "rectangle",
@ -197,9 +164,13 @@ return {
height = 64,
rotation = 0,
visible = true,
properties = {}
properties = {
["collision"] = "player_collision",
["has_controller"] = true
}
},
{
id = 2,
name = "romata",
type = "",
shape = "rectangle",
@ -209,7 +180,40 @@ return {
height = 64,
rotation = 0,
visible = true,
properties = {}
properties = {
["collision"] = "romata_collision",
["has_controller"] = true
}
},
{
id = 3,
name = "player_collision",
type = "",
shape = "rectangle",
x = 332,
y = 521,
width = 8,
height = 8,
rotation = 0,
visible = true,
properties = {
["collidable"] = "true"
}
},
{
id = 5,
name = "romata_collision",
type = "",
shape = "rectangle",
x = 328,
y = 203,
width = 14,
height = 18,
rotation = 0,
visible = true,
properties = {
["collidable"] = "true"
}
}
}
},
@ -222,30 +226,12 @@ return {
height = 20,
visible = true,
opacity = 1,
offsetx = 0,
offsety = 0,
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
}
encoding = "base64",
compression = "zlib",
data = "eJxjYBgFo4C+4Asjdc0TYaKueaMAAhQ4GRgSOAfaFaNgMAMALD4Bng=="
},
{
type = "tilelayer",
@ -256,30 +242,14 @@ return {
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
}
offsetx = 0,
offsety = 0,
properties = {
["collidable"] = "true"
},
encoding = "base64",
compression = "zlib",
data = "eJzbycDAsHOIY0KAFHOIMY8YM5HVUdM8YgGx+qnlPlLdSqn/aGleLuPgdt9gNo9Q3iLVbGqZR0maxVY2UKPMolb5RwvziMUADlyWBg=="
}
}
}

View File

@ -1,15 +1,20 @@
<?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">
<map version="1.0" orientation="orthogonal" renderorder="right-down" width="20" height="20" tilewidth="32" tileheight="32" nextobjectid="7">
<tileset firstgid="1" name="atlas00" tilewidth="32" tileheight="32" tilecount="1024" columns="32">
<image source="tiles/atlas00.png" width="1024" height="1024"/>
<tile id="184">
<properties>
<property name="collidable" type="bool" value="true"/>
</properties>
</tile>
</tileset>
<tileset firstgid="1025" name="atlas01" tilewidth="32" tileheight="32">
<tileset firstgid="1025" name="atlas01" tilewidth="32" tileheight="32" tilecount="1024" columns="32">
<image source="tiles/atlas01.png" width="1024" height="1024"/>
</tileset>
<tileset firstgid="2049" name="atlas03" tilewidth="32" tileheight="32">
<tileset firstgid="2049" name="atlas03" tilewidth="32" tileheight="32" tilecount="1024" columns="32">
<image source="tiles/atlas03.png" width="1024" height="1024"/>
</tileset>
<tileset firstgid="3073" name="atlas04" tilewidth="32" tileheight="32">
<tileset firstgid="3073" name="atlas04" tilewidth="32" tileheight="32" tilecount="1024" columns="32">
<image source="tiles/atlas04.png" width="1024" height="1024"/>
</tileset>
<layer name="background" width="20" height="20">
@ -19,7 +24,7 @@
</layer>
<layer name="foreground00" width="20" height="20">
<data encoding="base64" compression="zlib">
eJzdk7tOAkEUhg8ENmGT3dVEX4NLAq/h5RW05drCKyy3FmxMoMOSB8BoJ1LyAlqqFNhR8G1HMrM6s1rxJ19OMpPz5ZxJRuTv2abUM98VCeAETl0731zjy+MoQBFKcOOJ3MJd7ndfOa2eXeC4hCu4PphvbODTpY6jAU1oHfhGCX09HH0YwNDy/Wxi8n66tOnraNgl9EUJHZGuo9akmdH74Kg1aV7oXTpqPdacsdu55X5v/Ml3T39XxlWx9GV8kawff7/JmnnumWkCU3iMmc8mCxxP8GzgqgYiNahDA5b0vMIK1uz2/cN+uoQ4utCDPnzg+YSvf9grisc8PgQGc+0BNfsyEw==
eJztk71OAkEURq+E3QSS3cUX4SdZXgPlFaRVflp4hQW0BRoT7bTkATTSgZS8gJYKhXQWHCpIZoCZhZIvOZlkJvfk3puMyPH5u1Dv/LRIABm4TNv5hhpfFkcO8lCAG0+kAoPUYV+YUO9KOK7gGspb/fUNfLpUcdSgDo0tXy+mr4OjC/fwYLk/m5jsT5cmdS0N/zF960SuSNtVz7h5ofbVVc+4GVM7cdXznE2++JPfnv4tZFdFy30lfRHH3/2+cMw8j/T0BM/wvqM/m7zh+ICRges2ELmDKtRgQs0nTGHGbMs98+kS4WhDB7rwg+cX5ieYax2PfnwIDPpaAcTwMd4=
</data>
</layer>
<layer name="foreground01" width="20" height="20">
@ -28,17 +33,40 @@
</data>
</layer>
<objectgroup name="objects">
<object name="player" x="320" y="480" width="32" height="64"/>
<object name="romata" x="320" y="160" width="32" height="64"/>
<object id="1" name="player" x="320" y="480" width="32" height="64">
<properties>
<property name="collision" value="player_collision"/>
<property name="has_controller" type="bool" value="true"/>
</properties>
</object>
<object id="2" name="romata" x="320" y="160" width="32" height="64">
<properties>
<property name="collision" value="romata_collision"/>
<property name="has_controller" type="bool" value="true"/>
</properties>
</object>
<object id="3" name="player_collision" x="332" y="521" width="8" height="8">
<properties>
<property name="collidable" value="true"/>
</properties>
</object>
<object id="5" name="romata_collision" x="328" y="203" width="14" height="18">
<properties>
<property name="collidable" value="true"/>
</properties>
</object>
</objectgroup>
<layer name="overlay" width="20" height="20">
<data encoding="base64" compression="zlib">
eJxjYBgFo4C+4Asjdc0TYaKueaNgFIwC4gAA9U4BDA==
eJxjYBgFo4C+4Asjdc0TYaKueaMAAhQ4GRgSOAfaFaNgMAMALD4Bng==
</data>
</layer>
<layer name="collision" width="20" height="20" visible="0">
<properties>
<property name="collidable" value="true"/>
</properties>
<data encoding="base64" compression="zlib">
eJzbycDAsHOIY0KAFHOIMY8YM5HVUdM8YgGx+qnlPlLdSqn/aGleLuPgdh+9zCPHbEJ5a6DMoyTNYisbqFFmUWoGLc0jFgMAI62XeA==
eJzbycDAsHOIY0KAFHOIMY8YM5HVUdM8YgGx+qnlPlLdSqn/aGleLuPgdt9gNo9Q3iLVbGqZR0maxVY2UKPMolb5RwvziMUADlyWBg==
</data>
</layer>
</map>

View File

@ -1,19 +1,19 @@
local engine = require 'engine'
local Engine = require 'engine'
function love.load(arg)
if arg[#arg] == "-debug" then require("mobdebug").start() end -- debugging in ZeroBraineStudio
engine = Engine()
if arg[#arg] == '-debug' then require('mobdebug').start() end -- debugging in ZeroBraineStudio
engine = Engine()
end
function love.update(dt)
engine:update(dt)
engine:update(dt)
end
function love.keyreleased(key)
engine:checkObjectAnimation(key)
engine:checkObjectAnimation(key)
end
function love.draw()
engine:draw()
engine:draw()
end

View File

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

View File

@ -1,8 +1,5 @@
local object = require "engine/object"
local gridwalker = require "engine/controllers/gridwalker"
return {
spritesheet = "assets/characters/romata.png",
spritesheet = 'assets/characters/romata.png',
pose = 'play',
controller = Gridwalker()
controller = 'gridwalker'
}

View File

@ -1,14 +1,10 @@
local story = {}
function story:start(engine)
engine:loadLevel('01')
local stopRomata = function()
engine:stopAnimate('romata')
end
engine:load('01.lua')
engine:animate('romata', 'play')
engine:playSound('gebet', false, stopRomata)
engine:playSound('gebet', false, function() engine:stopAnimate('romata') end)
engine:showMessage('Du befindest dich auf einer sturmgebeutelten Insel. Wo ist denn nur der Met?')
end