diff --git a/README.md b/README.md index 14f95dc..6d21025 100644 --- a/README.md +++ b/README.md @@ -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. diff --git a/src/conf.lua b/src/conf.lua index b21a0ea..cf7f834 100644 --- a/src/conf.lua +++ b/src/conf.lua @@ -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 diff --git a/src/engine/animator.lua b/src/engine/animator.lua index cc075e6..a0ee064 100644 --- a/src/engine/animator.lua +++ b/src/engine/animator.lua @@ -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 diff --git a/src/engine/controllers/gridwalker.lua b/src/engine/controllers/gridwalker.lua index 08c1cc8..9e1945f 100644 --- a/src/engine/controllers/gridwalker.lua +++ b/src/engine/controllers/gridwalker.lua @@ -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 diff --git a/src/engine/global.lua b/src/engine/global.lua new file mode 100644 index 0000000..761ce00 --- /dev/null +++ b/src/engine/global.lua @@ -0,0 +1,6 @@ +local Global = {} + +Global.tilewidth = 64 +Global.tileheight = 64 + +return Global diff --git a/src/engine/init.lua b/src/engine/init.lua index c6b43b3..8ff26e9 100644 --- a/src/engine/init.lua +++ b/src/engine/init.lua @@ -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 diff --git a/src/engine/level.lua b/src/engine/level.lua new file mode 100644 index 0000000..ddf3727 --- /dev/null +++ b/src/engine/level.lua @@ -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 diff --git a/src/engine/libs/30log.lua b/src/engine/libs/30log.lua index 0325c29..d9281b5 100755 --- a/src/engine/libs/30log.lua +++ b/src/engine/libs/30log.lua @@ -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 ' +return setmetatable(class,{__call = function(_,...) return _class(...) end }) diff --git a/src/engine/libs/anim8.lua b/src/engine/libs/anim8.lua index 9197aec..8fefe99 100644 --- a/src/engine/libs/anim8.lua +++ b/src/engine/libs/anim8.lua @@ -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 ----------------------------------------------------------- diff --git a/src/engine/libs/bump.lua b/src/engine/libs/bump.lua new file mode 100644 index 0000000..a509125 --- /dev/null +++ b/src/engine/libs/bump.lua @@ -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 diff --git a/src/engine/libs/hardoncollider/README b/src/engine/libs/hardoncollider/README deleted file mode 100755 index 6c5f94c..0000000 --- a/src/engine/libs/hardoncollider/README +++ /dev/null @@ -1,4 +0,0 @@ -General Purpose 2D Collision Detection System - -Documentation and examples here: -http://vrld.github.com/HardonCollider diff --git a/src/engine/libs/hardoncollider/class.lua b/src/engine/libs/hardoncollider/class.lua deleted file mode 100755 index ce9a679..0000000 --- a/src/engine/libs/hardoncollider/class.lua +++ /dev/null @@ -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 = '' - 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 (""):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}) diff --git a/src/engine/libs/hardoncollider/gjk.lua b/src/engine/libs/hardoncollider/gjk.lua deleted file mode 100755 index ebaad05..0000000 --- a/src/engine/libs/hardoncollider/gjk.lua +++ /dev/null @@ -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 diff --git a/src/engine/libs/hardoncollider/init.lua b/src/engine/libs/hardoncollider/init.lua deleted file mode 100755 index e96a9a3..0000000 --- a/src/engine/libs/hardoncollider/init.lua +++ /dev/null @@ -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}) diff --git a/src/engine/libs/hardoncollider/polygon.lua b/src/engine/libs/hardoncollider/polygon.lua deleted file mode 100755 index ad1a92e..0000000 --- a/src/engine/libs/hardoncollider/polygon.lua +++ /dev/null @@ -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 diff --git a/src/engine/libs/hardoncollider/shapes.lua b/src/engine/libs/hardoncollider/shapes.lua deleted file mode 100755 index 29fb01d..0000000 --- a/src/engine/libs/hardoncollider/shapes.lua +++ /dev/null @@ -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, -} - diff --git a/src/engine/libs/hardoncollider/spatialhash.lua b/src/engine/libs/hardoncollider/spatialhash.lua deleted file mode 100755 index eba3794..0000000 --- a/src/engine/libs/hardoncollider/spatialhash.lua +++ /dev/null @@ -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) diff --git a/src/engine/libs/hardoncollider/vector-light.lua b/src/engine/libs/hardoncollider/vector-light.lua deleted file mode 100755 index 29e56bf..0000000 --- a/src/engine/libs/hardoncollider/vector-light.lua +++ /dev/null @@ -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, -} diff --git a/src/engine/libs/sti.lua b/src/engine/libs/sti.lua deleted file mode 100644 index f8fdcb9..0000000 --- a/src/engine/libs/sti.lua +++ /dev/null @@ -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 diff --git a/src/engine/libs/sti/LICENSE.md b/src/engine/libs/sti/LICENSE.md new file mode 100755 index 0000000..234f8f6 --- /dev/null +++ b/src/engine/libs/sti/LICENSE.md @@ -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 diff --git a/src/engine/libs/sti/README.md b/src/engine/libs/sti/README.md new file mode 100755 index 0000000..0b31e9a --- /dev/null +++ b/src/engine/libs/sti/README.md @@ -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 diff --git a/src/engine/libs/sti/init.lua b/src/engine/libs/sti/init.lua new file mode 100755 index 0000000..eac0ab1 --- /dev/null +++ b/src/engine/libs/sti/init.lua @@ -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 diff --git a/src/engine/libs/sti/map.lua b/src/engine/libs/sti/map.lua new file mode 100755 index 0000000..5ceee41 --- /dev/null +++ b/src/engine/libs/sti/map.lua @@ -0,0 +1,1388 @@ +--- Map object +-- @module map + +local path = (...):gsub('%.[^%.]+$', '') .. "." +local pluginPath = string.gsub(path, "[.]", "/") .. "plugins/" +local Map = {} + +-- https://github.com/stevedonovan/Penlight/blob/master/lua/pl/path.lua#L286 +local function formatPath(path) + local np_gen1,np_gen2 = '[^SEP]+SEP%.%.SEP?','SEP+%.?SEP' + local np_pat1, np_pat2 = np_gen1:gsub('SEP','/'), np_gen2:gsub('SEP','/') + local k + + repeat -- /./ -> / + path,k = path:gsub(np_pat2,'/') + until k == 0 + + repeat -- A/../ -> (empty) + path,k = path:gsub(np_pat1,'') + until k == 0 + + if path == '' then path = '.' end + + return path +end + +-- Compensation for scale/rotation shift +local function compensate(tile, x, y, tw, th) + local tx = x + tile.offset.x + local ty = y + tile.offset.y + local origx = tx + local origy = ty + local compx = 0 + local compy = 0 + + if tile.sx < 0 then compx = tw end + if tile.sy < 0 then compy = th end + + if tile.r > 0 then + tx = tx + th - compy + ty = ty + th - tw + compx + elseif tile.r < 0 then + tx = tx + compy + ty = ty + th - compx + else + tx = tx + compx + ty = ty + compy + end + + return tx, ty +end + +-- Cache images in main STI module +local function cache_image(sti, path) + local image = love.graphics.newImage(path) + image:setFilter("nearest", "nearest") + sti.cache[path] = image +end + +--- 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 nil +function Map:init(STI, path, plugins, ox, oy) + if type(plugins) == "table" then + self:loadPlugins(plugins) + end + + self:resize() + self.objects = {} + self.tiles = {} + self.tileInstances = {} + self.drawRange = { + sx = 1, + sy = 1, + ex = self.width, + ey = self.height, + } + self.offsetx = ox or 0 + self.offsety = oy or 0 + self.sti = STI + + -- Set tiles, images + local gid = 1 + for i, tileset in ipairs(self.tilesets) do + assert(tileset.image, "STI does not support Tile Collections.\nYou need to create a Texture Atlas.") + + -- Cache images + local formatted_path = formatPath(path .. tileset.image) + if not self.sti.cache[formatted_path] then + cache_image(self.sti, formatted_path) + end + + -- Pull images from cache + tileset.image = self.sti.cache[formatted_path] + + gid = self:setTiles(i, tileset, gid) + end + + -- Set layers + for i, layer in ipairs(self.layers) do + self:setLayer(layer, path) + end +end + +--- Load plugins +-- @param plugins A list of plugins to load +-- @return nil +function Map:loadPlugins(plugins) + for _, plugin in ipairs(plugins) do + local p = pluginPath .. plugin .. ".lua" + if love.filesystem.isFile(p) then + local file = love.filesystem.load(p)() + for k, func in pairs(file) do + if not self[k] then + self[k] = func + end + end + end + end +end + +--- Create Tiles +-- @param index Index of the Tileset +-- @param tileset Tileset data +-- @param gid First Global ID in Tileset +-- @return number Next Tileset's first Global ID +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 id = gid - tileset.firstgid + local qx = (x - 1) * tw + m + (x - 1) * s + local qy = (y - 1) * th + m + (y - 1) * s + local properties, terrain, animation, objectGroup + + for _, tile in pairs(tileset.tiles) do + if tile.id == id then + properties = tile.properties + animation = tile.animation + objectGroup = tile.objectGroup + + if tile.terrain then + terrain = {} + + for i=1, #tile.terrain do + terrain[i] = tileset.terrains[tile.terrain[i] + 1] + end + end + end + end + + local tile = { + id = id, + gid = gid, + tileset = index, + quad = quad(qx, qy, tw, th, iw, ih), + properties = properties or {}, + terrain = terrain, + animation = animation, + objectGroup = objectGroup, + frame = 1, + time = 0, + width = tw, + height = th, + sx = 1, + sy = 1, + r = 0, + offset = { + x = -mw + tileset.tileoffset.x, + y = -th + tileset.tileoffset.y, + }, + } + + if self.orientation == "isometric" then + tile.offset.x = -mw / 2 + end + + self.tiles[gid] = tile + gid = gid + 1 + end + end + + return gid +end + +--- Create Layers +-- @param layer Layer data +-- @param path (Optional) Path to an Image Layer's image +-- @return nil +function Map:setLayer(layer, path) + if layer.encoding then + if layer.encoding == "base64" then + local ffi = assert(require "ffi", "Compressed maps require LuaJIT FFI.\nPlease Switch your interperator to LuaJIT or your Tile Layer Format to \"CSV\".") + local fd = love.filesystem.newFileData(layer.data, "data", "base64"):getString() + + local function getDecompressedData(data) + local d = {} + local decoded = ffi.cast("uint32_t*", data) + + for i=0, data:len() / ffi.sizeof("uint32_t") do + table.insert(d, tonumber(decoded[i])) + end + + return d + end + + if not layer.compression then + layer.data = getDecompressedData(fd) + else + assert(love.math.decompress, "zlib and gzip compression require LOVE 0.10.0+.\nPlease set your Tile Layer Format to \"Base64 (uncompressed)\" or \"CSV\".") + + if layer.compression == "zlib" then + local data = love.math.decompress(fd, "zlib") + layer.data = getDecompressedData(data) + end + + if layer.compression == "gzip" then + local data = love.math.decompress(fd, "gzip") + layer.data = getDecompressedData(data) + end + end + end + end + + layer.x = (layer.x or 0) + self.offsetx + layer.y = (layer.y or 0) + self.offsety + 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 + self:setObjectData(layer) + self:setObjectCoordinates(layer) + self:setObjectSpriteBatches(layer) + layer.draw = function() self:drawObjectLayer(layer) end + elseif layer.type == "imagelayer" then + layer.draw = function() self:drawImageLayer(layer) end + + if layer.image ~= "" then + local formatted_path = formatPath(path .. layer.image) + if not self.sti.cache[formatted_path] then + cache_image(self.sti, formatted_path) + end + + layer.image = self.sti.cache[formatted_path] + layer.width = layer.image:getWidth() + layer.height = layer.image:getHeight() + end + end + + self.layers[layer.name] = layer +end + +--- Add Tiles to Tile Layer +-- @param layer The Tile Layer +-- @return nil +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 + map[y][x] = self.tiles[gid] or self:setFlippedGID(gid) + end + + i = i + 1 + end + end + + layer.data = map +end + +--- Add Objects to Layer +-- @param layer The Object Layer +-- @return nil +function Map:setObjectData(layer) + for _, object in ipairs(layer.objects) do + object.layer = layer + self.objects[object.id] = object + end +end + +--- Correct position and orientation of Objects in an Object Layer +-- @param layer The Object Layer +-- @return nil +function Map:setObjectCoordinates(layer) + 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) + local vertex = { + x = v.x, + y = v.y, + } + + 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 + end + + local function updateVertex(vertex, x, y, cos, sin) + if self.orientation == "isometric" then + x, y = self:convertIsometricToScreen(x, y) + vertex.x, vertex.y = self:convertIsometricToScreen(vertex.x, vertex.y) + end + + return rotateVertex(vertex, x, y, cos, sin) + end + + for _, object in ipairs(layer.objects) do + local x = layer.x + object.x + local y = layer.y + object.y + local w = object.width + local h = object.height + local r = object.rotation + local cos = math.cos(math.rad(r)) + local sin = math.sin(math.rad(r)) + + if object.shape == "rectangle" and not object.gid then + object.rectangle = {} + + local vertices = { + { x=x, y=y }, + { x=x + w, y=y }, + { x=x + w, y=y + h }, + { x=x, y=y + h }, + } + + for _, vertex in ipairs(vertices) do + vertex.x, vertex.y = updateVertex(vertex, x, y, cos, sin) + table.insert(object.rectangle, { x = vertex.x, y = vertex.y }) + end + elseif object.shape == "ellipse" then + object.ellipse = {} + local vertices = convertEllipseToPolygon(x, y, w, h) + + for _, vertex in ipairs(vertices) do + vertex.x, vertex.y = updateVertex(vertex, x, y, cos, sin) + table.insert(object.ellipse, { x = vertex.x, y = vertex.y }) + end + elseif object.shape == "polygon" then + for _, vertex in ipairs(object.polygon) do + vertex.x = vertex.x + x + vertex.y = vertex.y + y + vertex.x, vertex.y = updateVertex(vertex, x, y, cos, sin) + end + elseif object.shape == "polyline" then + for _, vertex in ipairs(object.polyline) do + vertex.x = vertex.x + x + vertex.y = vertex.y + y + vertex.x, vertex.y = updateVertex(vertex, x, y, cos, sin) + end + end + end +end + +--- Batch Tiles in Tile Layer for improved draw speed +-- @param layer The Tile Layer +-- @return nil +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) + local sx = 1 + local sy = 1 + local ex = layer.width + local ey = layer.height + local ix = 1 + local iy = 1 + + -- Determine order to add tiles to sprite batch + -- Defaults to right-down + if self.renderorder == "right-up" then + sx, ex, ix = sx, ex, 1 + sy, ey, iy = ey, sy, -1 + elseif self.renderorder == "left-down" then + sx, ex, ix = ex, sx, -1 + sy, ey, iy = sy, ey, 1 + elseif self.renderorder == "left-up" then + sx, ex, ix = ex, sx, -1 + sy, ey, iy = ey, sy, -1 + end + + -- 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=sy, ey, iy do + local by = math.ceil(y / bh) + + for x=sx, ex, ix do + local tile = layer.data[y][x] + local bx = math.ceil(x / bw) + local id + + 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, ty = compensate(tile, x*tw, y*th, tw, th) + elseif self.orientation == "isometric" then + tx = (x - y) * (tw / 2) + tile.offset.x + layer.width * tw / 2 + ty = (x + y) * (th / 2) + tile.offset.y + elseif self.orientation == "staggered" or self.orientation == "hexagonal" then + if self.staggeraxis == "y" then + if self.staggerindex == "odd" then + if y % 2 == 0 then + tx = x * tw + tw / 2 + (self.hexsidelength or 0) + tile.offset.x + else + tx = x * tw + (self.hexsidelength or 0) + tile.offset.x + end + else + if y % 2 == 0 then + tx = x * tw + (self.hexsidelength or 0) + tile.offset.x + else + tx = x * tw + tw / 2 + (self.hexsidelength or 0) + tile.offset.x + end + end + + if self.orientation == "hexagonal" then + ty = y * (th - (th - self.hexsidelength) / 2) + tile.offset.y + (th - (th - self.hexsidelength) / 2) + else + ty = y * th / 2 + tile.offset.y + th / 2 + end + else + if self.staggerindex == "odd" then + if x % 2 == 0 then + ty = y * th + th / 2 + (self.hexsidelength or 0) + tile.offset.y + else + ty = y * th + (self.hexsidelength or 0) + tile.offset.y + end + else + if x % 2 == 0 then + ty = y * th + (self.hexsidelength or 0) + tile.offset.y + else + ty = y * th + th / 2 + (self.hexsidelength or 0) + tile.offset.y + end + end + + if self.orientation == "hexagonal" then + tx = x * (tw - (tw - self.hexsidelength) / 2) + tile.offset.x + (tw - (tw - self.hexsidelength) / 2) + else + tx = x * tw / 2 + tile.offset.x + tw / 2 + end + end + end + + id = batch:add(tile.quad, tx, ty, tile.r, tile.sx, tile.sy) + self.tileInstances[tile.gid] = self.tileInstances[tile.gid] or {} + table.insert(self.tileInstances[tile.gid], { + layer = layer, + batch = batch, + id = id, + gid = tile.gid, + x = tx, + y = ty, + r = tile.r, + oy = 0 + }) + end + end + end + + layer.batches = batches +end + +--- Batch Tiles in Object Layer for improved draw speed +-- @param layer The Object Layer +-- @return nil +function Map:setObjectSpriteBatches(layer) + local newBatch = love.graphics.newSpriteBatch + local tw = self.tilewidth + local th = self.tileheight + local batches = {} + + for _, object in ipairs(layer.objects) do + if object.gid then + local tile = self.tiles[object.gid] or self:setFlippedGID(object.gid) + local ts = tile.tileset + local image = self.tilesets[tile.tileset].image + + batches[ts] = batches[ts] or newBatch(image, 100) + + local batch = batches[ts] + local tx = object.x + tw + tile.offset.x + local ty = object.y + tile.offset.y + local tr = math.rad(object.rotation) + local oy = 0 + + -- Compensation for scale/rotation shift + if tile.sx == 1 and tile.sy == 1 then + if tr ~= 0 then + ty = ty + th + oy = th + end + else + if tile.sx < 0 then tx = tx + tw end + if tile.sy < 0 then ty = ty + th end + if tr > 0 then tx = tx + tw end + if tr < 0 then ty = ty + th end + end + + id = batch:add(tile.quad, tx, ty, tr, tile.sx, tile.sy, 0, oy) + self.tileInstances[tile.gid] = self.tileInstances[tile.gid] or {} + table.insert(self.tileInstances[tile.gid], { + layer = layer, + batch = batch, + id = id, + gid = tile.gid, + x = tx, + y = ty, + r = tr, + oy = oy + }) + end + end + + layer.batches = batches +end + +--- Only draw what is visible on screen for improved draw speed +-- @param tx Translate X axis (in pixels) +-- @param ty Translate Y axis (in pixels) +-- @param w Width of screen (in pixels) +-- @param h Height of screen (in pixels) +-- @return nil +function Map:setDrawRange(tx, ty, w, h) + local tw, th = self.tilewidth, 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" or self.orientation == "hexagonal" 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 + self.drawRange.sy = sy + self.drawRange.ex = ex + self.drawRange.ey = ey +end + +--- Create a Custom Layer to place userdata in (such as player sprites) +-- @param name Name of Custom Layer +-- @param index Draw order within Layer stack +-- @return table Custom Layer +function Map:addCustomLayer(name, index) + local index = index or #self.layers + 1 + 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] + + return layer +end + +--- Convert another Layer into a Custom Layer +-- @param index Index or name of Layer to convert +-- @return table Custom Layer +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 + + return layer +end + +--- Remove a Layer from the Layer stack +-- @param index Index or name of Layer to convert +-- @return nil +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) + self.layers[index] = nil + break + end + end + else + local name = self.layers[index].name + table.remove(self.layers, index) + self.layers[name] = nil + end + + -- Remove tile instances + if layer.batches then + for gid, tiles in pairs(self.tileInstances) do + for i=#tiles, 1, -1 do + local tile = tiles[i] + if tile.layer == layer then + table.remove(tiles, i) + end + end + end + end + + -- Remove objects + if layer.objects then + for i, object in pairs(self.objects) do + if object.layer == layer then + self.objects[i] = nil + end + end + end +end + +--- Animate Tiles and update every Layer +-- @param dt Delta Time +-- @return nil +function Map:update(dt) + for gid, tile in pairs(self.tiles) do + local update = false + + if tile.animation then + tile.time = tile.time + dt * 1000 + + while tile.time > tonumber(tile.animation[tile.frame].duration) do + update = true + tile.time = tile.time - tonumber(tile.animation[tile.frame].duration) + tile.frame = tile.frame + 1 + + if tile.frame > #tile.animation then tile.frame = 1 end + end + + if update and self.tileInstances[tile.gid] then + for _, j in pairs(self.tileInstances[tile.gid]) do + local t = self.tiles[tonumber(tile.animation[tile.frame].tileid) + self.tilesets[tile.tileset].firstgid] + j.batch:set(j.id, t.quad, j.x, j.y, j.r, tile.sx, tile.sy, 0, j.oy) + end + end + end + end + + + for _, layer in ipairs(self.layers) do + layer:update(dt) + end +end + +--- Draw every Layer +-- @return nil +function Map:draw() + local current_canvas = love.graphics.getCanvas() + love.graphics.setCanvas(self.canvas) + if self.canvas.clear then + self.canvas:clear() + else + local r,g,b,a = love.graphics.getBackgroundColor() + love.graphics.clear(r,g,b,a,self.canvas) + end + + for _, layer in ipairs(self.layers) do + if layer.visible and layer.opacity > 0 then + self:drawLayer(layer) + end + end + + love.graphics.setCanvas(current_canvas) + love.graphics.push() + love.graphics.origin() + love.graphics.draw(self.canvas) + love.graphics.pop() +end + +--- Draw an individual Layer +-- @param layer The Layer to draw +-- @return nil +function Map:drawLayer(layer) + love.graphics.setColor(255, 255, 255, 255 * layer.opacity) + layer:draw() + love.graphics.setColor(255, 255, 255, 255) +end + +--- Default draw function for Tile Layers +-- @param layer The Tile Layer to draw +-- @return nil +function Map:drawTileLayer(layer) + if type(layer) == "string" or type(layer) == "number" then + layer = self.layers[layer] + end + + 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 ix = 1 + local iy = 1 + local mx = math.ceil(self.width / bw) + local my = math.ceil(self.height / bh) + + -- Determine order to draw batches + -- Defaults to right-down + if self.renderorder == "right-up" then + sx, ex, ix = sx, ex, 1 + sy, ey, iy = ey, sy, -1 + elseif self.renderorder == "left-down" then + sx, ex, ix = ex, sx, -1 + sy, ey, iy = sy, ey, 1 + elseif self.renderorder == "left-up" then + sx, ex, ix = ex, sx, -1 + sy, ey, iy = ey, sy, -1 + end + + for by=sy, ey, iy do + for bx=sx, ex, ix 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 + +--- Default draw function for Object Layers +-- @param layer The Object Layer to draw +-- @return nil +function Map:drawObjectLayer(layer) + if type(layer) == "string" or type(layer) == "number" then + layer = self.layers[layer] + end + + 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 reset = { 255, 255, 255, 255 * layer.opacity } + + local function sortVertices(obj) + local vertices = {{},{}} + + for _, vertex in ipairs(obj) do + table.insert(vertices[1], vertex.x) + table.insert(vertices[1], vertex.y) + table.insert(vertices[2], vertex.x+1) + table.insert(vertices[2], vertex.y+1) + end + + return vertices + end + + local function drawShape(obj, shape) + local vertices = sortVertices(obj) + + if shape == "polyline" then + love.graphics.setColor(shadow) + love.graphics.line(vertices[2]) + love.graphics.setColor(line) + love.graphics.line(vertices[1]) + + return + elseif shape == "polygon" then + love.graphics.setColor(fill) + if not love.math.isConvex(vertices[1]) then + local triangles = love.math.triangulate(vertices[1]) + for _, triangle in ipairs(triangles) do + love.graphics.polygon("fill", triangle) + end + else + love.graphics.polygon("fill", vertices[1]) + end + else + love.graphics.setColor(fill) + love.graphics.polygon("fill", vertices[1]) + end + + love.graphics.setColor(shadow) + love.graphics.polygon("line", vertices[2]) + love.graphics.setColor(line) + love.graphics.polygon("line", vertices[1]) + end + + for _, object in ipairs(layer.objects) do + if object.shape == "rectangle" and not object.gid then + drawShape(object.rectangle, "rectangle") + elseif object.shape == "ellipse" then + drawShape(object.ellipse, "ellipse") + elseif object.shape == "polygon" then + drawShape(object.polygon, "polygon") + elseif object.shape == "polyline" then + drawShape(object.polyline, "polyline") + end + end + + love.graphics.setColor(reset) + for _, batch in pairs(layer.batches) do + love.graphics.draw(batch, 0, 0) + end +end + +--- Default draw function for Image Layers +-- @param layer The Image Layer to draw +-- @return nil +function Map:drawImageLayer(layer) + if type(layer) == "string" or type(layer) == "number" then + layer = self.layers[layer] + end + + 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 + +--- Resize the drawable area of the Map +-- @param w The new width of the drawable area (in pixels) +-- @param h The new Height of the drawable area (in pixels) +-- @return nil +function Map:resize(w, h) + w = w or love.graphics.getWidth() + h = h or love.graphics.getHeight() + + self.canvas = love.graphics.newCanvas(w, h) + self.canvas:setFilter("nearest", "nearest") +end + +--- Create flipped or rotated Tiles based on bitop flags +-- @param gid The flagged Global ID +-- @return table Flipped Tile +function Map:setFlippedGID(gid) + local bit31 = 2147483648 + local bit30 = 1073741824 + local bit29 = 536870912 + local flipX = false + local flipY = false + local flipD = false + local realgid = gid + + if realgid >= bit31 then + realgid = realgid - bit31 + flipX = not flipX + end + + if realgid >= bit30 then + realgid = realgid - bit30 + flipY = not flipY + end + + if realgid >= bit29 then + realgid = realgid - bit29 + flipD = not flipD + end + + local tile = self.tiles[realgid] + local data = { + id = tile.id, + gid = gid, + tileset = tile.tileset, + frame = tile.frame, + time = tile.time, + width = tile.width, + height = tile.height, + offset = tile.offset, + quad = tile.quad, + properties = tile.properties, + terrain = tile.terrain, + animation = tile.animation, + sx = tile.sx, + sy = tile.sy, + r = tile.r, + } + + if flipX then + if flipY and flipD then + data.r = math.rad(-90) + data.sy = -1 + elseif 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 + + return self.tiles[gid] +end + +--- Get custom properties from Layer +-- @param layer The Layer +-- @return table List of properties +function Map:getLayerProperties(layer) + local l = self.layers[layer] + + if not l then return {} end + + return l.properties +end + +--- Get custom properties from Tile +-- @param layer The Layer that the Tile belongs to +-- @param x The X axis location of the Tile (in tiles) +-- @param y The Y axis location of the Tile (in tiles) +-- @return table List of properties +function Map:getTileProperties(layer, x, y) + local tile = self.layers[layer].data[y][x] + + if not tile then return {} end + + return tile.properties +end + +--- Get custom properties from Object +-- @param layer The Layer that the Object belongs to +-- @param object The index or name of the Object +-- @return table List of properties +function Map:getObjectProperties(layer, object) + local o = self.layers[layer].objects + + if type(object) == "number" then + o = o[object] + else + for _, v in ipairs(o) do + if v.name == object then + o = v + break + end + end + end + + if not o then return {} end + + return o.properties +end + +--- Project isometric position to orthoganal position +-- @param x The X axis location of the point (in pixels) +-- @param y The Y axis location of the point (in pixels) +-- @return number The X axis location of the point (in pixels) +-- @return number The Y axis location of the point (in pixels) +function Map:convertIsometricToScreen(x, y) + local mw = self.width + local tw = self.tilewidth + local th = self.tileheight + local ox = mw * tw / 2 + local sx = (x - y) + ox + local sy = (x + y) / 2 + + return sx, sy +end + +--- Project orthoganal position to isometric position +-- @param x The X axis location of the point (in pixels) +-- @param y The Y axis location of the point (in pixels) +-- @return number The X axis location of the point (in pixels) +-- @return number The Y axis location of the point (in pixels) +function Map:convertScreenToIsometric(x, y) + local mw = self.width + local mh = self.height + local tw = self.tilewidth + local th = self.tileheight + local ox = mw * tw / 2 + local oy = mh * th / 2 + local tx = (x / 2 + y) - ox / 2 + local ty = (-x / 2 + y) + oy + + return tx, ty +end + +--- Convert tile space to screen space +-- @param x The X axis location of the point (in tiles) +-- @param y The Y axis location of the point (in tiles) +-- @return number The X axis location of the point (in pixels) +-- @return number The Y axis location of the point (in pixels) +function Map:convertWorldToScreen(x,y) + if self.orientation == "orthogonal" then + local tw = self.tilewidth + local th = self.tileheight + local sx = x * tw + local sy = y * th + + return sx, sy + elseif self.orientation == "isometric" then + local mw = self.width + local tw = self.tilewidth + local th = self.tileheight + local ox = mw * tw / 2 + local sx = (x - y) * tw / 2 + ox + local sy = (x + y) * th / 2 + + return sx, sy + elseif self.orientation == "staggered" then + local tw = self.tilewidth + local th = self.tileheight + local sx = x * tw + math.abs(math.ceil(y) % 2) * (tw / 2) - (math.ceil(y) % 2 * tw/2) + local sy = y * (th / 2) + th/2 + + return sx, sy + end +end + +--- Convert screen space to tile space +-- @param x The X axis location of the point (in pixels) +-- @param y The Y axis location of the point (in pixels) +-- @return number The X axis location of the point (in tiles) +-- @return number The Y axis location of the point (in tiles) +function Map:convertScreenToWorld(x,y) + if self.orientation == "orthogonal" then + local tw = self.tilewidth + local th = self.tileheight + local tx = x / tw + local ty = y / th + + return tx, ty + elseif self.orientation == "isometric" then + local mw = self.width + local tw = self.tilewidth + local th = self.tileheight + local ox = mw * tw / 2 + local tx = y / th + (x - ox) / tw + local ty = y / th - (x - ox) / tw + + return tx, ty + elseif self.orientation == "staggered" then + local function topLeft(x, y) + if (math.ceil(y) % 2) then + return x, y - 1 + else + return x - 1, y - 1 + end + end + + local function topRight(x, y) + if (math.ceil(y) % 2) then + return x + 1, y - 1 + else + return x, y - 1 + end + end + + local function bottomLeft(x, y) + if (math.ceil(y) % 2) then + return x, y + 1 + else + return x - 1, y + 1 + end + end + + local function bottomRight(x, y) + if (math.ceil(y) % 2) then + return x + 1, y + 1 + else + return x, y + 1 + end + end + + local tw = self.tilewidth + local th = self.tileheight + local hh = th / 2 + local ratio = th / tw + local tx = x / tw + local ty = y / th * 2 + local ctx = math.ceil(x / tw) + local cty = math.ceil(y / th) * 2 + local rx = x - ctx * tw + local ry = y - (cty / 2) * th + + if (hh - rx * ratio > ry) then + return topLeft(tx, ty) + elseif (-hh + rx * ratio > ry) then + return topRight(tx, ty) + elseif (hh + rx * ratio < ry) then + return bottomLeft(tx, ty) + elseif (hh * 3 - rx * ratio < ry) then + return bottomRight(tx, ty) + end + + return tx, ty + end +end + +return Map + +--- A list of individual layers indexed both by draw order and name +-- @table Map.layers +-- @see TileLayer +-- @see ObjectLayer +-- @see ImageLayer +-- @see CustomLayer + +--- A list of individual tiles indexed by Global ID +-- @table Map.tiles +-- @see Tile +-- @see Map.tileInstances + +--- A list of tile instances indexed by Global ID +-- @table Map.tileInstances +-- @see TileInstance +-- @see Tile +-- @see Map.tiles + +--- A list of individual objects indexed by Global ID +-- @table Map.objects +-- @see Object + +--- @table TileLayer +-- @field name The name of the layer +-- @field x Position on the X axis (in pixels) +-- @field y Position on the Y axis (in pixels) +-- @field width Width of layer (in tiles) +-- @field height Height of layer (in tiles) +-- @field visible Toggle if layer is visible or hidden +-- @field opacity Opacity of layer +-- @field properties Custom properties +-- @field data A two dimensional table filled with individual tiles indexed by [y][x] (in tiles) +-- @field update Update function +-- @field draw Draw function +-- @see Map.layers +-- @see Tile + +--- @table ObjectLayer +-- @field name The name of the layer +-- @field x Position on the X axis (in pixels) +-- @field y Position on the Y axis (in pixels) +-- @field visible Toggle if layer is visible or hidden +-- @field opacity Opacity of layer +-- @field properties Custom properties +-- @field objects List of objects indexed by draw order +-- @field update Update function +-- @field draw Draw function +-- @see Map.layers +-- @see Object + +--- @table ImageLayer +-- @field name The name of the layer +-- @field x Position on the X axis (in pixels) +-- @field y Position on the Y axis (in pixels) +-- @field visible Toggle if layer is visible or hidden +-- @field opacity Opacity of layer +-- @field properties Custom properties +-- @field image Image to be drawn +-- @field update Update function +-- @field draw Draw function +-- @see Map.layers + +--- Custom Layers are used to place userdata such as sprites within the draw order of the map. +-- @table CustomLayer +-- @field name The name of the layer +-- @field x Position on the X axis (in pixels) +-- @field y Position on the Y axis (in pixels) +-- @field visible Toggle if layer is visible or hidden +-- @field opacity Opacity of layer +-- @field properties Custom properties +-- @field update Update function +-- @field draw Draw function +-- @see Map.layers +-- @usage +-- -- Create a Custom Layer +-- local spriteLayer = map:addCustomLayer("Sprite Layer", 3) +-- +-- -- Add data to Custom 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 + +--- @table Tile +-- @field id Local ID within Tileset +-- @field gid Global ID +-- @field tileset Tileset ID +-- @field quad Quad object +-- @field properties Custom properties +-- @field terrain Terrain data +-- @field animation Animation data +-- @field frame Current animation frame +-- @field time Time spent on current animation frame +-- @field width Width of tile +-- @field height Height of tile +-- @field sx Scale value on the X axis +-- @field sy Scale value on the Y axis +-- @field r Rotation of tile (in radians) +-- @field offset Offset drawing position +-- @field offset.x Offset value on the X axis +-- @field offset.y Offset value on the Y axis +-- @see Map.tiles + +--- @table TileInstance +-- @field batch Spritebatch the Tile Instance belongs to +-- @field id ID within the spritebatch +-- @field gid Global ID +-- @field x Position on the X axis (in pixels) +-- @field y Position on the Y axis (in pixels) +-- @see Map.tileInstances +-- @see Tile + +--- @table Object +-- @field id Global ID +-- @field name Name of object (non-unique) +-- @field shape Shape of object +-- @field x Position of object on X axis (in pixels) +-- @field y Position of object on Y axis (in pixels) +-- @field width Width of object (in pixels) +-- @field height Heigh tof object (in pixels) +-- @field rotation Rotation of object (in radians) +-- @field visible Toggle if object is visible or hidden +-- @field properties Custom properties +-- @field ellipse List of verticies of specific shape +-- @field rectangle List of verticies of specific shape +-- @field polygon List of verticies of specific shape +-- @field polyline List of verticies of specific shape +-- @see Map.objects diff --git a/src/engine/libs/sti/plugins/box2d.lua b/src/engine/libs/sti/plugins/box2d.lua new file mode 100755 index 0000000..6140cd1 --- /dev/null +++ b/src/engine/libs/sti/plugins/box2d.lua @@ -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 diff --git a/src/engine/libs/sti/plugins/bump.lua b/src/engine/libs/sti/plugins/bump.lua new file mode 100755 index 0000000..c0f8f75 --- /dev/null +++ b/src/engine/libs/sti/plugins/bump.lua @@ -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 +} diff --git a/src/engine/object.lua b/src/engine/object.lua deleted file mode 100644 index 7fb9e33..0000000 --- a/src/engine/object.lua +++ /dev/null @@ -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 diff --git a/src/engine/sound.lua b/src/engine/sound.lua index c17916b..6641f32 100644 --- a/src/engine/sound.lua +++ b/src/engine/sound.lua @@ -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 diff --git a/src/engine/ui.lua b/src/engine/ui.lua index 4b55586..8269e70 100644 --- a/src/engine/ui.lua +++ b/src/engine/ui.lua @@ -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 diff --git a/src/levels/01.lua b/src/levels/01.lua index 9e7619d..6b2acf0 100644 --- a/src/levels/01.lua +++ b/src/levels/01.lua @@ -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==" } } } diff --git a/src/levels/01.tmx b/src/levels/01.tmx index f6448a5..61e6814 100644 --- a/src/levels/01.tmx +++ b/src/levels/01.tmx @@ -1,15 +1,20 @@ - - + + + + + + + - + - + - + @@ -19,7 +24,7 @@ - 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= @@ -28,17 +33,40 @@ - - + + + + + + + + + + + + + + + + + + + + + + - eJxjYBgFo4C+4Asjdc0TYaKueaNgFIwC4gAA9U4BDA== + eJxjYBgFo4C+4Asjdc0TYaKueaMAAhQ4GRgSOAfaFaNgMAMALD4Bng== + + + - eJzbycDAsHOIY0KAFHOIMY8YM5HVUdM8YgGx+qnlPlLdSqn/aGleLuPgdh+9zCPHbEJ5a6DMoyTNYisbqFFmUWoGLc0jFgMAI62XeA== + eJzbycDAsHOIY0KAFHOIMY8YM5HVUdM8YgGx+qnlPlLdSqn/aGleLuPgdt9gNo9Q3iLVbGqZR0maxVY2UKPMolb5RwvziMUADlyWBg== diff --git a/src/main.lua b/src/main.lua index 89e8951..ee74be4 100644 --- a/src/main.lua +++ b/src/main.lua @@ -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 diff --git a/src/objects/player.lua b/src/objects/player.lua index 15e5a34..666e7cb 100644 --- a/src/objects/player.lua +++ b/src/objects/player.lua @@ -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' } } diff --git a/src/objects/romata.lua b/src/objects/romata.lua index b4881d5..4590c7e 100644 --- a/src/objects/romata.lua +++ b/src/objects/romata.lua @@ -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' } diff --git a/src/story.lua b/src/story.lua index 79aaa8c..72f10ec 100644 --- a/src/story.lua +++ b/src/story.lua @@ -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