diff --git a/CREDITS.md b/CREDITS.md
new file mode 100644
index 0000000..c821935
--- /dev/null
+++ b/CREDITS.md
@@ -0,0 +1,21 @@
+# Credits
+
+## GAMECORE credits
+
+- [Binser](https://github.com/bakpakin/binser) under MIT Licence
+
+- [Classic](https://github.com/rxi/classic) under MIT Licence
+
+- An altered version of [CScreen](https://github.com/CodeNMore/CScreen) under a custom licence.
+
+- [LoveBird](https://github.com/rxi/lovebird) under MIT Licence
+
+- [Bump.lua](https://github.com/kikito/bump.lua), under MIT Licence
+
+- [Bump.3DPD](https://github.com/oniietzschan/bump-3dpd), under MIT Licence
+
+- [Tsort](https://github.com/bungle/lua-resty-tsort), under BSD-2 Clause
+
+- [Simple Tiled Implementation](https://github.com/karai17/Simple-Tiled-Implementation), under MIT Licence
+
+- [tween.lua](https://github.com/kikito/tween.lua), under MIT Licence
diff --git a/bootleg.love/conf.lua b/bootleg.love/conf.lua
new file mode 100644
index 0000000..24c8db8
--- /dev/null
+++ b/bootleg.love/conf.lua
@@ -0,0 +1,41 @@
+function love.conf(t)
+ t.identity = "net.chlore.Bootleg" -- The name of the save directory (string)
+ t.version = "11.1" -- The LÖVE version this game was made for (string)
+ t.console = false -- Attach a console (boolean, Windows only)
+ t.accelerometerjoystick = false -- Enable the accelerometer on iOS and Android by exposing it as a Joystick (boolean)
+ t.gammacorrect = false -- Enable gamma-correct rendering, when supported by the system (boolean)
+
+ t.window.title = "GameCore Example" -- The window title (string)
+ t.window.icon = nil -- Filepath to an image to use as the window's icon (string)
+ t.window.width = 424 -- The window width (number)
+ t.window.height = 240 -- 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 = "exclusive" -- 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), 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/bootleg.love/core/callbacks.lua b/bootleg.love/core/callbacks.lua
new file mode 100644
index 0000000..39fe422
--- /dev/null
+++ b/bootleg.love/core/callbacks.lua
@@ -0,0 +1,46 @@
+-- callbacks.lua :: load the callbacks from love2D
+
+--[[
+ Copyright © 2019 Kazhnuz
+
+ 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.
+]]
+
+function love.update(dt)
+ core:update(dt)
+end
+
+function love.draw()
+ core:draw()
+end
+
+function love.mousemoved(x, y, dx, dy)
+ core:mousemoved(x, y, dx, dy)
+end
+
+function love.mousepressed( x, y, button, istouch )
+ core:mousepressed(x, y, button, istouch)
+end
+
+function love.keypressed( key, scancode, isrepeat )
+ core:keypressed( key, scancode, isrepeat )
+end
+
+function love.keyreleased(key)
+ core:keyreleased( key )
+end
diff --git a/bootleg.love/core/debug.lua b/bootleg.love/core/debug.lua
new file mode 100644
index 0000000..0c1097a
--- /dev/null
+++ b/bootleg.love/core/debug.lua
@@ -0,0 +1,61 @@
+-- core/debug.lua :: Debug functions for the core system.
+
+--[[
+ Copyright © 2019 Kazhnuz
+
+ 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.
+]]
+
+local DebugSystem = Object:extend()
+
+local cwd = (...):gsub('%.debug$', '') .. "."
+local lovebird = require(cwd .. "libs.lovebird")
+
+function DebugSystem:new(controller, active)
+ self.controller = controller
+ lovebird.update()
+ self.active = active or false
+end
+
+function DebugSystem:update(dt)
+ lovebird.update(dt)
+end
+
+-- PRINT FUNCTIONS
+-- Print and log debug string
+
+function DebugSystem:print(context, string)
+ if (self.active) then
+ print("[DEBUG] ".. context .. ": " .. string)
+ end
+end
+
+function DebugSystem:warning(context, string)
+ if (self.active) then
+ print("[WARNING] " .. context .. ": " .. string)
+ end
+end
+
+function DebugSystem:error(context, string)
+ if (self.active) then
+ error("[ERROR] " .. context .. ": " .. string)
+ end
+end
+
+
+return DebugSystem
diff --git a/bootleg.love/core/init.lua b/bootleg.love/core/init.lua
new file mode 100644
index 0000000..b172785
--- /dev/null
+++ b/bootleg.love/core/init.lua
@@ -0,0 +1,122 @@
+-- core/init.lua :: The main file of the core system, an object full of subsystem
+-- loaded by the game to handle the main functions (like screen, translation,
+-- inputs…)
+
+--[[
+ Copyright © 2019 Kazhnuz
+
+ 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.
+]]
+
+local cwd = (...):gsub('%.init$', '') .. "."
+
+-- GLOBAL UTILS/FUNCTION LOADING
+-- Load in the global namespace utilities that'll need to be reusable everywhere
+-- in the game
+
+Object = require(cwd .. "libs.classic")
+utils = require(cwd .. "utils")
+
+local CoreSystem = Object:extend()
+
+local DebugSystem = require(cwd .. "debug")
+local Options = require(cwd .. "options")
+local Input = require(cwd .. "input")
+local Screen = require(cwd .. "screen")
+local Lang = require(cwd .. "lang")
+local SceneManager = require(cwd .. "scenemanager")
+
+local modules = require(cwd .. "modules")
+
+require(cwd .. "callbacks")
+
+-- INIT FUNCTIONS
+-- Initialize and configure the core object
+
+function CoreSystem:new(DEBUGMODE)
+ self.modules = modules
+
+ self.debug = DebugSystem(self, DEBUGMODE)
+ self.options = Options(self)
+ self.input = Input(self)
+ self.screen = Screen(self)
+ self.scenemanager = SceneManager(self)
+ self.lang = Lang(self)
+end
+
+function CoreSystem:registerGameSystem(gamesystem)
+ self.game = gamesystem
+end
+
+-- MOUSE FUNCTIONS
+-- get directly the mouse when needed
+
+function CoreSystem:mousemoved(x, y, dx, dy)
+ local x, y = self.screen:project(x, y)
+ local dx, dy = self.screen:project(dx, dy)
+ self.scenemanager:mousemoved(x, y, dx, dy)
+end
+
+function CoreSystem:mousepressed( x, y, button, istouch )
+ local x, y = self.screen:project(x, y)
+ self.scenemanager:mousepressed( x, y, button, istouch )
+end
+
+-- KEYBOARD FUNCTIONS
+-- get directly the keyboard when needed
+
+function CoreSystem:keypressed( key, scancode, isrepeat )
+ self.scenemanager:keypressed( key, scancode, isrepeat )
+end
+
+function CoreSystem:keyreleased( key )
+ self.scenemanager:keyreleased( key )
+end
+
+-- UPDATE FUNCTIONS
+-- Load every sytem every update functions of the scene and objects
+
+function CoreSystem:update(dt)
+ self.debug:update(dt)
+ self.input:update(dt)
+ self.screen:update(dt)
+
+ if (self.game ~= nil) then
+ self.game:update(dt)
+ end
+
+ self.scenemanager:update(dt)
+end
+
+-- DRAW FUNCTIONS
+-- Draw the whole game
+
+function CoreSystem:draw()
+ self.scenemanager:draw()
+ self.screen:drawFade()
+end
+
+-- EXIT FUNCTIONS
+-- Quit the game
+
+function CoreSystem:exit()
+ self.options:save()
+ love.event.quit()
+end
+
+return CoreSystem
diff --git a/bootleg.love/core/input.lua b/bootleg.love/core/input.lua
new file mode 100644
index 0000000..4653aa1
--- /dev/null
+++ b/bootleg.love/core/input.lua
@@ -0,0 +1,206 @@
+-- core/input.lua :: The input system. This object take care of transforming the
+-- differents inputs source in a virtual controller for the game.
+
+--[[
+ Copyright © 2019 Kazhnuz
+
+ 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.
+]]
+
+local InputManager = Object:extend()
+local VirtualPad = Object:extend()
+
+-- INIT FUNCTIONS
+-- Initialize and configure the controller system
+
+function InputManager:new(controller)
+ self.controller = controller
+ self.data = self.controller.options:getInputData()
+
+ self:initSources()
+end
+
+function InputManager:initSources()
+ self.sources = {}
+ for sourceid, data in ipairs(self.data) do
+ local source = VirtualPad(self, sourceid, data)
+ table.insert(self.sources, source)
+ end
+
+ self.fakekeys = self:getKeyList(1)
+end
+
+-- INFO FUNCTIONS
+-- Get functions from the controller object
+
+function InputManager:isDown(sourceid, key)
+ self.controller.debug:warning("core/input", "core.input:isDown is deprecated since 0.7.0 and will be removed in 0.8.0")
+ return self.sources[sourceid]:isDown(key)
+end
+
+function InputManager:getKeyList(sourceid)
+ local keys = {}
+ if self.data[sourceid] ~= nil then
+ for k,v in pairs(self.data[sourceid].keys) do
+ keys[k] = {}
+ keys[k].isDown = false
+ keys[k].isPressed = false
+ keys[k].isReleased = false
+ end
+ end
+
+ return keys
+end
+
+function InputManager:getKey(sourceid, padkey)
+ local padkey = padkey
+ for k,v in pairs(self.data[sourceid].keys) do
+ if (k == padkey) then key = v end
+ end
+ return key
+end
+
+function InputManager:getSources()
+ return self.sources
+end
+
+-- KEY MANAGEMENT FUNCTIONS
+-- Manage pressed keys
+
+function InputManager:flushKeys()
+ for i, source in ipairs(self.sources) do
+ source:flushKeys()
+ end
+end
+
+function InputManager:flushSourceKeys(sourceid)
+ self.keys = {}
+ self.sources[sourceid]:flushKeys()
+end
+
+-- UPDATE FUNCTIONS
+-- Check every step pressed keys
+
+function InputManager:update(dt)
+ for i, source in ipairs(self.sources) do
+ source:checkKeys()
+ end
+end
+
+------------------------------------ VIRTUALPADS -------------------------------
+-- Virtual representation of a pad
+-- The role of the virtualpad is to return all the data a controller at any time
+-- They can be flushed and deactivated for a while when needed
+
+-- INIT FUNCTIONS
+-- Initialize and configure the controller system
+
+function VirtualPad:new(controller, id, data)
+ self.controller = controller
+ self.id = id
+ self.data = data
+
+ self.type = self.data.type or "nil"
+
+ self:initKeys()
+end
+
+function VirtualPad:initKeys()
+ local keys = {}
+ if (self.data ~= nil) then
+ for k,v in pairs(self.data.keys) do
+ keys[k] = {}
+ keys[k].isDown = false
+ keys[k].isPressed = false
+ keys[k].isReleased = false
+ end
+ end
+
+ self.keys = keys
+ self.fakekeys = keys
+end
+
+function VirtualPad:isDown(key)
+ local isdown = false
+
+ if self.type == "keyboard" then
+ isdown = love.keyboard.isDown(self.data.keys[key])
+ else
+ local warnstring = "unsupported input device " .. self.type .. " for source " .. self.id
+ core.debug:warning("core/input", warnstring)
+ end
+
+ return isdown
+end
+
+function VirtualPad:checkKeys()
+ for key, keydata in pairs(self.keys) do
+ self:checkKey(key)
+ end
+end
+
+function VirtualPad:checkKey(key)
+ local isDown = self:isDown(key)
+ if (isDown) then
+ if not (self.keys[key].isDown) then
+ core.debug:print("virtualpad", "key " .. key .. " is Pressed")
+ self.keys[key].isDown = true
+ self.keys[key].isPressed = true
+ self.keys[key].isReleased = false
+ else
+ if (self.keys[key].isPressed) then
+ core.debug:print("virtualpad", "key " .. key .. " is Down")
+ self.keys[key].isPressed = false
+ end
+ end
+ else
+ if (self.keys[key].isDown) then
+ self.keys[key].isDown = false
+ self.keys[key].isPressed = false
+ self.keys[key].isReleased = true
+ else
+ if (self.keys[key].isReleased) then
+ core.debug:print("virtualpad", "key " .. key .. " is Released")
+ self.keys[key].isReleased = false
+ end
+ end
+ end
+end
+
+function VirtualPad:getKeys()
+ return self.keys
+end
+
+function VirtualPad:getKey(key)
+ return self.keys[key]
+end
+
+function VirtualPad:flushKeys()
+ for key, _ in pairs(self.keys) do
+ self:flushKey(key)
+ end
+end
+
+function VirtualPad:flushKey(key)
+ self.keys[key].isDown = false
+ self.keys[key].isPressed = false
+ self.keys[key].isReleased = false
+end
+
+
+return InputManager
diff --git a/bootleg.love/core/lang.lua b/bootleg.love/core/lang.lua
new file mode 100644
index 0000000..260c82d
--- /dev/null
+++ b/bootleg.love/core/lang.lua
@@ -0,0 +1,129 @@
+-- core/langs.lua :: The translation system. Transform a string to another
+-- according to the translations files in the datas/ folder.
+
+--[[
+ Copyright © 2019 Kazhnuz
+
+ 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.
+]]
+
+local LanguageManager = Object:extend()
+
+local TRANSLATION_PATH = "datas/languages/"
+
+-- INIT FUNCTIONS
+-- Initialize and configure the translation system
+
+function LanguageManager:new(controller)
+ self.controller = controller
+
+ self.data = self:getTranslationData()
+ self:setLang(self.controller.options.data.language.current)
+end
+
+function LanguageManager:setLang(lang)
+ self.controller.options.data.language.current = lang
+ self.lang = self.controller.options.data.language.current
+end
+
+-- INFO FUNCTIONS
+-- Get informations from the translation manager
+
+function LanguageManager:getCurrentLang()
+ return self.data.language.current
+end
+
+function LanguageManager:getDefaultLang()
+ return self.data.language.default
+end
+
+function LanguageManager:getTranslationData()
+ return self.controller.options.data.language
+end
+
+function LanguageManager:getLangMetadata(lang)
+ local langfilepath = self.data.path .. lang
+
+ return require(langfilepath)
+end
+
+function LanguageManager:getLangName(lang)
+ local metadata = self:getLangMetadata(lang)
+
+ return metadata.name
+end
+
+function LanguageManager:getCurrentLangName()
+ return self:getLangName(self.data.current)
+end
+
+function LanguageManager:isLangAvailable(lang)
+ local isAvailable = false
+
+ for i,v in ipairs(self.data.available) do
+ if v == lang then
+ isAvailable = true
+ end
+ end
+
+ return isAvailable
+end
+
+-- TRANSLATION FUNCTIONS
+-- get the translation of a string
+
+function LanguageManager:getTranslationStringList(lang, library)
+ local _path = self.data.path .. lang .. "/" .. library
+ local fileinfo = love.filesystem.getInfo(_path .. ".lua")
+ local list = nil
+
+ if fileinfo ~= nil then
+ list = require(_path)
+ else
+ core.debug:warning("core/lang","file " .. _path .. " do not exists")
+ end
+
+ return list
+end
+
+function LanguageManager:translateFromLang(lang, library, stringToTranslate)
+ local _stringlist = self:getTranslationStringList(lang, library)
+
+ if _stringlist == nil then
+ return nil
+ else
+ return _stringlist[stringToTranslate]
+ end
+end
+
+function LanguageManager:translate(library, string)
+ local translation = self:translateFromLang(self.data.current, library, string)
+
+ if (translation == nil) then
+ translation = self:translateFromLang(self.data.default, library, string)
+ end
+
+ if (translation == nil) then
+ translation = string
+ core.debug:warning("core/lang", "no translation path found for " .. string .. " in " .. library)
+ end
+
+ return translation
+end
+
+return LanguageManager
diff --git a/bootleg.love/core/libs/classic.lua b/bootleg.love/core/libs/classic.lua
new file mode 100644
index 0000000..cbd6f81
--- /dev/null
+++ b/bootleg.love/core/libs/classic.lua
@@ -0,0 +1,68 @@
+--
+-- classic
+--
+-- Copyright (c) 2014, rxi
+--
+-- This module is free software; you can redistribute it and/or modify it under
+-- the terms of the MIT license. See LICENSE for details.
+--
+
+
+local Object = {}
+Object.__index = Object
+
+
+function Object:new()
+end
+
+
+function Object:extend()
+ local cls = {}
+ for k, v in pairs(self) do
+ if k:find("__") == 1 then
+ cls[k] = v
+ end
+ end
+ cls.__index = cls
+ cls.super = self
+ setmetatable(cls, self)
+ return cls
+end
+
+
+function Object:implement(...)
+ for _, cls in pairs({...}) do
+ for k, v in pairs(cls) do
+ if self[k] == nil and type(v) == "function" then
+ self[k] = v
+ end
+ end
+ end
+end
+
+
+function Object:is(T)
+ local mt = getmetatable(self)
+ while mt do
+ if mt == T then
+ return true
+ end
+ mt = getmetatable(mt)
+ end
+ return false
+end
+
+
+function Object:__tostring()
+ return "Object"
+end
+
+
+function Object:__call(...)
+ local obj = setmetatable({}, self)
+ obj:new(...)
+ return obj
+end
+
+
+return Object
diff --git a/bootleg.love/core/libs/cscreen.lua b/bootleg.love/core/libs/cscreen.lua
new file mode 100644
index 0000000..e8c3243
--- /dev/null
+++ b/bootleg.love/core/libs/cscreen.lua
@@ -0,0 +1,107 @@
+--[[
+CScreen v1.3 by CodeNMore
+A simple way to make resolution-independent Love2D games
+Tested for LOVE 0.10.1
+See: https://github.com/CodeNMore/CScreen
+Zlib License:
+Copyright (c) 2016 CodeNMore
+This software is provided 'as-is', without any express or implied warranty.
+In no event will the authors be held liable for any damages arising from
+the use of this software.
+Permission is granted to anyone to use this software for any purpose,
+including commercial applications, and to alter it and redistribute it
+freely, subject to the following restrictions:
+1. The origin of this software must not be misrepresented; you must not
+claim that you wrote the original software. If you use this software in
+a product, an acknowledgment in the product documentation would be appreciated
+but is not required.
+2. Altered source versions must be plainly marked as such, and must not be
+misrepresented as being the original software.
+3. This notice may not be removed or altered from any source distribution.
+--]]
+
+local CScreen = {}
+local rx, ry, ctr = 800, 600, true
+local rxv, ryv, fsv, fsvr = 800, 600, 1.0, 1.0
+local tx, ty, rwf, rhf = 0, 0, 800, 600
+local cr, cg, cb, ca = 0, 0, 0, 255
+
+-- Initializes CScreen with the initial size values
+function CScreen.init(tw, th, cntr)
+ rx = tw or 800
+ ry = th or 600
+ ctr = cntr or false
+ CScreen.update(love.graphics.getWidth(), love.graphics.getHeight())
+end
+
+-- Draws letterbox borders
+function CScreen.cease()
+ if ctr then
+ local pr, pg, pb, pa = love.graphics.getColor()
+ love.graphics.setColor(cr, cg, cb, ca)
+ love.graphics.scale(fsvr, fsvr)
+
+ if tx ~= 0 then
+ love.graphics.rectangle("fill", -tx, 0, tx, rhf)
+ love.graphics.rectangle("fill", rxv, 0, tx, rhf)
+ elseif ty ~= 0 then
+ love.graphics.rectangle("fill", 0, -ty, rwf, ty)
+ love.graphics.rectangle("fill", 0, ryv, rwf, ty)
+ end
+
+ love.graphics.setColor(pr, pg, pb, pa)
+ end
+end
+
+-- Scales and centers all graphics properly
+function CScreen.apply()
+ if ctr then
+ love.graphics.translate(tx, ty)
+ end
+ love.graphics.scale(fsv, fsv)
+end
+
+-- Updates CScreen when the window size changes
+function CScreen.update(w, h)
+ local sx = w / rx
+ local sy = h / ry
+ fsv = math.min(sx, sy)
+ fsvr = 1 / fsv
+ -- Centering
+ if ctr and fsv == sx then -- Vertically
+ tx = 0
+ ty = (h / 2) - (ry * fsv / 2)
+ elseif ctr and fsv == sy then -- Horizontally
+ ty = 0
+ tx = (w / 2) - (rx * fsv / 2)
+ end
+ -- Variable sets
+ rwf = w
+ rhf = h
+ rxv = rx * fsv
+ ryv = ry * fsv
+end
+
+-- Convert from window coordinates to target coordinates
+function CScreen.project(x, y)
+ return math.floor((x - tx) / fsv), math.floor((y - ty) / fsv)
+end
+
+function CScreen.getScale()
+ return fsv
+end
+
+function CScreen.getScreenCoordinate(x, y)
+ return math.floor((x + tx) * fsv), math.floor((y + ty) * fsv)
+end
+
+-- Change letterbox color
+function CScreen.setColor(r, g, b, a)
+ cr = r
+ cg = g
+ cb = b
+ ca = a
+end
+
+-- Return the table for use
+return CScreen
diff --git a/bootleg.love/core/libs/lovebird.lua b/bootleg.love/core/libs/lovebird.lua
new file mode 100644
index 0000000..8b296eb
--- /dev/null
+++ b/bootleg.love/core/libs/lovebird.lua
@@ -0,0 +1,737 @@
+--
+-- lovebird
+--
+-- Copyright (c) 2017 rxi
+--
+-- This library is free software; you can redistribute it and/or modify it
+-- under the terms of the MIT license. See LICENSE for details.
+--
+
+local socket = require "socket"
+
+local lovebird = { _version = "0.4.3" }
+
+lovebird.loadstring = loadstring or load
+lovebird.inited = false
+lovebird.host = "*"
+lovebird.buffer = ""
+lovebird.lines = {}
+lovebird.connections = {}
+lovebird.pages = {}
+
+lovebird.wrapprint = true
+lovebird.timestamp = true
+lovebird.allowhtml = false
+lovebird.echoinput = true
+lovebird.port = 8000
+lovebird.whitelist = { "127.0.0.1" }
+lovebird.maxlines = 200
+lovebird.updateinterval = .5
+
+
+lovebird.pages["index"] = [[
+
+
+
+
+
+
+
+ lovebird
+
+
+
+
+
+
+
+
+]]
+
+
+lovebird.pages["buffer"] = [[ ]]
+
+
+lovebird.pages["env.json"] = [[
+
+{
+ "valid": true,
+ "path": "",
+ "vars": [
+
+ {
+ "key": "",
+ "value": ,
+ "type": "",
+ },
+
+ ]
+}
+]]
+
+
+
+function lovebird.init()
+ -- Init server
+ lovebird.server = assert(socket.bind(lovebird.host, lovebird.port))
+ lovebird.addr, lovebird.port = lovebird.server:getsockname()
+ lovebird.server:settimeout(0)
+ -- Wrap print
+ lovebird.origprint = print
+ if lovebird.wrapprint then
+ local oldprint = print
+ print = function(...)
+ oldprint(...)
+ lovebird.print(...)
+ end
+ end
+ -- Compile page templates
+ for k, page in pairs(lovebird.pages) do
+ lovebird.pages[k] = lovebird.template(page, "lovebird, req",
+ "pages." .. k)
+ end
+ lovebird.inited = true
+end
+
+
+function lovebird.template(str, params, chunkname)
+ params = params and ("," .. params) or ""
+ local f = function(x) return string.format(" echo(%q)", x) end
+ str = ("?>"..str.."(.-)<%?lua", f)
+ str = "local echo " .. params .. " = ..." .. str
+ local fn = assert(lovebird.loadstring(str, chunkname))
+ return function(...)
+ local output = {}
+ local echo = function(str) table.insert(output, str) end
+ fn(echo, ...)
+ return table.concat(lovebird.map(output, tostring))
+ end
+end
+
+
+function lovebird.map(t, fn)
+ local res = {}
+ for k, v in pairs(t) do res[k] = fn(v) end
+ return res
+end
+
+
+function lovebird.trace(...)
+ local str = "[lovebird] " .. table.concat(lovebird.map({...}, tostring), " ")
+ print(str)
+ if not lovebird.wrapprint then lovebird.print(str) end
+end
+
+
+function lovebird.unescape(str)
+ local f = function(x) return string.char(tonumber("0x"..x)) end
+ return (str:gsub("%+", " "):gsub("%%(..)", f))
+end
+
+
+function lovebird.parseurl(url)
+ local res = {}
+ res.path, res.search = url:match("/([^%?]*)%??(.*)")
+ res.query = {}
+ for k, v in res.search:gmatch("([^&^?]-)=([^&^#]*)") do
+ res.query[k] = lovebird.unescape(v)
+ end
+ return res
+end
+
+
+local htmlescapemap = {
+ ["<"] = "<",
+ ["&"] = "&",
+ ['"'] = """,
+ ["'"] = "'",
+}
+
+function lovebird.htmlescape(str)
+ return ( str:gsub("[<&\"']", htmlescapemap) )
+end
+
+
+function lovebird.truncate(str, len)
+ if #str <= len then
+ return str
+ end
+ return str:sub(1, len - 3) .. "..."
+end
+
+
+function lovebird.compare(a, b)
+ local na, nb = tonumber(a), tonumber(b)
+ if na then
+ if nb then return na < nb end
+ return false
+ elseif nb then
+ return true
+ end
+ return tostring(a) < tostring(b)
+end
+
+
+function lovebird.checkwhitelist(addr)
+ if lovebird.whitelist == nil then return true end
+ for _, a in pairs(lovebird.whitelist) do
+ local ptn = "^" .. a:gsub("%.", "%%."):gsub("%*", "%%d*") .. "$"
+ if addr:match(ptn) then return true end
+ end
+ return false
+end
+
+
+function lovebird.clear()
+ lovebird.lines = {}
+ lovebird.buffer = ""
+end
+
+
+function lovebird.pushline(line)
+ line.time = os.time()
+ line.count = 1
+ table.insert(lovebird.lines, line)
+ if #lovebird.lines > lovebird.maxlines then
+ table.remove(lovebird.lines, 1)
+ end
+ lovebird.recalcbuffer()
+end
+
+
+function lovebird.recalcbuffer()
+ local function doline(line)
+ local str = line.str
+ if not lovebird.allowhtml then
+ str = lovebird.htmlescape(line.str):gsub("\n", "
")
+ end
+ if line.type == "input" then
+ str = '' .. str .. ''
+ else
+ if line.type == "error" then
+ str = '! ' .. str
+ str = '' .. str .. ''
+ end
+ if line.count > 1 then
+ str = '' .. line.count .. ' ' .. str
+ end
+ if lovebird.timestamp then
+ str = os.date('%H:%M:%S ', line.time) ..
+ str
+ end
+ end
+ return str
+ end
+ lovebird.buffer = table.concat(lovebird.map(lovebird.lines, doline), "
")
+end
+
+
+function lovebird.print(...)
+ local t = {}
+ for i = 1, select("#", ...) do
+ table.insert(t, tostring(select(i, ...)))
+ end
+ local str = table.concat(t, " ")
+ local last = lovebird.lines[#lovebird.lines]
+ if last and str == last.str then
+ -- Update last line if this line is a duplicate of it
+ last.time = os.time()
+ last.count = last.count + 1
+ lovebird.recalcbuffer()
+ else
+ -- Create new line
+ lovebird.pushline({ type = "output", str = str })
+ end
+end
+
+
+function lovebird.onerror(err)
+ lovebird.pushline({ type = "error", str = err })
+ if lovebird.wrapprint then
+ lovebird.origprint("[lovebird] ERROR: " .. err)
+ end
+end
+
+
+function lovebird.onrequest(req, client)
+ local page = req.parsedurl.path
+ page = page ~= "" and page or "index"
+ -- Handle "page not found"
+ if not lovebird.pages[page] then
+ return "HTTP/1.1 404\r\nContent-Length: 8\r\n\r\nBad page"
+ end
+ -- Handle page
+ local str
+ xpcall(function()
+ local data = lovebird.pages[page](lovebird, req)
+ local contenttype = "text/html"
+ if string.match(page, "%.json$") then
+ contenttype = "application/json"
+ end
+ str = "HTTP/1.1 200 OK\r\n" ..
+ "Content-Type: " .. contenttype .. "\r\n" ..
+ "Content-Length: " .. #data .. "\r\n" ..
+ "\r\n" .. data
+ end, lovebird.onerror)
+ return str
+end
+
+
+function lovebird.receive(client, pattern)
+ while 1 do
+ local data, msg = client:receive(pattern)
+ if not data then
+ if msg == "timeout" then
+ -- Wait for more data
+ coroutine.yield(true)
+ else
+ -- Disconnected -- yielding nil means we're done
+ coroutine.yield(nil)
+ end
+ else
+ return data
+ end
+ end
+end
+
+
+function lovebird.send(client, data)
+ local idx = 1
+ while idx < #data do
+ local res, msg = client:send(data, idx)
+ if not res and msg == "closed" then
+ -- Handle disconnect
+ coroutine.yield(nil)
+ else
+ idx = idx + res
+ coroutine.yield(true)
+ end
+ end
+end
+
+
+function lovebird.onconnect(client)
+ -- Create request table
+ local requestptn = "(%S*)%s*(%S*)%s*(%S*)"
+ local req = {}
+ req.socket = client
+ req.addr, req.port = client:getsockname()
+ req.request = lovebird.receive(client, "*l")
+ req.method, req.url, req.proto = req.request:match(requestptn)
+ req.headers = {}
+ while 1 do
+ local line, msg = lovebird.receive(client, "*l")
+ if not line or #line == 0 then break end
+ local k, v = line:match("(.-):%s*(.*)$")
+ req.headers[k] = v
+ end
+ if req.headers["Content-Length"] then
+ req.body = lovebird.receive(client, req.headers["Content-Length"])
+ end
+ -- Parse body
+ req.parsedbody = {}
+ if req.body then
+ for k, v in req.body:gmatch("([^&]-)=([^&^#]*)") do
+ req.parsedbody[k] = lovebird.unescape(v)
+ end
+ end
+ -- Parse request line's url
+ req.parsedurl = lovebird.parseurl(req.url)
+ -- Handle request; get data to send and send
+ local data = lovebird.onrequest(req)
+ lovebird.send(client, data)
+ -- Clear up
+ client:close()
+end
+
+
+function lovebird.update()
+ if not lovebird.inited then lovebird.init() end
+ -- Handle new connections
+ while 1 do
+ -- Accept new connections
+ local client = lovebird.server:accept()
+ if not client then break end
+ client:settimeout(0)
+ local addr = client:getsockname()
+ if lovebird.checkwhitelist(addr) then
+ -- Connection okay -- create and add coroutine to set
+ local conn = coroutine.wrap(function()
+ xpcall(function() lovebird.onconnect(client) end, function() end)
+ end)
+ lovebird.connections[conn] = true
+ else
+ -- Reject connection not on whitelist
+ lovebird.trace("got non-whitelisted connection attempt: ", addr)
+ client:close()
+ end
+ end
+ -- Handle existing connections
+ for conn in pairs(lovebird.connections) do
+ -- Resume coroutine, remove if it has finished
+ local status = conn()
+ if status == nil then
+ lovebird.connections[conn] = nil
+ end
+ end
+end
+
+
+return lovebird
diff --git a/bootleg.love/core/modules/assets/animator.lua b/bootleg.love/core/modules/assets/animator.lua
new file mode 100644
index 0000000..9a42ba8
--- /dev/null
+++ b/bootleg.love/core/modules/assets/animator.lua
@@ -0,0 +1,149 @@
+-- assets/animator :: the animator object. The animator object handle what
+-- frame a sprite should draw.
+
+--[[
+ Copyright © 2019 Kazhnuz
+
+ 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.
+]]
+
+local Animator = Object:extend()
+
+-- INIT FUNCTIONS
+-- Initilizing and configuring option
+
+function Animator:new(sprite)
+ self.sprite = sprite
+ self.frame = 1
+ self.frameTimer = 0
+ self.currentAnimation = ""
+ self.animationData = {}
+
+ self.customSpeed = 0
+
+ self:changeToDefaultAnimation()
+end
+
+function Animator:setCustomSpeed(customSpeed)
+ self.customSpeed = customSpeed or 0
+end
+
+-- UPDATE FUNCTIONS
+-- Update the animation of the animator
+
+function Animator:update(dt)
+ if (self.currentAnimation == "") then
+ core.debug:warning("animator", "no current animation data")
+ return 0
+ end
+
+ local speed = self.animationData.speed
+ if (self.animationData.speed) == -1 then
+ speed = self.customSpeed --math.abs(self.xsp / 16)
+ end
+ self.frameTimer = self.frameTimer + (speed * dt)
+ if self.frameTimer > 1 then
+ self.frameTimer = 0
+ if self.frame == self.animationData.endAt then
+ self:sendCallback()
+ if not (self.animationData.pauseAtEnd) then
+ self.frame = self.animationData.loop
+ end
+ else
+ self.frame = self.frame + 1
+ end
+ end
+end
+
+-- ANIMATION HANDLING FUNCTIONS
+-- Change the animation of the animator
+
+function Animator:changeAnimation(name, restart)
+ -- Force restart if animation name is different
+ if (self.currentAnimation ~= name) then
+ restart = true
+ else
+ restart = restart or false
+ end
+
+ self.currentAnimation = name
+ self.animationData = self.sprite.data.animations[self.currentAnimation]
+
+ if (restart == true) then
+ self.frame = self.animationData.startAt
+ self.frameTimer = 0
+ end
+end
+
+function Animator:changeToDefaultAnimation(restart)
+ self:changeAnimation(self.sprite.data.metadata.defaultAnim, restart)
+end
+
+-- INFO FUNCTIONS
+-- get information with these functions
+
+function Animator:getCurrentAnimation()
+ return self.currentAnimation
+end
+
+function Animator:getAnimationDuration(animation)
+ return (self.animationData.endAt - self.animationData.startAt) / self.animationData.speed
+end
+
+function Animator:getFrame()
+ return self.frame
+end
+
+function Animator:getRelativeFrame()
+ return self.frame - (self.animationData.startAt) + 1
+end
+
+function Animator:animationExist(name)
+ return (self.sprite.data.animations[self.currentAnimation] ~= nil)
+end
+
+function Animator:getDimensions()
+ return self.sprite:getDimensions()
+end
+
+-- CALLBACK FUNCTIONS
+-- Handle getting a calback from the animation system
+
+function Animator:setCallbackTarget(callbackTarget)
+ self.callbackTarget = callbackTarget
+end
+
+function Animator:sendCallback()
+ if (self.callbackTarget ~= nil) then
+ if (self.callbackTarget.animationEnded ~= nil) then
+ self.callbackTarget:animationEnded(self.currentAnimation)
+ end
+ end
+end
+-- DRAW FUNCTIONS
+-- Draw animations using these functions
+
+function Animator:draw(x, y, r, sx, sy, ox, oy, kx, ky)
+ self.sprite:drawFrame(self.frame, x, y, r, sx, sy, ox, oy, kx, ky)
+end
+
+function Animator:drawMask(x, y, r, sx, sy, ox, oy, kx, ky)
+ self.sprite:drawFrameMask(self.frame, x, y, r, sx, sy, ox, oy, kx, ky)
+end
+
+return Animator
diff --git a/bootleg.love/core/modules/assets/autotile.lua b/bootleg.love/core/modules/assets/autotile.lua
new file mode 100644
index 0000000..b441ab1
--- /dev/null
+++ b/bootleg.love/core/modules/assets/autotile.lua
@@ -0,0 +1,117 @@
+-- assets/autotile :: The autotile object : this is an object that draw tiles
+-- automatically with borders in rectangles.
+-- It works with a 3×3 tileset showing all borders.
+
+--[[
+ Copyright © 2019 Kazhnuz
+
+ 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.
+]]
+
+local cwd = (...):gsub('%.autotile$', '') .. "."
+local Tileset = require(cwd .. "tileset")
+local Autotile = Object:extend()
+
+-- INIT FUNCTIONS
+-- Initilizing and configuring option
+
+function Autotile:new(filepath)
+ self.tileset = Tileset(filepath)
+
+ self.data = require(filepath .. ".lua")
+ self.metadata = self.data.metadata
+
+ self.tilesize = self.metadata.width
+end
+
+-- DRAW FUNCTIONS
+-- Draw tileset using these functions
+
+function Autotile:drawtile(i, j, x, y, r, sx, sy, ox, oy, kx, ky)
+ local i = i or 1
+ local j = j or 1
+ local tilesize = self.tilesize / 2
+ i = (i - 1) * 2 + 1
+ j = (j - 1) * 2 + 1
+ self.tileset:drawTile_Grid(i , j , x , y , r, sx, sy, ox, oy, kx, ky)
+ self.tileset:drawTile_Grid(i + 1, j , x + tilesize, y , r, sx, sy, ox, oy, kx, ky)
+ self.tileset:drawTile_Grid(i , j + 1, x , y + tilesize, r, sx, sy, ox, oy, kx, ky)
+ self.tileset:drawTile_Grid(i + 1, j + 1, x + tilesize, y + tilesize, r, sx, sy, ox, oy, kx, ky)
+end
+
+function Autotile:draw(x, y, w, h)
+ local w = w or self.tilesize
+ local h = h or self.tilesize
+ w = math.max(math.floor(w / self.tilesize), 1)
+ h = math.max(math.floor(h / self.tilesize), 1)
+ local halfsize = self.tilesize / 2
+ local tilesize = self.tilesize
+ if (w == 1) then
+ self.tileset:drawtile_Grid(1, 1, x , y)
+ self.tileset:drawtile_Grid(1, 6, x , y + (h*2 - 1) * halfsize)
+ self.tileset:drawtile_Grid(6, 1, x + (w*2 - 1) * halfsize, y)
+ self.tileset:drawtile_Grid(6, 6, x + (w*2 - 1) * halfsize, y + (h*2 - 1) * halfsize)
+ if (h > 1) then
+ h = h - 1
+ for i = 1, h do
+ self.tileset:drawtile_Grid(1, 3, x, y + (i * tilesize) - halfsize)
+ self.tileset:drawtile_Grid(6, 3, x + halfsize, y + (i * tilesize) - halfsize)
+
+ self.tileset:drawtile_Grid(1, 4, x , y + (i * tilesize))
+ self.tileset:drawtile_Grid(6, 4, x + halfsize, y + (i * tilesize))
+ end
+ end
+ -- draw just one stuff
+ else
+ if (h == 1) then
+ self.tileset:drawtile_Grid(1, 1, x , y)
+ self.tileset:drawtile_Grid(1, 6, x , y + (h*2 - 1) * halfsize)
+ self.tileset:drawtile_Grid(6, 1, x + (w*2 - 1) * halfsize, y)
+ self.tileset:drawtile_Grid(6, 6, x + (w*2 - 1) * halfsize, y + (h*2 - 1) * halfsize)
+ w = w - 1
+ for i = 1, w do
+ self.tileset:drawtile_Grid(3, 1, x + (i * tilesize) - halfsize, y)
+ self.tileset:drawtile_Grid(3, 6, x + (i * tilesize) - halfsize, y + halfsize)
+
+ self.tileset:drawtile_Grid(4, 1, x + (i * tilesize) , y )
+ self.tileset:drawtile_Grid(4, 6, x + (i * tilesize), y +halfsize)
+ end
+ else
+ self:drawtile(1, 1, x , y)
+ self:drawtile(1, 3, x , y + (h - 1) * tilesize)
+ self:drawtile(3, 1, x + (w - 1) * tilesize, y)
+ self:drawtile(3, 3, x + (w - 1) * tilesize, y + (h - 1) * tilesize)
+ w = w - 2
+ h = h - 2
+ for i=1, w do
+ self:drawtile(2, 1, i * tilesize, y)
+ self:drawtile(2, 3, i * tilesize, y + (h + 1) * tilesize)
+ for j=1, h do
+ self:drawtile(2, 2, i * tilesize, j * tilesize)
+ end
+ end
+
+ for i=1, h do
+ self:drawtile(1, 2, x , i * tilesize)
+ self:drawtile(3, 2, x + (w + 1) * tilesize, i * tilesize)
+ end
+ end
+ end
+end
+
+return Autotile
diff --git a/bootleg.love/core/modules/assets/background.lua b/bootleg.love/core/modules/assets/background.lua
new file mode 100644
index 0000000..7a0ac56
--- /dev/null
+++ b/bootleg.love/core/modules/assets/background.lua
@@ -0,0 +1,54 @@
+-- assets/sprite :: the background object, which is an image that draw itself
+-- automatically to fill a texture.
+
+--[[
+ Copyright © 2019 Kazhnuz
+
+ 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.
+]]
+
+local Background = Object:extend()
+
+-- INIT FUNCTIONS
+-- Initilizing and configuring option
+
+function Background:new(filepath)
+ self.image = love.graphics.newImage(filepath)
+ self.batch = love.graphics.newSpriteBatch(self.image , 1000 )
+
+ self.width, self.height = self.image:getDimensions()
+ screenwidth, screenheight = core.screen:getDimensions()
+
+ local w = math.floor(screenwidth / self.width) * self.width + 1
+ local h = math.floor(screenheight / self.height) * self.height + 1
+
+ for i=-1, w do
+ for j=-1, h do
+ self.batch:add(i * self.width, j * self.height)
+ j = j + 1
+ end
+ i = i + 1
+ end
+end
+
+function Background:draw(ox, oy)
+ love.graphics.setColor(1, 1, 1)
+ love.graphics.draw(self.batch, ox, oy)
+end
+
+return Background
diff --git a/bootleg.love/core/modules/assets/fonts.lua b/bootleg.love/core/modules/assets/fonts.lua
new file mode 100644
index 0000000..a0f052a
--- /dev/null
+++ b/bootleg.love/core/modules/assets/fonts.lua
@@ -0,0 +1,191 @@
+-- assets/fonts :: the fonts object, which is a simple way to draw text with font.
+-- Some of these functions are quite slow, so it's better to use them to generate
+-- texture instead of text.
+
+--[[
+ Copyright © 2019 Kazhnuz
+
+ 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.
+]]
+
+local Font = Object:extend()
+
+-- INIT FUNCTIONS
+-- Initilizing and configuring option
+
+function Font:new(filename, size)
+ local filename = filename
+ self.font = love.graphics.newFont(filename, size)
+ self.filter = ""
+ self:setColor(1, 1, 1, 1)
+ self:setSpacing(false, 0)
+ self.align = "left"
+end
+
+function Font:set()
+ love.graphics.setFont(self.font)
+end
+
+function Font:setColor(r, g, b, a)
+ self.color = {}
+ self.color.r = r
+ self.color.g = g
+ self.color.b = b
+ self.color.a = a
+end
+
+function Font:setColorFromTable(color)
+ self.color = color
+end
+
+function Font:setSpacing(use_custom, size)
+ self.spacing = {}
+ self.spacing.active = use_custom
+ self.spacing.size = size
+end
+
+function Font:setAlign(align)
+ self.align = align
+end
+
+function Font:setFilter(filter)
+ local filter = filter or ""
+ self.filter = filter
+end
+
+function Font:setLineHeight(height)
+ self.font:setLineHeight(height)
+end
+
+-- INFO FUNCTIONS
+-- get information with these functions
+
+function Font:getHeight()
+ local font = self.font
+ return font:getHeight()
+end
+
+function Font:getWidth(string)
+ local spacing = 0
+ if (self.spacing.active == true) then
+ local charNumber = string.len(string)
+ spacing = self.spacing.size * charNumber
+ end
+ local width = self.font:getWidth(string) + spacing
+ return width
+end
+
+function Font:getColor()
+ return self.color
+end
+
+function Font:getFilter()
+ return self.filter
+end
+
+function Font:haveFilter()
+ return ((self.filter ~= "") and (self.filter ~= nil))
+end
+
+-- DRAW FUNCTIONS
+-- print text using theses functions
+
+function Font:draw(text, x, y, limit, align, r, sx, sy, ox, oy, kx, ky)
+ -- draw text with color and effect applied
+ local limit = limit or 0
+ local align = align or self.align
+
+ self:applyFilter(text, x, y, limit, align, r, sx, sy, ox, oy, kx, ky)
+ love.graphics.setColor(self.color.r, self.color.g, self.color.b, self.color.a)
+ self:printf(text, x, y, limit, align, r, sx, sy, ox, oy, kx, ky)
+end
+
+function Font:print(text, x, y, align, r, sx, sy, ox, oy, kx, ky)
+
+ self:set()
+ if (self.spacing.active) then
+ utils.graphics.printWithSpacing(text, x, y, self.spacing.size, align, r, sx, sy, ox, oy, kx, ky)
+ else
+ utils.graphics.print(text, x, y, align, r, sx, sy, ox, oy, kx, ky)
+ end
+
+end
+
+function Font:printf(text, x, y, limit, align, r, sx, sy, ox, oy, kx, ky)
+ self:set()
+ if (limit > 0) then
+ love.graphics.printf(text, x, y, limit, align, r, sx, sy, ox, oy, kx, ky)
+ else
+ self:print(text, x, y, align, r, sx, sy, ox, oy, kx, ky)
+ end
+end
+
+-- FILTER SYSTEM
+-- With these filter, you can apply custom effects to the fonts
+
+function Font:applyFilter(text, x, y, limit, align, r, sx, sy, ox, oy, kx, ky)
+ if self.filter == "shadow" then
+ self:applyFilterShadow(text, x, y, limit, align, r, sx, sy, ox, oy, kx, ky)
+ elseif self.filter == "border" then
+ self:applyFilterBorder(text, x, y, limit, align, r, sx, sy, ox, oy, kx, ky)
+ elseif self.filter == "doubleborder" then
+ self:applyFilterDoubleBorder(text, x, y, limit, align, r, sx, sy, ox, oy, kx, ky)
+ end
+end
+
+function Font:applyFilterShadow(text, x, y, limit, align, r, sx, sy, ox, oy, kx, ky)
+ love.graphics.setColor(0, 0, 0, 1)
+ self:printf(text, x+1, y+1, limit, align, align, r, sx, sy, ox, oy, kx, ky)
+ utils.graphics.resetColor()
+end
+
+function Font:applyFilterBorder(text, x, y, limit, align, r, sx, sy, ox, oy, kx, ky)
+ love.graphics.setColor(0, 0, 0, 1)
+
+ self:printf(text, x-1, y-1, limit, align, r, sx, sy, ox, oy, kx, ky)
+ self:printf(text, x , y-1, limit, align, r, sx, sy, ox, oy, kx, ky)
+ self:printf(text, x+1, y-1, limit, align, r, sx, sy, ox, oy, kx, ky)
+
+ self:printf(text, x+1, y , limit, align, r, sx, sy, ox, oy, kx, ky)
+ self:printf(text, x-1, y , limit, align, r, sx, sy, ox, oy, kx, ky)
+
+ self:printf(text, x-1, y+1, limit, align, r, sx, sy, ox, oy, kx, ky)
+ self:printf(text, x , y+1, limit, align, r, sx, sy, ox, oy, kx, ky)
+ self:printf(text, x+1, y+1, limit, align, r, sx, sy, ox, oy, kx, ky)
+
+ utils.graphics.resetColor()
+end
+
+function Font:applyFilterDoubleBorder(text, x, y, limit, align, r, sx, sy, ox, oy, kx, ky)
+ love.graphics.setColor(0, 0, 0, 1)
+
+ self:printf(text, x-2, y-2, limit, align, r, sx, sy, ox, oy, kx, ky)
+ self:printf(text, x , y-2, limit, align, r, sx, sy, ox, oy, kx, ky)
+ self:printf(text, x+2, y-2, limit, align, r, sx, sy, ox, oy, kx, ky)
+
+ self:printf(text, x+2, y , limit, align, r, sx, sy, ox, oy, kx, ky)
+ self:printf(text, x-2, y , limit, align, r, sx, sy, ox, oy, kx, ky)
+
+ self:printf(text, x-2, y+2, limit, align, r, sx, sy, ox, oy, kx, ky)
+ self:printf(text, x , y+2, limit, align, r, sx, sy, ox, oy, kx, ky)
+ self:printf(text, x+2, y+2, limit, align, r, sx, sy, ox, oy, kx, ky)
+
+ utils.graphics.resetColor()
+end
+
+return Font
diff --git a/bootleg.love/core/modules/assets/imagefonts.lua b/bootleg.love/core/modules/assets/imagefonts.lua
new file mode 100644
index 0000000..86f318b
--- /dev/null
+++ b/bootleg.love/core/modules/assets/imagefonts.lua
@@ -0,0 +1,43 @@
+-- assets/fonts :: the imagefonts object, which are basically a bitmap version
+-- of the font object.
+
+--[[
+ Copyright © 2019 Kazhnuz
+
+ 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.
+]]
+
+local cwd = (...):gsub('%.imagefonts$', '') .. "."
+
+local Font = require(cwd.. "fonts")
+local ImageFont = Font:extend()
+
+-- INIT FUNCTIONS
+-- Initilizing and configuring option
+
+function ImageFont:new(filename, extraspacing)
+ local data = require(filename)
+ local extraspacing = extraspacing or data.extraspacing or 1
+ self.font = love.graphics.newImageFont(filename .. ".png", data.glyphs, extraspacing)
+ self.filter = ""
+ self:setColor(1, 1, 1, 1)
+ self:setSpacing(false, 0)
+ self.align = "left"
+end
+
+return ImageFont
diff --git a/bootleg.love/core/modules/assets/init.lua b/bootleg.love/core/modules/assets/init.lua
new file mode 100644
index 0000000..8dd7fb4
--- /dev/null
+++ b/bootleg.love/core/modules/assets/init.lua
@@ -0,0 +1,294 @@
+-- modules/assets :: a simple assets manager, aim to put every assets in a simple
+-- serie of table in order to find them easily.
+
+--[[
+ Copyright © 2019 Kazhnuz
+
+ 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.
+]]
+
+local Assets = Object:extend()
+
+local cwd = (...):gsub('%.init$', '') .. "."
+
+local Texture = require(cwd .. "texture")
+
+local Sprite = require(cwd .. "sprites")
+local Font = require(cwd .. "fonts")
+local ImageFont = require(cwd .. "imagefonts")
+
+local Tileset = require(cwd .. "tileset")
+local Autotile = require(cwd .. "autotile")
+local Background = require(cwd .. "background")
+
+-- INIT FUNCTIONS
+-- Initilizing and configuring option
+
+function Assets:new()
+ self:clear()
+
+ self.isActive = true
+end
+
+function Assets:clear()
+ -- TODO: destroy individually each texture/image when assets are cleared
+ self:clearSprites()
+ self:clearSFX()
+ self:clearFonts()
+ self:resetMusic()
+ self:clearBackgrounds()
+ self:clearFonts()
+ self:clearTileset()
+
+ self:clearImages()
+end
+
+function Assets:update(dt)
+ if (self.isActive) then
+ self:animationsUpdate(dt)
+ end
+end
+
+-- IMPORT FUNCTIONS
+-- Easilly import assets
+
+function Assets:batchImport(datafile)
+ local datas = require(datafile)
+
+ for asset_type, assets in pairs(datas) do
+ if (asset_type == "autotiles") then
+ self:importAutotiles(assets)
+ elseif (asset_type == "backgrounds") then
+ self:importBackgrounds(assets)
+ elseif (asset_type == "fonts") then
+ self:importFonts(assets)
+ elseif (asset_type == "imagefonts") then
+ self:importImageFonts(assets)
+ elseif (asset_type == "images") then
+ self:importTextures(assets)
+ elseif (asset_type == "sprites") then
+ self:importSprites(assets)
+ elseif (asset_type == "textures") then
+ self:importTextures(assets)
+ elseif (asset_type == "tilesets") then
+ self:importTilesets(assets)
+ elseif (asset_type == "sfx") then
+ self:importSFX(assets)
+ else
+ core.debug:warning("assets/importer", "unkown asset type " .. asset_type)
+ end
+ end
+end
+
+function Assets:importAutotiles(assets)
+ for i, asset in ipairs(assets) do
+ self:addAutotile(asset[1], asset[2])
+ end
+end
+
+function Assets:importBackgrounds(assets)
+ for i, asset in ipairs(assets) do
+ self:addBackground(asset[1], asset[2])
+ end
+end
+
+function Assets:importFonts(assets)
+ for i, asset in ipairs(assets) do
+ self:addFont(asset[1], asset[2], asset[3])
+ end
+end
+
+function Assets:importImageFonts(assets)
+ for i, asset in ipairs(assets) do
+ self:addImageFont(asset[1], asset[2], asset[3])
+ end
+end
+
+function Assets:importSprites(assets)
+ for i, asset in ipairs(assets) do
+ self:addSprite(asset[1], asset[2])
+ end
+end
+
+function Assets:importTextures(assets)
+ for i, asset in ipairs(assets) do
+ self:addImage(asset[1], asset[2])
+ end
+end
+
+function Assets:importTilesets(assets)
+ for i, asset in ipairs(assets) do
+ self:addTileset(asset[1], asset[2])
+ end
+end
+
+function Assets:importSFX(assets)
+ for i, asset in ipairs(assets) do
+ self:addSFX(asset[1], asset[2])
+ end
+end
+
+-- SFX & MUSICS
+-- Handle sound effects and musics
+
+function Assets:addSFX(name, filepath)
+ self:newSFX(name, filepath)
+end
+
+function Assets:newSFX(name, filepath)
+ self.sfx[name] = love.audio.newSource( filepath, "static" )
+end
+
+function Assets:clearSFX()
+ love.audio.stop( )
+ self.sfx = {}
+end
+
+function Assets:playSFX(filename)
+ if not (self.sfx[filename] == nil) then
+ self.sfx[filename]:stop()
+ self.sfx[filename]:setVolume(core.options.data.audio.sfx / 100)
+ love.audio.play( self.sfx[filename] )
+ end
+end
+
+function Assets:setMusic(filename)
+ if filename ~= nil then
+ love.audio.stop( )
+ self.music = love.audio.newSource(filename, "stream" )
+ self.music:setVolume(core.options.data.audio.music / 100)
+ end
+end
+
+function Assets:silence()
+ love.audio.stop()
+end
+
+function Assets:resetMusic()
+ self.music = nil
+end
+
+function Assets:playMusic()
+ if not (self.music == nil) then
+ love.audio.play(self.music)
+ end
+end
+
+-- IMAGES FUNCTIONS
+-- Create directly texture items
+
+function Assets:addImage(name, filename)
+ self.images[name] = Texture(filename)
+end
+
+function Assets:drawImage(name, x, y, r, sx, sy, ox, oy, kx, ky)
+ self.images[name]:draw(x, y, r, sx, sy, ox, oy, kx, ky)
+end
+
+function Assets:clearImages()
+ self.images = {}
+end
+
+-- BACKGROUNDS FUNCTIONS
+-- Automatic tiling texture
+
+function Assets:addBackground(name, filepath)
+ -- TODO: rework entirely background to work at any size
+ self.backgrounds[name] = Background(filepath)
+end
+
+function Assets:clearBackgrounds()
+ self.backgrounds = {}
+end
+
+-- SPRITES FUNCTIONS
+-- Animated tileset
+
+function Assets:addSprite(name, filepath)
+ self.sprites[name] = Sprite(filepath)
+end
+
+function Assets:animationsUpdate(dt)
+ for i,v in pairs(self.sprites) do
+ v:update(dt)
+ end
+end
+
+function Assets:clearSprites()
+ self.sprites = {}
+end
+
+-- FONTS FUNCTIONS
+-- Handles fonts and imagesfonts
+
+function Assets:addFont(key, filename, size)
+ local font = Font(filename, size)
+ self.fonts[key] = font
+end
+
+function Assets:addImageFont(key, filename, extraspacing)
+ local font = ImageFont(filename, extraspacing)
+ self.fonts[key] = font
+end
+
+function Assets:getFont(filename)
+ return self.fonts[filename]
+end
+
+function Assets:clearFonts()
+ self.fonts = {}
+end
+
+-- TILESET FUNCTIONS
+-- Automatically create quads for a texture
+
+function Assets:addTileset(name, filepath)
+ self.tileset[name] = Tileset(filepath)
+end
+
+function Assets:clearTileset()
+ self.tileset = {}
+end
+
+-- AUTOTILE FUNCTIONS
+-- Automatically draw tiles
+
+function Assets:addAutotile(name, tilesize)
+ self.autotile[name] = Autotile(name, tilesize)
+end
+
+function Assets:clearAutotile()
+ self.autotile = {}
+end
+
+-- ACTIVITY FUNCTIONS
+-- Handle activity
+
+function Assets:setActivity(activity)
+ self.isActive = activity
+end
+
+function Assets:switchActivity()
+ self.isActive = (self.isActive == false)
+end
+
+function Assets:getActivity()
+ return self.isActive
+end
+
+return Assets
diff --git a/bootleg.love/core/modules/assets/sprites.lua b/bootleg.love/core/modules/assets/sprites.lua
new file mode 100644
index 0000000..f33b919
--- /dev/null
+++ b/bootleg.love/core/modules/assets/sprites.lua
@@ -0,0 +1,125 @@
+-- assets/sprite :: the assets object, which is basically a tileset animated by
+-- an animator object. An animator object is always tied to a sprite, but a sprite
+-- can use different animator in order to make several animator share the same sprite
+
+--[[
+ Copyright © 2019 Kazhnuz
+
+ 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.
+]]
+
+local Sprite = Object:extend()
+local cwd = (...):gsub('%.sprites$', '') .. "."
+
+local Animator = require(cwd .. "animator")
+local Tileset = require(cwd .. "tileset")
+
+-- INIT FUNCTIONS
+-- Initilizing and configuring option
+
+function Sprite:new(filepath)
+ self.tileset = Tileset(filepath)
+ self.data = require(filepath)
+
+ self.animator = Animator(self)
+
+ self.customSpeed = 0
+
+ self:changeToDefaultAnimation(true)
+end
+
+function Sprite:update(dt)
+ self.animator:update(dt)
+end
+
+function Sprite:clone()
+ return Animator(self)
+end
+
+function Sprite:setCustomSpeed(customSpeed)
+ self.animator:setCustomSpeed(customSpeed)
+end
+
+function Sprite:changeToDefaultAnimation(restart)
+ self.animator:changeToDefaultAnimation(restart)
+end
+
+function Sprite:changeAnimation(name, restart)
+ self.animator:changeAnimation(name, restart)
+end
+
+-- INFO FUNCTIONS
+-- get information with these functions
+
+function Sprite:getCurrentAnimation()
+ return self.animator:getCurrentAnimation()
+end
+
+function Sprite:animationExist(name)
+ return self.animator:animationExist(name)
+end
+
+function Sprite:getDimensions()
+ return self.tileset:getDimensions()
+end
+
+function Sprite:getFrame()
+ return self.animator:getFrame()
+end
+
+function Sprite:getAnimationDuration(animation)
+ return self.animator:getAnimationDuration(animation)
+end
+
+function Sprite:getRelativeFrame()
+ return self.animator:getRelativeFrame()
+end
+
+-- DRAW FUNCTIONS
+-- Draw sprites using these functions
+
+function Sprite:drawAnimation(x, y, r, sx, sy, ox, oy, kx, ky)
+ self.animator:draw(x, y, r, sx, sy, ox, oy, kx, ky)
+end
+
+function Sprite:drawAnimationMask(x, y, r, sx, sy, ox, oy, kx, ky)
+ self.animator:drawMask(x, y, r, sx, sy, ox, oy, kx, ky)
+end
+
+function Sprite:drawFrame(frame, x, y, r, sx, sy, ox, oy, kx, ky)
+ self.tileset:drawTile(frame, x, y, r, sx, sy, ox, oy, kx, ky)
+end
+
+function Sprite:drawFrameMask(frame, x, y, r, sx, sy, ox, oy, kx, ky)
+ self.tileset:drawTileMask(frame, x, y, r, sx, sy, ox, oy, kx, ky)
+end
+
+function Sprite:drawPart(x, y, w, h, r, sx, sy, ox, oy, kx, ky)
+ local w = math.floor(w)
+ local h = math.floor(h)
+
+ if w >= 0 and h <= 0 then
+ return 0
+ end
+
+ love.graphics.setScissor(x - ox, y - oy, w, h)
+ self:drawAnimation(x, y, r, sx, sy, ox, oy, kx, ky)
+ love.graphics.setScissor( )
+end
+
+return Sprite
diff --git a/bootleg.love/core/modules/assets/texture.lua b/bootleg.love/core/modules/assets/texture.lua
new file mode 100644
index 0000000..30901f3
--- /dev/null
+++ b/bootleg.love/core/modules/assets/texture.lua
@@ -0,0 +1,74 @@
+-- assets/texture :: the texture object, essentially used to be able to draw easily
+-- the mask of the texture (used for stuff like flashing sprite, etc)
+
+--[[
+ Copyright © 2019 Kazhnuz
+
+ 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.
+]]
+
+
+local Texture = Object:extend()
+
+local function getMask(x, y, r, g, b, a)
+ -- template for defining your own pixel mapping function
+ -- perform computations giving the new values for r, g, b and a
+ -- ...
+ return 1, 1, 1, a
+end
+
+-- INIT FUNCTIONS
+-- Initilizing and configuring option
+
+function Texture:new(filename)
+ self.imageData = love.image.newImageData(filename)
+
+ local maskData = self.imageData:clone()
+ maskData:mapPixel( getMask )
+
+ self.image = love.graphics.newImage( self.imageData )
+ self.mask = love.graphics.newImage( maskData )
+end
+
+-- INFO FUNCTIONS
+-- get information with these functions
+
+function Texture:getDimensions()
+ return self.image:getDimensions()
+end
+
+-- DRAW FUNCTIONS
+-- Draw texture using these functions
+
+function Texture:draw(x, y, r, sx, sy, ox, oy, kx, ky)
+ love.graphics.draw(self.image, x, y, r, sx, sy, ox, oy, kx, ky)
+end
+
+function Texture:drawQuad(quad, x, y, r, sx, sy, ox, oy, kx, ky)
+ love.graphics.draw(self.image, quad, x, y, r, sx, sy, ox, oy, kx, ky)
+end
+
+function Texture:drawMask(x, y, r, sx, sy, ox, oy, kx, ky)
+ love.graphics.draw(self.mask, x, y, r, sx, sy, ox, oy, kx, ky)
+end
+
+function Texture:drawMaskQuad(quad, x, y, r, sx, sy, ox, oy, kx, ky)
+ love.graphics.draw(self.mask, quad, x, y, r, sx, sy, ox, oy, kx, ky)
+end
+
+return Texture
diff --git a/bootleg.love/core/modules/assets/tileset.lua b/bootleg.love/core/modules/assets/tileset.lua
new file mode 100644
index 0000000..55d6643
--- /dev/null
+++ b/bootleg.love/core/modules/assets/tileset.lua
@@ -0,0 +1,120 @@
+-- assets/tileset :: tileset are automatic breakage of texture into quads. they
+-- have the adventage of being automatized, reducing the ammount of code needed
+-- to create quads.
+
+-- They have two manners to be draw: with their quad id (in 1D) or by using their
+-- place in the grid, in 2D.
+
+--[[
+ Copyright © 2019 Kazhnuz
+
+ 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.
+]]
+
+local Tileset = Object:extend()
+local cwd = (...):gsub('%.tileset$', '') .. "."
+
+local Texture = require(cwd .. "texture")
+
+-- INIT FUNCTIONS
+-- Initilizing and configuring option
+
+function Tileset:new(filepath)
+ self.texture = Texture(filepath .. ".png")
+
+ local data = require(filepath)
+ self.metadata = data.metadata
+
+ self:createQuads()
+end
+
+function Tileset:createGrid()
+ self.textureWidth, self.textureHeight = self.texture:getDimensions()
+ self.width, self.height = self.metadata.width, self.metadata.height
+ self.gridWidth, self.gridHeight = math.floor(self.textureWidth / self.width),
+ math.floor(self.textureHeight / self.height)
+end
+
+function Tileset:createQuads()
+ self.quads = {}
+
+
+ self:createGrid()
+
+ local quad, n
+
+ n = 1
+ for i=0, (self.gridHeight-1) do
+ for j=0, (self.gridWidth-1) do
+ quad = love.graphics.newQuad(j * self.width, i * self.height, self.width, self.height, self.textureWidth, self.textureHeight)
+ self.quads[n] = quad
+ n = n + 1
+ end
+ end
+
+end
+
+-- INFO FUNCTIONS
+-- get information with these functions
+
+function Tileset:getTileID_Grid(x, y)
+ local n = (y - 1) * self.gridWidth + x
+
+ return n
+end
+
+function Tileset:getTile_Grid(x, y)
+ return self:getTile(self:getTileID_Grid(x, y))
+end
+
+function Tileset:getTile(n)
+ return self.quads[n]
+end
+
+function Tileset:getDimensions()
+ return self.width, self.height
+end
+
+-- DRAW FUNCTIONS
+-- Draw tileset using these functions
+
+function Tileset:drawTile_Grid(i, j, x, y, r, sx, sy, ox, oy, kx, ky)
+ local tileID = self:getTileID_Grid(i, j)
+ local ox = ox or self.metadata.ox
+ local oy = oy or self.metadata.oy
+ self.texture:drawQuad(self.quads[tileID], x, y, r, sx, sy, ox, oy, kx, ky)
+end
+
+function Tileset:drawTile(id, x, y, r, sx, sy, ox, oy, kx, ky)
+ local ox = ox or self.metadata.ox
+ local oy = oy or self.metadata.oy
+ self.texture:drawQuad(self.quads[id], x, y, r, sx, sy, ox, oy, kx, ky)
+end
+
+function Tileset:drawTileMask_Grid(i, j, x, y, r, sx, sy, ox, oy, kx, ky)
+ local tileID = self:getTileID_Grid(i, j)
+ local ox = ox or self.metadata.ox
+ local oy = oy or self.metadata.oy
+ self.texture:drawMaskQuad(self.quads[tileID], x, y, r, sx, sy, ox, oy, kx, ky)
+end
+
+function Tileset:drawTileMask(id, x, y, r, sx, sy, ox, oy, kx, ky)
+ self.texture:drawMaskQuad(self.quads[id], x, y, r, sx, sy, ox, oy, kx, ky)
+end
+
+return Tileset
diff --git a/bootleg.love/core/modules/gamesystem/init.lua b/bootleg.love/core/modules/gamesystem/init.lua
new file mode 100644
index 0000000..c3c7fe8
--- /dev/null
+++ b/bootleg.love/core/modules/gamesystem/init.lua
@@ -0,0 +1,145 @@
+-- game :: The main game subsystem. Basically a big object that handle all the
+-- game-related data like characters, monsters, etc. While the core aim to be
+-- reusable at will, the game is specifically made for the current game.
+
+-- It's also what handle the savedata for games
+
+--[[
+ Copyright © 2019 Kazhnuz
+
+ 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.
+]]
+
+local cwd = (...):gsub('%.init$', '') .. "."
+local cwd2 = (...):gsub('%.gamesystem.init$', '') .. "."
+
+local GameSystem = Object:extend()
+local binser = require(cwd2 .. "libs.binser")
+
+local DEFAULT_SAVENUMBER = 3
+
+function GameSystem:new()
+ self.currentSlot = -1
+
+ self.submodules = {}
+ self.playtime = 0
+ self:register()
+end
+
+function GameSystem:register()
+ core:registerGameSystem(self)
+end
+
+function GameSystem:registerSubmodules(submodule)
+ local name = submodule.name
+ self.submodules[name] = submodule
+end
+
+-- UPDATE FUNCTIONS
+-- Update every submodules
+
+function GameSystem:update(dt)
+ self.playtime = self.playtime + dt
+ for k, submodule in pairs(self.submodules) do
+ submodule:update(dt)
+ end
+end
+
+-- DATA MANAGEMENT FUNCTIONS
+-- Get and set data in the gamesystem object
+
+function GameSystem:setData(data)
+ local data = data
+ self.playtime = data.playtime
+ for k, submodule in pairs(self.submodules) do
+ submodule:setData(data[k])
+ end
+end
+
+function GameSystem:getData()
+ local data = {}
+ data.playtime = self.playtime
+ for k, submodule in pairs(self.submodules) do
+ data[k] = submodule:getData()
+ end
+ return data
+end
+
+-- SAVE MANAGEMENT FUNCTIONS
+-- Get and set data in the gamesystem object
+
+function GameSystem:getSaveNumber()
+ return DEFAULT_SAVENUMBER
+end
+
+function GameSystem:resetSave(saveid)
+ if utils.filesystem.exists("save" .. saveid .. ".save") then
+ love.filesystem.remove( "save" .. saveid .. ".save" )
+ end
+end
+
+function GameSystem:resetAllSaves()
+ for i=1, self:getSaveNumber() do
+ self:resetSave(i)
+ end
+end
+
+function GameSystem:getSavePath(saveid, absolute)
+ local saveDir = ""
+ if (absolute) then
+ saveDir = love.filesystem.getSaveDirectory() .. "/"
+ if not utils.filesystem.exists(saveDir) then
+ love.filesystem.createDirectory( "" )
+ end
+ end
+
+ local filepath = saveDir .. self:getSaveName(saveid)
+
+ return filepath
+end
+
+function GameSystem:getSaveName(saveid)
+ return "save" .. saveid .. ".save"
+end
+
+function GameSystem:saveFileExist(saveid)
+ return utils.filesystem.exists(self:getSaveName(saveid))
+end
+
+function GameSystem:read(saveid)
+ self.currentSlot = saveid or self.currentSlot
+ if (self.currentSlot > 0) then
+ local savepath = self:getSavePath(self.currentSlot, true)
+ if self:saveFileExist(self.currentSlot) then
+ local datas = binser.readFile(savepath)
+
+ self:setData(datas[1])
+ end
+ end
+end
+
+function GameSystem:write()
+ if (self.currentSlot > 0) then
+ local data = self:getData()
+
+ savepath = self:getSavePath(self.currentSlot, true)
+ binser.writeFile(savepath, data)
+ end
+end
+
+return GameSystem
diff --git a/bootleg.love/core/modules/gamesystem/libs/binser.lua b/bootleg.love/core/modules/gamesystem/libs/binser.lua
new file mode 100644
index 0000000..5aa1299
--- /dev/null
+++ b/bootleg.love/core/modules/gamesystem/libs/binser.lua
@@ -0,0 +1,687 @@
+-- binser.lua
+
+--[[
+Copyright (c) 2016 Calvin Rose
+
+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.
+]]
+
+local assert = assert
+local error = error
+local select = select
+local pairs = pairs
+local getmetatable = getmetatable
+local setmetatable = setmetatable
+local tonumber = tonumber
+local type = type
+local loadstring = loadstring or load
+local concat = table.concat
+local char = string.char
+local byte = string.byte
+local format = string.format
+local sub = string.sub
+local dump = string.dump
+local floor = math.floor
+local frexp = math.frexp
+local unpack = unpack or table.unpack
+
+-- Lua 5.3 frexp polyfill
+-- From https://github.com/excessive/cpml/blob/master/modules/utils.lua
+if not frexp then
+ local log, abs, floor = math.log, math.abs, math.floor
+ local log2 = log(2)
+ frexp = function(x)
+ if x == 0 then return 0, 0 end
+ local e = floor(log(abs(x)) / log2 + 1)
+ return x / 2 ^ e, e
+ end
+end
+
+-- NIL = 202
+-- FLOAT = 203
+-- TRUE = 204
+-- FALSE = 205
+-- STRING = 206
+-- TABLE = 207
+-- REFERENCE = 208
+-- CONSTRUCTOR = 209
+-- FUNCTION = 210
+-- RESOURCE = 211
+-- INT64 = 212
+
+local mts = {}
+local ids = {}
+local serializers = {}
+local deserializers = {}
+local resources = {}
+local resources_by_name = {}
+
+local function pack(...)
+ return {...}, select("#", ...)
+end
+
+local function not_array_index(x, len)
+ return type(x) ~= "number" or x < 1 or x > len or x ~= floor(x)
+end
+
+local function type_check(x, tp, name)
+ assert(type(x) == tp,
+ format("Expected parameter %q to be of type %q.", name, tp))
+end
+
+local bigIntSupport = false
+local isInteger
+if math.type then -- Detect Lua 5.3
+ local mtype = math.type
+ bigIntSupport = loadstring[[
+ local char = string.char
+ return function(n)
+ local nn = n < 0 and -(n + 1) or n
+ local b1 = nn // 0x100000000000000
+ local b2 = nn // 0x1000000000000 % 0x100
+ local b3 = nn // 0x10000000000 % 0x100
+ local b4 = nn // 0x100000000 % 0x100
+ local b5 = nn // 0x1000000 % 0x100
+ local b6 = nn // 0x10000 % 0x100
+ local b7 = nn // 0x100 % 0x100
+ local b8 = nn % 0x100
+ if n < 0 then
+ b1, b2, b3, b4 = 0xFF - b1, 0xFF - b2, 0xFF - b3, 0xFF - b4
+ b5, b6, b7, b8 = 0xFF - b5, 0xFF - b6, 0xFF - b7, 0xFF - b8
+ end
+ return char(212, b1, b2, b3, b4, b5, b6, b7, b8)
+ end]]()
+ isInteger = function(x)
+ return mtype(x) == 'integer'
+ end
+else
+ isInteger = function(x)
+ return floor(x) == x
+ end
+end
+
+-- Copyright (C) 2012-2015 Francois Perrad.
+-- number serialization code modified from https://github.com/fperrad/lua-MessagePack
+-- Encode a number as a big-endian ieee-754 double, big-endian signed 64 bit integer, or a small integer
+local function number_to_str(n)
+ if isInteger(n) then -- int
+ if n <= 100 and n >= -27 then -- 1 byte, 7 bits of data
+ return char(n + 27)
+ elseif n <= 8191 and n >= -8192 then -- 2 bytes, 14 bits of data
+ n = n + 8192
+ return char(128 + (floor(n / 0x100) % 0x100), n % 0x100)
+ elseif bigIntSupport then
+ return bigIntSupport(n)
+ end
+ end
+ local sign = 0
+ if n < 0.0 then
+ sign = 0x80
+ n = -n
+ end
+ local m, e = frexp(n) -- mantissa, exponent
+ if m ~= m then
+ return char(203, 0xFF, 0xF8, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00)
+ elseif m == 1/0 then
+ if sign == 0 then
+ return char(203, 0x7F, 0xF0, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00)
+ else
+ return char(203, 0xFF, 0xF0, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00)
+ end
+ end
+ e = e + 0x3FE
+ if e < 1 then -- denormalized numbers
+ m = m * 2 ^ (52 + e)
+ e = 0
+ else
+ m = (m * 2 - 1) * 2 ^ 52
+ end
+ return char(203,
+ sign + floor(e / 0x10),
+ (e % 0x10) * 0x10 + floor(m / 0x1000000000000),
+ floor(m / 0x10000000000) % 0x100,
+ floor(m / 0x100000000) % 0x100,
+ floor(m / 0x1000000) % 0x100,
+ floor(m / 0x10000) % 0x100,
+ floor(m / 0x100) % 0x100,
+ m % 0x100)
+end
+
+-- Copyright (C) 2012-2015 Francois Perrad.
+-- number deserialization code also modified from https://github.com/fperrad/lua-MessagePack
+local function number_from_str(str, index)
+ local b = byte(str, index)
+ if b < 128 then
+ return b - 27, index + 1
+ elseif b < 192 then
+ return byte(str, index + 1) + 0x100 * (b - 128) - 8192, index + 2
+ end
+ local b1, b2, b3, b4, b5, b6, b7, b8 = byte(str, index + 1, index + 8)
+ if b == 212 then
+ local flip = b1 >= 128
+ if flip then -- negative
+ b1, b2, b3, b4 = 0xFF - b1, 0xFF - b2, 0xFF - b3, 0xFF - b4
+ b5, b6, b7, b8 = 0xFF - b5, 0xFF - b6, 0xFF - b7, 0xFF - b8
+ end
+ local n = ((((((b1 * 0x100 + b2) * 0x100 + b3) * 0x100 + b4) * 0x100 + b5) * 0x100 + b6) * 0x100 + b7) * 0x100 + b8
+ if flip then
+ return (-n) - 1, index + 9
+ else
+ return n, index + 9
+ end
+ end
+ local sign = b1 > 0x7F and -1 or 1
+ local e = (b1 % 0x80) * 0x10 + floor(b2 / 0x10)
+ local m = ((((((b2 % 0x10) * 0x100 + b3) * 0x100 + b4) * 0x100 + b5) * 0x100 + b6) * 0x100 + b7) * 0x100 + b8
+ local n
+ if e == 0 then
+ if m == 0 then
+ n = sign * 0.0
+ else
+ n = sign * (m / 2 ^ 52) * 2 ^ -1022
+ end
+ elseif e == 0x7FF then
+ if m == 0 then
+ n = sign * (1/0)
+ else
+ n = 0.0/0.0
+ end
+ else
+ n = sign * (1.0 + m / 2 ^ 52) * 2 ^ (e - 0x3FF)
+ end
+ return n, index + 9
+end
+
+local types = {}
+
+types["nil"] = function(x, visited, accum)
+ accum[#accum + 1] = "\202"
+end
+
+function types.number(x, visited, accum)
+ accum[#accum + 1] = number_to_str(x)
+end
+
+function types.boolean(x, visited, accum)
+ accum[#accum + 1] = x and "\204" or "\205"
+end
+
+function types.string(x, visited, accum)
+ local alen = #accum
+ if visited[x] then
+ accum[alen + 1] = "\208"
+ accum[alen + 2] = number_to_str(visited[x])
+ else
+ visited[x] = visited.next
+ visited.next = visited.next + 1
+ accum[alen + 1] = "\206"
+ accum[alen + 2] = number_to_str(#x)
+ accum[alen + 3] = x
+ end
+end
+
+local function check_custom_type(x, visited, accum)
+ local res = resources[x]
+ if res then
+ accum[#accum + 1] = "\211"
+ types[type(res)](res, visited, accum)
+ return true
+ end
+ local mt = getmetatable(x)
+ local id = mt and ids[mt]
+ if id then
+ if x == visited.temp then
+ error("Infinite loop in constructor.")
+ end
+ visited.temp = x
+ accum[#accum + 1] = "\209"
+ types[type(id)](id, visited, accum)
+ local args, len = pack(serializers[id](x))
+ accum[#accum + 1] = number_to_str(len)
+ for i = 1, len do
+ local arg = args[i]
+ types[type(arg)](arg, visited, accum)
+ end
+ visited[x] = visited.next
+ visited.next = visited.next + 1
+ return true
+ end
+end
+
+function types.userdata(x, visited, accum)
+ if visited[x] then
+ accum[#accum + 1] = "\208"
+ accum[#accum + 1] = number_to_str(visited[x])
+ else
+ if check_custom_type(x, visited, accum) then return end
+ error("Cannot serialize this userdata.")
+ end
+end
+
+function types.table(x, visited, accum)
+ if visited[x] then
+ accum[#accum + 1] = "\208"
+ accum[#accum + 1] = number_to_str(visited[x])
+ else
+ if check_custom_type(x, visited, accum) then return end
+ visited[x] = visited.next
+ visited.next = visited.next + 1
+ local xlen = #x
+ accum[#accum + 1] = "\207"
+ accum[#accum + 1] = number_to_str(xlen)
+ for i = 1, xlen do
+ local v = x[i]
+ types[type(v)](v, visited, accum)
+ end
+ local key_count = 0
+ for k in pairs(x) do
+ if not_array_index(k, xlen) then
+ key_count = key_count + 1
+ end
+ end
+ accum[#accum + 1] = number_to_str(key_count)
+ for k, v in pairs(x) do
+ if not_array_index(k, xlen) then
+ types[type(k)](k, visited, accum)
+ types[type(v)](v, visited, accum)
+ end
+ end
+ end
+end
+
+types["function"] = function(x, visited, accum)
+ if visited[x] then
+ accum[#accum + 1] = "\208"
+ accum[#accum + 1] = number_to_str(visited[x])
+ else
+ if check_custom_type(x, visited, accum) then return end
+ visited[x] = visited.next
+ visited.next = visited.next + 1
+ local str = dump(x)
+ accum[#accum + 1] = "\210"
+ accum[#accum + 1] = number_to_str(#str)
+ accum[#accum + 1] = str
+ end
+end
+
+types.cdata = function(x, visited, accum)
+ if visited[x] then
+ accum[#accum + 1] = "\208"
+ accum[#accum + 1] = number_to_str(visited[x])
+ else
+ if check_custom_type(x, visited, #accum) then return end
+ error("Cannot serialize this cdata.")
+ end
+end
+
+types.thread = function() error("Cannot serialize threads.") end
+
+local function deserialize_value(str, index, visited)
+ local t = byte(str, index)
+ if not t then return end
+ if t < 128 then
+ return t - 27, index + 1
+ elseif t < 192 then
+ return byte(str, index + 1) + 0x100 * (t - 128) - 8192, index + 2
+ elseif t == 202 then
+ return nil, index + 1
+ elseif t == 203 then
+ return number_from_str(str, index)
+ elseif t == 204 then
+ return true, index + 1
+ elseif t == 205 then
+ return false, index + 1
+ elseif t == 206 then
+ local length, dataindex = deserialize_value(str, index + 1, visited)
+ local nextindex = dataindex + length
+ local substr = sub(str, dataindex, nextindex - 1)
+ visited[#visited + 1] = substr
+ return substr, nextindex
+ elseif t == 207 then
+ local count, nextindex = number_from_str(str, index + 1)
+ local ret = {}
+ visited[#visited + 1] = ret
+ for i = 1, count do
+ ret[i], nextindex = deserialize_value(str, nextindex, visited)
+ end
+ count, nextindex = number_from_str(str, nextindex)
+ for i = 1, count do
+ local k, v
+ k, nextindex = deserialize_value(str, nextindex, visited)
+ v, nextindex = deserialize_value(str, nextindex, visited)
+ ret[k] = v
+ end
+ return ret, nextindex
+ elseif t == 208 then
+ local ref, nextindex = number_from_str(str, index + 1)
+ return visited[ref], nextindex
+ elseif t == 209 then
+ local count
+ local name, nextindex = deserialize_value(str, index + 1, visited)
+ count, nextindex = number_from_str(str, nextindex)
+ local args = {}
+ for i = 1, count do
+ args[i], nextindex = deserialize_value(str, nextindex, visited)
+ end
+ local ret = deserializers[name](unpack(args))
+ visited[#visited + 1] = ret
+ return ret, nextindex
+ elseif t == 210 then
+ local length, dataindex = deserialize_value(str, index + 1, visited)
+ local nextindex = dataindex + length
+ local ret = loadstring(sub(str, dataindex, nextindex - 1))
+ visited[#visited + 1] = ret
+ return ret, nextindex
+ elseif t == 211 then
+ local res, nextindex = deserialize_value(str, index + 1, visited)
+ return resources_by_name[res], nextindex
+ elseif t == 212 then
+ return number_from_str(str, index)
+ else
+ error("Could not deserialize type byte " .. t .. ".")
+ end
+end
+
+local function serialize(...)
+ local visited = {next = 1}
+ local accum = {}
+ for i = 1, select("#", ...) do
+ local x = select(i, ...)
+ types[type(x)](x, visited, accum)
+ end
+ return concat(accum)
+end
+
+local function make_file_writer(file)
+ return setmetatable({}, {
+ __newindex = function(_, _, v)
+ file:write(v)
+ end
+ })
+end
+
+local function serialize_to_file(path, mode, ...)
+ local file, err = io.open(path, mode)
+ assert(file, err)
+ local visited = {next = 1}
+ local accum = make_file_writer(file)
+ for i = 1, select("#", ...) do
+ local x = select(i, ...)
+ types[type(x)](x, visited, accum)
+ end
+ -- flush the writer
+ file:flush()
+ file:close()
+end
+
+local function writeFile(path, ...)
+ return serialize_to_file(path, "wb", ...)
+end
+
+local function appendFile(path, ...)
+ return serialize_to_file(path, "ab", ...)
+end
+
+local function deserialize(str, index)
+ assert(type(str) == "string", "Expected string to deserialize.")
+ local vals = {}
+ index = index or 1
+ local visited = {}
+ local len = 0
+ local val
+ while index do
+ val, index = deserialize_value(str, index, visited)
+ if index then
+ len = len + 1
+ vals[len] = val
+ end
+ end
+ return vals, len
+end
+
+local function deserializeN(str, n, index)
+ assert(type(str) == "string", "Expected string to deserialize.")
+ n = n or 1
+ assert(type(n) == "number", "Expected a number for parameter n.")
+ assert(n > 0 and floor(n) == n, "N must be a poitive integer.")
+ local vals = {}
+ index = index or 1
+ local visited = {}
+ local len = 0
+ local val
+ while index and len < n do
+ val, index = deserialize_value(str, index, visited)
+ if index then
+ len = len + 1
+ vals[len] = val
+ end
+ end
+ vals[len + 1] = index
+ return unpack(vals, 1, n + 1)
+end
+
+local function readFile(path)
+ local file, err = io.open(path, "rb")
+ assert(file, err)
+ local str = file:read("*all")
+ file:close()
+ return deserialize(str)
+end
+
+local function default_deserialize(metatable)
+ return function(...)
+ local ret = {}
+ for i = 1, select("#", ...), 2 do
+ ret[select(i, ...)] = select(i + 1, ...)
+ end
+ return setmetatable(ret, metatable)
+ end
+end
+
+local function default_serialize(x)
+ assert(type(x) == "table",
+ "Default serialization for custom types only works for tables.")
+ local args = {}
+ local len = 0
+ for k, v in pairs(x) do
+ args[len + 1], args[len + 2] = k, v
+ len = len + 2
+ end
+ return unpack(args, 1, len)
+end
+
+-- Templating
+
+local function normalize_template(template)
+ local ret = {}
+ for i = 1, #template do
+ ret[i] = template[i]
+ end
+ local non_array_part = {}
+ -- The non-array part of the template (nested templates) have to be deterministic, so they are sorted.
+ -- This means that inherently non deterministicly sortable keys (tables, functions) should NOT be used
+ -- in templates. Looking for way around this.
+ for k in pairs(template) do
+ if not_array_index(k, #template) then
+ non_array_part[#non_array_part + 1] = k
+ end
+ end
+ table.sort(non_array_part)
+ for i = 1, #non_array_part do
+ local name = non_array_part[i]
+ ret[#ret + 1] = {name, normalize_template(template[name])}
+ end
+ return ret
+end
+
+local function templatepart_serialize(part, argaccum, x, len)
+ local extras = {}
+ local extracount = 0
+ for k, v in pairs(x) do
+ extras[k] = v
+ extracount = extracount + 1
+ end
+ for i = 1, #part do
+ extracount = extracount - 1
+ if type(part[i]) == "table" then
+ extras[part[i][1]] = nil
+ len = templatepart_serialize(part[i][2], argaccum, x[part[i][1]], len)
+ else
+ extras[part[i]] = nil
+ len = len + 1
+ argaccum[len] = x[part[i]]
+ end
+ end
+ if extracount > 0 then
+ argaccum[len + 1] = extras
+ else
+ argaccum[len + 1] = nil
+ end
+ return len + 1
+end
+
+local function templatepart_deserialize(ret, part, values, vindex)
+ for i = 1, #part do
+ local name = part[i]
+ if type(name) == "table" then
+ local newret = {}
+ ret[name[1]] = newret
+ vindex = templatepart_deserialize(newret, name[2], values, vindex)
+ else
+ ret[name] = values[vindex]
+ vindex = vindex + 1
+ end
+ end
+ local extras = values[vindex]
+ if extras then
+ for k, v in pairs(extras) do
+ ret[k] = v
+ end
+ end
+ return vindex + 1
+end
+
+local function template_serializer_and_deserializer(metatable, template)
+ return function(x)
+ argaccum = {}
+ local len = templatepart_serialize(template, argaccum, x, 0)
+ return unpack(argaccum, 1, len)
+ end, function(...)
+ local ret = {}
+ local len = select("#", ...)
+ local args = {...}
+ templatepart_deserialize(ret, template, args, 1)
+ return setmetatable(ret, metatable)
+ end
+end
+
+local function register(metatable, name, serialize, deserialize)
+ name = name or metatable.name
+ serialize = serialize or metatable._serialize
+ deserialize = deserialize or metatable._deserialize
+ if not serialize then
+ if metatable._template then
+ local t = normalize_template(metatable._template)
+ serialize, deserialize = template_serializer_and_deserializer(metatable, t)
+ elseif not deserialize then
+ serialize = default_serialize
+ deserialize = default_deserialize(metatable)
+ else
+ serialize = metatable
+ end
+ end
+ type_check(metatable, "table", "metatable")
+ type_check(name, "string", "name")
+ type_check(serialize, "function", "serialize")
+ type_check(deserialize, "function", "deserialize")
+ assert(not ids[metatable], "Metatable already registered.")
+ assert(not mts[name], ("Name %q already registered."):format(name))
+ mts[name] = metatable
+ ids[metatable] = name
+ serializers[name] = serialize
+ deserializers[name] = deserialize
+ return metatable
+end
+
+local function unregister(item)
+ local name, metatable
+ if type(item) == "string" then -- assume name
+ name, metatable = item, mts[item]
+ else -- assume metatable
+ name, metatable = ids[item], item
+ end
+ type_check(name, "string", "name")
+ type_check(metatable, "table", "metatable")
+ mts[name] = nil
+ ids[metatable] = nil
+ serializers[name] = nil
+ deserializers[name] = nil
+ return metatable
+end
+
+local function registerClass(class, name)
+ name = name or class.name
+ if class.__instanceDict then -- middleclass
+ register(class.__instanceDict, name)
+ else -- assume 30log or similar library
+ register(class, name)
+ end
+ return class
+end
+
+local function registerResource(resource, name)
+ type_check(name, "string", "name")
+ assert(not resources[resource],
+ "Resource already registered.")
+ assert(not resources_by_name[name],
+ format("Resource %q already exists.", name))
+ resources_by_name[name] = resource
+ resources[resource] = name
+ return resource
+end
+
+local function unregisterResource(name)
+ type_check(name, "string", "name")
+ assert(resources_by_name[name], format("Resource %q does not exist.", name))
+ local resource = resources_by_name[name]
+ resources_by_name[name] = nil
+ resources[resource] = nil
+ return resource
+end
+
+return {
+ -- aliases
+ s = serialize,
+ d = deserialize,
+ dn = deserializeN,
+ r = readFile,
+ w = writeFile,
+ a = appendFile,
+
+ serialize = serialize,
+ deserialize = deserialize,
+ deserializeN = deserializeN,
+ readFile = readFile,
+ writeFile = writeFile,
+ appendFile = appendFile,
+ register = register,
+ unregister = unregister,
+ registerResource = registerResource,
+ unregisterResource = unregisterResource,
+ registerClass = registerClass
+}
diff --git a/bootleg.love/core/modules/gamesystem/submodule.lua b/bootleg.love/core/modules/gamesystem/submodule.lua
new file mode 100644
index 0000000..5ea5486
--- /dev/null
+++ b/bootleg.love/core/modules/gamesystem/submodule.lua
@@ -0,0 +1,23 @@
+local SubModule = Object:extend()
+
+function SubModule:new(game, name)
+ self.name = name or error("SUBMODULE must have a name")
+ self.game = game
+ self.data = {}
+
+ self:register()
+end
+
+function SubModule:register()
+ self.game:registerSubmodules(self)
+end
+
+function SubModule:getData()
+ return self.data
+end
+
+function SubModule:setData(data)
+ self.data = data
+end
+
+return SubModule
diff --git a/bootleg.love/core/modules/init.lua b/bootleg.love/core/modules/init.lua
new file mode 100644
index 0000000..da2199a
--- /dev/null
+++ b/bootleg.love/core/modules/init.lua
@@ -0,0 +1,33 @@
+-- modules : different modules that are usable as part of gamecore
+
+--[[
+ Copyright © 2019 Kazhnuz
+
+ 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.
+]]
+
+local modules = {}
+
+local cwd = (...):gsub('%.init$', '') .. "."
+
+modules.Assets = require(cwd .. "assets")
+modules.GameSystem = require(cwd .. "gamesystem")
+modules.MenuSystem = require(cwd .. "menusystem")
+modules.Timers = require(cwd .. "timers")
+
+return modules
diff --git a/bootleg.love/core/modules/menusystem/flowbox.lua b/bootleg.love/core/modules/menusystem/flowbox.lua
new file mode 100644
index 0000000..c3d9426
--- /dev/null
+++ b/bootleg.love/core/modules/menusystem/flowbox.lua
@@ -0,0 +1,230 @@
+-- flowbox :: flexible box menu, that handle in grid the widgets
+
+--[[
+ Copyright © 2019 Kazhnuz
+
+ 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.
+]]
+
+local cwd = (...):gsub('%.flowbox$', '') .. "."
+
+local Menu = require(cwd .. "parent")
+local FlowBox = Menu:extend()
+
+local menuutils = require(cwd .. "widgets.utils")
+
+-- INIT FUNCTIONS
+-- Initialize and configure the flowbox
+
+function FlowBox:new(menusystem, name, x, y, w, h, slots_hor, slots_vert)
+ self.view = {}
+ self.view.slotNumber = slots_hor * slots_vert
+ self.view.lineNumber = slots_vert
+ self.view.colNumber = slots_hor
+ self.view.firstSlot = 1
+ FlowBox.super.new(self, menusystem, name, x, y, w, h)
+ self.widget.h = math.floor( self.h / slots_vert )
+ self.widget.w = math.floor( self.w / slots_hor )
+ self.h = slots_vert * self.widget.h -- On fait en sorte que la hauteur
+ self.w = slots_hor * self.widget.w -- et la largeur
+ -- soit un multiple du nombre de slot et de leur dimensions
+end
+
+-- UPDATE FUNCTIONS
+-- Update the menu and its view
+
+function FlowBox:updateWidgetSize()
+ self.widget.h = math.floor( self.h / self.view.lineNumber )
+ self.widget.w = math.floor( self.w / self.view.colNumber )
+end
+
+function FlowBox:update(dt)
+ self:updateView()
+end
+
+function FlowBox:updateView()
+ local col, line = self:getCoord(self.widget.selected)
+ local begincol, beginline = self:getCoord(self.view.firstSlot)
+
+ if line < beginline then
+ beginline = line
+ end
+
+ if line > beginline + self.view.lineNumber - 1 then
+ beginline = line - self.view.lineNumber + 1
+ end
+
+ if beginline < 0 then
+ beginline = 0
+ end
+
+ self.view.firstSlot = beginline * self.view.colNumber + 1
+end
+
+-- INFO FUNCTIONS
+-- Get informations
+
+function FlowBox:getCoord(id_selected)
+ id_selected = id_selected - 1 -- On simplifie les calcul en prenant 0 comme départ
+ local line, col
+ line = math.floor(id_selected / self.view.colNumber)
+ col = id_selected - (line * self.view.colNumber)
+ return col, line
+end
+
+-- CURSOR FUNCTIONS
+-- Handle the cursor in a 2D menu
+
+function FlowBox:moveCursor(new_col, new_line)
+ local col, line = self:getCoord(self.widget.selected)
+ local lastcol, lastline = self:getCoord(#self.widget.list)
+
+
+ if new_line < 0 then
+ new_line = lastline
+ end
+
+ if new_line > lastline then
+ new_line = 0
+ end
+
+ if (new_line == lastline) then
+ if new_col < 0 then
+ new_col = lastcol
+ end
+
+ if new_col > lastcol then
+ if (line == lastline) then
+ new_col = 0
+ else
+ new_col = lastcol
+ end
+ end
+ else
+ if new_col < 0 then
+ new_col = self.view.colNumber - 1
+ end
+
+ if new_col == self.view.colNumber then
+ new_col = 0
+ end
+ end
+
+ self.widget.selected = (new_line * self.view.colNumber) + new_col + 1
+end
+
+-- KEYS FUNCTIONS
+-- Handle the keyboard/controller inputs
+
+function FlowBox:keyreleased(key, code)
+ local col, line = self:getCoord(self.widget.selected)
+ if key == 'left' then
+ self:moveCursor(col - 1, line)
+ end
+
+ if key == 'right' then
+ self:moveCursor(col + 1, line)
+ end
+
+ if key == 'up' then
+ self:moveCursor(col, line - 1)
+ end
+
+ if key == 'down' then
+ self:moveCursor(col, line + 1)
+ end
+
+ if key == "A" then
+ if (self.widget.selected >= 1 and self.widget.selected <= #self.widget.list) then
+ self.widget.list[self.widget.selected]:action("key")
+ end
+ end
+end
+
+-- MOUSE FUNCTIONS
+-- Handle the mouse/touch pointer
+
+function FlowBox:mousemoved(x, y)
+ local col, line = self:getCoord(self.widget.selected)
+ local begincol, beginline = self:getCoord(self.view.firstSlot)
+ local newcol, newline, widget_selected
+
+ newline = beginline + math.floor(y / self.widget.h)
+ newcol = math.floor(x / self.widget.w)
+ widget_selected = (newline * self.view.colNumber) + newcol + 1
+
+ if widget_selected >= 1 and widget_selected <= #self.widget.list then
+ self.widget.selected = widget_selected
+ self:getFocus()
+ end
+end
+
+function FlowBox:mousepressed(x, y, button, isTouch)
+ local col, line = self:getCoord(self.widget.selected)
+ local begincol, beginline = self:getCoord(self.view.firstSlot)
+ local newline, newcol, widget_selected
+
+ newline = beginline + math.floor(y / self.widget.h)
+ newcol = math.floor(x / self.widget.w)
+ widget_selected = (newline * self.view.colNumber) + newcol + 1
+
+ if widget_selected >= 1 and widget_selected <= #self.widget.list then
+ self.widget.selected = widget_selected
+ self:getFocus()
+ self.widget.list[self.widget.selected]:action("pointer")
+ end
+
+end
+
+-- DRAW FUNCTIONS
+-- Draw the menu and its content
+
+function FlowBox:draw()
+ self:updateView()
+ local widgety = self.y
+ local widgetx = self.x
+ for i,v in ipairs(self.widget.list) do
+ if (i >= self.view.firstSlot) and (i < self.view.firstSlot + self.view.slotNumber) then
+ v:draw(widgetx, widgety, self.widget.w, self.widget.h)
+ if self.widget.selected == i and self:haveFocus() == true then
+ v:drawSelected(widgetx, widgety, self.widget.w, self.widget.h)
+ else
+ v:draw(widgetx, widgety, self.widget.w, self.widget.h)
+ end
+ widgetx = widgetx + self.widget.w
+ if widgetx == (self.x + self.w) then
+ widgetx = self.x
+ widgety = widgety + self.widget.h
+ end
+ end
+ end
+end
+
+function FlowBox:drawCursor()
+ self:updateView()
+ local begincol, beginline = self:getCoord(self.view.firstSlot)
+ if (self.widget.selected >= 1 and self.widget.selected <= #self.widget.list) then
+ local w, h = self:getWidgetSize()
+ local col, line = self:getCoord(self.widget.selected)
+ local x = (col) * h
+ local y = (line - beginline) * h
+ menuutils.drawCursor(self.x + x, self.y + y, w, h)
+ end
+end
+
+return FlowBox
diff --git a/bootleg.love/core/modules/menusystem/grid.lua b/bootleg.love/core/modules/menusystem/grid.lua
new file mode 100644
index 0000000..e1fd8ed
--- /dev/null
+++ b/bootleg.love/core/modules/menusystem/grid.lua
@@ -0,0 +1,276 @@
+-- grid :: a menu with arbitrary widget placement and size on a grid.
+
+--[[
+ Copyright © 2019 Kazhnuz
+
+ 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.
+]]
+
+local cwd = (...):gsub('%.grid$', '') .. "."
+
+local Menu = require(cwd .. "parent")
+local GridBox = Menu:extend()
+
+local menuutils = require(cwd .. "widgets.utils")
+
+-- INIT FUNCTIONS
+-- Initialize and configure the menu
+
+function GridBox:new(menusystem, name, x, y, w, h, colNumber, lineNumber)
+ self.view = {}
+ self.view.slotNumber = colNumber * lineNumber
+ self.view.colNumber = colNumber
+ self.view.lineNumber = lineNumber
+ self.view.firstSlot = 1
+ GridBox.super.new(self, menusystem, name, x, y, w, h)
+ self.h = lineNumber * self.widget.h -- On fait en sorte que la hauteur
+ self.w = colNumber * self.widget.w -- et la largeur
+ -- soit un multiple du nombre de slot et de leur dimensions
+ self.cursor = {}
+ self.cursor.x = 0
+ self.cursor.y = 0
+
+ -- La gridbox possède la particularité de pouvoir fusioner des slots, on fait
+ -- donc une liste de slots disponibles, qui serviront par la suite.
+ self.slots = {}
+end
+
+function GridBox:addSlot(widgetID, x, y, w, h)
+ local slot = {}
+ slot.x = x
+ slot.y = y
+ slot.w = w
+ slot.h = h
+ slot.widgetID = widgetID
+
+ table.insert(self.slots, slot)
+end
+
+function GridBox:updateWidgetSize()
+ self.widget.h = math.floor( self.h / self.view.lineNumber )
+ self.widget.w = math.floor( self.w / self.view.colNumber )
+end
+
+-- INFO FUNCTIONS
+-- Get the info of the widgets
+
+function GridBox:getWidgetSize(id)
+ local slot = self:getWidgetSlot(id)
+ if slot == 0 then
+ return 1, 1
+ else
+ return self.widget.w * self.slots[slot].w, self.widget.h * self.slots[slot].h
+ end
+end
+
+function GridBox:getSlotHitbox(slot)
+ local x, y, w, h
+ x = self.slots[slot].x * self.widget.w
+ y = self.slots[slot].y * self.widget.h
+ w = self.slots[slot].w * self.widget.w
+ h = self.slots[slot].h * self.widget.h
+
+ return x, y, w, h
+end
+
+function GridBox:getSlotCenter(slot)
+ local x, y, w, h = self:getSlotHitbox(slot)
+
+ return x + (w/2), y + (h/2)
+end
+
+function GridBox:getWidgetID(slot)
+ local widgetID
+ if self.slots[slot] ~= nil then
+ widgetID = self.slots[slot].widgetID
+ else
+ widgetID = 0
+ end
+ return widgetID
+end
+
+function GridBox:haveWidget(slot)
+ local id = self:getWidgetID(slot)
+ return self.widget.list[id] ~= nil
+end
+
+function GridBox:getWidgetSlot(widgetID)
+ local slot = 0
+ for i,v in ipairs(self.slots) do
+ if (self.slots[i].widgetID == widgetID) then
+ slot = i
+ end
+ end
+
+ return slot
+end
+
+function GridBox:getWidgetAtPoint(x, y)
+ local x = x or 0
+ local y = y or 0
+ local widgetID = nil
+
+ for i,v in ipairs(self.slots) do
+ local xx, yy, ww, hh = self:getSlotHitbox(i)
+ if (x >= xx) and (y >= yy) and (x < xx + ww) and (y < yy + hh) then
+ widgetID = v.widgetID
+ end
+ end
+
+ return widgetID
+end
+
+-- UPDATE FUNCTIONS
+-- Update the Grid and its view
+
+function GridBox:update(dt)
+ self.view.firstSlot = 1
+end
+
+-- KEYS FUNCTIONS
+-- Handle the keyboard/manette functions
+
+function GridBox:keyreleased(key, code)
+ slotID = self:getWidgetSlot(self.widget.selected)
+ local col, line = self.cursor.x, self.cursor.y
+ if key == 'left' then
+ self:moveCol(-1)
+ end
+
+ if key == 'right' then
+ self:moveCol(1)
+ end
+
+ if key == 'up' then
+ self:moveLine(-1)
+ end
+
+ if key == 'down' then
+ self:moveLine(1)
+ end
+
+ if key == "A" and self.widget.selected <= #self.widget.list then
+ self.widget.list[self.widget.selected]:action("key")
+ end
+end
+
+function GridBox:moveCol(direction)
+ local orig_x, orig_y = self:getSlotCenter(self.widget.selected)
+ local distance = self.w -- on met directement à la distance max possible le système
+ local nearastWidget = 0
+ for i,v in ipairs(self.slots) do
+ local xx, yy = self:getSlotCenter(i)
+ -- On commence par vérifier si le slot est bien positionné par rapport au
+ -- widget de base
+ if utils.math.sign(xx - orig_x) == direction then
+ if utils.math.pointDistance(orig_x, orig_y, xx, yy) < distance then
+ distance = utils.math.pointDistance(orig_x, orig_y, xx, yy)
+ nearestWidget = v.widgetID
+ end
+ end
+ end
+
+ if nearestWidget ~= 0 then
+ self.widget.selected = nearestWidget
+ end
+end
+
+function GridBox:moveLine(direction)
+ local orig_x, orig_y = self:getSlotCenter(self.widget.selected)
+ local distance = self.h -- on met directement à la distance max possible le système
+ local nearastWidget = 0
+ for i,v in ipairs(self.slots) do
+ local xx, yy = self:getSlotCenter(i)
+ -- On commence par vérifier si le slot est bien positionné par rapport au
+ -- widget de base
+ if utils.math.sign(yy - orig_y) == direction then
+ if utils.math.pointDistance(orig_x, orig_y, xx, yy) < distance then
+ distance = utils.math.pointDistance(orig_x, orig_y, xx, yy)
+ nearestWidget = v.widgetID
+ end
+ end
+ end
+
+ if nearestWidget ~= 0 then
+ self.widget.selected = nearestWidget
+ end
+end
+
+-- MOUSE FUNCTIONS
+-- Handle the mouse and activate the widgets with it
+
+function GridBox:mousemoved(x, y)
+ local widgetID = self:getWidgetAtPoint(x, y)
+
+ if widgetID ~= nil then
+ self.widget.selected = widgetID
+ self:getFocus()
+ end
+
+ if self.widget.selected < 1 then
+ self.widget.selected = 1
+ end
+ if self.widget.selected > #self.widget.list then
+ self.widget.selected = #self.widget.list
+ end
+
+end
+
+function GridBox:mousepressed(x, y, button, isTouch)
+ local widgetID = self:getWidgetAtPoint(x, y)
+
+ if widgetID ~= nil then
+ self.widget.selected = widgetID
+ self:getFocus()
+
+ if #self.widget.list > 0 and self.widget.selected > 1 and self.widget.selected <= #self.widget.list then
+ self.widget.list[self.widget.selected]:action("pointer")
+ end
+ end
+end
+
+-- DRAW FUNCTIONS
+-- Draw the menu and its content
+
+function GridBox:draw()
+
+ for i,v in ipairs(self.slots) do
+ if self:haveWidget(i) then
+ local widgetx = self.x + (v.x * self.widget.w)
+ local widgety = self.y + (v.y * self.widget.h)
+ if self.widget.selected == v.widgetID and self:haveFocus() == true then
+ self.widget.list[v.widgetID]:drawSelected(widgetx, widgety)
+ else
+ self.widget.list[v.widgetID]:draw(widgetx, widgety)
+ end
+ end
+ end
+end
+
+function GridBox:drawCursor()
+ self:updateView()
+ if (self.widget.selected >= 1 and self.widget.selected <= #self.widget.list) then
+ local slot = self:getWidgetSlot(self.widget.selected)
+ local w, h = self:getWidgetSize(slot)
+ local x = self.slots[slot].x * self.widget.w
+ local y = self.slots[slot].y * self.widget.h
+ menuutils.drawCursor(self.x + x, self.y + y, w, h)
+ end
+end
+
+return GridBox
diff --git a/bootleg.love/core/modules/menusystem/hlistbox.lua b/bootleg.love/core/modules/menusystem/hlistbox.lua
new file mode 100644
index 0000000..067de32
--- /dev/null
+++ b/bootleg.love/core/modules/menusystem/hlistbox.lua
@@ -0,0 +1,148 @@
+-- hlistbox : add an horizontal list of widgets.
+
+--[[
+ Copyright © 2019 Kazhnuz
+
+ 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.
+]]
+
+local cwd = (...):gsub('%.hlistbox$', '') .. "."
+
+local Menu = require(cwd .. "parent")
+local HListBox = Menu:extend()
+
+local menuutils = require(cwd .. "widgets.utils")
+
+-- INIT FUNCTIONS
+-- Initialize and configure functions.
+
+function HListBox:new(menusystem, name, x, y, w, h, slotNumber)
+ self.view = {}
+ self.view.slotNumber = slotNumber
+ self.view.firstSlot = 1
+ HListBox.super.new(self, menusystem, name, x, y, w, h)
+ self.w = slotNumber * self.widget.w -- On fait en sorte que la hauteur
+ -- soit un multiple du nombre de slot et de leur hauteur
+end
+
+-- UPDATE FUNCTIONS
+-- Update the menu every step.
+
+function HListBox:updateWidgetSize()
+ self.widget.h = self.h
+ self.widget.w = math.floor( self.w / self.view.slotNumber )
+end
+
+function HListBox:update(dt)
+ self:updateView()
+end
+
+function HListBox:updateView()
+ if self.widget.selected < self.view.firstSlot then
+ self.view.firstSlot = self.widget.selected
+ end
+ if self.widget.selected > self.view.firstSlot + self.view.slotNumber - 1 then
+ self.view.firstSlot = self.widget.selected - self.view.slotNumber + 1
+ end
+
+ if self.view.firstSlot < 1 then
+ self.view.firstSlot = 1
+ end
+end
+
+-- KEYBOARD FUNCTIONS
+-- Handle key check.
+
+function HListBox:keyreleased(key, code)
+
+ if key == 'left' then
+ self:moveCursor(self.widget.selected - 1)
+ end
+
+ if key == 'right' then
+ self:moveCursor(self.widget.selected + 1)
+ end
+
+ if key == "A" then
+ if (self.widget.selected >= 1 and self.widget.selected <= #self.widget.list) then
+ self.widget.list[self.widget.selected]:action("key")
+ end
+ end
+
+ if key == "B" then
+ if (self.widget.cancel >= 1 and self.widget.cancel <= #self.widget.list) then
+ self.widget.list[self.widget.cancel]:action("key")
+ end
+ end
+
+end
+
+-- MOUSE FUNCTIONS
+-- Click and stuff like that.
+
+function HListBox:mousemoved(x, y)
+ local widget_selected = self.view.firstSlot + math.floor(x / self.widget.w)
+
+ if widget_selected >= 1 and widget_selected <= #self.widget.list then
+ self.widget.selected = widget_selected
+ self:getFocus()
+ end
+end
+
+function HListBox:mousepressed(x, y, button, isTouch)
+ local widget_selected = self.view.firstSlot + math.floor(x / self.widget.w)
+
+ if widget_selected >= 1 and widget_selected <= #self.widget.list then
+ self.widget.selected = widget_selected
+ self:getFocus()
+ if #self.widget.list > 0 then
+ self.widget.list[self.widget.selected]:action("pointer")
+ end
+ end
+
+end
+
+-- DRAW FUNCTIONS
+-- Draw the menu and its content
+
+function HListBox:draw()
+ self:updateView()
+ local widgetx = self.x
+ for i,v in ipairs(self.widget.list) do
+ if (i >= self.view.firstSlot) and (i < self.view.firstSlot + self.view.slotNumber) then
+ v:draw(widgetx, self.y, self.widget.w, self.h)
+ if self.widget.selected == i and self:haveFocus() == true then
+ v:drawSelected(widgetx, self.y, self.widget.w, self.h)
+ else
+ v:draw(widgetx, self.y, self.widget.w, self.h)
+ end
+ widgetx = widgetx + self.widget.w
+ end
+ end
+end
+
+function HListBox:drawCursor()
+ self:updateView()
+ if (self.widget.selected >= 1 and self.widget.selected <= #self.widget.list) then
+ local w, h = self:getWidgetSize()
+ local x = (self.widget.selected - self.view.firstSlot) * w
+ menuutils.drawCursor(self.x + x,self.y, w, h)
+ end
+end
+
+return HListBox
diff --git a/bootleg.love/core/modules/menusystem/init.lua b/bootleg.love/core/modules/menusystem/init.lua
new file mode 100644
index 0000000..780796d
--- /dev/null
+++ b/bootleg.love/core/modules/menusystem/init.lua
@@ -0,0 +1,285 @@
+-- menusystem :: the controller of the menu system. This object handle the
+-- different menu objects
+
+--[[
+ Copyright © 2019 Kazhnuz
+
+ 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.
+]]
+
+local cwd = (...):gsub('%.init$', '') .. "."
+
+local MenuSystem = Object:extend()
+
+-- Load the differents menu object to get an easy access
+MenuSystem.Parent = require(cwd .. "parent")
+MenuSystem.ListBox = require(cwd .. "listbox")
+MenuSystem.FlowBox = require(cwd .. "flowbox")
+MenuSystem.Grid = require(cwd .. "grid")
+
+-- load widgets object
+MenuSystem.Widget = require(cwd .. "widgets")
+
+-- INIT FUNCTIONS
+-- Initialize and configure the menu controller
+
+function MenuSystem:new(scene)
+ self.scene = scene
+ self.menus = {}
+ self.focusedMenu = ""
+ self.isActive = true
+ self.lockWorld = false
+ self.lockAssets = false
+end
+
+function MenuSystem:reset()
+ self.menus = {}
+end
+
+-- ACTIVATION FUNCTIONS
+-- Activate and deactivate the whole menusystem
+
+function MenuSystem:activate()
+ self.isActive = true
+
+ if (self.lockWorld) then
+ if (self.scene.world ~= nil) then
+ self.scene.world:setActivity(false)
+ end
+ end
+
+ if (self.lockAssets) then
+ if (self.scene.assets ~= nil) then
+ self.scene.assets:setActivity(false)
+ end
+ end
+end
+
+function MenuSystem:deactivate()
+ self.isActive = false
+
+ if (self.lockWorld) then
+ if (self.scene.world ~= nil) then
+ self.scene.world:setActivity(true)
+ end
+ end
+
+ if (self.lockAssets) then
+ if (self.scene.assets ~= nil) then
+ self.scene.assets:setActivity(true)
+ end
+ end
+end
+
+function MenuSystem:getActiveState()
+ return self.isActive
+end
+
+function MenuSystem:lockWorldWhenActive(state)
+ self.lockWorld = state
+end
+
+function MenuSystem:lockAssetsWhenActive(state)
+ self.lockAssets = state
+end
+
+
+-- MENUS FUNCTIONS
+-- Controle the menus of the menusystem
+
+function MenuSystem:addMenu(name, menu)
+ self.menus[name] = menu
+end
+
+function MenuSystem:menuExist(name)
+ return (self.menus[name] ~= nil)
+end
+
+function MenuSystem:switchMenu(menu)
+ for k,v in pairs(self.menus) do
+ if k == menu then
+ v:getFocus()
+ v:setVisibility(true)
+ v:setActivity(true)
+ else
+ v:setVisibility(false)
+ v:setActivity(false)
+ end
+ end
+end
+
+function MenuSystem:lockMenu(menu, lock)
+ local lock = lock or true
+ if self:menuExist(menu) then
+ self.menus[menu]:lock(lock)
+ end
+end
+
+function MenuSystem:lockMenuVisibility(menu, lock)
+ local lock = lock or true
+ if self:menuExist(menu) then
+ self.menus[menu]:lockVisibility(lock)
+ end
+end
+
+function MenuSystem:setMenuActivity(menu, activity)
+ local activity = activity or true
+ if self:menuExist(menu) then
+ self.menus[menu]:setActivity(activity)
+ if activity == true then
+ -- if we make the menu active, he have to be visible too
+ self.menus[menu]:setVisibility(true)
+ end
+ end
+end
+
+function MenuSystem:setMenuVisibility(menu, visibility)
+ local visibility = visibility or true
+ if self:menuExist(menu) then
+ self.menus[menu]:setVisibility(visibility)
+ end
+end
+
+function MenuSystem:setAllMenuVisibility(visibility)
+ for k,v in pairs(self.menus) do
+ v:setVisibility(visibility)
+ end
+end
+
+function MenuSystem:setAllMenuActivity(activity)
+ for k,v in pairs(self.menus) do
+ v.isActive = activity
+ end
+end
+
+function MenuSystem:removeDestroyedMenus()
+ -- On retire les entitées marquées comme supprimées
+ for k,v in pairs(self.menus) do
+ if (v.isDestroyed == true) then
+ self.menus[k] = nil
+ end
+ end
+end
+
+-- SOUND FUNCTIONS
+-- Add sounds to every menus
+
+function MenuSystem:setSoundFromSceneAssets(soundname)
+ for k,v in pairs(self.menus) do
+ v:setSoundFromSceneAssets(soundname)
+ end
+end
+
+function MenuSystem:setSound(soundasset)
+ for k,v in pairs(self.menus) do
+ v:setSound(soundasset)
+ end
+end
+
+-- UPDATE FUNCTIONS
+-- Update the menus of the menusystem
+
+function MenuSystem:update(dt)
+ if (self.isActive) then
+ self:removeDestroyedMenus()
+ for k,v in pairs(self.menus) do
+ v:update(dt)
+ v:updateWidgets(dt)
+ end
+
+ if self.menus[self.focusedMenu] ~= nil then
+ -- Only check buttons if the current focused menu is actually active
+ if self.menus[self.focusedMenu].isActive then
+ for k,v in pairs(self.keys) do
+ if self.keys[k].isPressed then
+ self.menus[self.focusedMenu]:keyreleased(k)
+ end
+ end
+ end
+ end
+ end
+end
+
+-- MOUSE FUNCTIONS
+-- Send mouse inputs to the menu
+
+function MenuSystem:mousemoved(x, y, dx, dy)
+ if (self.isActive) then
+
+ for k,v in pairs(self.menus) do
+ if v.isActive then
+ if (x > v.x) and (x < v.x + v.w) and (y > v.y) and (y < v.y + v.h) then
+ v:mousemoved(x - v.x, y - v.y)
+ break;
+ end
+ end
+ end
+
+ end
+end
+
+function MenuSystem:mousepressed( x, y, button, istouch )
+ if (self.isActive) then
+ for k,v in pairs(self.menus) do
+ if v.isActive then
+ if (x > v.x) and (x < v.x + v.w) and (y > v.y) and (y < v.y + v.h) then
+ v:mousepressed(x - v.x, y - v.y, button, istouch )
+ break;
+ end
+ end
+ end
+ end
+end
+
+-- DRAW FUNCTIONS
+-- All functions to draw the menus of the menusystem
+
+function MenuSystem:getDrawList()
+ local drawList = {}
+ for k,v in pairs(self.menus) do
+ local drawObject = {}
+ drawObject.name = k
+ drawObject.depth = v.depth
+ table.insert(drawList, drawObject)
+ end
+ table.sort(drawList, function(a,b) return a.depth > b.depth end)
+
+ return drawList
+end
+
+function MenuSystem:draw(dt)
+ if (self.isActive) then
+ -- Draw all the menus
+ self.drawList = self:getDrawList()
+
+ for i,v in ipairs(self.drawList) do
+ local v2 = self.menus[v.name]
+ if (v2.isVisible) then
+ v2:draw(dt)
+ end
+ end
+
+ if self.menus[self.focusedMenu] ~= nil then
+ if (self.menus[self.focusedMenu].isVisible) then
+ self.menus[self.focusedMenu]:drawCursor()
+ end
+ end
+ end
+end
+
+return MenuSystem
diff --git a/bootleg.love/core/modules/menusystem/listbox.lua b/bootleg.love/core/modules/menusystem/listbox.lua
new file mode 100644
index 0000000..19c2ea2
--- /dev/null
+++ b/bootleg.love/core/modules/menusystem/listbox.lua
@@ -0,0 +1,148 @@
+-- listbox : add a vertical list of widgets.
+
+--[[
+ Copyright © 2019 Kazhnuz
+
+ 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.
+]]
+
+local cwd = (...):gsub('%.listbox$', '') .. "."
+local Menu = require(cwd .. "parent")
+
+local ListBox = Menu:extend()
+
+local menuutils = require(cwd .. "widgets.utils")
+
+-- INIT FUNCTIONS
+-- Initialize and configure functions.
+
+function ListBox:new(menusystem, name, x, y, w, h, slotNumber)
+ self.view = {}
+ self.view.slotNumber = slotNumber
+ self.view.firstSlot = 1
+ ListBox.super.new(self, menusystem, name, x, y, w, h)
+ self.h = slotNumber * self.widget.h -- On fait en sorte que la hauteur
+ -- soit un multiple du nombre de slot et de leur hauteur
+end
+
+-- UPDATE FUNCTIONS
+-- Update the menu every step.
+
+function ListBox:updateWidgetSize()
+ self.widget.h = math.floor( self.h / self.view.slotNumber )
+ self.widget.w = self.w
+end
+
+function ListBox:update(dt)
+ self:updateView()
+end
+
+function ListBox:updateView()
+ if self.widget.selected < self.view.firstSlot then
+ self.view.firstSlot = self.widget.selected
+ end
+ if self.widget.selected > self.view.firstSlot + self.view.slotNumber - 1 then
+ self.view.firstSlot = self.widget.selected - self.view.slotNumber + 1
+ end
+
+ if self.view.firstSlot < 1 then
+ self.view.firstSlot = 1
+ end
+end
+
+-- KEYBOARD FUNCTIONS
+-- Handle input from keyboard/controllers.
+
+function ListBox:keyreleased(key, code)
+
+ if key == 'up' then
+ self:moveCursor(self.widget.selected - 1)
+ end
+
+ if key == 'down' then
+ self:moveCursor(self.widget.selected + 1)
+ end
+
+ if key == "A" then
+ if (self.widget.selected >= 1 and self.widget.selected <= #self.widget.list) then
+ self.widget.list[self.widget.selected]:action("key")
+ end
+ end
+
+ if key == "B" then
+ if (self.widget.cancel >= 1 and self.widget.cancel <= #self.widget.list) then
+ self.widget.list[self.widget.cancel]:action("key")
+ end
+ end
+
+end
+
+-- MOUSE FUNCTIONS
+-- Handle input from pointers.
+
+function ListBox:mousemoved(x, y)
+ local widget_selected = self.view.firstSlot + math.floor(y / self.widget.h)
+
+ if widget_selected >= 1 and widget_selected <= #self.widget.list then
+ self.widget.selected = widget_selected
+ self:getFocus()
+ end
+end
+
+function ListBox:mousepressed(x, y, button, isTouch)
+ local widget_selected = self.view.firstSlot + math.floor(y / self.widget.h)
+
+ if widget_selected >= 1 and widget_selected <= #self.widget.list then
+ self.widget.selected = widget_selected
+ self:getFocus()
+ if #self.widget.list > 0 then
+ self.widget.list[self.widget.selected]:action("pointer")
+ end
+ end
+
+end
+
+-- DRAW FUNCTIONS
+-- draw the menu and the rest of content.
+
+function ListBox:draw()
+ self:updateView()
+ local widgety = self.y
+ for i,v in ipairs(self.widget.list) do
+ if (i >= self.view.firstSlot) and (i < self.view.firstSlot + self.view.slotNumber) then
+ v:draw(self.x, widgety, self.w, self.widget.h)
+ if self.widget.selected == i and self:haveFocus() == true then
+ v:drawSelected(self.x, widgety, self.w, self.widget.h)
+ else
+ v:draw(self.x, widgety, self.w, self.widget.h)
+ end
+ widgety = widgety + self.widget.h
+ end
+ end
+end
+
+function ListBox:drawCursor()
+ self:updateView()
+ if (self.widget.selected >= 1 and self.widget.selected <= #self.widget.list) then
+ local w, h = self:getWidgetSize()
+ local y = (self.widget.selected - self.view.firstSlot) * h
+ menuutils.drawCursor(self.x,self.y + y, w, h)
+ end
+end
+
+return ListBox
diff --git a/bootleg.love/core/modules/menusystem/parent.lua b/bootleg.love/core/modules/menusystem/parent.lua
new file mode 100644
index 0000000..12777e6
--- /dev/null
+++ b/bootleg.love/core/modules/menusystem/parent.lua
@@ -0,0 +1,291 @@
+-- parent.lua : The parent of the functions.
+
+--[[
+ Copyright © 2019 Kazhnuz
+
+ 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.
+]]
+
+local Menu = Object:extend()
+
+local function updateWidgetByOrder(a, b)
+ if a.order ~= b.order then
+ return a.order < b.order
+ else
+ return a.creationID < b.creationID
+ end
+end
+
+-- INIT FUNCTIONS
+-- Initialize and configure functions.
+
+function Menu:new(menusystem, name, x, y, w, h)
+ self.menusystem = menusystem
+ self.name = name
+
+ self.x = x
+ self.y = y
+ self.w = w
+ self.h = h
+
+ self.widget = {}
+ self.widget.list = {}
+ self.widget.selected = 0
+ self.widget.selectedPrevious = 0
+ self.widget.cancel = 0
+ self:updateWidgetSize()
+
+ self.isDestroyed = false
+ self.isVisible = true
+ self.isActive = true
+ self.isLocked = false
+ self.isAlwaysVisible = false
+
+ self.depth = 0
+
+ self:resetSound()
+
+ self:register()
+end
+
+function Menu:setDepth(depth)
+ self.depth = depth or 0
+end
+
+function Menu:setVisibility(visibility)
+ if self.isLocked == false and self.isAlwaysVisible == false then
+ -- if the menu is locked (thus is always active), it should also
+ -- be always visible.
+ self.isVisible = visibility
+ else
+ self.isVisible = true
+ end
+end
+
+function Menu:setActivity(activity)
+ if self.isLocked == false then
+ self.isActive = activity
+ else
+ self.isActive = true
+ end
+end
+
+function Menu:lock(lock)
+ self.isLocked = lock
+end
+
+function Menu:lockVisibility(lock)
+ self.isAlwaysVisible = lock
+end
+
+function Menu:getFocus()
+ self.menusystem.focusedMenu = self.name
+end
+
+function Menu:haveFocus()
+ return (self.menusystem.focusedMenu == self.name)
+end
+
+function Menu:register()
+ self.menusystem:addMenu(self.name, self)
+end
+
+function Menu:setCancelWidget(id)
+ self.widget.cancel = #self.widget.list
+end
+
+function Menu:updateWidgetSize()
+ self.widget.h = 0
+ self.widget.w = 0
+end
+
+function Menu:getWidgetSize(id)
+ return self.widget.w, self.widget.h
+end
+
+function Menu:getWidgetNumber()
+ return #self.widget.list
+end
+
+-- ACTION FUNCTIONS
+-- Send actions to the widgets
+
+function Menu:cancelAction()
+ if (self.widget.cancel ~= 0) then
+ self.widget.list[self.widget.cancel]:action("key")
+ end
+end
+
+function Menu:clear()
+ self.widget.list = {}
+ self.widget.cancel = 0
+end
+
+function Menu:resize(x,y,w,h)
+ self.x = x
+ self.y = y
+ self.w = w
+ self.h = h
+
+ self:updateWidgetSize()
+end
+
+function Menu:destroy()
+ self.destroyed = true
+end
+
+function Menu:updateWidgetsOrder()
+ table.sort(self.widget.list, updateWidgetByOrder)
+end
+
+-- UPDATE FUNCTIONS
+-- Update the menu every game update
+
+function Menu:update(dt)
+ -- Cette fonction ne contient rien par défaut
+end
+
+-- DRAW FUNCTIONS
+-- Draw the menu and its content
+
+function Menu:draw()
+ -- nothing here
+end
+
+function Menu:drawCursor()
+ -- nothing here
+end
+
+function Menu:drawCanvas()
+
+end
+
+-- KEYBOARD FUNCTIONS
+-- Handle key press
+
+function Menu:keyreleased(key)
+ -- Cette fonction ne contient rien par défaut
+end
+
+-- MOUSE FUNCTIONS
+-- Handle pointers (clic/touch)
+
+function Menu:mousemoved(x, y)
+ -- Cette fonction ne contient rien par défaut
+end
+
+function Menu:mousepressed( x, y, button, istouch )
+ -- Cette fonction ne contient rien par défaut
+end
+
+-- WIDGET FUNCTIONS
+-- Handle widgets of the functions
+
+function Menu:addWidget(newwidget)
+ if #self.widget.list == 0 then
+ self.widget.selected = 1
+ end
+ table.insert(self.widget.list, newwidget)
+ self:updateWidgetsID()
+ self:updateWidgetsOrder()
+end
+
+function Menu:updateWidgets(dt)
+ self:removeDestroyedWidgets()
+ for i,v in ipairs(self.widget.list) do
+ v.id = i
+ v:update(dt)
+ end
+end
+
+function Menu:updateWidgetsID()
+ for i,v in ipairs(self.widget.list) do
+ v.id = i
+ end
+end
+
+function Menu:removeDestroyedWidgets() -- On retire les widgets marquées comme supprimées
+ for i,v in ipairs(self.widget.list) do
+ if (v.destroyed == true) then
+ table.remove(self.widget.list, i)
+ end
+ end
+end
+
+-- CURSOR FUNCTIONS
+-- Set or move the cursor of the menu
+
+function Menu:setCursor(cursorid)
+ self.widget.selected = cursorid --math.max(1, math.min(cursorid, #self.widget.list))
+end
+
+function Menu:moveCursor(new_selected)
+ self:playNavigationSound()
+ if new_selected < 1 then
+ self.widget.selected = #self.widget.list + new_selected
+ else
+ if new_selected > #self.widget.list then
+ self.widget.selected = new_selected - #self.widget.list
+ else
+ self.widget.selected = new_selected
+ end
+ end
+end
+
+-- SOUND FUNCTION
+-- Handle SFX
+
+function Menu:resetSound()
+ self.sound = {}
+ self.sound.active = false
+ self.sound.asset = nil
+end
+
+function Menu:setSoundFromSceneAssets(name)
+ self:setSound(self.menusystem.scene.assets.sfx[name])
+end
+
+function Menu:setSound(soundasset)
+ self.sound.active = true
+ self.sound.asset = soundasset
+end
+
+function Menu:playNavigationSound()
+ if self.sound.active == true then
+ love.audio.stop( self.sound.asset )
+ self.sound.asset:setVolume(core.options.data.audio.sfx / 100)
+ love.audio.play( self.sound.asset )
+ end
+end
+
+-- VIEW FUNCTIONS
+-- Handle the view of the menu
+
+function Menu:resetView()
+ -- ne sert à rien ici, c'est juste pour éviter des crash
+end
+
+function Menu:updateView()
+ -- ne sert à rien ici, c'est juste pour éviter des crash
+end
+
+function Menu:moveView()
+ -- ne sert à rien ici, c'est juste pour éviter des crash
+end
+
+return Menu
diff --git a/bootleg.love/core/modules/menusystem/widgets/init.lua b/bootleg.love/core/modules/menusystem/widgets/init.lua
new file mode 100644
index 0000000..9c02e5a
--- /dev/null
+++ b/bootleg.love/core/modules/menusystem/widgets/init.lua
@@ -0,0 +1,144 @@
+-- widgets :: basic widget object
+
+--[[
+ Copyright © 2019 Kazhnuz
+
+ 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.
+]]
+
+local Widget = {}
+
+BaseWidget = Object:extend()
+TextWidget = BaseWidget:extend()
+
+-- INIT FUNCTIONS
+-- Initialize and configure the widget
+
+function BaseWidget:new(menu)
+ self.menu = menu
+
+ self.destroyed = false
+ self.selectable = false
+ self.selection_margin = 0
+ self.margin = 2
+
+ self.canvas = {}
+ self.canvas.texture = nil
+ self.canvas.needRedraw = true
+
+ self.order = 0
+ self:register()
+end
+
+function BaseWidget:register()
+ self.creationID = self.menu:getWidgetNumber()
+ self.menu:addWidget(self)
+end
+
+function BaseWidget:redrawCanvas()
+ self.width, self.height = self.menu:getWidgetSize(self.id)
+
+ local canvas = love.graphics.newCanvas(self.width, self.height)
+ love.graphics.setCanvas( canvas )
+
+ self:drawCanvas()
+ self.canvas.needRedraw = false
+
+ love.graphics.setCanvas( )
+ local imageData = canvas:newImageData( )
+ self.canvas.texture = love.graphics.newImage( imageData )
+ canvas:release( )
+ imageData:release( )
+end
+
+function BaseWidget:invalidateCanvas()
+ self.canvas.needRedraw = true
+end
+
+function BaseWidget:drawCanvas()
+ self.r = love.math.random(128)/256
+ self.g = love.math.random(128)/256
+ self.b = love.math.random(128)/256
+
+ love.graphics.setColor(self.r, self.g, self.b, 70)
+ love.graphics.rectangle("fill", 0, 0, self.width, self.height)
+ love.graphics.setColor(self.r, self.g, self.b)
+ love.graphics.rectangle("line", 0, 0, self.width, self.height)
+ utils.graphics.resetColor()
+end
+
+function BaseWidget:selectAction()
+ -- Do nothing
+end
+
+-- DRAW WIDGETS
+-- Draw the widget
+
+function BaseWidget:draw(x, y)
+ if self.canvas.texture ~= nil then
+ utils.graphics.resetColor()
+ love.graphics.draw(self.canvas.texture, x, y)
+ end
+end
+
+function BaseWidget:drawSelected(x,y,w,h)
+ self:draw(x, y, w, h)
+end
+
+-- UPDATE FUNCTIONS
+-- Update the widget
+
+function BaseWidget:update(dt)
+ if (self.canvas.needRedraw) then
+ self:redrawCanvas()
+ end
+ -- N/A
+end
+
+-- ACTION FUNCTION
+-- Functions to handle actions and selection.
+
+function BaseWidget:action(source)
+ --self:destroy()
+end
+
+function BaseWidget:destroy()
+ self.destroyed = true
+end
+
+-- TEXT WIDGET
+-- Simple text widget
+
+function TextWidget:new(menu, font, label)
+ TextWidget.super.new(self, menu)
+ self.font = font
+ self.label = label
+end
+
+function TextWidget:drawCanvas()
+ local w, h
+ w = math.floor(self.width / 2)
+ h = math.floor(self.height / 2) - (self.font:getHeight() / 2)
+ self.font:draw(self.label, w, h, -1, "center")
+end
+
+-- Add the widget as subvariable to the returned table
+Widget.Base = BaseWidget
+Widget.Text = TextWidget
+
+return Widget
diff --git a/bootleg.love/core/modules/menusystem/widgets/utils.lua b/bootleg.love/core/modules/menusystem/widgets/utils.lua
new file mode 100644
index 0000000..e69c6d7
--- /dev/null
+++ b/bootleg.love/core/modules/menusystem/widgets/utils.lua
@@ -0,0 +1,57 @@
+-- widgets/utils :: basic utility functions for the widgets
+
+--[[
+ Copyright © 2019 Kazhnuz
+
+ 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.
+]]
+
+local menuUtils = {}
+
+function menuUtils.drawCursor(x, y, w, h)
+ love.graphics.setColor(0,0,0)
+
+ love.graphics.rectangle("fill", x, y, 4, 8)
+ love.graphics.rectangle("fill", x, y, 8, 4)
+
+ love.graphics.rectangle("fill", x + w, y, -4, 8)
+ love.graphics.rectangle("fill", x + w, y, -8, 4)
+
+ love.graphics.rectangle("fill", x, y + h, 4, -8)
+ love.graphics.rectangle("fill", x, y + h, 8, -4)
+
+ love.graphics.rectangle("fill", x + w, y + h, -4, -8)
+ love.graphics.rectangle("fill", x + w, y + h, -8, -4)
+
+ love.graphics.setColor(255,255,255)
+
+ love.graphics.rectangle("fill", x + 1, y + 1, 2, 6)
+ love.graphics.rectangle("fill", x + 1, y + 1, 6, 2)
+
+ love.graphics.rectangle("fill", x + w - 1, y + 1, -2, 6)
+ love.graphics.rectangle("fill", x + w - 1, y + 1, -6, 2)
+
+ love.graphics.rectangle("fill", x + 1, y + h - 1, 2, -6)
+ love.graphics.rectangle("fill", x + 1, y + h - 1, 6, -2)
+
+ love.graphics.rectangle("fill", x + w - 1, y + h - 1, -2, -6)
+ love.graphics.rectangle("fill", x + w - 1, y + h - 1, -6, -2)
+
+end
+
+return menuUtils
diff --git a/bootleg.love/core/modules/scenes.lua b/bootleg.love/core/modules/scenes.lua
new file mode 100644
index 0000000..c615f6f
--- /dev/null
+++ b/bootleg.love/core/modules/scenes.lua
@@ -0,0 +1,173 @@
+-- scenes.lua :: the scene object, that aim to give a better control to the engine
+-- to the different scene, without having to call too much boilerplate
+
+--[[
+ Copyright © 2019 Kazhnuz
+
+ 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.
+]]
+
+local cwd = (...):gsub('%.scenes$', '') .. "."
+
+local Scene = Object:extend()
+
+local Assets = require(cwd .. "assets")
+local MenuSystem = require(cwd .. "menusystem")
+
+-- INIT FUNCTIONS
+-- Initialize and configure the scene
+
+function Scene:new()
+ self.mouse = {}
+ self.mouse.x, self.mouse.y = core.screen:getMousePosition()
+
+ self.assets = Assets()
+ self.menusystem = MenuSystem(self)
+ self.sources = core.input:getSources()
+
+ self.inputLocked = true
+ self.inputLockedTimer = 2
+ self:flushKeys()
+ self.isActive = false
+
+ self:initWorld()
+
+ self:register()
+end
+
+function Scene:register()
+ core.scenemanager:setScene(self)
+end
+
+function Scene:clear()
+ -- TODO: send automatic cleanups to the different elements of the scene
+end
+
+-- UPDATE FUNCTIONS
+-- Handle stuff that happens every steps
+
+function Scene:updateStart(dt)
+
+end
+
+function Scene:update(dt)
+ -- Empty function, is just here to avoid crash
+end
+
+function Scene:updateEnd(dt)
+
+end
+
+function Scene:updateWorld(dt)
+ if (self.world ~= nil) and (self.world.isActive) then
+ self.world:update(dt)
+ end
+end
+
+-- MOUSE FUNCTIONS
+-- Make the scene support the pointer
+
+function Scene:mousemoved(x, y, dx, dy)
+ -- Empty function, is just here to avoid crash
+end
+
+function Scene:mousepressed( x, y, button, istouch )
+ -- Empty function, is just here to avoid crash
+end
+
+-- WORLD FUNCTIONS
+-- Basic functions to manage the world
+
+function Scene:initWorld()
+ self.world = nil
+end
+
+function Scene:registerWorld(world)
+ self.world = world
+end
+
+-- KEYBOARD FUNCTIONS
+-- Add send keys functions to the scene
+
+function Scene:keypressed( key, scancode, isrepeat )
+
+end
+
+function Scene:keyreleased( key )
+
+end
+
+-- DRAW FUNCTIONS
+-- Draw the scene and its content
+
+function Scene:drawStart()
+
+end
+
+function Scene:draw()
+
+end
+
+function Scene:drawEnd()
+
+end
+
+function Scene:drawWorld(dt)
+ if (self.world ~= nil) then
+ self.world:draw()
+ end
+end
+
+-- INPUT FUNCTIONS
+-- Handle inputs from keyboard/controllers
+
+function Scene:setKeys()
+ if (self.inputLocked) or (not self.isActive) then
+ self.inputLockedTimer = self.inputLockedTimer - 1
+ if (self.inputLockedTimer <= 0 ) then
+ self.inputLocked = false
+ end
+ self.menusystem.keys = self:getKeys(1)
+ else
+ self.sources = core.input.sources
+ self.menusystem.keys = self:getKeys(1)
+ end
+end
+
+function Scene:getKeys(sourceid)
+ if sourceid == nil then
+ core.debug:warning("scene", "no sourceid detected, will default to 1")
+ end
+
+ local sourceid = sourceid or 1
+ if (self.inputLocked) or (not self.isActive) then
+ core.debug:print("scene", "inputs are currently locked")
+ return core.input.fakekeys
+ else
+ return self.sources[sourceid].keys
+ end
+end
+
+function Scene:flushKeys(timer)
+ core.input:flushKeys()
+ self.sources = core.input.sources
+ self.inputLockedTimer = timer or 2
+ self.inputLocked = true
+end
+
+return Scene
diff --git a/bootleg.love/core/modules/timers/init.lua b/bootleg.love/core/modules/timers/init.lua
new file mode 100644
index 0000000..50206f4
--- /dev/null
+++ b/bootleg.love/core/modules/timers/init.lua
@@ -0,0 +1,131 @@
+-- timers :: a basic implementation of a timer system.
+
+--[[
+ Copyright © 2019 Kazhnuz
+
+ 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.
+]]
+
+local cwd = (...):gsub('%.init$', '') .. "."
+
+local TimersManager = Object:extend()
+local Timer = require(cwd .. "libs.timer")
+local Tween = require(cwd .. "libs.tween")
+
+
+function TimersManager:new(subject)
+ self.subject = subject
+ self.time = 0
+
+ self.timers = {}
+ self.switches = {}
+ self.tweens = {}
+end
+
+-- UPDATE
+-- Update every timers
+
+function TimersManager:update(dt)
+ self.time = self.time + dt
+
+ for k, timer in pairs(self.timers) do
+ timer:update(dt)
+ end
+
+ self:updateSwitches(dt)
+ self:updateTweens(dt)
+end
+
+-- TIMER FUNCTIONS
+-- Handle the timers
+
+function TimersManager:newTimer(duration, name)
+ self.timers[name] = Timer(self, duration, name)
+end
+
+function TimersManager:timerResponse(timername)
+ if self.subject.timerResponse == nil then
+ core.debug:warning("TimersManager", "the subject have no timerResponse function")
+ return 0
+ end
+ self.subject:timerResponse(timername)
+end
+
+-- SWITCH FUNCTIONS
+-- Handle switches
+
+function TimersManager:newSwitch(start, bools)
+ local newSwitch = {}
+ -- we add the data into a switch wrapper
+ newSwitch.bools = bools
+ newSwitch.start = self.time + start -- /!\ START IS RELATIVE TO CURRENT TIME
+ newSwitch.clear = newSwitch.start + 1
+
+ table.insert(self.switches, newSwitch)
+end
+
+function TimersManager:updateSwitches(dt)
+ for i, switch in ipairs(self.switches) do
+ if (self.time > switch.start) then
+ -- We test each boolean in the switch
+ for i, bool in ipairs(switch.bools) do
+ -- if it's nil, we set it to true
+ if self.subject[bool] == nil then
+ self.subject[bool] = true
+ else
+ -- if it's not nil, we reverse the boolean
+ self.subject[bool] = (self.subject[bool] == false)
+ end
+ end
+ table.remove(self.switches, i)
+ end
+ end
+end
+
+-- TWEENING FUNCTIONS
+-- Handle tween support via tween.lua
+
+function TimersManager:newTween(start, duration, target, easing)
+ local newTween = {}
+ -- we add the data into a tween wrapper
+ newTween.tween = Tween.new(duration, self.subject, target, easing)
+ newTween.start = self.time + start -- /!\ START IS RELATIVE TO CURRENT TIME
+ newTween.clear = newTween.start + duration
+
+ table.insert(self.tweens, newTween)
+end
+
+function TimersManager:updateTweens(dt)
+ for i, tweenWrapper in ipairs(self.tweens) do
+ if (self.time > tweenWrapper.start) then
+ tweenWrapper.tween:update(dt)
+ end
+ end
+
+ self:clearEndedTweens()
+end
+
+function TimersManager:clearEndedTweens()
+ for i, tweenWrapper in ipairs(self.tweens) do
+ if (self.time > tweenWrapper.clear) then
+ table.remove(self.tweens, i)
+ end
+ end
+end
+
+return TimersManager
diff --git a/bootleg.love/core/modules/timers/libs/timer.lua b/bootleg.love/core/modules/timers/libs/timer.lua
new file mode 100644
index 0000000..375ef32
--- /dev/null
+++ b/bootleg.love/core/modules/timers/libs/timer.lua
@@ -0,0 +1,45 @@
+-- timer.lua :: a basic implementation of a timer for the actor system.
+
+--[[
+ Copyright © 2019 Kazhnuz
+
+ 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.
+]]
+
+local Timer = Object:extend()
+
+function Timer:new(controller, duration, name)
+ self.controller = controller
+
+ self.time = duration
+ self.name = name
+end
+
+function Timer:update(dt)
+ self.time = self.time - dt
+ if (self.time <= 0) then
+ self:finish()
+ end
+end
+
+function Timer:finish()
+ self.controller:timerResponse(self.name)
+ self.controller.timers[self.name] = nil
+end
+
+return Timer
diff --git a/bootleg.love/core/modules/timers/libs/tween.lua b/bootleg.love/core/modules/timers/libs/tween.lua
new file mode 100644
index 0000000..4f2c681
--- /dev/null
+++ b/bootleg.love/core/modules/timers/libs/tween.lua
@@ -0,0 +1,367 @@
+local tween = {
+ _VERSION = 'tween 2.1.1',
+ _DESCRIPTION = 'tweening for lua',
+ _URL = 'https://github.com/kikito/tween.lua',
+ _LICENSE = [[
+ MIT LICENSE
+
+ Copyright (c) 2014 Enrique García Cota, Yuichi Tateno, Emmanuel Oga
+
+ 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.
+ ]]
+}
+
+-- easing
+
+-- Adapted from https://github.com/EmmanuelOga/easing. See LICENSE.txt for credits.
+-- For all easing functions:
+-- t = time == how much time has to pass for the tweening to complete
+-- b = begin == starting property value
+-- c = change == ending - beginning
+-- d = duration == running time. How much time has passed *right now*
+
+local pow, sin, cos, pi, sqrt, abs, asin = math.pow, math.sin, math.cos, math.pi, math.sqrt, math.abs, math.asin
+
+-- linear
+local function linear(t, b, c, d) return c * t / d + b end
+
+-- quad
+local function inQuad(t, b, c, d) return c * pow(t / d, 2) + b end
+local function outQuad(t, b, c, d)
+ t = t / d
+ return -c * t * (t - 2) + b
+end
+local function inOutQuad(t, b, c, d)
+ t = t / d * 2
+ if t < 1 then return c / 2 * pow(t, 2) + b end
+ return -c / 2 * ((t - 1) * (t - 3) - 1) + b
+end
+local function outInQuad(t, b, c, d)
+ if t < d / 2 then return outQuad(t * 2, b, c / 2, d) end
+ return inQuad((t * 2) - d, b + c / 2, c / 2, d)
+end
+
+-- cubic
+local function inCubic (t, b, c, d) return c * pow(t / d, 3) + b end
+local function outCubic(t, b, c, d) return c * (pow(t / d - 1, 3) + 1) + b end
+local function inOutCubic(t, b, c, d)
+ t = t / d * 2
+ if t < 1 then return c / 2 * t * t * t + b end
+ t = t - 2
+ return c / 2 * (t * t * t + 2) + b
+end
+local function outInCubic(t, b, c, d)
+ if t < d / 2 then return outCubic(t * 2, b, c / 2, d) end
+ return inCubic((t * 2) - d, b + c / 2, c / 2, d)
+end
+
+-- quart
+local function inQuart(t, b, c, d) return c * pow(t / d, 4) + b end
+local function outQuart(t, b, c, d) return -c * (pow(t / d - 1, 4) - 1) + b end
+local function inOutQuart(t, b, c, d)
+ t = t / d * 2
+ if t < 1 then return c / 2 * pow(t, 4) + b end
+ return -c / 2 * (pow(t - 2, 4) - 2) + b
+end
+local function outInQuart(t, b, c, d)
+ if t < d / 2 then return outQuart(t * 2, b, c / 2, d) end
+ return inQuart((t * 2) - d, b + c / 2, c / 2, d)
+end
+
+-- quint
+local function inQuint(t, b, c, d) return c * pow(t / d, 5) + b end
+local function outQuint(t, b, c, d) return c * (pow(t / d - 1, 5) + 1) + b end
+local function inOutQuint(t, b, c, d)
+ t = t / d * 2
+ if t < 1 then return c / 2 * pow(t, 5) + b end
+ return c / 2 * (pow(t - 2, 5) + 2) + b
+end
+local function outInQuint(t, b, c, d)
+ if t < d / 2 then return outQuint(t * 2, b, c / 2, d) end
+ return inQuint((t * 2) - d, b + c / 2, c / 2, d)
+end
+
+-- sine
+local function inSine(t, b, c, d) return -c * cos(t / d * (pi / 2)) + c + b end
+local function outSine(t, b, c, d) return c * sin(t / d * (pi / 2)) + b end
+local function inOutSine(t, b, c, d) return -c / 2 * (cos(pi * t / d) - 1) + b end
+local function outInSine(t, b, c, d)
+ if t < d / 2 then return outSine(t * 2, b, c / 2, d) end
+ return inSine((t * 2) -d, b + c / 2, c / 2, d)
+end
+
+-- expo
+local function inExpo(t, b, c, d)
+ if t == 0 then return b end
+ return c * pow(2, 10 * (t / d - 1)) + b - c * 0.001
+end
+local function outExpo(t, b, c, d)
+ if t == d then return b + c end
+ return c * 1.001 * (-pow(2, -10 * t / d) + 1) + b
+end
+local function inOutExpo(t, b, c, d)
+ if t == 0 then return b end
+ if t == d then return b + c end
+ t = t / d * 2
+ if t < 1 then return c / 2 * pow(2, 10 * (t - 1)) + b - c * 0.0005 end
+ return c / 2 * 1.0005 * (-pow(2, -10 * (t - 1)) + 2) + b
+end
+local function outInExpo(t, b, c, d)
+ if t < d / 2 then return outExpo(t * 2, b, c / 2, d) end
+ return inExpo((t * 2) - d, b + c / 2, c / 2, d)
+end
+
+-- circ
+local function inCirc(t, b, c, d) return(-c * (sqrt(1 - pow(t / d, 2)) - 1) + b) end
+local function outCirc(t, b, c, d) return(c * sqrt(1 - pow(t / d - 1, 2)) + b) end
+local function inOutCirc(t, b, c, d)
+ t = t / d * 2
+ if t < 1 then return -c / 2 * (sqrt(1 - t * t) - 1) + b end
+ t = t - 2
+ return c / 2 * (sqrt(1 - t * t) + 1) + b
+end
+local function outInCirc(t, b, c, d)
+ if t < d / 2 then return outCirc(t * 2, b, c / 2, d) end
+ return inCirc((t * 2) - d, b + c / 2, c / 2, d)
+end
+
+-- elastic
+local function calculatePAS(p,a,c,d)
+ p, a = p or d * 0.3, a or 0
+ if a < abs(c) then return p, c, p / 4 end -- p, a, s
+ return p, a, p / (2 * pi) * asin(c/a) -- p,a,s
+end
+local function inElastic(t, b, c, d, a, p)
+ local s
+ if t == 0 then return b end
+ t = t / d
+ if t == 1 then return b + c end
+ p,a,s = calculatePAS(p,a,c,d)
+ t = t - 1
+ return -(a * pow(2, 10 * t) * sin((t * d - s) * (2 * pi) / p)) + b
+end
+local function outElastic(t, b, c, d, a, p)
+ local s
+ if t == 0 then return b end
+ t = t / d
+ if t == 1 then return b + c end
+ p,a,s = calculatePAS(p,a,c,d)
+ return a * pow(2, -10 * t) * sin((t * d - s) * (2 * pi) / p) + c + b
+end
+local function inOutElastic(t, b, c, d, a, p)
+ local s
+ if t == 0 then return b end
+ t = t / d * 2
+ if t == 2 then return b + c end
+ p,a,s = calculatePAS(p,a,c,d)
+ t = t - 1
+ if t < 0 then return -0.5 * (a * pow(2, 10 * t) * sin((t * d - s) * (2 * pi) / p)) + b end
+ return a * pow(2, -10 * t) * sin((t * d - s) * (2 * pi) / p ) * 0.5 + c + b
+end
+local function outInElastic(t, b, c, d, a, p)
+ if t < d / 2 then return outElastic(t * 2, b, c / 2, d, a, p) end
+ return inElastic((t * 2) - d, b + c / 2, c / 2, d, a, p)
+end
+
+-- back
+local function inBack(t, b, c, d, s)
+ s = s or 1.70158
+ t = t / d
+ return c * t * t * ((s + 1) * t - s) + b
+end
+local function outBack(t, b, c, d, s)
+ s = s or 1.70158
+ t = t / d - 1
+ return c * (t * t * ((s + 1) * t + s) + 1) + b
+end
+local function inOutBack(t, b, c, d, s)
+ s = (s or 1.70158) * 1.525
+ t = t / d * 2
+ if t < 1 then return c / 2 * (t * t * ((s + 1) * t - s)) + b end
+ t = t - 2
+ return c / 2 * (t * t * ((s + 1) * t + s) + 2) + b
+end
+local function outInBack(t, b, c, d, s)
+ if t < d / 2 then return outBack(t * 2, b, c / 2, d, s) end
+ return inBack((t * 2) - d, b + c / 2, c / 2, d, s)
+end
+
+-- bounce
+local function outBounce(t, b, c, d)
+ t = t / d
+ if t < 1 / 2.75 then return c * (7.5625 * t * t) + b end
+ if t < 2 / 2.75 then
+ t = t - (1.5 / 2.75)
+ return c * (7.5625 * t * t + 0.75) + b
+ elseif t < 2.5 / 2.75 then
+ t = t - (2.25 / 2.75)
+ return c * (7.5625 * t * t + 0.9375) + b
+ end
+ t = t - (2.625 / 2.75)
+ return c * (7.5625 * t * t + 0.984375) + b
+end
+local function inBounce(t, b, c, d) return c - outBounce(d - t, 0, c, d) + b end
+local function inOutBounce(t, b, c, d)
+ if t < d / 2 then return inBounce(t * 2, 0, c, d) * 0.5 + b end
+ return outBounce(t * 2 - d, 0, c, d) * 0.5 + c * .5 + b
+end
+local function outInBounce(t, b, c, d)
+ if t < d / 2 then return outBounce(t * 2, b, c / 2, d) end
+ return inBounce((t * 2) - d, b + c / 2, c / 2, d)
+end
+
+tween.easing = {
+ linear = linear,
+ inQuad = inQuad, outQuad = outQuad, inOutQuad = inOutQuad, outInQuad = outInQuad,
+ inCubic = inCubic, outCubic = outCubic, inOutCubic = inOutCubic, outInCubic = outInCubic,
+ inQuart = inQuart, outQuart = outQuart, inOutQuart = inOutQuart, outInQuart = outInQuart,
+ inQuint = inQuint, outQuint = outQuint, inOutQuint = inOutQuint, outInQuint = outInQuint,
+ inSine = inSine, outSine = outSine, inOutSine = inOutSine, outInSine = outInSine,
+ inExpo = inExpo, outExpo = outExpo, inOutExpo = inOutExpo, outInExpo = outInExpo,
+ inCirc = inCirc, outCirc = outCirc, inOutCirc = inOutCirc, outInCirc = outInCirc,
+ inElastic = inElastic, outElastic = outElastic, inOutElastic = inOutElastic, outInElastic = outInElastic,
+ inBack = inBack, outBack = outBack, inOutBack = inOutBack, outInBack = outInBack,
+ inBounce = inBounce, outBounce = outBounce, inOutBounce = inOutBounce, outInBounce = outInBounce
+}
+
+
+
+-- private stuff
+
+local function copyTables(destination, keysTable, valuesTable)
+ valuesTable = valuesTable or keysTable
+ local mt = getmetatable(keysTable)
+ if mt and getmetatable(destination) == nil then
+ setmetatable(destination, mt)
+ end
+ for k,v in pairs(keysTable) do
+ if type(v) == 'table' then
+ destination[k] = copyTables({}, v, valuesTable[k])
+ else
+ destination[k] = valuesTable[k]
+ end
+ end
+ return destination
+end
+
+local function checkSubjectAndTargetRecursively(subject, target, path)
+ path = path or {}
+ local targetType, newPath
+ for k,targetValue in pairs(target) do
+ targetType, newPath = type(targetValue), copyTables({}, path)
+ table.insert(newPath, tostring(k))
+ if targetType == 'number' then
+ assert(type(subject[k]) == 'number', "Parameter '" .. table.concat(newPath,'/') .. "' is missing from subject or isn't a number")
+ elseif targetType == 'table' then
+ checkSubjectAndTargetRecursively(subject[k], targetValue, newPath)
+ else
+ assert(targetType == 'number', "Parameter '" .. table.concat(newPath,'/') .. "' must be a number or table of numbers")
+ end
+ end
+end
+
+local function checkNewParams(duration, subject, target, easing)
+ assert(type(duration) == 'number' and duration > 0, "duration must be a positive number. Was " .. tostring(duration))
+ local tsubject = type(subject)
+ assert(tsubject == 'table' or tsubject == 'userdata', "subject must be a table or userdata. Was " .. tostring(subject))
+ assert(type(target)== 'table', "target must be a table. Was " .. tostring(target))
+ assert(type(easing)=='function', "easing must be a function. Was " .. tostring(easing))
+ checkSubjectAndTargetRecursively(subject, target)
+end
+
+local function getEasingFunction(easing)
+ easing = easing or "linear"
+ if type(easing) == 'string' then
+ local name = easing
+ easing = tween.easing[name]
+ if type(easing) ~= 'function' then
+ error("The easing function name '" .. name .. "' is invalid")
+ end
+ end
+ return easing
+end
+
+local function performEasingOnSubject(subject, target, initial, clock, duration, easing)
+ local t,b,c,d
+ for k,v in pairs(target) do
+ if type(v) == 'table' then
+ performEasingOnSubject(subject[k], v, initial[k], clock, duration, easing)
+ else
+ t,b,c,d = clock, initial[k], v - initial[k], duration
+ subject[k] = easing(t,b,c,d)
+ end
+ end
+end
+
+-- Tween methods
+
+local Tween = {}
+local Tween_mt = {__index = Tween}
+
+function Tween:set(clock)
+ assert(type(clock) == 'number', "clock must be a positive number or 0")
+
+ self.initial = self.initial or copyTables({}, self.target, self.subject)
+ self.clock = clock
+
+ if self.clock <= 0 then
+
+ self.clock = 0
+ copyTables(self.subject, self.initial)
+
+ elseif self.clock >= self.duration then -- the tween has expired
+
+ self.clock = self.duration
+ copyTables(self.subject, self.target)
+
+ else
+
+ performEasingOnSubject(self.subject, self.target, self.initial, self.clock, self.duration, self.easing)
+
+ end
+
+ return self.clock >= self.duration
+end
+
+function Tween:reset()
+ return self:set(0)
+end
+
+function Tween:update(dt)
+ assert(type(dt) == 'number', "dt must be a number")
+ return self:set(self.clock + dt)
+end
+
+
+-- Public interface
+
+function tween.new(duration, subject, target, easing)
+ easing = getEasingFunction(easing)
+ checkNewParams(duration, subject, target, easing)
+ return setmetatable({
+ duration = duration,
+ subject = subject,
+ target = target,
+ easing = easing,
+ clock = 0
+ }, Tween_mt)
+end
+
+return tween
diff --git a/bootleg.love/core/modules/world/actors/actor2D.lua b/bootleg.love/core/modules/world/actors/actor2D.lua
new file mode 100644
index 0000000..1b8c794
--- /dev/null
+++ b/bootleg.love/core/modules/world/actors/actor2D.lua
@@ -0,0 +1,207 @@
+-- actor2D.lua :: the implementation of a 2D actor. It contain every element
+-- needed to create your own 2D actors.
+
+--[[
+ Copyright © 2019 Kazhnuz
+
+ 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.
+]]
+
+local cwd = (...):gsub('%.actor2D$', '') .. "."
+local BaseActor = require(cwd .. "baseactor")
+local Actor2D = BaseActor:extend()
+
+local Hitbox = require(cwd .. "utils.hitbox2D")
+
+-- INIT FUNCTIONS
+-- Initialise the actor and its base functions
+
+function Actor2D:new(world, type, x, y, w, h, isSolid)
+ Actor2D.super.new(self, world, type, x, y, 0, w, h, 0, isSolid)
+ self:initHitboxes()
+end
+
+function Actor2D:destroy()
+ self.world:removeActor(self)
+ self.mainHitbox:destroy()
+ self.isDestroyed = true
+end
+
+-- PHYSICS FUNCTIONS
+-- Handle movement and collisions.
+
+function Actor2D:autoMove(dt)
+ self:updateHitboxes()
+ self.onGround = false
+ self:applyGravity(dt)
+
+ local dx, dy = self:getFuturePosition(dt)
+ local newx, newy, cols, colNumber = self:move(dx, dy)
+
+ -- apply after the movement the friction, until the player stop
+ -- note: the friction is applied according to the delta time,
+ -- thus the friction should be how much speed is substracted in 1 second
+
+ self:solveAllCollisions(cols)
+
+ self:applyFriction(dt)
+end
+
+function Actor2D:changeSpeedToCollisionNormal(normal)
+ local xsp, ysp = self.xsp, self.ysp
+ local nx, ny = normal.x, normal.y
+
+ if (nx < 0 and xsp > 0) or (nx > 0 and xsp < 0) then
+ xsp = -xsp * self.bounceFactor
+ end
+
+ if (ny < 0 and ysp > 0) or (ny > 0 and ysp < 0) then
+ ysp = -ysp * self.bounceFactor
+ end
+
+ self.xsp, self.ysp = xsp, ysp
+end
+
+function Actor2D:move(dx, dy)
+ local cols, colNumber = {}, 0
+ if (self.isDestroyed == false) then
+ self.x, self.y, cols, colNumber = self.mainHitbox:checkCollision(dx, dy, self.filter)
+ self.mainHitbox:updatePosition()
+ end
+ return self.x, self.y, cols, colNumber
+end
+
+function Actor2D:checkCollision(dx, dy)
+ local x, y, cols, colNumber = dx, dy, {}, 0
+ if (self.isDestroyed == false) then
+ x, y, cols, colNumber = self.mainHitbox:checkCollision(dx, dy, self.filter)
+ end
+ return self.x, self.y, cols, colNumber
+end
+
+-- GRAVITY SYSTEM FUNCTIONS
+-- All functions related to gravity
+
+function Actor2D:applyGravity(dt)
+ self.ysp = self.ysp + self.grav * dt
+
+ if utils.math.sign(self.ysp) == utils.math.sign(self.grav) then
+ self:checkGround( )
+ end
+end
+
+function Actor2D:checkGround()
+ local dx, dy = self.x, self.y + utils.math.sign(self.grav)
+ local newx, newy, cols, colNumber = self:checkCollision(dx, dy)
+
+ for i, col in ipairs(cols) do
+ if (col.type == "touch") or (col.type == "bounce") or (col.type == "slide") then
+ if not (self.grav == 0) then
+ if col.normal.y ~= utils.math.sign(self.grav) then self.onGround = true end
+ end
+ end
+ end
+end
+
+-- COORDINATE/MOVEMENT FUNCTIONS
+-- Handle coordinate
+
+function Actor2D:getViewCenter()
+ local x, y = self:getCenter()
+ return x, y
+end
+
+-- HITBOXES FUNCTIONS
+-- Functions related to actor hitboxes
+
+function Actor2D:addHitboxFromFrameData(framedata, animationID, frameID, hitboxID)
+ local sx, sy = self:getSpriteScalling()
+ local type = framedata[1]
+ local ox = framedata[2]
+ local oy = framedata[3]
+ local w = framedata[4]
+ local h = framedata[5]
+ local isSolid = framedata[6] or false
+ local anim = animationID or "null"
+ local frame = frameID or 0
+ local id = hitboxID or 0
+ if (sx < 0) then
+ ox = self.w - ox - w
+ end
+ if (sy < 0) then
+ oy = self.h - oy - h
+ end
+
+ if (type == "main") then
+ self.mainHitbox:modify(ox, oy, w, h)
+ else
+ local hitboxName = anim .. frame .. type .. id
+ self:addHitbox(hitboxName, type, ox, oy, w, h, isSolid)
+ return hitboxName
+ end
+end
+
+function Actor2D:initMainHitbox()
+ self.mainHitbox = Hitbox(self, self.type, 0, 0, self.w, self.h, self.isSolid)
+ self.mainHitbox:advertiseAsMainHitbox()
+end
+
+function Actor2D:addHitbox(name, type, ox, oy, w, h, isSolid)
+ if (self.hitboxes[name] ~= nil) then
+ core.debug:warning("actor2D", "the hitbox " .. name .. " already exists")
+ else
+ local hitbox = Hitbox(self, type, ox, oy, w, h, isSolid)
+ self.hitboxes[name] = hitbox
+ return hitbox
+ end
+end
+
+function Actor2D:checkHitboxCollisions(name, filter)
+ self:checkHitboxCollisionsAtPoint(name, self.x, self.y, filter)
+end
+
+function Actor2D:checkHitboxCollisionsAtPoint(name, dx, dy, filter)
+ local x, y, cols, colNumber = dx, dy, {}, 0
+ local filter = filter or self.filter
+ if (self.isDestroyed == false) and (self.hitboxes[name] ~= nil) then
+ x, y, cols, colNumber = self.hitboxes[name]:checkCollision(dx, dy, filter)
+ local type = self.hitboxes[name].type
+
+ for i, col in ipairs(cols) do
+ self:hitboxResponse(name, type, col)
+ end
+ end
+
+ return x, y, cols, colNumber
+end
+
+-- DRAW FUNCTIONS
+-- Draw the actors.
+
+function Actor2D:getShape()
+ return (self.x), (self.y), self.w, (self.h)
+end
+
+function Actor2D:draw()
+ self:drawStart()
+ local x, y = math.floor(self.x), math.floor(self.y)
+ self:drawSprite(x, y)
+ self:drawEnd()
+end
+
+return Actor2D
diff --git a/bootleg.love/core/modules/world/actors/actor3D.lua b/bootleg.love/core/modules/world/actors/actor3D.lua
new file mode 100644
index 0000000..c5f4208
--- /dev/null
+++ b/bootleg.love/core/modules/world/actors/actor3D.lua
@@ -0,0 +1,297 @@
+-- actor3D.lua :: the implementation of a 2D actor. It contain every element
+-- needed to create your own 2D actors.
+
+--[[
+ Copyright © 2019 Kazhnuz
+
+ 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.
+]]
+
+local cwd = (...):gsub('%.actor3D$', '') .. "."
+local BaseActor = require(cwd .. "baseactor")
+local Actor3D = BaseActor:extend()
+
+local Hitbox = require(cwd .. "utils.hitbox3D")
+local Boxes = require(cwd .. "utils.boxes")
+
+-- INIT FUNCTIONS
+-- Initialise the actor and its base functions
+
+function Actor3D:new(world, type, x, y, z, w, h, d, isSolid)
+ Actor3D.super.new(self, world, type, x, y, z, w, h, d, isSolid)
+ self:initHitboxes()
+ self.world:registerShape(self)
+ self.boxes = Boxes
+ self.doCastShadows = true
+end
+
+function Actor3D:destroy()
+ self:removeOldShadowTargets()
+ if self.box ~= nil then
+ self.world:removeTerrain(self)
+ end
+ self.world:removeActor(self)
+ self.mainHitbox:destroy()
+ self.world:removeShape(self)
+ self.isDestroyed = true
+end
+
+-- PHYSICS FUNCTIONS
+-- Handle movement and collisions.
+
+function Actor3D:autoMove(dt)
+ self:updateHitboxes()
+ self.onGround = false
+ self:applyGravity(dt)
+
+ local dx, dy, dz = self:getFuturePosition(dt)
+ local newx, newy, newz, cols, colNumber = self:move(dx, dy, dz)
+
+ -- apply after the movement the friction, until the player stop
+ -- note: the friction is applied according to the delta time,
+ -- thus the friction should be how much speed is substracted in 1 second
+
+ self:solveAllCollisions(cols)
+
+ self:applyFriction(dt)
+end
+
+function Actor3D:changeSpeedToCollisionNormal(normal)
+ local xsp, ysp, zsp = self.xsp, self.ysp, self.zsp
+ local nx, ny, nz = normal.x, normal.y, normal.z
+
+ if (nx < 0 and xsp > 0) or (nx > 0 and xsp < 0) then
+ xsp = -xsp * self.bounceFactor
+ end
+
+ if (ny < 0 and ysp > 0) or (ny > 0 and ysp < 0) then
+ ysp = -ysp * self.bounceFactor
+ end
+
+ if (nz < 0 and zsp > 0) or (nz > 0 and zsp < 0) then
+ zsp = -zsp * self.bounceFactor
+ end
+
+ self.xsp, self.ysp, self.zsp = xsp, ysp, zsp
+end
+
+function Actor3D:move(dx, dy, dz)
+ local cols, colNumber = {}, 0
+ local oldx, oldy, oldz = self.x, self.y, self.z
+ if (self.isDestroyed == false) then
+ self.x, self.y, self.z, cols, colNumber = self.mainHitbox:checkCollision(dx, dy, dz, self.filter)
+ self.mainHitbox:updatePosition()
+ self.world:updateShape(self)
+ end
+
+ if (oldx ~= self.x) or (oldy ~= self.y) or (oldz ~= self.z) or (self.shadowTargetsPrevious == nil) then
+ if (self.doCastShadows) then
+ self:castShadow()
+ end
+ end
+
+ return self.x, self.y, self.z, cols, colNumber
+end
+
+function Actor3D:checkCollision(dx, dy, dz)
+ local x, y, z, cols, colNumber = dx, dy, dz, {}, 0
+ if (self.isDestroyed == false) then
+ x, y, z, cols, colNumber = self.mainHitbox:checkCollision(dx, dy, dz, self.filter)
+ end
+ return self.x, self.y, self.z, cols, colNumber
+end
+
+-- GRAVITY SYSTEM FUNCTIONS
+-- All functions related to gravity
+
+function Actor3D:applyGravity(dt)
+ local grav = self.grav * -1
+ self.zsp = self.zsp + (grav * dt)
+
+ if utils.math.sign(self.zsp) == utils.math.sign(grav) then
+ self:checkGround( )
+ end
+end
+
+function Actor3D:checkGround()
+ local dx, dy, dz = self.x, self.y, self.z - utils.math.sign(self.grav)
+ local newx, newy, newz, cols, colNumber = self:checkCollision(dx, dy, dz)
+ for i, col in ipairs(cols) do
+ if (col.type == "touch") or (col.type == "bounce") or (col.type == "slide") then
+ if not (self.grav == 0) then
+ if col.normal.z == utils.math.sign(self.grav) then self.onGround = true end
+ end
+ end
+ end
+end
+
+-- COORDINATE/MOVEMENT FUNCTIONS
+-- Handle coordinate
+
+function Actor3D:getViewCenter()
+ local x, y, z = self:getCenter()
+ return x, y - (self.d/2)
+end
+
+-- HITBOXES FUNCTIONS
+-- Functions related to actor hitboxes
+
+function Actor3D:addHitboxFromFrameData(framedata, animationID, frameID, hitboxID)
+ local sx, sy = self:getSpriteScalling()
+ local type = framedata[1]
+ local ox = framedata[2]
+ local oy = framedata[3]
+ local oz = framedata[4]
+ local w = framedata[5]
+ local h = framedata[6]
+ local d = framedata[7]
+ local isSolid = framedata[8] or false
+ local anim = animationID or "null"
+ local frame = frameID or 0
+ local id = hitboxID or 0
+ if (sx < 0) then
+ ox = self.w - ox - w
+ end
+ if (sy < 0) then
+ oz = self.d - oz - d
+ end
+
+ if (type == "main") then
+ self.mainHitbox:modify(ox, oy, oz, w, h, d)
+ else
+ local hitboxName = anim .. frame .. type .. id
+ self:addHitbox(hitboxName, type, ox, oy, oz, w, h, d, isSolid)
+ return hitboxName
+ end
+end
+
+function Actor3D:initMainHitbox()
+ self.mainHitbox = Hitbox(self, self.type, 0, 0, 0, self.w, self.h, self.d, self.isSolid)
+ self.mainHitbox:advertiseAsMainHitbox()
+end
+
+function Actor3D:addHitbox(name, type, ox, oy, oz, w, h, d, isSolid)
+ if (self.hitboxes[name] ~= nil) then
+ core.debug:warning("actor3D", "the hitbox " .. name .. " already exists")
+ else
+ local hitbox = Hitbox(self, type, ox, oy, oz, w, h, d, isSolid)
+ self.hitboxes[name] = hitbox
+ return hitbox
+ end
+end
+
+function Actor3D:checkHitboxCollisions(name, filter)
+ self:checkHitboxCollisionsAtPoint(name, self.x, self.y, self.z, filter)
+end
+
+function Actor3D:checkHitboxCollisionsAtPoint(name, dx, dy, dz, filter)
+ local x, y, z, cols, colNumber = dx, dy, dz, {}, 0
+ local filter = filter or self.filter
+ if (self.isDestroyed == false) and (self.hitboxes[name] ~= nil) then
+ x, y, z, cols, colNumber = self.hitboxes[name]:checkCollision(dx, dy, dz, filter)
+ local type = self.hitboxes[name].type
+
+ for i, col in ipairs(cols) do
+ self:hitboxResponse(name, type, col)
+ end
+ end
+
+ return x, y, z, cols, colNumber
+end
+
+-- SHADOW FUNCTIONS
+-- Handle everything related to shadow
+
+function Actor3D:castShadow()
+ local shadowTargets = self.world:getTerrainInRect(self.x, self.y, self.w, self.d)
+ -- initialize the shadowTargetsPrevious variable if it doesn't exist
+ if (self.shadowTargetsPrevious == nil) then
+ self.shadowTargetsPrevious = {}
+ end
+
+ for i, target in ipairs(shadowTargets) do
+ -- We test if the actor is below the current actor
+ if (target ~= self) and (target.box ~= nil) then
+
+ if (target.z + target.d <= self.z + self.d) then
+ -- Remove the target of the list of item targeted last update,
+ -- in order to only have object no longer shadowed after the
+ -- end of the loop
+ for j, oldtarget in ipairs(self.shadowTargetsPrevious) do
+ if (target == oldtarget) then
+ table.remove(self.shadowTargetsPrevious, j)
+ end
+ end
+
+ -- We update the shadow source
+ local x, y = math.floor(self.x - target.x), math.floor(self.y - target.y)
+ target.box:setShadowSource(self, x, y)
+ end
+
+ end
+
+ end
+
+ -- At this point, if a target is still in the shadowTargetsPrevious list,
+ -- it mean that it's not shadowed. So we can simply remove the shadow.
+ self:removeOldShadowTargets()
+
+ self.shadowTargetsPrevious = shadowTargets
+end
+
+function Actor3D:removeOldShadowTargets()
+ if (self.shadowTargetsPrevious ~= nil) then
+ for i, target in ipairs(self.shadowTargetsPrevious) do
+ if (target.box ~= nil) then
+ target.box:removeShadowSource(self)
+ end
+ end
+ end
+end
+
+function Actor3D:redrawShadowCanvas()
+ if (self.box ~= nil) then
+ self.box:redrawShadowCanvas()
+ end
+end
+
+-- DRAW FUNCTIONS
+-- Draw the actors.
+
+function Actor3D:drawShadow(x, y)
+ love.graphics.setColor(0, 0, 0, 1)
+ love.graphics.rectangle("fill", x, y, self.w, self.h)
+ utils.graphics.resetColor()
+end
+
+function Actor3D:getShape()
+ return (self.x), (self.y - self.z - self.d), self.w, (self.h + self.d)
+end
+
+function Actor3D:draw()
+ self:drawStart()
+ if (self.box ~= nil) then
+ self.box:draw(self.x, self.y, self.z)
+ else
+ local x, y = math.floor(self.x), math.floor(self.y - self.z - self.d + (self.h/2))
+ self:drawSprite(x, y)
+ end
+ self:drawEnd()
+end
+
+return Actor3D
diff --git a/bootleg.love/core/modules/world/actors/baseactor.lua b/bootleg.love/core/modules/world/actors/baseactor.lua
new file mode 100644
index 0000000..f034895
--- /dev/null
+++ b/bootleg.love/core/modules/world/actors/baseactor.lua
@@ -0,0 +1,479 @@
+-- BaseActor.lua :: the global implementation of an actor. Basically, it abstract
+-- everything that isn't only 2D or 3D related to the actor system.
+
+--[[
+ Copyright © 2019 Kazhnuz
+
+ 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.
+]]
+
+local cwd = (...):gsub('%.baseactor$', '') .. "."
+local BaseActor = Object:extend()
+
+local Timer = require(cwd .. "utils.timer")
+
+-- INIT FUNCTIONS
+-- Initialise the actor and its base functions
+
+function BaseActor:new(world, type, x, y, z, w, h, d, isSolid)
+ self.type = type or ""
+ self.isSolid = isSolid or false
+ self.depth = 0
+
+ self:setManagers(world)
+ self:initKeys()
+ self:initTimers()
+ self:setSprite()
+ self:initPhysics(x, y, z, w, h, d)
+
+ self:setDebugColor(1, 1, 1)
+ self:register()
+end
+
+function BaseActor:setManagers(world)
+ self.world = world
+ self.scene = world.scene
+ self.obj = world.obj
+ self.assets = self.scene.assets
+end
+
+function BaseActor:setDebugColor(r,g,b)
+ self.debug = {}
+ self.debug.r = r
+ self.debug.g = g
+ self.debug.b = b
+end
+
+function BaseActor:register()
+ self.world:registerActor(self)
+ self.isDestroyed = false
+end
+
+function BaseActor:destroy()
+ self.world:removeActor(self)
+ self.isDestroyed = true
+end
+
+-- PHYSICS FUNCTIONS
+-- Raw implementation of everything common in physics
+
+function BaseActor:initPhysics(x, y, z, w, h, d)
+ self:setCoordinate(x, y, z)
+
+ self.w = w or 0
+ self.h = h or 0
+ self.d = d or 0
+
+ self.xsp = 0
+ self.ysp = 0
+ self.zsp = 0
+
+ self.xfrc = 0
+ self.yfrc = 0
+ self.zfrc = 0
+
+ self:initGravity()
+
+ self:setBounceFactor()
+ self:setFilter()
+end
+
+function BaseActor:setCoordinate(x, y, z, w, h, d)
+ self.x = x or self.x
+ self.y = y or self.y
+ self.z = z or self.z
+end
+
+function BaseActor:setBounceFactor(newBounceFactor)
+ self.bounceFactor = newBounceFactor or 0
+end
+
+function BaseActor:setFilter()
+ -- Init the bump filter
+ self.filter = function(item, other)
+ if (other.owner == self) then
+ -- ignore every collision with our own hitboxes
+ return nil
+ elseif (other.isSolid) then
+ return "slide"
+ else
+ return "cross"
+ end
+ end
+end
+
+function BaseActor:getFuturePosition(dt)
+ local dx, dy
+ dx = self.x + self.xsp * dt
+ dy = self.y + self.ysp * dt
+ dz = self.z + self.zsp * dt
+
+ return dx, dy, dz
+end
+
+function BaseActor:applyFriction(dt)
+ self.xsp = utils.math.toZero(self.xsp, self.xfrc * dt)
+ self.ysp = utils.math.toZero(self.ysp, self.yfrc * dt)
+ self.zsp = utils.math.toZero(self.zsp, self.zfrc * dt)
+end
+
+function BaseActor:solveAllCollisions(cols)
+ for i, col in ipairs(cols) do
+ self:collisionResponse(col)
+ if (col.type == "touch") or (col.type == "bounce") or (col.type == "slide") then
+ self:changeSpeedToCollisionNormal(col.normal)
+ end
+ end
+end
+
+function BaseActor:collisionResponse(collision)
+ -- here come the response to the collision
+end
+
+function BaseActor:changeSpeedToCollisionNormal(normal)
+ -- Empty function in baseactor
+end
+
+-- COORDINATE/MOVEMENT FUNCTIONS
+-- Handle coordinate
+
+function BaseActor:getCenter()
+ return (self.x + (self.w / 2)), (self.y + (self.h / 2)), (self.z + (self.d / 2))
+end
+
+function BaseActor:getViewCenter()
+ return self:getCenter()
+end
+
+-- GRAVITY SYSTEM FUNCTIONS
+-- All functions related to gravity
+
+function BaseActor:initGravity()
+ if (self.world.gravity.isDefault) then
+ self.grav = self.world.gravity.grav
+ else
+ self.grav = 0
+ end
+
+ self.onGround = false
+end
+
+function BaseActor:setGravity(grav)
+ -- It's basically now a function with two roles at once :
+ -- - activate the gravity
+ -- - use the gravity value the dev want
+
+ self.grav = grav or self.world.gravity.grav
+end
+
+function BaseActor:applyGravity(dt)
+ -- Empty function in baseactor
+end
+
+function BaseActor:checkGround()
+ -- Empty function in baseactor
+end
+
+-- UPDATE FUNCTIONS
+-- Theses functions are activated every steps
+
+function BaseActor:updateStart(dt)
+
+end
+
+function BaseActor:update(dt)
+ self:updateStart(dt)
+ self:updateTimers(dt)
+ self:autoMove(dt)
+ self:updateSprite(dt)
+ self:updateEnd(dt)
+end
+
+function BaseActor:updateEnd(dt)
+
+end
+
+function BaseActor:autoMove(dt)
+ -- The base actor don't have coordinate
+ -- so the autoMove is only usefull to its
+ -- 2D and 3D childrens
+end
+
+-- INPUT FUNCTIONS
+-- get input from the world object
+
+function BaseActor:initKeys()
+ self.keys = core.input.fakekeys
+end
+
+function BaseActor:getInput(keys)
+ self.keys = keys or core.input.fakekeys
+end
+
+-- TIMER FUNCTIONS
+-- Control the integrated timers of the actor
+
+function BaseActor:initTimers()
+ self.timers = core.modules.Timers(self)
+end
+
+function BaseActor:addTimer(name, t)
+ core.debug:warning("actor", "function actor:addTimer is deprecated, prefer actor.timers:newTimer")
+ self.timers:newTimer(t, name)
+end
+
+function BaseActor:updateTimers(dt)
+ self.timers:update(dt)
+end
+
+function BaseActor:timerResponse(name)
+ -- here come the timer responses
+end
+
+-- HITBOX FUNCTIONS
+-- All functions to handle hitboxes
+
+function BaseActor:initHitboxes()
+ self:initMainHitbox()
+
+ self.hitboxes = {}
+ self.hitboxListFile = ""
+ self.hitboxList = nil
+end
+
+function BaseActor:initMainHitbox()
+ -- Empty function : don't load ANY real hitbox function into baseactor
+end
+
+function BaseActor:setHitboxFile(file)
+ self.hitboxList = require(file)
+ self.hitboxListFile = file
+end
+
+function BaseActor:getAutomaticHitboxLoading()
+ return (self.hitboxList ~= nil)
+end
+
+function BaseActor:getHitboxFile()
+ return self.hitboxListFile
+end
+
+function BaseActor:getHitboxList(animation, frame)
+ if (animation == nil) or (self.hitboxList == nil) then
+ return self.hitboxList
+ else
+ local list = self.hitboxList[animation]
+
+ if (frame == nil) or (list == nil) then
+ return list
+ else
+ return list[frame]
+ end
+ end
+end
+
+function BaseActor:updateHitboxes()
+ if (self.hitboxList ~= nil) then
+ self:purgeHitbox()
+ local animation, frame
+ animation = self:getCurrentAnimation()
+ frame = self:getRelativeFrame()
+ local hitboxList = self:getHitboxList(animation, frame)
+
+ if (hitboxList ~= nil) then
+ for i,v in ipairs(hitboxList) do
+ self:addHitboxFromFrameData(v, animation, frame, i)
+ end
+ end
+ end
+end
+
+function BaseActor:checkHitboxesCollisions(filter)
+ for k,v in pairs(self.hitboxes) do
+ self:checkHitboxCollisions(k, filter)
+ end
+end
+
+function BaseActor:hitboxResponse(name, type, collision)
+ -- just a blank placeholder function
+end
+
+function BaseActor:removeHitbox(name)
+ if (self.hitboxes[name] ~= nil) then
+ self.hitboxes[name]:destroy()
+ self.hitboxes[name] = nil
+ end
+end
+
+function BaseActor:purgeHitbox()
+ for k,v in pairs(self.hitboxes) do
+ v:destroy()
+ end
+ self.hitboxes = {}
+end
+
+function BaseActor:drawHitboxes()
+ for k,v in pairs(self.hitboxes) do
+ v:draw()
+ end
+ self:drawMainHitbox()
+end
+
+function BaseActor:drawMainHitbox()
+ if (self.mainHitbox ~= nil) then
+ self.mainHitbox:draw()
+ end
+end
+
+-- DRAW FUNCTIONS
+-- Draw the actors.
+
+function BaseActor:drawStart()
+
+end
+
+function BaseActor:draw()
+ self:drawStart()
+ self:drawEnd()
+end
+
+function BaseActor:drawEnd()
+
+end
+
+function BaseActor:drawHUD(id, height, width)
+
+end
+
+-- SPRITES FUNCTIONS
+-- Handle the sprite of the actor
+
+function BaseActor:setSprite(spritename, ox, oy)
+ self.sprite = {}
+ self.sprite.name = spritename or nil
+ self.sprite.ox = ox or 0
+ self.sprite.oy = oy or 0
+ self.sprite.sx = 1
+ self.sprite.sy = 1
+ self.sprite.exist = (spritename ~= nil)
+ self.sprite.clone = nil
+end
+
+function BaseActor:cloneSprite()
+ if self.sprite.name ~= nil then
+ self.sprite.clone = self.assets.sprites[self.sprite.name]:clone()
+ self.sprite.clone:setCallbackTarget(self)
+ end
+end
+
+function BaseActor:changeAnimation(animation, restart)
+ if (self.sprite.clone == nil) then
+ self.assets.sprites[self.sprite.name]:changeAnimation(animation, restart)
+ else
+ self.sprite.clone:changeAnimation(animation, restart)
+ end
+end
+
+function BaseActor:animationEnded(animation)
+ -- Empty placeholder function
+end
+
+function BaseActor:setCustomSpeed(customSpeed)
+ if (self.sprite.clone == nil) then
+ self.assets.sprites[self.sprite.name]:setCustomSpeed(customSpeed)
+ else
+ self.sprite.clone:setCustomSpeed(customSpeed)
+ end
+end
+
+function BaseActor:updateSprite(dt)
+ if (self.sprite.clone ~= nil) then
+ self.sprite.clone:update(dt)
+ end
+end
+
+function BaseActor:setSpriteScallingX(sx)
+ local sx = sx or 1
+
+ self.sprite.sx = sx
+end
+
+function BaseActor:setSpriteScallingY(sy)
+ local sy = sy or 1
+
+ self.sprite.sy = sy
+end
+
+function BaseActor:getCurrentAnimation()
+ if (self.sprite.clone == nil) then
+ return self.assets.sprites[self.sprite.name]:getCurrentAnimation()
+ else
+ return self.sprite.clone:getCurrentAnimation()
+ end
+end
+
+
+function BaseActor:getSpriteScalling()
+ return self.sprite.sx, self.sprite.sy
+end
+
+function BaseActor:getFrame()
+ if (self.sprite.name ~= nil) then
+ if (self.sprite.clone ~= nil) then
+ return self.sprite.clone:getFrame()
+ else
+ return self.assets.sprites[self.sprite.name]:getFrame()
+ end
+ end
+end
+
+function BaseActor:getRelativeFrame()
+ if (self.sprite.name ~= nil) then
+ if (self.sprite.clone ~= nil) then
+ return self.sprite.clone:getRelativeFrame()
+ else
+ return self.assets.sprites[self.sprite.name]:getRelativeFrame()
+ end
+ end
+end
+
+function BaseActor:getAnimationDuration()
+ if (self.sprite.name ~= nil) then
+ if (self.sprite.clone ~= nil) then
+ return self.sprite.clone:getAnimationDuration()
+ else
+ return self.assets.sprites[self.sprite.name]:getAnimationDuration()
+ end
+ end
+end
+
+function BaseActor:drawSprite(x, y, r, sx, sy, ox, oy, kx, ky)
+ if (self.sprite.name ~= nil) then
+ local x = x + self.sprite.ox
+ local y = y + self.sprite.oy
+ local sx = sx or self.sprite.sx
+ local sy = sy or self.sprite.sy
+ if (self.sprite.clone ~= nil) then
+ self.sprite.clone:draw(x, y, r, sx, sy, ox, oy, kx, ky)
+ else
+ self.assets.sprites[self.sprite.name]:drawAnimation(x, y, r, sx, sy, ox, oy, kx, ky)
+ end
+ end
+end
+
+return BaseActor
diff --git a/bootleg.love/core/modules/world/actors/gfx2D.lua b/bootleg.love/core/modules/world/actors/gfx2D.lua
new file mode 100644
index 0000000..900654c
--- /dev/null
+++ b/bootleg.love/core/modules/world/actors/gfx2D.lua
@@ -0,0 +1,41 @@
+-- gfx.lua :: a basic 2D GFX.
+
+--[[
+ Copyright © 2019 Kazhnuz
+
+ 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.
+]]
+
+local cwd = (...):gsub('%.gfx2D$', '') .. "."
+local Actor2D = require(cwd .. "actor2D")
+local GFX = Actor2D:extend()
+
+function GFX:new(world, x, y, spritename)
+ local width, height = world.scene.assets.sprites[spritename]:getDimensions()
+
+ GFX.super.new(self, world, "gfx", x - (width/2), y - (height/2), width, height)
+ self:setSprite(spritename)
+ self:cloneSprite()
+end
+
+function GFX:animationEnded(animation)
+ core.debug:print("gfx2D", 'Current animation "' .. animation .. '" have ended, destroying gfx')
+ self:destroy()
+end
+
+return GFX
diff --git a/bootleg.love/core/modules/world/actors/gfx3D.lua b/bootleg.love/core/modules/world/actors/gfx3D.lua
new file mode 100644
index 0000000..fa40c05
--- /dev/null
+++ b/bootleg.love/core/modules/world/actors/gfx3D.lua
@@ -0,0 +1,41 @@
+-- gfx.lua :: a basic 2D GFX.
+
+--[[
+ Copyright © 2019 Kazhnuz
+
+ 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.
+]]
+
+local cwd = (...):gsub('%.gfx3D$', '') .. "."
+local Actor3D = require(cwd .. "actor3D")
+local GFX = Actor3D:extend()
+
+function GFX:new(world, x, y, z, spritename)
+ local width, height = world.scene.assets.sprites[spritename]:getDimensions()
+
+ GFX.super.new(self, world, "gfx", x - (width/2), y - (width/2), z - (height/2), width, width, height)
+ self:setSprite(spritename)
+ self:cloneSprite()
+end
+
+function GFX:animationEnded(animation)
+ core.debug:print("gfx2D", 'Current animation "' .. animation .. '" have ended, destroying gfx')
+ self:destroy()
+end
+
+return GFX
diff --git a/bootleg.love/core/modules/world/actors/utils/boxes/init.lua b/bootleg.love/core/modules/world/actors/utils/boxes/init.lua
new file mode 100644
index 0000000..922f7b1
--- /dev/null
+++ b/bootleg.love/core/modules/world/actors/utils/boxes/init.lua
@@ -0,0 +1,32 @@
+-- box3D :: drawable box with shadow handling for fake3D actors
+
+--[[
+ Copyright © 2019 Kazhnuz
+
+ 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.
+]]
+
+local cwd = (...):gsub('%.init$', '') .. "."
+
+local Boxes = {}
+
+Boxes.Base = require(cwd .. "parent")
+Boxes.Textured = require(cwd .. "textured")
+Boxes.Mapped = require(cwd .. "mapped")
+
+return Boxes
diff --git a/bootleg.love/core/modules/world/actors/utils/boxes/mapped.lua b/bootleg.love/core/modules/world/actors/utils/boxes/mapped.lua
new file mode 100644
index 0000000..e733cab
--- /dev/null
+++ b/bootleg.love/core/modules/world/actors/utils/boxes/mapped.lua
@@ -0,0 +1,51 @@
+-- mapped.lua :: a sti-mapped box
+
+--[[
+ Copyright © 2019 Kazhnuz
+
+ 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.
+]]
+
+local cwd = (...):gsub('%.mapped$', '') .. "."
+local Box3D = require(cwd .. "parent")
+
+local MappedBox = Box3D:extend()
+
+function MappedBox:new(owner, x, y, z, w, h, d)
+ self.x = x
+ self.y = y
+ self.z = z
+
+ MappedBox.super.new(self, owner, w, h, d)
+ self.haveLine = false
+end
+
+function MappedBox:drawTextureContent()
+ local tx, ty = self.x, self.y - (self.z + self.d)
+ core.debug:print("mappedbox", "getting map layers at position " .. tx .. ";" .. ty)
+ love.graphics.push()
+ love.graphics.origin()
+ love.graphics.translate(math.floor(-tx), math.floor(-ty))
+
+ self.world:drawMap()
+
+ love.graphics.pop()
+end
+
+
+return MappedBox
diff --git a/bootleg.love/core/modules/world/actors/utils/boxes/parent.lua b/bootleg.love/core/modules/world/actors/utils/boxes/parent.lua
new file mode 100644
index 0000000..45c570b
--- /dev/null
+++ b/bootleg.love/core/modules/world/actors/utils/boxes/parent.lua
@@ -0,0 +1,164 @@
+-- box3D :: drawable box with shadow handling for fake3D actors
+
+--[[
+ Copyright © 2019 Kazhnuz
+
+ 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.
+]]
+
+local Box3D = Object:extend()
+
+function Box3D:new(owner, w, h, d, isVisible)
+ self.owner = owner
+ self.world = owner.world
+ self.cameras = self.world.cameras
+
+ self.w = w
+ self.h = h
+ self.d = d
+
+ self.haveLine = true
+
+ self.shadowSources = {}
+ self.needRedraw = false
+
+ if (isVisible == nil) then
+ self.isVisible = true
+ else
+ self.isVisible = isVisible
+ end
+
+ if (self.isVisible) then
+ self:setTexture()
+ end
+
+ self.shadows = love.graphics.newCanvas(self.w, self.h)
+
+ self:register()
+end
+
+function Box3D:register()
+ self.owner.box = self
+ self.world:registerTerrain(self.owner)
+end
+
+function Box3D:setSizeFromOwner()
+ self:setSize(self.owner.w, self.owner.h, self.owner.d)
+end
+
+function Box3D:setSize()
+ self.w = w
+ self.h = h
+ self.d = d
+
+ self:regenerate()
+end
+
+function Box3D:setTexture()
+ local canvas = love.graphics.newCanvas(self.w, self.h + self.d)
+ love.graphics.setCanvas( canvas )
+ utils.graphics.resetColor()
+
+ self:drawTextureContent()
+
+ love.graphics.setCanvas( )
+ local imagedata = canvas:newImageData()
+ self.texture = love.graphics.newImage( imagedata )
+ imagedata:release()
+ canvas:release()
+end
+
+function Box3D:drawTextureContent()
+ self:drawTopTexture()
+ self:drawBottomTexture()
+end
+
+function Box3D:drawTopTexture()
+ utils.graphics.resetColor()
+ love.graphics.rectangle("fill", 0, 0, self.w, self.h)
+end
+
+function Box3D:drawBottomTexture()
+ love.graphics.setColor(0.9, 0.9, 0.9, 1)
+ love.graphics.rectangle("fill", 0, self.h, self.w, self.d)
+end
+
+function Box3D:setShadowSource(actor, x, y)
+ local foundShadow = false
+
+ for i,v in ipairs(self.shadowSources) do
+ if (v.actor == actor) then
+ if (v.x ~= x) or (v.y ~= y) then
+ v.x = x
+ v.y = y
+ self.needRedraw = true
+ end
+ foundShadow = true
+ end
+ end
+
+ if (foundShadow == false) then
+ local shadow = {}
+ shadow.actor = actor
+ shadow.x = x
+ shadow.y = y
+ self.needRedraw = true
+
+ table.insert(self.shadowSources, shadow)
+ end
+end
+
+function Box3D:removeShadowSource(actor)
+ for i,v in ipairs(self.shadowSources) do
+ if (v.actor == actor) then
+ table.remove(self.shadowSources, i)
+ self.needRedraw = true
+ end
+ end
+end
+
+function Box3D:redrawShadowCanvas()
+ if (self.needRedraw) then
+ love.graphics.setCanvas( self.shadows )
+ love.graphics.clear()
+ for i,v in ipairs(self.shadowSources) do
+ v.actor:drawShadow(v.x, v.y)
+ end
+
+ love.graphics.setCanvas( )
+
+ self.needRedraw = false
+ end
+end
+
+function Box3D:draw(x, y, z)
+ if (self.isVisible) then
+ love.graphics.setColor(0, 0, 0, 1)
+ if (self.haveLine) then
+ love.graphics.rectangle("line", x, (y-z) - (self.d), self.w, self.d + self.h)
+ end
+ utils.graphics.resetColor()
+ love.graphics.draw(self.texture, x, (y-z) - (self.d))
+ end
+
+ if (self.shadows ~= nil) and (#self.shadowSources > 0) then
+ love.graphics.draw(self.shadows, x, (y-z) - (self.d))
+ end
+end
+
+return Box3D
diff --git a/bootleg.love/core/modules/world/actors/utils/boxes/textured.lua b/bootleg.love/core/modules/world/actors/utils/boxes/textured.lua
new file mode 100644
index 0000000..1b79162
--- /dev/null
+++ b/bootleg.love/core/modules/world/actors/utils/boxes/textured.lua
@@ -0,0 +1,53 @@
+-- textured.lua :: a textured box
+
+--[[
+ Copyright © 2019 Kazhnuz
+
+ 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.
+]]
+
+local cwd = (...):gsub('%.textured$', '') .. "."
+local Box3D = require(cwd .. "parent")
+
+local TexturedBox = Box3D:extend()
+
+function TexturedBox:new(owner, w, h, d, topTexture, bottomTexture)
+ local bottomTexture = bottomTexture or topTexture
+
+ self.topTexture = owner.assets.images[topTexture]
+ self.bottomTexture = owner.assets.images[bottomTexture]
+
+ TexturedBox.super.new(self, owner, w, h, d)
+ self.haveLine = false
+end
+
+function TexturedBox:drawTopTexture()
+ local w, h = self.topTexture:getDimensions()
+ local sx = self.w / w
+ local sy = self.h / h
+ self.topTexture:draw(0, 0, 0, sx, sy)
+end
+
+function TexturedBox:drawBottomTexture()
+ local w, h = self.bottomTexture:getDimensions()
+ local sx = self.w / w
+ local sy = self.d / h
+ self.bottomTexture:draw(0, self.h, 0, sx, sy)
+end
+
+return TexturedBox
diff --git a/bootleg.love/core/modules/world/actors/utils/hitbox2D.lua b/bootleg.love/core/modules/world/actors/utils/hitbox2D.lua
new file mode 100644
index 0000000..6728908
--- /dev/null
+++ b/bootleg.love/core/modules/world/actors/utils/hitbox2D.lua
@@ -0,0 +1,123 @@
+-- hitbox2D.lua :: a basic 2D hitbox object. It's used by the actors to check
+-- collisions and to handle different type of responses.
+
+--[[
+ Copyright © 2019 Kazhnuz
+
+ 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.
+]]
+
+local Hitbox2D = Object:extend()
+
+-- INIT FUNCTIONS
+-- Initialise the actor and its base functions
+
+function Hitbox2D:new(owner, type, ox, oy, w, h, isSolid)
+ self.owner = owner
+ self.world = owner.world
+
+ self.type = type
+ self.ox = ox
+ self.oy = oy
+ self.x, self.y = self:getPosition()
+ self.w = w
+ self.h = h
+ self.isSolid = isSolid
+
+ self.isMainHitBox = false
+
+ self:setDebugColor(0,0,0)
+ self:register()
+end
+
+function Hitbox2D:advertiseAsMainHitbox()
+ self.isMainHitBox = true
+end
+
+function Hitbox2D:modify(ox, oy, w, h)
+ self.ox = ox
+ self.oy = oy
+ self.x, self.y = self:getPosition()
+ self.w = w
+ self.h = h
+end
+
+function Hitbox2D:setDebugColor(r,g,b)
+ self.debug = {}
+ self.debug.r = r
+ self.debug.g = g
+ self.debug.b = b
+end
+
+function Hitbox2D:register()
+ self.world:registerBody(self)
+end
+
+function Hitbox2D:destroy()
+ self.world:removeBody(self)
+end
+
+-- COORDINATE FUNCTIONS
+-- Handle Hitbox position
+
+function Hitbox2D:updatePosition()
+ self.x, self.y = self:getPosition()
+ self.world:updateBody(self)
+ return self.x, self.y
+end
+
+function Hitbox2D:getPosition()
+ return self.ox + self.owner.x, self.oy + self.owner.y
+end
+
+function Hitbox2D:getOwnerPosition()
+ return self.x - self.ox, self.y - self.oy
+end
+
+function Hitbox2D:getNewOwnerPosition(x, y)
+ return x - self.ox, y - self.oy
+end
+
+function Hitbox2D:getCenter()
+ return self.x + (self.w/2), self.y + (self.h/2)
+end
+
+-- COLLISION FUNCTIONS
+-- Handle Hitbox position
+
+function Hitbox2D:checkCollision(dx, dy, filter)
+ self:updatePosition()
+
+ local dx, dy = self.ox + dx, self.oy + dy
+ local x, y, cols, colNumber = self.world:checkCollision(self, dx, dy, filter)
+ local newx, newy = self:getNewOwnerPosition(x, y)
+
+ return newx, newy, cols, colNumber
+end
+
+-- DRAW FUNCTIONS
+-- Just some debug function to draw hitbox
+
+function Hitbox2D:draw()
+ local x, y = self:getPosition()
+ love.graphics.setColor(self.debug.r, self.debug.g, self.debug.b, 1)
+ utils.graphics.box(x, y, self.w, self.h)
+ utils.graphics.resetColor()
+end
+
+return Hitbox2D
diff --git a/bootleg.love/core/modules/world/actors/utils/hitbox3D.lua b/bootleg.love/core/modules/world/actors/utils/hitbox3D.lua
new file mode 100644
index 0000000..61d444c
--- /dev/null
+++ b/bootleg.love/core/modules/world/actors/utils/hitbox3D.lua
@@ -0,0 +1,129 @@
+-- hitbox3D.lua :: a basic 3D hitbox object. It's used by the actors to check
+-- collisions and to handle different type of responses.
+
+--[[
+ Copyright © 2019 Kazhnuz
+
+ 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.
+]]
+
+local Hitbox3D = Object:extend()
+
+-- INIT FUNCTIONS
+-- Initialise the actor and its base functions
+
+function Hitbox3D:new(owner, type, ox, oy, oz, w, h, d, isSolid)
+ self.owner = owner
+ self.world = owner.world
+
+ self.type = type
+ self.ox = ox
+ self.oy = oy
+ self.oz = oz
+ self.x, self.y, self.z = self:getPosition()
+ self.w = w
+ self.h = h
+ self.d = d
+ self.isSolid = isSolid
+
+ self.isMainHitBox = false
+
+ self:setDebugColor(0,0,0)
+ self:register()
+end
+
+function Hitbox3D:advertiseAsMainHitbox()
+ self.isMainHitBox = true
+end
+
+function Hitbox3D:modify(ox, oy, oz, w, h, d)
+ self.ox = ox
+ self.oy = oy
+ self.oz = oz
+ self.x, self.y, self.z = self:getPosition()
+ self.w = w
+ self.h = h
+ self.d = d
+end
+
+function Hitbox3D:setDebugColor(r,g,b)
+ self.debug = {}
+ self.debug.r = r
+ self.debug.g = g
+ self.debug.b = b
+end
+
+function Hitbox3D:register()
+ self.world:registerBody(self)
+end
+
+function Hitbox3D:destroy()
+ self.world:removeBody(self)
+end
+
+-- COORDINATE FUNCTIONS
+-- Handle Hitbox position
+
+function Hitbox3D:updatePosition()
+ self.x, self.y, self.z = self:getPosition()
+ self.world:updateBody(self)
+ return self.x, self.y, self.z
+end
+
+function Hitbox3D:getPosition()
+ return self.ox + self.owner.x, self.oy + self.owner.y, self.oz + self.owner.z
+end
+
+function Hitbox3D:getOwnerPosition()
+ return self.x - self.ox, self.y - self.oy, self.z - self.oz
+end
+
+function Hitbox3D:getNewOwnerPosition(x, y, z)
+ return x - self.ox, y - self.oy, z - self.oz
+end
+
+function Hitbox3D:getCenter()
+ return self.x + (self.w/2), self.y + (self.h/2), self.z + (self.d/2)
+end
+
+-- COLLISION FUNCTIONS
+-- Handle Hitbox position
+
+function Hitbox3D:checkCollision(dx, dy, dz, filter)
+ self:updatePosition()
+
+ local dx, dy = self.ox + dx, self.oy + dy, self.oz + dz
+ local x, y, z, cols, colNumber = self.world:checkCollision(self, dx, dy, dz, filter)
+ local newx, newy, newz = self:getNewOwnerPosition(x, y, z)
+
+ return newx, newy, newz, cols, colNumber
+end
+
+-- DRAW FUNCTIONS
+-- Just some debug function to draw hitbox
+
+function Hitbox3D:draw()
+ local x, y, z = self:getPosition()
+ love.graphics.setColor(self.debug.r, self.debug.g, self.debug.b, 1)
+ utils.graphics.box(x, (y-z) - (self.d), self.w, self.h)
+ love.graphics.setColor(self.debug.r/2, self.debug.g/2, self.debug.b/2, 1)
+ utils.graphics.box(x, (y-z) - (self.d) + (self.h), self.w, self.d)
+ utils.graphics.resetColor()
+end
+
+return Hitbox3D
diff --git a/bootleg.love/core/modules/world/actors/utils/timer.lua b/bootleg.love/core/modules/world/actors/utils/timer.lua
new file mode 100644
index 0000000..5ba7c3f
--- /dev/null
+++ b/bootleg.love/core/modules/world/actors/utils/timer.lua
@@ -0,0 +1,44 @@
+-- timer.lua :: a basic implementation of a timer for the actor system.
+
+--[[
+ Copyright © 2019 Kazhnuz
+
+ 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.
+]]
+
+local Timer = Object:extend()
+
+function Timer:new(actor, name, t)
+ self.time = t
+ self.actor = actor
+ self.name = name
+end
+
+function Timer:update(dt)
+ self.time = self.time - dt
+ if (self.time <= 0) then
+ self:finish()
+ end
+end
+
+function Timer:finish()
+ self.actor:timerResponse(self.name)
+ self.actor.timers[self.name] = nil
+end
+
+return Timer
diff --git a/bootleg.love/core/modules/world/baseworld.lua b/bootleg.love/core/modules/world/baseworld.lua
new file mode 100644
index 0000000..3804867
--- /dev/null
+++ b/bootleg.love/core/modules/world/baseworld.lua
@@ -0,0 +1,413 @@
+-- baseworld.lua :: the base world object, that contain just a fast implementation
+-- of a 2D world. It doesn't support collision and stuff.
+
+--[[
+ Copyright © 2019 Kazhnuz
+
+ 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.
+]]
+
+local cwd = (...):gsub('%.baseworld$', '') .. "."
+
+local BaseWorld = Object:extend()
+
+local mapObjects = require(cwd .. "maps")
+local CameraSystem = require(cwd .. "camera")
+
+local PADDING_VALUE = 10/100
+
+-- INIT FUNCTIONS
+-- All functions to init the world and the map
+
+function BaseWorld:new(scene, actorlist, mapfile, maptype)
+ self.scene = scene
+ self.actorlist = actorlist
+ self.mapfile = mapfile
+ self.mapObjects = mapObjects
+
+ self.cameras = CameraSystem(self)
+ self:initActors()
+
+ self:initPlayers()
+ self:setActorList(self.actorlist)
+ self:initMap(self.mapfile, maptype)
+ self:setGravity()
+
+ self:register()
+
+ self.isActive = true
+end
+
+function BaseWorld:setActorList(actorlist)
+ if actorlist == nil then
+ error("FATAL: You must set an actorlist to your world")
+ else
+ self.obj = require(actorlist)
+ end
+end
+
+function BaseWorld:initMap(mapfile, maptype)
+ if (mapfile ~= nil) then
+ self.maptype = maptype or "sti"
+ else
+ self.maptype = "empty"
+ end
+
+ self.mapfile = mapfile
+end
+
+function BaseWorld:setGravity(grav, isDefault)
+ local grav = grav or 0
+ local isDefault = isDefault or false
+
+ self.gravity = {}
+ self.gravity.grav = grav
+ self.gravity.isDefault = isDefault
+end
+
+function BaseWorld:register()
+ self.scene:registerWorld(self)
+end
+
+function BaseWorld:reset()
+ self:initActors()
+ self:initPlayers()
+ self:initMap(self.mapfile, self.maptype)
+ self.cameras:initViews()
+ collectgarbage()
+
+ self:loadMap()
+end
+
+-- ACTOR MANAGEMENT FUNCTIONS
+-- Basic function to handle actors
+
+function BaseWorld:initActors( )
+ self.actors = {}
+ self.currentCreationID = 0
+end
+
+function BaseWorld:newActor(name, x, y, z)
+ local debugstring = " at (" .. x .. ";" .. y .. ")."
+ core.debug:print("world2D", "adding actor " .. name .. debugstring)
+ self.obj.index[name](self, x, y)
+end
+
+function BaseWorld:newCollision(name, x, y, z, w, h, d)
+ local debugstringpos = "at (" .. x .. ";" .. y .. ")"
+ local debugstringsize = "size is (" .. w .. ";" .. h .. ")"
+ local debugstring = " " .. debugstringpos .. ". " .. debugstringsize .. "."
+ core.debug:print("world2D", "creating collision " .. name .. debugstring)
+ self.obj.collisions[name](self, x, y, w, h)
+end
+
+function BaseWorld:registerActor(actor)
+ actor.creationID = self.currentCreationID
+ self.currentCreationID = self.currentCreationID + 1
+ table.insert(self.actors, actor)
+end
+
+function BaseWorld:removeActor(actor)
+ for i,v in ipairs(self.actors) do
+ if v == actor then
+ table.remove(self.actors, i)
+ end
+ end
+end
+
+function BaseWorld:countActors()
+ return #self.actors
+end
+
+function BaseWorld:getActors()
+ return self.actors
+end
+
+function BaseWorld:getActorsInRect(x, y, w, h)
+ local query = {}
+ local x2, y2 = x + w, y + h
+ for i,v in ipairs(self.actors) do
+ if (v.x >= x) and (v.x + v.w >= x1) and
+ (v.y >= y) and (v.y + v.h >= y1) then
+
+ table.insert(query, v)
+ end
+ end
+
+ return v
+end
+
+
+function BaseWorld:getVisibleActors(id)
+ local actors = {}
+ if (id ~= nil) then
+ local camx, camy, camw, camh = self.cameras:getViewCoordinate(id)
+ local paddingw = camw * PADDING_VALUE
+ local paddingh = camh * PADDING_VALUE
+ local x = camx - paddingw
+ local y = camy - paddingh
+ local w = camw + paddingw * 2
+ local h = camh + paddingh * 2
+
+ actors = self:getActorsInRect(x, y, w, h)
+ else
+ actors = self:getActors()
+ end
+
+ table.sort(actors, function(a,b)
+ if (a.depth == b.depth) then
+ return a.creationID < b.creationID
+ else
+ return a.depth > b.depth
+ end
+ end)
+
+ return actors
+end
+
+-- BODIES MANAGEMENT FUNCTIONS
+-- Basic function to handle bodies. Empty function here as baseworld doesn't
+-- handle bodies
+
+function BaseWorld:registerBody(body)
+ return nil
+end
+
+function BaseWorld:updateBody(body)
+ return x, y, {}, 0
+end
+
+function BaseWorld:removeBody(body)
+ return nil
+end
+
+function BaseWorld:moveActor(actor, x, y, filter)
+ -- as the baseworld have no collision function, we return empty collision
+ -- datas, but from the same type than bump2D will return
+ return x, y, {}, 0
+end
+
+function BaseWorld:checkCollision(actor, x, y, filter)
+ -- as the baseworld have no collision function, we return empty collision
+ -- datas, but from the same type than bump2D will return
+ return x, y, {}, 0
+end
+
+function BaseWorld:getBodiesInRect(x, y, w, h)
+ return {}
+end
+
+-- INFO FUNCTIONS
+-- Give infos about the world
+
+function BaseWorld:isActorIndexed(name)
+ return (self.obj.index[name] ~= nil)
+end
+
+function BaseWorld:isCollisionIndexed(name)
+ return (self.obj.collisions[name] ~= nil)
+end
+
+-- PLAYER MANAGEMENT FUNCTIONS
+-- Basic function to handle player actors
+
+function BaseWorld:initPlayers()
+ self.players = {}
+ self.playerNumber = 1
+end
+
+function BaseWorld:setPlayerNumber(playerNumber)
+ self.playerNumber = playerNumber or 1
+end
+
+function BaseWorld:addPlayer(x, y, z, id)
+ local player = {}
+ if id <= self.playerNumber then
+ player.actor = self:newPlayer(x, y, z)
+ player.sourceid = id or 1
+
+ table.insert(self.players, player)
+
+ self.cameras:addTarget(player.actor)
+ end
+end
+
+function BaseWorld:newPlayer(x, y, z)
+ return self.obj.Player(self, x, y)
+end
+
+function BaseWorld:sendInputToPlayers(actor)
+ for i, player in ipairs(self.players) do
+ local keys = self.scene:getKeys(player.sourceid)
+ player.actor:getInput(keys)
+ end
+end
+
+function BaseWorld:removePlayer(actor)
+ for i,v in ipairs(self.players) do
+ if (v.actor == actor) then
+ table.remove(self.players, i)
+ end
+ end
+end
+
+-- MAP FUNCTIONS
+-- All map wrappers
+
+function BaseWorld:loadMap()
+ self:createMapController()
+ self:loadMapObjects()
+end
+
+function BaseWorld:createMapController()
+ if (self.maptype == "empty") then
+ self.mapObjects.Base(self)
+ elseif (self.maptype == "sti") then
+ self.mapObjects.Sti(self, self.mapfile)
+ end
+end
+
+function BaseWorld:loadMapObjects()
+ if (self.map ~= nil) then
+ self.map:loadObjects()
+ end
+end
+
+function BaseWorld:getBox()
+ if (self.map ~= nil) then
+ return self.map:getBox()
+ else
+ local w, h = core.screen:getDimensions()
+ return 0, 0, w, h
+ end
+end
+
+function BaseWorld:getDimensions()
+ if (self.map ~= nil) then
+ return self.map:getDimensions()
+ else
+ return core.screen:getDimensions()
+ end
+end
+
+function BaseWorld:setBackgroundColor(r, g, b)
+ if (self.map ~= nil) then
+ self.map:setBackgroundColor(r, g, b)
+ end
+end
+
+function BaseWorld:removeBackgroundColor()
+ if (self.map ~= nil) then
+ self.map:setBackgroundColor()
+ end
+end
+
+function BaseWorld:getBackgroundColor()
+ if (self.map ~= nil) then
+ return self.map:getBackgroundColor()
+ else
+ return 0, 0, 0
+ end
+end
+
+function BaseWorld:resizeMap(w, h)
+ if (self.map ~= nil) then
+ local w, h = utils.math.floorCoord(w, h)
+ self.map:resize(w, h)
+ end
+end
+
+-- Lock MANAGEMENT FUNCTIONS
+-- Basic function to handle the lock
+
+function BaseWorld:setActivity(activity)
+ self.isActive = activity
+end
+
+function BaseWorld:switchActivity()
+ self.isActive = (self.isActive == false)
+end
+
+function BaseWorld:getActivity()
+ return self.isActive
+end
+
+-- UPDATE FUNCTIONS
+-- All update functions
+
+function BaseWorld:update(dt)
+ self:updateMap(dt)
+ self:sendInputToPlayers(dt)
+ self:updateActors(dt)
+ self.cameras:update(dt)
+end
+
+function BaseWorld:updateActors(dt)
+ local actors = self:getActors()
+ for i,v in ipairs(actors) do
+ v:update(dt)
+ end
+end
+
+function BaseWorld:updateMap(dt)
+ if (self.map ~= nil) then
+ self.map:update(dt)
+ end
+end
+
+-- DRAW FUNCTIONS
+-- All function to draw the map, world and actors
+
+function BaseWorld:draw(dt)
+ self:drawBackgroundColor()
+ local camNumber = self.cameras:getViewNumber()
+
+ if (camNumber == 0) then
+ self:drawMap()
+ self:drawActors()
+ else
+ for i=1, camNumber do
+ self.cameras:attachView(i)
+ self:drawMap(i)
+ self:drawActors(i)
+ self.cameras:detachView(i)
+ end
+ end
+end
+
+function BaseWorld:drawActors(id)
+ local actors = self:getVisibleActors(id)
+
+ for i,v in ipairs(actors) do
+ v:draw()
+ end
+end
+
+function BaseWorld:drawMap(id)
+ if (self.map ~= nil) then
+ self.map:draw()
+ end
+end
+
+function BaseWorld:drawBackgroundColor()
+ if (self.map ~= nil) then
+ self.map:drawBackgroundColor()
+ end
+end
+
+return BaseWorld
diff --git a/bootleg.love/core/modules/world/camera/init.lua b/bootleg.love/core/modules/world/camera/init.lua
new file mode 100644
index 0000000..9a136c4
--- /dev/null
+++ b/bootleg.love/core/modules/world/camera/init.lua
@@ -0,0 +1,401 @@
+-- camera.lua :: a basic camera adapted to the asset/world system.
+
+--[[
+ Copyright © 2019 Kazhnuz
+
+ 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.
+]]
+
+local cwd = (...):gsub('%.init$', '') .. "."
+
+local CameraSystem = Object:extend()
+--local View = require(cwd .. "libs.hump.camera")
+
+local camutils = require(cwd .. "utils")
+
+local SPLITSCREEN_ISVERTICAL = false
+local SCREEN_LIMIT = 4
+
+-- INIT FUNCTIONS
+-- Initialize the camera system
+
+function CameraSystem:new(world)
+ self.scene = world.scene
+ self.world = world
+
+ self.mode = "split"
+ self.verticalSplit = SPLITSCREEN_ISVERTICAL
+ self.targets = {}
+
+ self.xLocked = nil
+ self.yLocked = nil
+
+ self:initViews()
+end
+
+function CameraSystem:setMode(mode)
+ self.mode = mode
+ core.debug:print("camera", "mode is now set to " .. mode)
+ return mode
+end
+
+function CameraSystem:initViews()
+ self.views = {}
+
+ self.views.list = {}
+ self.views.basewidth, self.views.baseheight = core.screen:getDimensions()
+ self.views.width, self.views.height = self:getViewsDimensions()
+
+ self.views.posList = camutils.getViewsPositions(self.views.basewidth, self.views.baseheight, self.verticalSplit)
+end
+
+-- INFO FUNCTIONS
+-- Get informations from the camera system
+
+function CameraSystem:getViewNumber()
+ return #self.views.list
+end
+
+function CameraSystem:haveView()
+ return (self:getViewNumber() == 0)
+end
+
+function CameraSystem:getViewsDimensions()
+ local basewidth, baseheight = self.views.basewidth, self.views.baseheight
+ local viewnumber = self:getViewNumber()
+
+ return camutils.getViewsDimensions(viewnumber, basewidth, baseheight, self.verticalSplit)
+end
+
+function CameraSystem:recalculateViewsPositions()
+ if #self.views.list == 1 then
+ self.views.list[1].onScreen.x = 0
+ self.views.list[1].onScreen.y = 0
+ else
+ for i,v in ipairs(self.views.list) do
+ local x, y = self:getViewPositions(i)
+ self.views.list[i].onScreen.x = x
+ self.views.list[i].onScreen.y = y
+ end
+ end
+end
+
+function CameraSystem:getViewPositions(id)
+ local viewNumber = #self.views.list
+
+ if (viewNumber == 2) and ((id == 1) or (id == 2)) then
+ return self.views.posList.dual[id].x, self.views.posList.dual[id].y
+ elseif (viewNumber > 2) then
+ return self.views.posList.multi[id].x, self.views.posList.multi[id].y
+ end
+
+end
+
+function CameraSystem:lockX(x)
+ self.xLocked = x
+end
+
+function CameraSystem:unlockX()
+ self.xLocked = nil
+end
+
+function CameraSystem:lockY(y)
+ self.yLocked = y
+end
+
+function CameraSystem:unlockY()
+ self.yLocked = nil
+end
+
+
+-- WRAPPER and UTILS
+-- Access data from the views
+
+function CameraSystem:addTarget(target)
+ if (#self.views < SCREEN_LIMIT) then
+ if (#self.views.list == 0) or (self.mode == "split") then
+ local x, y = target:getViewCenter()
+ self:addView(x, y, target)
+ table.insert(self.targets, target)
+ elseif (self.mode == "middle") or (self.mode == "zoom") then
+ table.insert(self.targets, target)
+ end
+ end
+end
+
+function CameraSystem:addView(x, y, target)
+ if (#self.views.list < SCREEN_LIMIT) then
+ local id = #self.views.list + 1
+ local view = {}
+
+ view.pos = {}
+ view.x = x or 0
+ view.y = y or 0
+ view.scale = 1
+
+ view.onScreen = {}
+
+ -- TODO: add a target system in order to make a camera able
+ -- to target a specific object
+ view.target = target
+
+ table.insert(self.views.list, view)
+ self.views.width, self.views.height = self:getViewsDimensions()
+ self:regenerateCanvases()
+ self:recalculateViewsPositions()
+ end
+end
+
+function CameraSystem:regenerateCanvases()
+ for i, view in ipairs(self.views.list) do
+ view.canvas = love.graphics.newCanvas(self.views.width, self.views.height)
+ end
+end
+
+function CameraSystem:getView(id)
+ return self.views.list[id]
+end
+
+function CameraSystem:attachView(id)
+ if (id ~= nil) then
+ local view = self:getView(id)
+
+ self.current_canvas = love.graphics.getCanvas()
+ love.graphics.setCanvas(view.canvas)
+ love.graphics.clear()
+
+ if id ~= nil then
+ -- Du à la manière dont fonctionne STI, on est obligé de récupérer les info
+ -- de position de camera pour afficher la carte par rapport à ces infos
+ tx, ty = self:getViewCoordinate(id)
+ scale = self:getViewScale(id) or 1
+ tx = math.floor(tx) * -1
+ ty = math.floor(ty) * -1
+
+ local w, h = core.screen:getDimensions()
+ end
+
+ love.graphics.push()
+ love.graphics.origin()
+ love.graphics.translate(math.floor(tx), math.floor(ty))
+ end
+end
+
+function CameraSystem:detachView(id)
+ if (id ~= nil) then
+ local view = self:getView(id)
+ love.graphics.pop()
+
+ love.graphics.setCanvas(self.current_canvas)
+ local tx, ty = self:getOnScreenViewCoordinate(id)
+ local scale = core.screen:getScale() * view.scale
+
+ love.graphics.push()
+ love.graphics.origin()
+ love.graphics.translate(math.floor(tx), math.floor(ty))
+ love.graphics.scale(scale, scale)
+
+ love.graphics.draw(view.canvas)
+
+ local unscale = 1 / view.scale
+ love.graphics.scale(unscale, unscale)
+ self:drawHUD(id)
+
+ love.graphics.pop()
+ end
+end
+
+function CameraSystem:getViewCoordinate(id)
+ local view = self:getView(id)
+
+ local viewx, viewy, vieww, viewh
+
+ vieww = self.views.width / view.scale
+ viewh = self.views.height / view.scale
+
+ viewx = view.x - (vieww / 2)
+ viewy = view.y - (viewh / 2)
+
+ return viewx, viewy, vieww, viewh
+end
+
+function CameraSystem:getOnScreenViewCoordinate(id)
+ local view = self:getView(id)
+
+ local viewx, viewy, vieww, viewh
+ local basex, basey = (self.views.basewidth / 2), (self.views.baseheight / 2)
+ viewx = (basex) + view.onScreen.x - (self.views.width / 2)
+ viewy = (basey) + view.onScreen.y - (self.views.height / 2)
+
+ viewx, viewy = core.screen:getScreenCoordinate(viewx, viewy)
+
+ vieww = self.views.width * core.screen:getScale()
+ viewh = self.views.height * core.screen:getScale()
+ return viewx, viewy, vieww, viewh
+end
+
+function CameraSystem:getOnScreenViewRelativePosition(id)
+ local view = self:getView(id)
+
+ local viewx, viewy
+ local basex, basey = (self.views.basewidth / 2), (self.views.baseheight / 2)
+ viewx = view.onScreen.x
+ viewy = view.onScreen.y
+
+ return core.screen:getScreenCoordinate(viewx, viewy)
+end
+
+function CameraSystem:getOnScreenViewCenter(id)
+ local view = self:getView(id)
+
+ local viewx, viewy
+ local basex, basey = (self.views.basewidth / 2), (self.views.baseheight / 2)
+ viewx = (basex) + view.onScreen.x
+ viewy = (basey) + view.onScreen.y
+
+ return core.screen:getScreenCoordinate(viewx, viewy)
+end
+
+function CameraSystem:setViewScale(id, scale)
+ local view = self:getView(id)
+
+ view.scale = scale
+
+ local _, _, w, h = self:getViewCoordinate(id)
+
+ view.canvas = love.graphics.newCanvas(math.ceil(w), math.ceil(h))
+end
+
+function CameraSystem:getViewScale(id)
+ local view = self:getView(id)
+
+ return view.scale
+end
+
+function CameraSystem:limitView(id)
+ local viewx, viewy, vieww, viewh = self:getViewCoordinate(id)
+ local worldx, worldy, worldw, worldh = self.world:getBox()
+ local posx = self.views.list[id].x
+ local posy = self.views.list[id].y
+ local minx = worldx + self.views.width / 2
+ local miny = worldy + self.views.height / 2
+ local maxx = worldw - self.views.width / 2
+ local maxy = worldh - self.views.height / 2
+
+ self.views.list[id].x = utils.math.between(posx, minx, maxx)
+ self.views.list[id].y = utils.math.between(posy, miny, maxy)
+
+ if (self.xLocked ~= nil) then
+ self.views.list[id].x = self.xLocked
+ end
+
+ if (self.yLocked ~= nil) then
+ self.views.list[id].y = self.yLocked
+ end
+end
+
+-- UPDATE and MOVE functions
+-- Move and update the camera system
+
+function CameraSystem:update(dt)
+ for i,v in ipairs(self.views.list) do
+ if (self.mode == "middle") or (self.mode == "zoom") then
+ self:followAllActors(i)
+ else
+ self:followActor(i)
+ end
+ end
+end
+
+function CameraSystem:moveView(id, x, y)
+ self.views.list[id].x = x
+ self.views.list[id].y = y
+
+ self:limitView(id)
+end
+
+function CameraSystem:followActor(id)
+ local view = self:getView(id)
+
+ if view.target ~= nil then
+ local x, y = view.target:getViewCenter()
+ x = math.floor(x)
+ y = math.floor(y)
+ self:moveView(id, x, y)
+ end
+end
+
+function CameraSystem:followAllActors(id)
+ local view = self:getView(id)
+ local PADDING = 16
+ -- TODO: make all the padding and stuff part of object definition instead ?
+ -- It would especially allow better work in future fake3D worlds
+
+ if (#self.targets > 0) then
+ local minX, minY, maxX, maxY
+ for i, target in ipairs(self.targets) do
+ local x, y, w, h = target:getShape()
+ local x2, y2 = x + w, y + h
+ -- If start by initializing the value at the first found value
+ if (minX == nil) then minX = x end
+ if (maxX == nil) then maxX = x2 end
+ if (minY == nil) then minY = y end
+ if (maxY == nil) then maxY = y2 end
+
+
+ minX = math.min(minX, x)
+ maxX = math.max(maxX, x2)
+ minY = math.min(minY, y)
+ maxY = math.max(maxY, y2)
+ end
+
+ -- Add padding
+ minX = minX - PADDING
+ minY = minY - PADDING
+ maxX = maxX + PADDING
+ maxY = maxY + PADDING
+ local x, y
+ x = (minX + maxX) / 2
+ y = (minY + maxY) / 2
+
+ local scale = 1
+ if (self.mode == "zoom") then
+ local ww, hh = core.screen:getDimensions()
+ local scalex = (maxX-minX)/ww
+ local scaley = (maxY-minY)/hh
+ scale = math.max(scale, scalex, scaley)
+ self:setViewScale(id, 1/scale)
+ self.world:resizeMap(ww * 3, hh * 3)
+ end
+
+ self:moveView(id, x, y)
+ end
+end
+
+
+-- DRAW FUNCTIONS
+-- Basic callback to draw stuff
+
+function CameraSystem:drawHUD(id)
+ local view = self:getView(id)
+ local viewx, viewy, vieww, viewh = self:getOnScreenViewCoordinate(id)
+
+ view.target:drawHUD(id, vieww, viewh)
+end
+
+return CameraSystem
diff --git a/bootleg.love/core/modules/world/camera/utils.lua b/bootleg.love/core/modules/world/camera/utils.lua
new file mode 100644
index 0000000..251b9aa
--- /dev/null
+++ b/bootleg.love/core/modules/world/camera/utils.lua
@@ -0,0 +1,83 @@
+-- camutils.lua :: some camera utilities
+
+--[[
+ Copyright © 2019 Kazhnuz
+
+ 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.
+]]
+
+local camutils = {}
+
+function camutils.getViewsPositions(basewidth, baseheight, verticalSplit)
+ local posList = {}
+
+ posList.dual = {}
+ posList.multi = {}
+
+ if (verticalSplit) then
+ posList.dual[1] = {}
+ posList.dual[1].x = 0
+ posList.dual[1].y = (baseheight/4)
+
+ posList.dual[2] = {}
+ posList.dual[2].x = 0
+ posList.dual[2].y = -(baseheight/4)
+ else
+ posList.dual[1] = {}
+ posList.dual[1].x = -(basewidth/4)
+ posList.dual[1].y = 0
+
+ posList.dual[2] = {}
+ posList.dual[2].x = (basewidth/4)
+ posList.dual[2].y = 0
+ end
+
+ posList.multi[1] = {}
+ posList.multi[1].x = -(basewidth /4)
+ posList.multi[1].y = -(baseheight/4)
+
+ posList.multi[2] = {}
+ posList.multi[2].x = (basewidth /4)
+ posList.multi[2].y = -(baseheight/4)
+
+ posList.multi[3] = {}
+ posList.multi[3].x = -(basewidth /4)
+ posList.multi[3].y = (baseheight/4)
+
+ posList.multi[4] = {}
+ posList.multi[4].x = (basewidth /4)
+ posList.multi[4].y = (baseheight/4)
+
+ return posList
+end
+
+function camutils.getViewsDimensions(viewnumber, basewidth, baseheight, verticalSplit)
+ if (viewnumber <= 1) then
+ return basewidth, baseheight
+ elseif (viewnumber == 2) then
+ if (verticalSplit) then
+ return (basewidth), (baseheight/2)
+ else
+ return (basewidth/2), (baseheight)
+ end
+ else
+ return (basewidth/2), (baseheight/2)
+ end
+end
+
+return camutils
diff --git a/bootleg.love/core/modules/world/libs/bump-3dpd.lua b/bootleg.love/core/modules/world/libs/bump-3dpd.lua
new file mode 100644
index 0000000..f094f8d
--- /dev/null
+++ b/bootleg.love/core/modules/world/libs/bump-3dpd.lua
@@ -0,0 +1,947 @@
+local bump = {
+ _VERSION = 'bump-3dpd v0.2.0',
+ _URL = 'https://github.com/oniietzschan/bump-3dpd',
+ _DESCRIPTION = 'A 3D collision detection library for Lua.',
+ _LICENSE = [[
+ MIT LICENSE
+
+ Copyright (c) 2014 Enrique García Cota
+
+ Permission is hereby granted, free of charge, to any person obtaining a
+ copy of this software and associated documentation files (the
+ "Software"), to deal in the Software without restriction, including
+ without limitation the rights to use, copy, modify, merge, publish,
+ distribute, sublicense, and/or sell copies of the Software, and to
+ permit persons to whom the Software is furnished to do so, subject to
+ the following conditions:
+
+ The above copyright notice and this permission notice shall be included
+ in all copies or substantial portions of the Software.
+
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS
+ OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
+ MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
+ IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY
+ CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT,
+ TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
+ SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
+ ]]
+}
+
+------------------------------------------
+-- Auxiliary functions
+------------------------------------------
+local DELTA = 1e-10 -- floating-point margin of error
+
+local abs, floor, ceil, min, max = math.abs, math.floor, math.ceil, math.min, math.max
+
+local function sign(x)
+ if x > 0 then return 1 end
+ if x == 0 then return 0 end
+ return -1
+end
+
+local function nearest(x, a, b)
+ if abs(a - x) < abs(b - x) then return a else return b end
+end
+
+local function assertType(desiredType, value, name)
+ if type(value) ~= desiredType then
+ error(name .. ' must be a ' .. desiredType .. ', but was ' .. tostring(value) .. '(a ' .. type(value) .. ')')
+ end
+end
+
+local function assertIsPositiveNumber(value, name)
+ if type(value) ~= 'number' or value <= 0 then
+ error(name .. ' must be a positive integer, but was ' .. tostring(value) .. '(' .. type(value) .. ')')
+ end
+end
+
+local function assertIsCube(x,y,z,w,h,d)
+ assertType('number', x, 'x')
+ assertType('number', y, 'y')
+ assertType('number', z, 'z')
+ assertIsPositiveNumber(w, 'w')
+ assertIsPositiveNumber(h, 'h')
+ assertIsPositiveNumber(d, 'd')
+end
+
+local defaultFilter = function()
+ return 'slide'
+end
+
+------------------------------------------
+-- Cube functions
+------------------------------------------
+
+local function cube_getNearestCorner(x,y,z,w,h,d, px, py, pz)
+ return nearest(px, x, x + w),
+ nearest(py, y, y + h),
+ nearest(pz, z, z + d)
+end
+
+-- This is a generalized implementation of the liang-barsky algorithm, which also returns
+-- the normals of the sides where the segment intersects.
+-- Returns nil if the segment never touches the cube
+-- Notice that normals are only guaranteed to be accurate when initially ti1, ti2 == -math.huge, math.huge
+local function cube_getSegmentIntersectionIndices(x,y,z,w,h,d, x1,y1,z1,x2,y2,z2, ti1,ti2)
+ ti1, ti2 = ti1 or 0, ti2 or 1
+ local dx = x2 - x1
+ local dy = y2 - y1
+ local dz = z2 - z1
+ local nx, ny, nz
+ local nx1, ny1, nz1, nx2, ny2, nz2 = 0,0,0,0,0,0
+ local p, q, r
+
+ for side = 1,6 do
+ if side == 1 then -- Left
+ nx,ny,nz,p,q = -1, 0, 0, -dx, x1 - x
+ elseif side == 2 then -- Right
+ nx,ny,nz,p,q = 1, 0, 0, dx, x + w - x1
+ elseif side == 3 then -- Top
+ nx,ny,nz,p,q = 0, -1, 0, -dy, y1 - y
+ elseif side == 4 then -- Bottom
+ nx,ny,nz,p,q = 0, 1, 0, dy, y + h - y1
+ elseif side == 5 then -- Front
+ nx,ny,nz,p,q = 0, 0, -1, -dz, z1 - z
+ else -- Back
+ nx,ny,nz,p,q = 0, 0, 1, dz, z + d - z1
+ end
+
+ if p == 0 then
+ if q <= 0 then
+ return nil
+ end
+ else
+ r = q / p
+ if p < 0 then
+ if r > ti2 then
+ return nil
+ elseif r > ti1 then
+ ti1, nx1,ny1,nz1 = r, nx,ny,nz
+ end
+ else -- p > 0
+ if r < ti1 then
+ return nil
+ elseif r < ti2 then
+ ti2, nx2,ny2,nz2 = r,nx,ny,nz
+ end
+ end
+ end
+ end
+
+ return ti1,ti2, nx1,ny1,nz1, nx2,ny2,nz2
+end
+
+-- Calculates the minkowsky difference between 2 cubes, which is another cube
+local function cube_getDiff(x1,y1,z1,w1,h1,d1, x2,y2,z2,w2,h2,d2)
+ return x2 - x1 - w1,
+ y2 - y1 - h1,
+ z2 - z1 - d1,
+ w1 + w2,
+ h1 + h2,
+ d1 + d2
+end
+
+local function cube_containsPoint(x,y,z,w,h,d, px,py,pz)
+ return px - x > DELTA
+ and py - y > DELTA
+ and pz - z > DELTA
+ and x + w - px > DELTA
+ and y + h - py > DELTA
+ and z + d - pz > DELTA
+end
+
+local function cube_isIntersecting(x1,y1,z1,w1,h1,d1, x2,y2,z2,w2,h2,d2)
+ return x1 < x2 + w2 and x2 < x1 + w1 and
+ y1 < y2 + h2 and y2 < y1 + h1 and
+ z1 < z2 + d2 and z2 < z1 + d1
+end
+
+local function cube_getCubeDistance(x1,y1,z1,w1,h1,d1, x2,y2,z2,w2,h2,d2)
+ local dx = x1 - x2 + (w1 - w2)/2
+ local dy = y1 - y2 + (h1 - h2)/2
+ local dz = z1 - z2 + (d1 - d2)/2
+ return (dx * dx) + (dy * dy) + (dz * dz)
+end
+
+local function cube_detectCollision(x1,y1,z1,w1,h1,d1, x2,y2,z2,w2,h2,d2, goalX, goalY, goalZ)
+ goalX = goalX or x1
+ goalY = goalY or y1
+ goalZ = goalZ or z1
+
+ local dx = goalX - x1
+ local dy = goalY - y1
+ local dz = goalZ - z1
+ local x,y,z,w,h,d = cube_getDiff(x1,y1,z1,w1,h1,d1, x2,y2,z2,w2,h2,d2)
+
+ local overlaps, ti, nx, ny, nz
+
+ if cube_containsPoint(x,y,z,w,h,d, 0,0,0) then -- item was intersecting other
+ local px, py, pz = cube_getNearestCorner(x,y,z,w,h,d, 0,0,0)
+ -- Volume of intersection:
+ local wi = min(w1, abs(px))
+ local hi = min(h1, abs(py))
+ local di = min(d1, abs(pz))
+ ti = wi * hi * di * -1 -- ti is the negative volume of intersection
+ overlaps = true
+ else
+ local ti1,ti2,nx1,ny1,nz1 = cube_getSegmentIntersectionIndices(x,y,z,w,h,d, 0,0,0,dx,dy,dz, -math.huge, math.huge)
+
+ -- item tunnels into other
+ if ti1
+ and ti1 < 1
+ and (abs(ti1 - ti2) >= DELTA) -- special case for cube going through another cube's corner
+ and (0 < ti1 + DELTA
+ or 0 == ti1 and ti2 > 0)
+ then
+ ti, nx, ny, nz = ti1, nx1, ny1, nz1
+ overlaps = false
+ end
+ end
+
+ if not ti then
+ return
+ end
+
+ local tx, ty, tz
+
+ if overlaps then
+ if dx == 0 and dy == 0 and dz == 0 then
+ -- intersecting and not moving - use minimum displacement vector
+ local px, py, pz = cube_getNearestCorner(x,y,z,w,h,d, 0,0,0)
+ if abs(px) <= abs(py) and abs(px) <= abs(pz) then
+ -- X axis has minimum displacement
+ py, pz = 0, 0
+ elseif abs(py) <= abs(pz) then
+ -- Y axis has minimum displacement
+ px, pz = 0, 0
+ else
+ -- Z axis has minimum displacement
+ px, py = 0, 0
+ end
+ nx, ny, nz = sign(px), sign(py), sign(pz)
+ tx = x1 + px
+ ty = y1 + py
+ tz = z1 + pz
+ else
+ -- intersecting and moving - move in the opposite direction
+ local ti1, _
+ ti1,_,nx,ny,nz = cube_getSegmentIntersectionIndices(x,y,z,w,h,d, 0,0,0,dx,dy,dz, -math.huge, 1)
+ if not ti1 then
+ return
+ end
+ tx = x1 + dx * ti1
+ ty = y1 + dy * ti1
+ tz = z1 + dz * ti1
+ end
+ else -- tunnel
+ tx = x1 + dx * ti
+ ty = y1 + dy * ti
+ tz = z1 + dz * ti
+ end
+
+ return {
+ overlaps = overlaps,
+ ti = ti,
+ move = {x = dx, y = dy, z = dz},
+ normal = {x = nx, y = ny, z = nz},
+ touch = {x = tx, y = ty, z = tz},
+ itemCube = {x = x1, y = y1, z = z1, w = w1, h = h1, d = d1},
+ otherCube = {x = x2, y = y2, z = z2, w = w2, h = h2, d = d2},
+ }
+end
+
+------------------------------------------
+-- Grid functions
+------------------------------------------
+
+local function grid_toWorld(cellSize, cx, cy, cz)
+ return (cx - 1) * cellSize,
+ (cy - 1) * cellSize,
+ (cz - 1) * cellSize
+end
+
+local function grid_toCell(cellSize, x, y, z)
+ return floor(x / cellSize) + 1,
+ floor(y / cellSize) + 1,
+ floor(z / cellSize) + 1
+end
+
+-- grid_traverse* functions are based on "A Fast Voxel Traversal Algorithm for Ray Tracing",
+-- by John Amanides and Andrew Woo - http://www.cse.yorku.ca/~amana/research/grid.pdf
+-- It has been modified to include both cells when the ray "touches a grid corner",
+-- and with a different exit condition
+
+local function grid_traverse_initStep(cellSize, ct, t1, t2)
+ local v = t2 - t1
+ if v > 0 then
+ return 1, cellSize / v, ((ct + v) * cellSize - t1) / v
+ elseif v < 0 then
+ return -1, -cellSize / v, ((ct + v - 1) * cellSize - t1) / v
+ else
+ return 0, math.huge, math.huge
+ end
+end
+
+local function grid_traverse(cellSize, x1,y1,z1,x2,y2,z2, f)
+ local cx1, cy1, cz1 = grid_toCell(cellSize, x1, y1, z1)
+ local cx2, cy2, cz2 = grid_toCell(cellSize, x2, y2, z2)
+ local stepX, dx, tx = grid_traverse_initStep(cellSize, cx1, x1, x2)
+ local stepY, dy, ty = grid_traverse_initStep(cellSize, cy1, y1, y2)
+ local stepZ, dz, tz = grid_traverse_initStep(cellSize, cz1, z1, z2)
+ local cx, cy, cz = cx1, cy1, cz1
+
+ f(cx, cy, cz)
+
+ -- The default implementation had an infinite loop problem when
+ -- approaching the last cell in some occassions. We finish iterating
+ -- when we are *next* to the last cell
+ while abs(cx - cx2) + abs(cy - cy2) + abs(cz - cz2) > 1 do
+ if tx < ty and tx < tz then -- tx is smallest
+ tx = tx + dx
+ cx = cx + stepX
+ f(cx, cy, cz)
+ elseif ty < tz then -- ty is smallest
+ -- Addition: include both cells when going through corners
+ if tx == ty then
+ f(cx + stepX, cy, cz)
+ end
+ ty = ty + dy
+ cy = cy + stepY
+ f(cx, cy, cz)
+ else -- tz is smallest
+ -- Addition: include both cells when going through corners
+ if tx == tz then
+ f(cx + stepX, cy, cz)
+ end
+ if ty == tz then
+ f(cx, cy + stepY, cz)
+ end
+ tz = tz + dz
+ cz = cz + stepZ
+ f(cx, cy, cz)
+ end
+ end
+
+ -- If we have not arrived to the last cell, use it
+ if cx ~= cx2 or cy ~= cy2 or cz ~= cz2 then
+ f(cx2, cy2, cz2)
+ end
+end
+
+local function grid_toCellCube(cellSize, x,y,z,w,h,d)
+ local cx,cy,cz = grid_toCell(cellSize, x, y, z)
+ local cx2 = ceil((x + w) / cellSize)
+ local cy2 = ceil((y + h) / cellSize)
+ local cz2 = ceil((z + d) / cellSize)
+
+ return cx,
+ cy,
+ cz,
+ cx2 - cx + 1,
+ cy2 - cy + 1,
+ cz2 - cz + 1
+end
+
+------------------------------------------
+-- Responses
+------------------------------------------
+
+local touch = function(world, col, x,y,z,w,h,d, goalX, goalY, goalZ, filter)
+ return col.touch.x, col.touch.y, col.touch.z, {}, 0
+end
+
+local cross = function(world, col, x,y,z,w,h,d, goalX, goalY, goalZ, filter)
+ local cols, len = world:project(col.item, x,y,z,w,h,d, goalX, goalY, goalZ, filter)
+
+ return goalX, goalY, goalZ, cols, len
+end
+
+local slide = function(world, col, x,y,z,w,h,d, goalX, goalY, goalZ, filter)
+ goalX = goalX or x
+ goalY = goalY or y
+ goalZ = goalZ or z
+
+ local tch, move = col.touch, col.move
+ if move.x ~= 0 or move.y ~= 0 or move.z ~= 0 then
+ if col.normal.x ~= 0 then
+ goalX = tch.x
+ end
+ if col.normal.y ~= 0 then
+ goalY = tch.y
+ end
+ if col.normal.z ~= 0 then
+ goalZ = tch.z
+ end
+ end
+
+ col.slide = {x = goalX, y = goalY, z = goalZ}
+
+ x, y, z = tch.x, tch.y, tch.z
+ local cols, len = world:project(col.item, x,y,z,w,h,d, goalX, goalY, goalZ, filter)
+
+ return goalX, goalY, goalZ, cols, len
+end
+
+local bounce = function(world, col, x,y,z,w,h,d, goalX, goalY, goalZ, filter)
+ goalX = goalX or x
+ goalY = goalY or y
+ goalZ = goalZ or z
+
+ local tch, move = col.touch, col.move
+ local tx, ty, tz = tch.x, tch.y, tch.z
+ local bx, by, bz = tx, ty, tz
+
+ if move.x ~= 0 or move.y ~= 0 or move.z ~= 0 then
+ local bnx = goalX - tx
+ local bny = goalY - ty
+ local bnz = goalZ - tz
+
+ if col.normal.x ~= 0 then
+ bnx = -bnx
+ end
+ if col.normal.y ~= 0 then
+ bny = -bny
+ end
+ if col.normal.z ~= 0 then
+ bnz = -bnz
+ end
+
+ bx = tx + bnx
+ by = ty + bny
+ bz = tz + bnz
+ end
+
+ col.bounce = {x = bx, y = by, z = bz}
+ x, y, z = tch.x, tch.y, tch.z
+ goalX, goalY, goalZ = bx, by, bz
+
+ local cols, len = world:project(col.item, x,y,z,w,h,d, goalX, goalY, goalZ, filter)
+
+ return goalX, goalY, goalZ, cols, len
+end
+
+------------------------------------------
+-- World
+------------------------------------------
+
+local World = {}
+local World_mt = {__index = World}
+
+-- Private functions and methods
+
+local function sortByWeight(a,b)
+ return a.weight < b.weight
+end
+
+local function sortByTiAndDistance(a,b)
+ if a.ti == b.ti then
+ local ir, ar, br = a.itemCube, a.otherCube, b.otherCube
+ local ad = cube_getCubeDistance(ir.x,ir.y,ir.z,ir.w,ir.h,ir.d, ar.x,ar.y,ar.z,ar.w,ar.h,ar.d)
+ local bd = cube_getCubeDistance(ir.x,ir.y,ir.z,ir.w,ir.h,ir.d, br.x,br.y,br.z,br.w,br.h,br.d)
+ return ad < bd
+ end
+ return a.ti < b.ti
+end
+
+local function addItemToCell(self, item, cx, cy, cz)
+ self.cells[cz] = self.cells[cz] or {}
+ self.cells[cz][cy] = self.cells[cz][cy] or setmetatable({}, {__mode = 'v'})
+ if self.cells[cz][cy][cx] == nil then
+ self.cells[cz][cy][cx] = {
+ itemCount = 0,
+ x = cx,
+ y = cy,
+ z = cz,
+ items = setmetatable({}, {__mode = 'k'})
+ }
+ end
+
+ local cell = self.cells[cz][cy][cx]
+ self.nonEmptyCells[cell] = true
+ if not cell.items[item] then
+ cell.items[item] = true
+ cell.itemCount = cell.itemCount + 1
+ end
+end
+
+local function removeItemFromCell(self, item, cx, cy, cz)
+ if not self.cells[cz]
+ or not self.cells[cz][cy]
+ or not self.cells[cz][cy][cx]
+ or not self.cells[cz][cy][cx].items[item]
+ then
+ return false
+ end
+
+ local cell = self.cells[cz][cy][cx]
+ cell.items[item] = nil
+
+ cell.itemCount = cell.itemCount - 1
+ if cell.itemCount == 0 then
+ self.nonEmptyCells[cell] = nil
+ end
+
+ return true
+end
+
+local function getDictItemsInCellCube(self, cx,cy,cz, cw,ch,cd)
+ local items_dict = {}
+
+ for z = cz, cz + cd - 1 do
+ local plane = self.cells[z]
+ if plane then
+ for y = cy, cy + ch - 1 do
+ local row = plane[y]
+ if row then
+ for x = cx, cx + cw - 1 do
+ local cell = row[x]
+ if cell and cell.itemCount > 0 then -- no cell.itemCount > 1 because tunneling
+ for item,_ in pairs(cell.items) do
+ items_dict[item] = true
+ end
+ end
+ end
+ end
+ end
+ end
+ end
+
+ return items_dict
+end
+
+local function getCellsTouchedBySegment(self, x1,y1,z1,x2,y2,z2)
+ local cells, cellsLen, visited = {}, 0, {}
+
+ grid_traverse(self.cellSize, x1,y1,z1,x2,y2,z2, function(cx, cy, cz)
+ local plane = self.cells[cz]
+ if not plane then
+ return
+ end
+
+ local row = plane[cy]
+ if not row then
+ return
+ end
+
+ local cell = row[cx]
+ if not cell or visited[cell] then
+ return
+ end
+
+ visited[cell] = true
+ cellsLen = cellsLen + 1
+ cells[cellsLen] = cell
+ end)
+
+ return cells, cellsLen
+end
+
+local function getInfoAboutItemsTouchedBySegment(self, x1,y1,z1, x2,y2,z2, filter)
+ local cells, len = getCellsTouchedBySegment(self, x1,y1,z1,x2,y2,z2)
+ local cell, cube, x,y,z,w,h,d, ti1, ti2, tii0,tii1
+ local visited, itemInfo, itemInfoLen = {}, {}, 0
+
+ for i = 1, len do
+ cell = cells[i]
+ for item in pairs(cell.items) do
+ if not visited[item] then
+ visited[item] = true
+ if (not filter or filter(item)) then
+ cube = self.cubes[item]
+ x, y, z, w, h, d = cube.x, cube.y, cube.z, cube.w, cube.h, cube.d
+
+ ti1, ti2 = cube_getSegmentIntersectionIndices(x,y,z,w,h,d, x1,y1,z1, x2,y2,z2, 0, 1)
+ if ti1 and ((0 < ti1 and ti1 < 1) or (0 < ti2 and ti2 < 1)) then
+ -- the sorting is according to the t of an infinite line, not the segment
+ tii0, tii1 = cube_getSegmentIntersectionIndices(x,y,z,w,h,d, x1,y1,z1, x2,y2,z2, -math.huge, math.huge)
+ itemInfoLen = itemInfoLen + 1
+ itemInfo[itemInfoLen] = {item = item, ti1 = ti1, ti2 = ti2, weight = min(tii0, tii1)}
+ end
+ end
+ end
+ end
+ end
+
+ table.sort(itemInfo, sortByWeight)
+
+ return itemInfo, itemInfoLen
+end
+
+local function getResponseByName(self, name)
+ local response = self.responses[name]
+ if not response then
+ error(('Unknown collision type: %s (%s)'):format(name, type(name)))
+ end
+
+ return response
+end
+
+
+-- Misc Public Methods
+
+function World:addResponse(name, response)
+ self.responses[name] = response
+end
+
+function World:projectMove(item, x,y,z,w,h,d, goalX,goalY,goalZ, filter)
+ local cols, len = {}, 0
+
+ filter = filter or defaultFilter
+
+ local visited = {[item] = true}
+ local visitedFilter = function(itm, other)
+ if visited[other] then
+ return false
+ end
+ return filter(itm, other)
+ end
+
+ local projected_cols, projected_len = self:project(item, x,y,z,w,h,d, goalX,goalY,goalZ, visitedFilter)
+
+ while projected_len > 0 do
+ local col = projected_cols[1]
+ len = len + 1
+ cols[len] = col
+
+ visited[col.other] = true
+
+ local response = getResponseByName(self, col.type)
+
+ goalX, goalY, goalZ, projected_cols, projected_len = response(
+ self,
+ col,
+ x, y, z, w, h, d,
+ goalX, goalY, goalZ,
+ visitedFilter
+ )
+ end
+
+ return goalX, goalY, goalZ, cols, len
+end
+
+function World:project(item, x,y,z,w,h,d, goalX,goalY,goalZ, filter)
+ assertIsCube(x, y, z, w, h, d)
+
+ goalX = goalX or x
+ goalY = goalY or y
+ goalZ = goalZ or z
+ filter = filter or defaultFilter
+
+ local collisions, len = {}, 0
+
+ local visited = {}
+ if item ~= nil then
+ visited[item] = true
+ end
+
+ -- This could probably be done with less cells using a polygon raster over the cells instead of a
+ -- bounding cube of the whole movement. Conditional to building a queryPolygon method
+ local tx = min(goalX, x)
+ local ty = min(goalY, y)
+ local tz = min(goalZ, z)
+ local tx2 = max(goalX + w, x + w)
+ local ty2 = max(goalY + h, y + h)
+ local tz2 = max(goalZ + d, z + d)
+ local tw = tx2 - tx
+ local th = ty2 - ty
+ local td = tz2 - tz
+
+ local cx,cy,cz,cw,ch,cd = grid_toCellCube(self.cellSize, tx,ty,tz, tw,th,td)
+
+ local dictItemsInCellCube = getDictItemsInCellCube(self, cx,cy,cz,cw,ch,cd)
+
+ for other,_ in pairs(dictItemsInCellCube) do
+ if not visited[other] then
+ visited[other] = true
+
+ local responseName = filter(item, other)
+ if responseName then
+ local ox,oy,oz,ow,oh,od = self:getCube(other)
+ local col = cube_detectCollision(x,y,z,w,h,d, ox,oy,oz,ow,oh,od, goalX, goalY, goalZ)
+
+ if col then
+ col.other = other
+ col.item = item
+ col.type = responseName
+
+ len = len + 1
+ collisions[len] = col
+ end
+ end
+ end
+ end
+
+ table.sort(collisions, sortByTiAndDistance)
+
+ return collisions, len
+end
+
+function World:countCells()
+ local count = 0
+
+ for _, plane in pairs(self.cells) do
+ for _, row in pairs(plane) do
+ for _,_ in pairs(row) do
+ count = count + 1
+ end
+ end
+ end
+
+ return count
+end
+
+function World:hasItem(item)
+ return not not self.cubes[item]
+end
+
+function World:getItems()
+ local items, len = {}, 0
+ for item,_ in pairs(self.cubes) do
+ len = len + 1
+ items[len] = item
+ end
+ return items, len
+end
+
+function World:countItems()
+ local len = 0
+ for _ in pairs(self.cubes) do len = len + 1 end
+ return len
+end
+
+function World:getCube(item)
+ local cube = self.cubes[item]
+ if not cube then
+ error('Item ' .. tostring(item) .. ' must be added to the world before getting its cube. Use world:add(item, x,y,z,w,h,d) to add it first.')
+ end
+
+ return cube.x, cube.y, cube.z, cube.w, cube.h, cube.d
+end
+
+function World:toWorld(cx, cy, cz)
+ return grid_toWorld(self.cellSize, cx, cy, cz)
+end
+
+function World:toCell(x,y,z)
+ return grid_toCell(self.cellSize, x, y, z)
+end
+
+
+-- Query methods
+
+function World:queryCube(x,y,z,w,h,d, filter)
+ assertIsCube(x,y,z,w,h,d)
+
+ local cx,cy,cz,cw,ch,cd = grid_toCellCube(self.cellSize, x,y,z,w,h,d)
+ local dictItemsInCellCube = getDictItemsInCellCube(self, cx,cy,cz,cw,ch,cd)
+
+ local items, len = {}, 0
+
+ local cube
+ for item,_ in pairs(dictItemsInCellCube) do
+ cube = self.cubes[item]
+ if (not filter or filter(item))
+ and cube_isIntersecting(x,y,z,w,h,d, cube.x, cube.y, cube.z, cube.w, cube.h, cube.d)
+ then
+ len = len + 1
+ items[len] = item
+ end
+ end
+
+ return items, len
+end
+
+function World:queryPoint(x,y,z, filter)
+ local cx,cy,cz = self:toCell(x,y,z)
+ local dictItemsInCellCube = getDictItemsInCellCube(self, cx,cy,cz, 1,1,1)
+
+ local items, len = {}, 0
+
+ local cube
+ for item,_ in pairs(dictItemsInCellCube) do
+ cube = self.cubes[item]
+ if (not filter or filter(item))
+ and cube_containsPoint(cube.x, cube.y, cube.z, cube.w, cube.h, cube.d, x, y, z)
+ then
+ len = len + 1
+ items[len] = item
+ end
+ end
+
+ return items, len
+end
+
+function World:querySegment(x1, y1, z1, x2, y2, z2, filter)
+ local itemInfo, len = getInfoAboutItemsTouchedBySegment(self, x1, y1, z1, x2, y2, z2, filter)
+ local items = {}
+ for i = 1, len do
+ items[i] = itemInfo[i].item
+ end
+
+ return items, len
+end
+
+-- function World:querySegmentWithCoords(x1, y1, z1, x2, y2, z2, filter)
+-- local itemInfo, len = getInfoAboutItemsTouchedBySegment(self, x1, y1, z1, x2, y2, z2, filter)
+-- local dx, dy, dz = x2 - x1, y2 - y1, z2 - z1
+-- local info, ti1, ti2
+-- for i=1, len do
+-- info = itemInfo[i]
+-- ti1 = info.ti1
+-- ti2 = info.ti2
+
+-- info.weight = nil
+-- info.x1 = x1 + dx * ti1
+-- info.y1 = y1 + dy * ti1
+-- info.x2 = x1 + dx * ti2
+-- info.y2 = y1 + dy * ti2
+-- end
+-- return itemInfo, len
+-- end
+
+
+--- Main methods
+
+function World:add(item, x,y,z,w,h,d)
+ local cube = self.cubes[item]
+ if cube then
+ error('Item ' .. tostring(item) .. ' added to the world twice.')
+ end
+ assertIsCube(x,y,z,w,h,d)
+
+ self.cubes[item] = {x=x,y=y,z=z,w=w,h=h,d=d}
+
+ local cl,ct,cs,cw,ch,cd = grid_toCellCube(self.cellSize, x,y,z,w,h,d)
+ for cz = cs, cs + cd - 1 do
+ for cy = ct, ct + ch - 1 do
+ for cx = cl, cl + cw - 1 do
+ addItemToCell(self, item, cx, cy, cz)
+ end
+ end
+ end
+
+ return item
+end
+
+function World:remove(item)
+ local x,y,z,w,h,d = self:getCube(item)
+
+ self.cubes[item] = nil
+ local cl,ct,cs,cw,ch,cd = grid_toCellCube(self.cellSize, x,y,z,w,h,d)
+ for cz = cs, cs + cd - 1 do
+ for cy = ct, ct + ch - 1 do
+ for cx = cl, cl + cw - 1 do
+ removeItemFromCell(self, item, cx, cy, cz)
+ end
+ end
+ end
+end
+
+function World:update(item, x2,y2,z2,w2,h2,d2)
+ local x1,y1,z1, w1,h1,d1 = self:getCube(item)
+ w2 = w2 or w1
+ h2 = h2 or h1
+ d2 = d2 or d1
+ assertIsCube(x2,y2,z2,w2,h2,d2)
+
+ if x1 == x2 and y1 == y2 and z1 == z2 and w1 == w2 and h1 == h2 and d1 == d2 then
+ return
+ end
+
+ local cl1,ct1,cs1,cw1,ch1,cd1 = grid_toCellCube(self.cellSize, x1,y1,z1, w1,h1,d1)
+ local cl2,ct2,cs2,cw2,ch2,cd2 = grid_toCellCube(self.cellSize, x2,y2,z2, w2,h2,d2)
+
+ if cl1 ~= cl2 or ct1 ~= ct2 or cs1 ~= cs2 or cw1 ~= cw2 or ch1 ~= ch2 or cd1 ~= cd2 then
+ local cr1 = cl1 + cw1 - 1
+ local cr2 = cl2 + cw2 - 1
+ local cb1 = ct1 + ch1 - 1
+ local cb2 = ct2 + ch2 - 1
+ local css1 = cs1 + cd1 - 1
+ local css2 = cs2 + cd2 - 1
+ local cyOut, czOut
+
+ for cz = cs1, css1 do
+ czOut = cz < cs2 or cz > css2
+ for cy = ct1, cb1 do
+ cyOut = cy < ct2 or cy > cb2
+ for cx = cl1, cr1 do
+ if czOut or cyOut or cx < cl2 or cx > cr2 then
+ removeItemFromCell(self, item, cx, cy, cz)
+ end
+ end
+ end
+ end
+
+ for cz = cs2, css2 do
+ czOut = cz < cs1 or cz > css1
+ for cy = ct2, cb2 do
+ cyOut = cy < ct1 or cy > cb1
+ for cx = cl2, cr2 do
+ if czOut or cyOut or cx < cl1 or cx > cr1 then
+ addItemToCell(self, item, cx, cy, cz)
+ end
+ end
+ end
+ end
+ end
+
+ local cube = self.cubes[item]
+ cube.x, cube.y, cube.z, cube.w, cube.h, cube.d = x2, y2, z2, w2, h2, d2
+end
+
+function World:move(item, goalX, goalY, goalZ, filter)
+ local actualX, actualY, actualZ, cols, len = self:check(item, goalX, goalY, goalZ, filter)
+
+ self:update(item, actualX, actualY, actualZ)
+
+ return actualX, actualY, actualZ, cols, len
+end
+
+function World:check(item, goalX, goalY, goalZ, filter)
+ local x,y,z,w,h,d = self:getCube(item)
+
+ return self:projectMove(item, x,y,z,w,h,d, goalX,goalY,goalZ, filter)
+end
+
+
+-- Public library functions
+
+bump.newWorld = function(cellSize)
+ cellSize = cellSize or 64
+ assertIsPositiveNumber(cellSize, 'cellSize')
+ local world = setmetatable({
+ cellSize = cellSize,
+ cubes = {},
+ cells = {},
+ nonEmptyCells = {},
+ responses = {},
+ }, World_mt)
+
+ world:addResponse('touch', touch)
+ world:addResponse('cross', cross)
+ world:addResponse('slide', slide)
+ world:addResponse('bounce', bounce)
+
+ return world
+end
+
+bump.cube = {
+ getNearestCorner = cube_getNearestCorner,
+ getSegmentIntersectionIndices = cube_getSegmentIntersectionIndices,
+ getDiff = cube_getDiff,
+ containsPoint = cube_containsPoint,
+ isIntersecting = cube_isIntersecting,
+ getCubeDistance = cube_getCubeDistance,
+ detectCollision = cube_detectCollision
+}
+
+bump.responses = {
+ touch = touch,
+ cross = cross,
+ slide = slide,
+ bounce = bounce
+}
+
+return bump
diff --git a/bootleg.love/core/modules/world/libs/bump.lua b/bootleg.love/core/modules/world/libs/bump.lua
new file mode 100644
index 0000000..66d4cf1
--- /dev/null
+++ b/bootleg.love/core/modules/world/libs/bump.lua
@@ -0,0 +1,769 @@
+local bump = {
+ _VERSION = 'bump v3.1.7',
+ _URL = 'https://github.com/kikito/bump.lua',
+ _DESCRIPTION = 'A collision detection library for Lua',
+ _LICENSE = [[
+ MIT LICENSE
+ Copyright (c) 2014 Enrique García Cota
+ Permission is hereby granted, free of charge, to any person obtaining a
+ copy of this software and associated documentation files (the
+ "Software"), to deal in the Software without restriction, including
+ without limitation the rights to use, copy, modify, merge, publish,
+ distribute, sublicense, and/or sell copies of the Software, and to
+ permit persons to whom the Software is furnished to do so, subject to
+ the following conditions:
+ The above copyright notice and this permission notice shall be included
+ in all copies or substantial portions of the Software.
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS
+ OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
+ MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
+ IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY
+ CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT,
+ TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
+ SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
+ ]]
+}
+
+------------------------------------------
+-- Auxiliary functions
+------------------------------------------
+local DELTA = 1e-10 -- floating-point margin of error
+
+local abs, floor, ceil, min, max = math.abs, math.floor, math.ceil, math.min, math.max
+
+local function sign(x)
+ if x > 0 then return 1 end
+ if x == 0 then return 0 end
+ return -1
+end
+
+local function nearest(x, a, b)
+ if abs(a - x) < abs(b - x) then return a else return b end
+end
+
+local function assertType(desiredType, value, name)
+ if type(value) ~= desiredType then
+ error(name .. ' must be a ' .. desiredType .. ', but was ' .. tostring(value) .. '(a ' .. type(value) .. ')')
+ end
+end
+
+local function assertIsPositiveNumber(value, name)
+ if type(value) ~= 'number' or value <= 0 then
+ error(name .. ' must be a positive integer, but was ' .. tostring(value) .. '(' .. type(value) .. ')')
+ end
+end
+
+local function assertIsRect(x,y,w,h)
+ assertType('number', x, 'x')
+ assertType('number', y, 'y')
+ assertIsPositiveNumber(w, 'w')
+ assertIsPositiveNumber(h, 'h')
+end
+
+local defaultFilter = function()
+ return 'slide'
+end
+
+------------------------------------------
+-- Rectangle functions
+------------------------------------------
+
+local function rect_getNearestCorner(x,y,w,h, px, py)
+ return nearest(px, x, x+w), nearest(py, y, y+h)
+end
+
+-- This is a generalized implementation of the liang-barsky algorithm, which also returns
+-- the normals of the sides where the segment intersects.
+-- Returns nil if the segment never touches the rect
+-- Notice that normals are only guaranteed to be accurate when initially ti1, ti2 == -math.huge, math.huge
+local function rect_getSegmentIntersectionIndices(x,y,w,h, x1,y1,x2,y2, ti1,ti2)
+ ti1, ti2 = ti1 or 0, ti2 or 1
+ local dx, dy = x2-x1, y2-y1
+ local nx, ny
+ local nx1, ny1, nx2, ny2 = 0,0,0,0
+ local p, q, r
+
+ for side = 1,4 do
+ if side == 1 then nx,ny,p,q = -1, 0, -dx, x1 - x -- left
+ elseif side == 2 then nx,ny,p,q = 1, 0, dx, x + w - x1 -- right
+ elseif side == 3 then nx,ny,p,q = 0, -1, -dy, y1 - y -- top
+ else nx,ny,p,q = 0, 1, dy, y + h - y1 -- bottom
+ end
+
+ if p == 0 then
+ if q <= 0 then return nil end
+ else
+ r = q / p
+ if p < 0 then
+ if r > ti2 then return nil
+ elseif r > ti1 then ti1,nx1,ny1 = r,nx,ny
+ end
+ else -- p > 0
+ if r < ti1 then return nil
+ elseif r < ti2 then ti2,nx2,ny2 = r,nx,ny
+ end
+ end
+ end
+ end
+
+ return ti1,ti2, nx1,ny1, nx2,ny2
+end
+
+-- Calculates the minkowsky difference between 2 rects, which is another rect
+local function rect_getDiff(x1,y1,w1,h1, x2,y2,w2,h2)
+ return x2 - x1 - w1,
+ y2 - y1 - h1,
+ w1 + w2,
+ h1 + h2
+end
+
+local function rect_containsPoint(x,y,w,h, px,py)
+ return px - x > DELTA and py - y > DELTA and
+ x + w - px > DELTA and y + h - py > DELTA
+end
+
+local function rect_isIntersecting(x1,y1,w1,h1, x2,y2,w2,h2)
+ return x1 < x2+w2 and x2 < x1+w1 and
+ y1 < y2+h2 and y2 < y1+h1
+end
+
+local function rect_getSquareDistance(x1,y1,w1,h1, x2,y2,w2,h2)
+ local dx = x1 - x2 + (w1 - w2)/2
+ local dy = y1 - y2 + (h1 - h2)/2
+ return dx*dx + dy*dy
+end
+
+local function rect_detectCollision(x1,y1,w1,h1, x2,y2,w2,h2, goalX, goalY)
+ goalX = goalX or x1
+ goalY = goalY or y1
+
+ local dx, dy = goalX - x1, goalY - y1
+ local x,y,w,h = rect_getDiff(x1,y1,w1,h1, x2,y2,w2,h2)
+
+ local overlaps, ti, nx, ny
+
+ if rect_containsPoint(x,y,w,h, 0,0) then -- item was intersecting other
+ local px, py = rect_getNearestCorner(x,y,w,h, 0, 0)
+ local wi, hi = min(w1, abs(px)), min(h1, abs(py)) -- area of intersection
+ ti = -wi * hi -- ti is the negative area of intersection
+ overlaps = true
+ else
+ local ti1,ti2,nx1,ny1 = rect_getSegmentIntersectionIndices(x,y,w,h, 0,0,dx,dy, -math.huge, math.huge)
+
+ -- item tunnels into other
+ if ti1
+ and ti1 < 1
+ and (abs(ti1 - ti2) >= DELTA) -- special case for rect going through another rect's corner
+ and (0 < ti1 + DELTA
+ or 0 == ti1 and ti2 > 0)
+ then
+ ti, nx, ny = ti1, nx1, ny1
+ overlaps = false
+ end
+ end
+
+ if not ti then return end
+
+ local tx, ty
+
+ if overlaps then
+ if dx == 0 and dy == 0 then
+ -- intersecting and not moving - use minimum displacement vector
+ local px, py = rect_getNearestCorner(x,y,w,h, 0,0)
+ if abs(px) < abs(py) then py = 0 else px = 0 end
+ nx, ny = sign(px), sign(py)
+ tx, ty = x1 + px, y1 + py
+ else
+ -- intersecting and moving - move in the opposite direction
+ local ti1, _
+ ti1,_,nx,ny = rect_getSegmentIntersectionIndices(x,y,w,h, 0,0,dx,dy, -math.huge, 1)
+ if not ti1 then return end
+ tx, ty = x1 + dx * ti1, y1 + dy * ti1
+ end
+ else -- tunnel
+ tx, ty = x1 + dx * ti, y1 + dy * ti
+ end
+
+ return {
+ overlaps = overlaps,
+ ti = ti,
+ move = {x = dx, y = dy},
+ normal = {x = nx, y = ny},
+ touch = {x = tx, y = ty},
+ itemRect = {x = x1, y = y1, w = w1, h = h1},
+ otherRect = {x = x2, y = y2, w = w2, h = h2}
+ }
+end
+
+------------------------------------------
+-- Grid functions
+------------------------------------------
+
+local function grid_toWorld(cellSize, cx, cy)
+ return (cx - 1)*cellSize, (cy-1)*cellSize
+end
+
+local function grid_toCell(cellSize, x, y)
+ return floor(x / cellSize) + 1, floor(y / cellSize) + 1
+end
+
+-- grid_traverse* functions are based on "A Fast Voxel Traversal Algorithm for Ray Tracing",
+-- by John Amanides and Andrew Woo - http://www.cse.yorku.ca/~amana/research/grid.pdf
+-- It has been modified to include both cells when the ray "touches a grid corner",
+-- and with a different exit condition
+
+local function grid_traverse_initStep(cellSize, ct, t1, t2)
+ local v = t2 - t1
+ if v > 0 then
+ return 1, cellSize / v, ((ct + v) * cellSize - t1) / v
+ elseif v < 0 then
+ return -1, -cellSize / v, ((ct + v - 1) * cellSize - t1) / v
+ else
+ return 0, math.huge, math.huge
+ end
+end
+
+local function grid_traverse(cellSize, x1,y1,x2,y2, f)
+ local cx1,cy1 = grid_toCell(cellSize, x1,y1)
+ local cx2,cy2 = grid_toCell(cellSize, x2,y2)
+ local stepX, dx, tx = grid_traverse_initStep(cellSize, cx1, x1, x2)
+ local stepY, dy, ty = grid_traverse_initStep(cellSize, cy1, y1, y2)
+ local cx,cy = cx1,cy1
+
+ f(cx, cy)
+
+ -- The default implementation had an infinite loop problem when
+ -- approaching the last cell in some occassions. We finish iterating
+ -- when we are *next* to the last cell
+ while abs(cx - cx2) + abs(cy - cy2) > 1 do
+ if tx < ty then
+ tx, cx = tx + dx, cx + stepX
+ f(cx, cy)
+ else
+ -- Addition: include both cells when going through corners
+ if tx == ty then f(cx + stepX, cy) end
+ ty, cy = ty + dy, cy + stepY
+ f(cx, cy)
+ end
+ end
+
+ -- If we have not arrived to the last cell, use it
+ if cx ~= cx2 or cy ~= cy2 then f(cx2, cy2) end
+
+end
+
+local function grid_toCellRect(cellSize, x,y,w,h)
+ local cx,cy = grid_toCell(cellSize, x, y)
+ local cr,cb = ceil((x+w) / cellSize), ceil((y+h) / cellSize)
+ return cx, cy, cr - cx + 1, cb - cy + 1
+end
+
+------------------------------------------
+-- Responses
+------------------------------------------
+
+local touch = function(world, col, x,y,w,h, goalX, goalY, filter)
+ return col.touch.x, col.touch.y, {}, 0
+end
+
+local cross = function(world, col, x,y,w,h, goalX, goalY, filter)
+ local cols, len = world:project(col.item, x,y,w,h, goalX, goalY, filter)
+ return goalX, goalY, cols, len
+end
+
+local slide = function(world, col, x,y,w,h, goalX, goalY, filter)
+ goalX = goalX or x
+ goalY = goalY or y
+
+ local tch, move = col.touch, col.move
+ if move.x ~= 0 or move.y ~= 0 then
+ if col.normal.x ~= 0 then
+ goalX = tch.x
+ else
+ goalY = tch.y
+ end
+ end
+
+ col.slide = {x = goalX, y = goalY}
+
+ x,y = tch.x, tch.y
+ local cols, len = world:project(col.item, x,y,w,h, goalX, goalY, filter)
+ return goalX, goalY, cols, len
+end
+
+local bounce = function(world, col, x,y,w,h, goalX, goalY, filter)
+ goalX = goalX or x
+ goalY = goalY or y
+
+ local tch, move = col.touch, col.move
+ local tx, ty = tch.x, tch.y
+
+ local bx, by = tx, ty
+
+ if move.x ~= 0 or move.y ~= 0 then
+ local bnx, bny = goalX - tx, goalY - ty
+ if col.normal.x == 0 then bny = -bny else bnx = -bnx end
+ bx, by = tx + bnx, ty + bny
+ end
+
+ col.bounce = {x = bx, y = by}
+ x,y = tch.x, tch.y
+ goalX, goalY = bx, by
+
+ local cols, len = world:project(col.item, x,y,w,h, goalX, goalY, filter)
+ return goalX, goalY, cols, len
+end
+
+------------------------------------------
+-- World
+------------------------------------------
+
+local World = {}
+local World_mt = {__index = World}
+
+-- Private functions and methods
+
+local function sortByWeight(a,b) return a.weight < b.weight end
+
+local function sortByTiAndDistance(a,b)
+ if a.ti == b.ti then
+ local ir, ar, br = a.itemRect, a.otherRect, b.otherRect
+ local ad = rect_getSquareDistance(ir.x,ir.y,ir.w,ir.h, ar.x,ar.y,ar.w,ar.h)
+ local bd = rect_getSquareDistance(ir.x,ir.y,ir.w,ir.h, br.x,br.y,br.w,br.h)
+ return ad < bd
+ end
+ return a.ti < b.ti
+end
+
+local function addItemToCell(self, item, cx, cy)
+ self.rows[cy] = self.rows[cy] or setmetatable({}, {__mode = 'v'})
+ local row = self.rows[cy]
+ row[cx] = row[cx] or {itemCount = 0, x = cx, y = cy, items = setmetatable({}, {__mode = 'k'})}
+ local cell = row[cx]
+ self.nonEmptyCells[cell] = true
+ if not cell.items[item] then
+ cell.items[item] = true
+ cell.itemCount = cell.itemCount + 1
+ end
+end
+
+local function removeItemFromCell(self, item, cx, cy)
+ local row = self.rows[cy]
+ if not row or not row[cx] or not row[cx].items[item] then return false end
+
+ local cell = row[cx]
+ cell.items[item] = nil
+ cell.itemCount = cell.itemCount - 1
+ if cell.itemCount == 0 then
+ self.nonEmptyCells[cell] = nil
+ end
+ return true
+end
+
+local function getDictItemsInCellRect(self, cl,ct,cw,ch)
+ local items_dict = {}
+ for cy=ct,ct+ch-1 do
+ local row = self.rows[cy]
+ if row then
+ for cx=cl,cl+cw-1 do
+ local cell = row[cx]
+ if cell and cell.itemCount > 0 then -- no cell.itemCount > 1 because tunneling
+ for item,_ in pairs(cell.items) do
+ items_dict[item] = true
+ end
+ end
+ end
+ end
+ end
+
+ return items_dict
+end
+
+local function getCellsTouchedBySegment(self, x1,y1,x2,y2)
+
+ local cells, cellsLen, visited = {}, 0, {}
+
+ grid_traverse(self.cellSize, x1,y1,x2,y2, function(cx, cy)
+ local row = self.rows[cy]
+ if not row then return end
+ local cell = row[cx]
+ if not cell or visited[cell] then return end
+
+ visited[cell] = true
+ cellsLen = cellsLen + 1
+ cells[cellsLen] = cell
+ end)
+
+ return cells, cellsLen
+end
+
+local function getInfoAboutItemsTouchedBySegment(self, x1,y1, x2,y2, filter)
+ local cells, len = getCellsTouchedBySegment(self, x1,y1,x2,y2)
+ local cell, rect, l,t,w,h, ti1,ti2, tii0,tii1
+ local visited, itemInfo, itemInfoLen = {},{},0
+ for i=1,len do
+ cell = cells[i]
+ for item in pairs(cell.items) do
+ if not visited[item] then
+ visited[item] = true
+ if (not filter or filter(item)) then
+ rect = self.rects[item]
+ l,t,w,h = rect.x,rect.y,rect.w,rect.h
+
+ ti1,ti2 = rect_getSegmentIntersectionIndices(l,t,w,h, x1,y1, x2,y2, 0, 1)
+ if ti1 and ((0 < ti1 and ti1 < 1) or (0 < ti2 and ti2 < 1)) then
+ -- the sorting is according to the t of an infinite line, not the segment
+ tii0,tii1 = rect_getSegmentIntersectionIndices(l,t,w,h, x1,y1, x2,y2, -math.huge, math.huge)
+ itemInfoLen = itemInfoLen + 1
+ itemInfo[itemInfoLen] = {item = item, ti1 = ti1, ti2 = ti2, weight = min(tii0,tii1)}
+ end
+ end
+ end
+ end
+ end
+ table.sort(itemInfo, sortByWeight)
+ return itemInfo, itemInfoLen
+end
+
+local function getResponseByName(self, name)
+ local response = self.responses[name]
+ if not response then
+ error(('Unknown collision type: %s (%s)'):format(name, type(name)))
+ end
+ return response
+end
+
+
+-- Misc Public Methods
+
+function World:addResponse(name, response)
+ self.responses[name] = response
+end
+
+function World:project(item, x,y,w,h, goalX, goalY, filter)
+ assertIsRect(x,y,w,h)
+
+ goalX = goalX or x
+ goalY = goalY or y
+ filter = filter or defaultFilter
+
+ local collisions, len = {}, 0
+
+ local visited = {}
+ if item ~= nil then visited[item] = true end
+
+ -- This could probably be done with less cells using a polygon raster over the cells instead of a
+ -- bounding rect of the whole movement. Conditional to building a queryPolygon method
+ local tl, tt = min(goalX, x), min(goalY, y)
+ local tr, tb = max(goalX + w, x+w), max(goalY + h, y+h)
+ local tw, th = tr-tl, tb-tt
+
+ local cl,ct,cw,ch = grid_toCellRect(self.cellSize, tl,tt,tw,th)
+
+ local dictItemsInCellRect = getDictItemsInCellRect(self, cl,ct,cw,ch)
+
+ for other,_ in pairs(dictItemsInCellRect) do
+ if not visited[other] then
+ visited[other] = true
+
+ local responseName = filter(item, other)
+ if responseName then
+ local ox,oy,ow,oh = self:getRect(other)
+ local col = rect_detectCollision(x,y,w,h, ox,oy,ow,oh, goalX, goalY)
+
+ if col then
+ col.other = other
+ col.item = item
+ col.type = responseName
+
+ len = len + 1
+ collisions[len] = col
+ end
+ end
+ end
+ end
+
+ table.sort(collisions, sortByTiAndDistance)
+
+ return collisions, len
+end
+
+function World:countCells()
+ local count = 0
+ for _,row in pairs(self.rows) do
+ for _,_ in pairs(row) do
+ count = count + 1
+ end
+ end
+ return count
+end
+
+function World:hasItem(item)
+ return not not self.rects[item]
+end
+
+function World:getItems()
+ local items, len = {}, 0
+ for item,_ in pairs(self.rects) do
+ len = len + 1
+ items[len] = item
+ end
+ return items, len
+end
+
+function World:countItems()
+ local len = 0
+ for _ in pairs(self.rects) do len = len + 1 end
+ return len
+end
+
+function World:getRect(item)
+ local rect = self.rects[item]
+ if not rect then
+ error('Item ' .. tostring(item) .. ' must be added to the world before getting its rect. Use world:add(item, x,y,w,h) to add it first.')
+ end
+ return rect.x, rect.y, rect.w, rect.h
+end
+
+function World:toWorld(cx, cy)
+ return grid_toWorld(self.cellSize, cx, cy)
+end
+
+function World:toCell(x,y)
+ return grid_toCell(self.cellSize, x, y)
+end
+
+
+--- Query methods
+
+function World:queryRect(x,y,w,h, filter)
+
+ assertIsRect(x,y,w,h)
+
+ local cl,ct,cw,ch = grid_toCellRect(self.cellSize, x,y,w,h)
+ local dictItemsInCellRect = getDictItemsInCellRect(self, cl,ct,cw,ch)
+
+ local items, len = {}, 0
+
+ local rect
+ for item,_ in pairs(dictItemsInCellRect) do
+ rect = self.rects[item]
+ if (not filter or filter(item))
+ and rect_isIntersecting(x,y,w,h, rect.x, rect.y, rect.w, rect.h)
+ then
+ len = len + 1
+ items[len] = item
+ end
+ end
+
+ return items, len
+end
+
+function World:queryPoint(x,y, filter)
+ local cx,cy = self:toCell(x,y)
+ local dictItemsInCellRect = getDictItemsInCellRect(self, cx,cy,1,1)
+
+ local items, len = {}, 0
+
+ local rect
+ for item,_ in pairs(dictItemsInCellRect) do
+ rect = self.rects[item]
+ if (not filter or filter(item))
+ and rect_containsPoint(rect.x, rect.y, rect.w, rect.h, x, y)
+ then
+ len = len + 1
+ items[len] = item
+ end
+ end
+
+ return items, len
+end
+
+function World:querySegment(x1, y1, x2, y2, filter)
+ local itemInfo, len = getInfoAboutItemsTouchedBySegment(self, x1, y1, x2, y2, filter)
+ local items = {}
+ for i=1, len do
+ items[i] = itemInfo[i].item
+ end
+ return items, len
+end
+
+function World:querySegmentWithCoords(x1, y1, x2, y2, filter)
+ local itemInfo, len = getInfoAboutItemsTouchedBySegment(self, x1, y1, x2, y2, filter)
+ local dx, dy = x2-x1, y2-y1
+ local info, ti1, ti2
+ for i=1, len do
+ info = itemInfo[i]
+ ti1 = info.ti1
+ ti2 = info.ti2
+
+ info.weight = nil
+ info.x1 = x1 + dx * ti1
+ info.y1 = y1 + dy * ti1
+ info.x2 = x1 + dx * ti2
+ info.y2 = y1 + dy * ti2
+ end
+ return itemInfo, len
+end
+
+
+--- Main methods
+
+function World:add(item, x,y,w,h)
+ local rect = self.rects[item]
+ if rect then
+ error('Item ' .. tostring(item) .. ' added to the world twice.')
+ end
+ assertIsRect(x,y,w,h)
+
+ self.rects[item] = {x=x,y=y,w=w,h=h}
+
+ local cl,ct,cw,ch = grid_toCellRect(self.cellSize, x,y,w,h)
+ for cy = ct, ct+ch-1 do
+ for cx = cl, cl+cw-1 do
+ addItemToCell(self, item, cx, cy)
+ end
+ end
+
+ return item
+end
+
+function World:remove(item)
+ local x,y,w,h = self:getRect(item)
+
+ self.rects[item] = nil
+ local cl,ct,cw,ch = grid_toCellRect(self.cellSize, x,y,w,h)
+ for cy = ct, ct+ch-1 do
+ for cx = cl, cl+cw-1 do
+ removeItemFromCell(self, item, cx, cy)
+ end
+ end
+end
+
+function World:update(item, x2,y2,w2,h2)
+ local x1,y1,w1,h1 = self:getRect(item)
+ w2,h2 = w2 or w1, h2 or h1
+ assertIsRect(x2,y2,w2,h2)
+
+ if x1 ~= x2 or y1 ~= y2 or w1 ~= w2 or h1 ~= h2 then
+
+ local cellSize = self.cellSize
+ local cl1,ct1,cw1,ch1 = grid_toCellRect(cellSize, x1,y1,w1,h1)
+ local cl2,ct2,cw2,ch2 = grid_toCellRect(cellSize, x2,y2,w2,h2)
+
+ if cl1 ~= cl2 or ct1 ~= ct2 or cw1 ~= cw2 or ch1 ~= ch2 then
+
+ local cr1, cb1 = cl1+cw1-1, ct1+ch1-1
+ local cr2, cb2 = cl2+cw2-1, ct2+ch2-1
+ local cyOut
+
+ for cy = ct1, cb1 do
+ cyOut = cy < ct2 or cy > cb2
+ for cx = cl1, cr1 do
+ if cyOut or cx < cl2 or cx > cr2 then
+ removeItemFromCell(self, item, cx, cy)
+ end
+ end
+ end
+
+ for cy = ct2, cb2 do
+ cyOut = cy < ct1 or cy > cb1
+ for cx = cl2, cr2 do
+ if cyOut or cx < cl1 or cx > cr1 then
+ addItemToCell(self, item, cx, cy)
+ end
+ end
+ end
+
+ end
+
+ local rect = self.rects[item]
+ rect.x, rect.y, rect.w, rect.h = x2,y2,w2,h2
+
+ end
+end
+
+function World:move(item, goalX, goalY, filter)
+ local actualX, actualY, cols, len = self:check(item, goalX, goalY, filter)
+
+ self:update(item, actualX, actualY)
+
+ return actualX, actualY, cols, len
+end
+
+function World:check(item, goalX, goalY, filter)
+ filter = filter or defaultFilter
+
+ local visited = {[item] = true}
+ local visitedFilter = function(itm, other)
+ if visited[other] then return false end
+ return filter(itm, other)
+ end
+
+ local cols, len = {}, 0
+
+ local x,y,w,h = self:getRect(item)
+
+ local projected_cols, projected_len = self:project(item, x,y,w,h, goalX,goalY, visitedFilter)
+
+ while projected_len > 0 do
+ local col = projected_cols[1]
+ len = len + 1
+ cols[len] = col
+
+ visited[col.other] = true
+
+ local response = getResponseByName(self, col.type)
+
+ goalX, goalY, projected_cols, projected_len = response(
+ self,
+ col,
+ x, y, w, h,
+ goalX, goalY,
+ visitedFilter
+ )
+ end
+
+ return goalX, goalY, cols, len
+end
+
+
+-- Public library functions
+
+bump.newWorld = function(cellSize)
+ cellSize = cellSize or 64
+ assertIsPositiveNumber(cellSize, 'cellSize')
+ local world = setmetatable({
+ cellSize = cellSize,
+ rects = {},
+ rows = {},
+ nonEmptyCells = {},
+ responses = {}
+ }, World_mt)
+
+ world:addResponse('touch', touch)
+ world:addResponse('cross', cross)
+ world:addResponse('slide', slide)
+ world:addResponse('bounce', bounce)
+
+ return world
+end
+
+bump.rect = {
+ getNearestCorner = rect_getNearestCorner,
+ getSegmentIntersectionIndices = rect_getSegmentIntersectionIndices,
+ getDiff = rect_getDiff,
+ containsPoint = rect_containsPoint,
+ isIntersecting = rect_isIntersecting,
+ getSquareDistance = rect_getSquareDistance,
+ detectCollision = rect_detectCollision
+}
+
+bump.responses = {
+ touch = touch,
+ cross = cross,
+ slide = slide,
+ bounce = bounce
+}
+
+return bump
diff --git a/bootleg.love/core/modules/world/libs/tsort.lua b/bootleg.love/core/modules/world/libs/tsort.lua
new file mode 100644
index 0000000..3f8dfc9
--- /dev/null
+++ b/bootleg.love/core/modules/world/libs/tsort.lua
@@ -0,0 +1,84 @@
+-- See: https://github.com/bungle/lua-resty-tsort
+--
+-- Copyright (c) 2016, Aapo Talvensaari
+-- All rights reserved.
+--
+-- Redistribution and use in source and binary forms, with or without modification,
+-- are permitted provided that the following conditions are met:
+--
+-- * Redistributions of source code must retain the above copyright notice, this
+-- list of conditions and the following disclaimer.
+--
+-- * Redistributions in binary form must reproduce the above copyright notice, this
+-- list of conditions and the following disclaimer in the documentation and/or
+-- other materials provided with the distribution.
+--
+-- THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
+-- ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
+-- WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+-- DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR
+-- ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
+-- (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
+-- LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON
+-- ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+-- (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
+-- SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+
+local setmetatable = setmetatable
+local pairs = pairs
+local type = type
+local function visit(k, n, m, s)
+ if m[k] == 0 then return 1 end
+ if m[k] == 1 then return end
+ m[k] = 0
+ local f = n[k]
+ for i=1, #f do
+ if visit(f[i], n, m, s) then return 1 end
+ end
+ m[k] = 1
+ s[#s+1] = k
+end
+local tsort = {}
+tsort.__index = tsort
+function tsort.new()
+ return setmetatable({ n = {} }, tsort)
+end
+function tsort:add(...)
+ local p = { ... }
+ local c = #p
+ if c == 0 then return self end
+ if c == 1 then
+ p = p[1]
+ if type(p) == "table" then
+ c = #p
+ else
+ p = { p }
+ end
+ end
+ local n = self.n
+ for i=1, c do
+ local f = p[i]
+ if n[f] == nil then n[f] = {} end
+ end
+ for i=2, c, 1 do
+ local f = p[i]
+ local t = p[i-1]
+ local o = n[f]
+ o[#o+1] = t
+ end
+ return self
+end
+function tsort:sort()
+ local n = self.n
+ local s = {}
+ local m = {}
+ for k in pairs(n) do
+ if m[k] == nil then
+ if visit(k, n, m, s) then
+ return nil, "There is a circular dependency in the graph. It is not possible to derive a topological sort."
+ end
+ end
+ end
+ return s
+end
+return tsort
diff --git a/bootleg.love/core/modules/world/maps/init.lua b/bootleg.love/core/modules/world/maps/init.lua
new file mode 100644
index 0000000..369bd99
--- /dev/null
+++ b/bootleg.love/core/modules/world/maps/init.lua
@@ -0,0 +1,7 @@
+local cwd = (...):gsub('%.init$', '') .. "."
+
+local mapObjects = {}
+mapObjects.Sti = require(cwd .. "sti")
+mapObjects.Base = require(cwd .. "parent")
+
+return mapObjects
diff --git a/bootleg.love/core/modules/world/maps/libs/sti/graphics.lua b/bootleg.love/core/modules/world/maps/libs/sti/graphics.lua
new file mode 100644
index 0000000..1d73379
--- /dev/null
+++ b/bootleg.love/core/modules/world/maps/libs/sti/graphics.lua
@@ -0,0 +1,128 @@
+local lg = love.graphics
+local graphics = { isCreated = lg and true or false }
+
+function graphics.newSpriteBatch(...)
+ if graphics.isCreated then
+ return lg.newSpriteBatch(...)
+ end
+end
+
+function graphics.newCanvas(...)
+ if graphics.isCreated then
+ return lg.newCanvas(...)
+ end
+end
+
+function graphics.newImage(...)
+ if graphics.isCreated then
+ return lg.newImage(...)
+ end
+end
+
+function graphics.newQuad(...)
+ if graphics.isCreated then
+ return lg.newQuad(...)
+ end
+end
+
+function graphics.getCanvas(...)
+ if graphics.isCreated then
+ return lg.getCanvas(...)
+ end
+end
+
+function graphics.setCanvas(...)
+ if graphics.isCreated then
+ return lg.setCanvas(...)
+ end
+end
+
+function graphics.clear(...)
+ if graphics.isCreated then
+ return lg.clear(...)
+ end
+end
+
+function graphics.push(...)
+ if graphics.isCreated then
+ return lg.push(...)
+ end
+end
+
+function graphics.origin(...)
+ if graphics.isCreated then
+ return lg.origin(...)
+ end
+end
+
+function graphics.scale(...)
+ if graphics.isCreated then
+ return lg.scale(...)
+ end
+end
+
+function graphics.translate(...)
+ if graphics.isCreated then
+ return lg.translate(...)
+ end
+end
+
+function graphics.pop(...)
+ if graphics.isCreated then
+ return lg.pop(...)
+ end
+end
+
+function graphics.draw(...)
+ if graphics.isCreated then
+ return lg.draw(...)
+ end
+end
+
+function graphics.rectangle(...)
+ if graphics.isCreated then
+ return lg.rectangle(...)
+ end
+end
+
+function graphics.getColor(...)
+ if graphics.isCreated then
+ return lg.getColor(...)
+ end
+end
+
+function graphics.setColor(...)
+ if graphics.isCreated then
+ return lg.setColor(...)
+ end
+end
+
+function graphics.line(...)
+ if graphics.isCreated then
+ return lg.line(...)
+ end
+end
+
+function graphics.polygon(...)
+ if graphics.isCreated then
+ return lg.polygon(...)
+ end
+end
+
+function graphics.getWidth()
+ if graphics.isCreated then
+ return lg.getWidth()
+ end
+
+ return 0
+end
+
+function graphics.getHeight()
+ if graphics.isCreated then
+ return lg.getHeight()
+ end
+
+ return 0
+end
+
+return graphics
diff --git a/bootleg.love/core/modules/world/maps/libs/sti/init.lua b/bootleg.love/core/modules/world/maps/libs/sti/init.lua
new file mode 100644
index 0000000..de1bd16
--- /dev/null
+++ b/bootleg.love/core/modules/world/maps/libs/sti/init.lua
@@ -0,0 +1,1485 @@
+--- Simple and fast Tiled map loader and renderer.
+-- @module sti
+-- @author Landon Manning
+-- @copyright 2016
+-- @license MIT/X11
+
+local STI = {
+ _LICENSE = "MIT/X11",
+ _URL = "https://github.com/karai17/Simple-Tiled-Implementation",
+ _VERSION = "0.18.2.1",
+ _DESCRIPTION = "Simple Tiled Implementation is a Tiled Map Editor library designed for the *awesome* LÖVE framework.",
+ cache = {}
+}
+STI.__index = STI
+
+local cwd = (...):gsub('%.init$', '') .. "."
+local utils = require(cwd .. "utils")
+local ceil = math.ceil
+local floor = math.floor
+local lg = require(cwd .. "graphics")
+local Map = {}
+Map.__index = Map
+
+local function new(map, plugins, ox, oy)
+ local dir = ""
+
+ if type(map) == "table" then
+ map = setmetatable(map, Map)
+ else
+ -- 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 directory of map
+ dir = map:reverse():find("[/\\]") or ""
+ if dir ~= "" then
+ dir = map:sub(1, 1 + (#map - dir))
+ end
+
+ -- Load map
+ map = setmetatable(assert(love.filesystem.load(map))(), Map)
+ end
+
+ map:init(dir, plugins, ox, oy)
+
+ return map
+end
+
+--- Instance a new map.
+-- @param map Path to the map file or the map table itself
+-- @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.__call(_, map, plugins, ox, oy)
+ return new(map, plugins, ox, oy)
+end
+
+--- Flush image cache.
+function STI:flush()
+ self.cache = {}
+end
+
+--- Map object
+
+--- 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)
+function Map:init(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.freeBatchSprites = {}
+ setmetatable(self.freeBatchSprites, { __mode = 'k' })
+
+ -- 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
+ if lg.isCreated then
+ local formatted_path = utils.format_path(path .. tileset.image)
+
+ if not STI.cache[formatted_path] then
+ utils.fix_transparent_color(tileset, formatted_path)
+ utils.cache_image(STI, formatted_path, tileset.image)
+ else
+ tileset.image = STI.cache[formatted_path]
+ end
+ end
+
+ gid = self:setTiles(i, tileset, gid)
+ end
+
+ -- Set layers
+ for _, layer in ipairs(self.layers) do
+ self:setLayer(layer, path)
+ end
+end
+
+--- Load plugins
+-- @param plugins A list of plugins to load
+function Map:loadPlugins(plugins)
+ for _, plugin in ipairs(plugins) do
+ local pluginModulePath = cwd .. 'plugins.' .. plugin
+ local ok, pluginModule = pcall(require, pluginModulePath)
+ if ok then
+ for k, func in pairs(pluginModule) 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 quad = lg.newQuad
+ local imageW = tileset.imagewidth
+ local imageH = tileset.imageheight
+ local tileW = tileset.tilewidth
+ local tileH = tileset.tileheight
+ local margin = tileset.margin
+ local spacing = tileset.spacing
+ local w = utils.get_tiles(imageW, tileW, margin, spacing)
+ local h = utils.get_tiles(imageH, tileH, margin, spacing)
+
+ for y = 1, h do
+ for x = 1, w do
+ local id = gid - tileset.firstgid
+ local quadX = (x - 1) * tileW + margin + (x - 1) * spacing
+ local quadY = (y - 1) * tileH + margin + (y - 1) * spacing
+ 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(
+ quadX, quadY,
+ tileW, tileH,
+ imageW, imageH
+ ),
+ properties = properties or {},
+ terrain = terrain,
+ animation = animation,
+ objectGroup = objectGroup,
+ frame = 1,
+ time = 0,
+ width = tileW,
+ height = tileH,
+ sx = 1,
+ sy = 1,
+ r = 0,
+ offset = tileset.tileoffset,
+ }
+
+ 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
+function Map:setLayer(layer, path)
+ if layer.encoding then
+ if layer.encoding == "base64" then
+ assert(require "ffi", "Compressed maps require LuaJIT FFI.\nPlease Switch your interperator to LuaJIT or your Tile Layer Format to \"CSV\".")
+ local fd = love.data.decode("string", "base64", layer.data)
+
+ if not layer.compression then
+ layer.data = utils.get_decompressed_data(fd)
+ else
+ assert(love.data.decompress, "zlib and gzip compression require LOVE 11.0+.\nPlease set your Tile Layer Format to \"Base64 (uncompressed)\" or \"CSV\".")
+
+ if layer.compression == "zlib" then
+ local data = love.data.decompress("string", "zlib", fd)
+ layer.data = utils.get_decompressed_data(data)
+ end
+
+ if layer.compression == "gzip" then
+ local data = love.data.decompress("string", "gzip", fd)
+ layer.data = utils.get_decompressed_data(data)
+ end
+ end
+ end
+ end
+
+ layer.x = (layer.x or 0) + layer.offsetx + self.offsetx
+ layer.y = (layer.y or 0) + layer.offsety + self.offsety
+ layer.update = function() 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 = utils.format_path(path .. layer.image)
+ if not STI.cache[formatted_path] then
+ utils.cache_image(STI, formatted_path)
+ end
+
+ layer.image = 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
+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
+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
+function Map:setObjectCoordinates(layer)
+ 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 = utils.rotate_vertex(self, 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 = utils.convert_ellipse_to_polygon(x, y, w, h)
+
+ for _, vertex in ipairs(vertices) do
+ vertex.x, vertex.y = utils.rotate_vertex(self, 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 = utils.rotate_vertex(self, 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 = utils.rotate_vertex(self, vertex, x, y, cos, sin)
+ end
+ end
+ end
+end
+
+--- Convert tile location to tile instance location
+-- @param layer Tile layer
+-- @param tile Tile
+-- @param x Tile location on X axis (in tiles)
+-- @param y Tile location on Y axis (in tiles)
+-- @return number Tile instance location on X axis (in pixels)
+-- @return number Tile instance location on Y axis (in pixels)
+function Map:getLayerTilePosition(layer, tile, x, y)
+ local tileW = self.tilewidth
+ local tileH = self.tileheight
+ local tileX, tileY
+
+ if self.orientation == "orthogonal" then
+ local tileset = self.tilesets[tile.tileset]
+ tileX = (x - 1) * tileW + tile.offset.x
+ tileY = (y - 0) * tileH + tile.offset.y - tileset.tileheight
+ tileX, tileY = utils.compensate(tile, tileX, tileY, tileW, tileH)
+ elseif self.orientation == "isometric" then
+ tileX = (x - y) * (tileW / 2) + tile.offset.x + layer.width * tileW / 2 - self.tilewidth / 2
+ tileY = (x + y - 2) * (tileH / 2) + tile.offset.y
+ else
+ local sideLen = self.hexsidelength or 0
+ if self.staggeraxis == "y" then
+ if self.staggerindex == "odd" then
+ if y % 2 == 0 then
+ tileX = (x - 1) * tileW + tileW / 2 + tile.offset.x
+ else
+ tileX = (x - 1) * tileW + tile.offset.x
+ end
+ else
+ if y % 2 == 0 then
+ tileX = (x - 1) * tileW + tile.offset.x
+ else
+ tileX = (x - 1) * tileW + tileW / 2 + tile.offset.x
+ end
+ end
+
+ local rowH = tileH - (tileH - sideLen) / 2
+ tileY = (y - 1) * rowH + tile.offset.y
+ else
+ if self.staggerindex == "odd" then
+ if x % 2 == 0 then
+ tileY = (y - 1) * tileH + tileH / 2 + tile.offset.y
+ else
+ tileY = (y - 1) * tileH + tile.offset.y
+ end
+ else
+ if x % 2 == 0 then
+ tileY = (y - 1) * tileH + tile.offset.y
+ else
+ tileY = (y - 1) * tileH + tileH / 2 + tile.offset.y
+ end
+ end
+
+ local colW = tileW - (tileW - sideLen) / 2
+ tileX = (x - 1) * colW + tile.offset.x
+ end
+ end
+
+ return tileX, tileY
+end
+
+--- Place new tile instance
+-- @param layer Tile layer
+-- @param tile Tile
+-- @param number Tile location on X axis (in tiles)
+-- @param number Tile location on Y axis (in tiles)
+function Map:addNewLayerTile(layer, tile, x, y)
+ local tileset = tile.tileset
+ local image = self.tilesets[tile.tileset].image
+
+ layer.batches[tileset] = layer.batches[tileset]
+ or lg.newSpriteBatch(image, layer.width * layer.height)
+
+ local batch = layer.batches[tileset]
+ local tileX, tileY = self:getLayerTilePosition(layer, tile, x, y)
+
+ local tab = {
+ layer = layer,
+ gid = tile.gid,
+ x = tileX,
+ y = tileY,
+ r = tile.r,
+ oy = 0
+ }
+
+ if batch then
+ tab.batch = batch
+ tab.id = batch:add(tile.quad, tileX, tileY, tile.r, tile.sx, tile.sy)
+ end
+
+ self.tileInstances[tile.gid] = self.tileInstances[tile.gid] or {}
+ table.insert(self.tileInstances[tile.gid], tab)
+end
+
+--- Batch Tiles in Tile Layer for improved draw speed
+-- @param layer The Tile Layer
+function Map:setSpriteBatches(layer)
+ layer.batches = {}
+
+ if self.orientation == "orthogonal" or self.orientation == "isometric" then
+ local startX = 1
+ local startY = 1
+ local endX = layer.width
+ local endY = layer.height
+ local incrementX = 1
+ local incrementY = 1
+
+ -- Determine order to add tiles to sprite batch
+ -- Defaults to right-down
+ if self.renderorder == "right-up" then
+ startX, endX, incrementX = startX, endX, 1
+ startY, endY, incrementY = endY, startY, -1
+ elseif self.renderorder == "left-down" then
+ startX, endX, incrementX = endX, startX, -1
+ startY, endY, incrementY = startY, endY, 1
+ elseif self.renderorder == "left-up" then
+ startX, endX, incrementX = endX, startX, -1
+ startY, endY, incrementY = endY, startY, -1
+ end
+
+ for y = startY, endY, incrementY do
+ for x = startX, endX, incrementX do
+ local tile = layer.data[y][x]
+
+ if tile then
+ self:addNewLayerTile(layer, tile, x, y)
+ end
+ end
+ end
+ else
+ local sideLen = self.hexsidelength or 0
+
+ if self.staggeraxis == "y" then
+ for y = 1, layer.height do
+ for x = 1, layer.width do
+ local tile = layer.data[y][x]
+
+ if tile then
+ self:addNewLayerTile(layer, tile, x, y)
+ end
+ end
+ end
+ else
+ local i = 0
+ local _x
+
+ if self.staggerindex == "odd" then
+ _x = 1
+ else
+ _x = 2
+ end
+
+ while i < layer.width * layer.height do
+ for _y = 1, layer.height + 0.5, 0.5 do
+ local y = floor(_y)
+
+ for x = _x, layer.width, 2 do
+ i = i + 1
+ local tile = layer.data[y][x]
+
+ if tile then
+ self:addNewLayerTile(layer, tile, x, y)
+ end
+ end
+
+ if _x == 1 then
+ _x = 2
+ else
+ _x = 1
+ end
+ end
+ end
+ end
+ end
+end
+
+--- Batch Tiles in Object Layer for improved draw speed
+-- @param layer The Object Layer
+function Map:setObjectSpriteBatches(layer)
+ local newBatch = lg.newSpriteBatch
+ local tileW = self.tilewidth
+ local tileH = self.tileheight
+ local batches = {}
+
+ if layer.draworder == "topdown" then
+ table.sort(layer.objects, function(a, b)
+ return a.y + a.height < b.y + b.height
+ end)
+ end
+
+ for _, object in ipairs(layer.objects) do
+ if object.gid then
+ local tile = self.tiles[object.gid] or self:setFlippedGID(object.gid)
+ local tileset = tile.tileset
+ local image = self.tilesets[tile.tileset].image
+
+ batches[tileset] = batches[tileset] or newBatch(image)
+
+ local sx = object.width / tile.width
+ local sy = object.height / tile.height
+
+ local batch = batches[tileset]
+ local tileX = object.x + tile.offset.x
+ local tileY = object.y + tile.offset.y - tile.height * sy
+ local tileR = math.rad(object.rotation)
+ local oy = 0
+
+ -- Compensation for scale/rotation shift
+ if tile.sx == 1 and tile.sy == 1 then
+ if tileR ~= 0 then
+ tileY = tileY + tileH
+ oy = tileH
+ end
+ else
+ if tile.sx < 0 then tileX = tileX + tileW end
+ if tile.sy < 0 then tileY = tileY + tileH end
+ if tileR > 0 then tileX = tileX + tileW end
+ if tileR < 0 then tileY = tileY + tileH end
+ end
+
+ local tab = {
+ layer = layer,
+ gid = tile.gid,
+ x = tileX,
+ y = tileY,
+ r = tileR,
+ oy = oy
+ }
+
+ if batch then
+ tab.batch = batch
+ tab.id = batch:add(tile.quad, tileX, tileY, tileR, tile.sx * sx, tile.sy * sy, 0, oy)
+ end
+
+ self.tileInstances[tile.gid] = self.tileInstances[tile.gid] or {}
+ table.insert(self.tileInstances[tile.gid], tab)
+ end
+ end
+
+ layer.batches = batches
+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)
+ index = index or #self.layers + 1
+ local layer = {
+ type = "customlayer",
+ name = name,
+ visible = true,
+ opacity = 1,
+ properties = {},
+ }
+
+ function layer.draw() end
+ function layer.update() 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() end
+ function layer.update() end
+
+ return layer
+end
+
+--- Remove a Layer from the Layer stack
+-- @param index Index or name of Layer to convert
+function Map:removeLayer(index)
+ local layer = assert(self.layers[index], "Layer not found: " .. index)
+
+ if type(index) == "string" then
+ for i, l in ipairs(self.layers) do
+ if l.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 _, batch in pairs(layer.batches) do
+ self.freeBatchSprites[batch] = nil
+ end
+
+ for _, 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
+function Map:update(dt)
+ for _, 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
+-- @param tx Translate on X
+-- @param ty Translate on Y
+-- @param sx Scale on X
+-- @param sy Scale on Y
+function Map:draw(tx, ty, sx, sy)
+ local current_canvas = lg.getCanvas()
+ lg.setCanvas(self.canvas)
+ lg.clear()
+
+ -- Scale map to 1.0 to draw onto canvas, this fixes tearing issues
+ -- Map is translated to correct position so the right section is drawn
+ lg.push()
+ lg.origin()
+ lg.translate(math.floor(tx or 0), math.floor(ty or 0))
+
+ for _, layer in ipairs(self.layers) do
+ if layer.visible and layer.opacity > 0 then
+ self:drawLayer(layer)
+ end
+ end
+
+ lg.pop()
+
+ -- Draw canvas at 0,0; this fixes scissoring issues
+ -- Map is scaled to correct scale so the right section is shown
+ lg.push()
+ lg.origin()
+ lg.scale(sx or 1, sy or sx or 1)
+
+ lg.setCanvas(current_canvas)
+ lg.draw(self.canvas)
+
+ lg.pop()
+end
+
+--- Draw an individual Layer
+-- @param layer The Layer to draw
+function Map.drawLayer(_, layer)
+ local r,g,b,a = lg.getColor()
+ lg.setColor(r, g, b, a * layer.opacity)
+ layer:draw()
+ lg.setColor(r,g,b,a)
+end
+
+--- Default draw function for Tile Layers
+-- @param layer The Tile Layer to draw
+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")
+
+ for _, batch in pairs(layer.batches) do
+ lg.draw(batch, floor(layer.x), floor(layer.y))
+ end
+end
+
+--- Default draw function for Object Layers
+-- @param layer The Object Layer to draw
+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.5 }
+ local r,g,b,a = lg.getColor()
+ local reset = { r, g, b, a * layer.opacity }
+
+ local function sortVertices(obj)
+ local vertex = {}
+
+ for _, v in ipairs(obj) do
+ table.insert(vertex, v.x)
+ table.insert(vertex, v.y)
+ end
+
+ return vertex
+ end
+
+ local function drawShape(obj, shape)
+ local vertex = sortVertices(obj)
+
+ if shape == "polyline" then
+ lg.setColor(line)
+ lg.line(vertex)
+ return
+ elseif shape == "polygon" then
+ lg.setColor(fill)
+ if not love.math.isConvex(vertex) then
+ local triangles = love.math.triangulate(vertex)
+ for _, triangle in ipairs(triangles) do
+ lg.polygon("fill", triangle)
+ end
+ else
+ lg.polygon("fill", vertex)
+ end
+ else
+ lg.setColor(fill)
+ lg.polygon("fill", vertex)
+ end
+
+ lg.setColor(line)
+ lg.polygon("line", vertex)
+ 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
+
+ lg.setColor(reset)
+ for _, batch in pairs(layer.batches) do
+ lg.draw(batch, 0, 0)
+ end
+ lg.setColor(r,g,b,a)
+end
+
+--- Default draw function for Image Layers
+-- @param layer The Image Layer to draw
+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
+ lg.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)
+function Map:resize(w, h)
+ if lg.isCreated then
+ w = w or lg.getWidth()
+ h = h or lg.getHeight()
+
+ self.canvas = lg.newCanvas(w, h)
+ self.canvas:setFilter("nearest", "nearest")
+ end
+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
+
+--- Change a tile in a layer to another 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)
+-- @param gid The gid of the new tile
+function Map:setLayerTile(layer, x, y, gid)
+ layer = self.layers[layer]
+
+ layer.data[y] = layer.data[y] or {}
+ local tile = layer.data[y][x]
+ local instance
+ if tile then
+ local tileX, tileY = self:getLayerTilePosition(layer, tile, x, y)
+ for _, inst in pairs(self.tileInstances[tile.gid]) do
+ if inst.x == tileX and inst.y == tileY then
+ instance = inst
+ break
+ end
+ end
+ end
+
+ if tile == self.tiles[gid] then
+ return
+ end
+
+ tile = self.tiles[gid]
+
+ if instance then
+ self:swapTile(instance, tile)
+ else
+ self:addNewLayerTile(layer, tile, x, y)
+ end
+ layer.data[y][x] = tile
+end
+
+--- Swap a tile in a spritebatch
+-- @param instance The current Instance object we want to replace
+-- @param tile The Tile object we want to use
+-- @return none
+function Map:swapTile(instance, tile)
+ -- Update sprite batch
+ if instance.batch then
+ if tile then
+ instance.batch:set(
+ instance.id,
+ tile.quad,
+ instance.x,
+ instance.y,
+ tile.r,
+ tile.sx,
+ tile.sy
+ )
+ else
+ instance.batch:set(
+ instance.id,
+ instance.x,
+ instance.y,
+ 0,
+ 0)
+
+ self.freeBatchSprites[instance.batch] =
+ self.freeBatchSprites[instance.batch] or {}
+
+ table.insert(self.freeBatchSprites[instance.batch], instance)
+ end
+ end
+
+ -- Remove old tile instance
+ for i, ins in ipairs(self.tileInstances[instance.gid]) do
+ if ins.batch == instance.batch and ins.id == instance.id then
+ table.remove(self.tileInstances[instance.gid], i)
+ break
+ end
+ end
+
+ -- Add new tile instance
+ if tile then
+ self.tileInstances[tile.gid] = self.tileInstances[tile.gid] or {}
+
+ local freeBatchSprites = self.freeBatchSprites[instance.batch]
+ local newInstance
+ if freeBatchSprites and #freeBatchSprites > 0 then
+ newInstance = freeBatchSprites[#freeBatchSprites]
+ freeBatchSprites[#freeBatchSprites] = nil
+ else
+ newInstance = {}
+ end
+
+ newInstance.layer = instance.layer
+ newInstance.batch = instance.batch
+ newInstance.id = instance.id
+ newInstance.gid = tile.gid or 0
+ newInstance.x = instance.x
+ newInstance.y = instance.y
+ newInstance.r = tile.r or 0
+ newInstance.oy = tile.r ~= 0 and tile.height or 0
+ table.insert(self.tileInstances[tile.gid], newInstance)
+ end
+end
+
+--- Convert tile location to pixel location
+-- @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:convertTileToPixel(x,y)
+ if self.orientation == "orthogonal" then
+ local tileW = self.tilewidth
+ local tileH = self.tileheight
+ return
+ x * tileW,
+ y * tileH
+ elseif self.orientation == "isometric" then
+ local mapH = self.height
+ local tileW = self.tilewidth
+ local tileH = self.tileheight
+ local offsetX = mapH * tileW / 2
+ return
+ (x - y) * tileW / 2 + offsetX,
+ (x + y) * tileH / 2
+ elseif self.orientation == "staggered" or
+ self.orientation == "hexagonal" then
+ local tileW = self.tilewidth
+ local tileH = self.tileheight
+ local sideLen = self.hexsidelength or 0
+
+ if self.staggeraxis == "x" then
+ return
+ x * tileW,
+ ceil(y) * (tileH + sideLen) + (ceil(y) % 2 == 0 and tileH or 0)
+ else
+ return
+ ceil(x) * (tileW + sideLen) + (ceil(x) % 2 == 0 and tileW or 0),
+ y * tileH
+ end
+ end
+end
+
+--- Convert pixel location to tile location
+-- @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:convertPixelToTile(x, y)
+ if self.orientation == "orthogonal" then
+ local tileW = self.tilewidth
+ local tileH = self.tileheight
+ return
+ x / tileW,
+ y / tileH
+ elseif self.orientation == "isometric" then
+ local mapH = self.height
+ local tileW = self.tilewidth
+ local tileH = self.tileheight
+ local offsetX = mapH * tileW / 2
+ return
+ y / tileH + (x - offsetX) / tileW,
+ y / tileH - (x - offsetX) / tileW
+ elseif self.orientation == "staggered" then
+ local staggerX = self.staggeraxis == "x"
+ local even = self.staggerindex == "even"
+
+ local function topLeft(x, y)
+ if staggerX then
+ if ceil(x) % 2 == 1 and even then
+ return x - 1, y
+ else
+ return x - 1, y - 1
+ end
+ else
+ if ceil(y) % 2 == 1 and even then
+ return x, y - 1
+ else
+ return x - 1, y - 1
+ end
+ end
+ end
+
+ local function topRight(x, y)
+ if staggerX then
+ if ceil(x) % 2 == 1 and even then
+ return x + 1, y
+ else
+ return x + 1, y - 1
+ end
+ else
+ if ceil(y) % 2 == 1 and even then
+ return x + 1, y - 1
+ else
+ return x, y - 1
+ end
+ end
+ end
+
+ local function bottomLeft(x, y)
+ if staggerX then
+ if ceil(x) % 2 == 1 and even then
+ return x - 1, y + 1
+ else
+ return x - 1, y
+ end
+ else
+ if ceil(y) % 2 == 1 and even then
+ return x, y + 1
+ else
+ return x - 1, y + 1
+ end
+ end
+ end
+
+ local function bottomRight(x, y)
+ if staggerX then
+ if ceil(x) % 2 == 1 and even then
+ return x + 1, y + 1
+ else
+ return x + 1, y
+ end
+ else
+ if ceil(y) % 2 == 1 and even then
+ return x + 1, y + 1
+ else
+ return x, y + 1
+ end
+ end
+ end
+
+ local tileW = self.tilewidth
+ local tileH = self.tileheight
+
+ if staggerX then
+ x = x - (even and tileW / 2 or 0)
+ else
+ y = y - (even and tileH / 2 or 0)
+ end
+
+ local halfH = tileH / 2
+ local ratio = tileH / tileW
+ local referenceX = ceil(x / tileW)
+ local referenceY = ceil(y / tileH)
+ local relativeX = x - referenceX * tileW
+ local relativeY = y - referenceY * tileH
+
+ if (halfH - relativeX * ratio > relativeY) then
+ return topLeft(referenceX, referenceY)
+ elseif (-halfH + relativeX * ratio > relativeY) then
+ return topRight(referenceX, referenceY)
+ elseif (halfH + relativeX * ratio < relativeY) then
+ return bottomLeft(referenceX, referenceY)
+ elseif (halfH * 3 - relativeX * ratio < relativeY) then
+ return bottomRight(referenceX, referenceY)
+ end
+
+ return referenceX, referenceY
+ elseif self.orientation == "hexagonal" then
+ local staggerX = self.staggeraxis == "x"
+ local even = self.staggerindex == "even"
+ local tileW = self.tilewidth
+ local tileH = self.tileheight
+ local sideLenX = 0
+ local sideLenY = 0
+
+ if staggerX then
+ sideLenX = self.hexsidelength
+ x = x - (even and tileW or (tileW - sideLenX) / 2)
+ else
+ sideLenY = self.hexsidelength
+ y = y - (even and tileH or (tileH - sideLenY) / 2)
+ end
+
+ local colW = ((tileW - sideLenX) / 2) + sideLenX
+ local rowH = ((tileH - sideLenY) / 2) + sideLenY
+ local referenceX = ceil(x) / (colW * 2)
+ local referenceY = ceil(y) / (rowH * 2)
+ local relativeX = x - referenceX * colW * 2
+ local relativeY = y - referenceY * rowH * 2
+ local centers
+
+ if staggerX then
+ local left = sideLenX / 2
+ local centerX = left + colW
+ local centerY = tileH / 2
+
+ centers = {
+ { x = left, y = centerY },
+ { x = centerX, y = centerY - rowH },
+ { x = centerX, y = centerY + rowH },
+ { x = centerX + colW, y = centerY },
+ }
+ else
+ local top = sideLenY / 2
+ local centerX = tileW / 2
+ local centerY = top + rowH
+
+ centers = {
+ { x = centerX, y = top },
+ { x = centerX - colW, y = centerY },
+ { x = centerX + colW, y = centerY },
+ { x = centerX, y = centerY + rowH }
+ }
+ end
+
+ local nearest = 0
+ local minDist = math.huge
+
+ local function len2(ax, ay)
+ return ax * ax + ay * ay
+ end
+
+ for i = 1, 4 do
+ local dc = len2(centers[i].x - relativeX, centers[i].y - relativeY)
+
+ if dc < minDist then
+ minDist = dc
+ nearest = i
+ end
+ end
+
+ local offsetsStaggerX = {
+ { x = 0, y = 0 },
+ { x = 1, y = -1 },
+ { x = 1, y = 0 },
+ { x = 2, y = 0 },
+ }
+
+ local offsetsStaggerY = {
+ { x = 0, y = 0 },
+ { x = -1, y = 1 },
+ { x = 0, y = 1 },
+ { x = 0, y = 2 },
+ }
+
+ local offsets = staggerX and offsetsStaggerX or offsetsStaggerY
+
+ return
+ referenceX + offsets[nearest].x,
+ referenceY + offsets[nearest].y
+ end
+end
+
+--- 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 no-longer-used batch sprites, indexed by batch
+--@table Map.freeBatchSprites
+
+--- 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 tileWo 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 = lg.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
+-- lg.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
+
+return setmetatable({}, STI)
diff --git a/bootleg.love/core/modules/world/maps/libs/sti/plugins/box2d.lua b/bootleg.love/core/modules/world/maps/libs/sti/plugins/box2d.lua
new file mode 100644
index 0000000..6d2e1b4
--- /dev/null
+++ b/bootleg.love/core/modules/world/maps/libs/sti/plugins/box2d.lua
@@ -0,0 +1,303 @@
+--- Box2D plugin for STI
+-- @module box2d
+-- @author Landon Manning
+-- @copyright 2017
+-- @license MIT/X11
+
+local utils = require((...):gsub('plugins.box2d', 'utils'))
+local lg = require((...):gsub('plugins.box2d', 'graphics'))
+
+return {
+ box2d_LICENSE = "MIT/X11",
+ box2d_URL = "https://github.com/karai17/Simple-Tiled-Implementation",
+ box2d_VERSION = "2.3.2.6",
+ box2d_DESCRIPTION = "Box2D hooks for STI.",
+
+ --- Initialize Box2D physics world.
+ -- @param world The Box2D world to add objects to.
+ box2d_init = function(map, world)
+ assert(love.physics, "To use the Box2D plugin, please enable the love.physics module.")
+
+ local body = love.physics.newBody(world, map.offsetx, map.offsety)
+ local collision = {
+ body = body,
+ }
+
+ local function addObjectToWorld(objshape, vertices, userdata, object)
+ local shape
+
+ if objshape == "polyline" then
+ if #vertices == 4 then
+ shape = love.physics.newEdgeShape(unpack(vertices))
+ else
+ shape = love.physics.newChainShape(false, unpack(vertices))
+ end
+ else
+ shape = love.physics.newPolygonShape(unpack(vertices))
+ end
+
+ local currentBody = body
+
+ if userdata.properties.dynamic == true then
+ currentBody = love.physics.newBody(world, map.offsetx, map.offsety, 'dynamic')
+ end
+
+ local fixture = love.physics.newFixture(currentBody, shape)
+
+ fixture:setUserData(userdata)
+
+ -- Set some custom properties from userdata (or use default set by box2d)
+ fixture:setFriction(userdata.properties.friction or 0.2)
+ fixture:setRestitution(userdata.properties.restitution or 0.0)
+ fixture:setSensor(userdata.properties.sensor or false)
+ fixture:setFilterData(userdata.properties.categories or 1,
+ userdata.properties.mask or 65535,
+ userdata.properties.group or 0)
+
+ local obj = {
+ object = object,
+ body = currentBody,
+ shape = shape,
+ fixture = fixture,
+ }
+
+ table.insert(collision, obj)
+ end
+
+ local function getPolygonVertices(object)
+ local vertices = {}
+ for _, vertex in ipairs(object.polygon) do
+ table.insert(vertices, vertex.x)
+ table.insert(vertices, vertex.y)
+ end
+
+ return vertices
+ end
+
+ local function calculateObjectPosition(object, tile)
+ local o = {
+ shape = object.shape,
+ x = (object.dx or object.x) + map.offsetx,
+ y = (object.dy or object.y) + map.offsety,
+ w = object.width,
+ h = object.height,
+ polygon = object.polygon or object.polyline or object.ellipse or object.rectangle
+ }
+
+ local userdata = {
+ object = o,
+ properties = object.properties
+ }
+
+ if o.shape == "rectangle" then
+ o.r = object.rotation or 0
+ local cos = math.cos(math.rad(o.r))
+ local sin = math.sin(math.rad(o.r))
+ local oy = 0
+
+ if object.gid then
+ local tileset = map.tilesets[map.tiles[object.gid].tileset]
+ local lid = object.gid - tileset.firstgid
+ local t = {}
+
+ -- This fixes a height issue
+ o.y = o.y + map.tiles[object.gid].offset.y
+ oy = tileset.tileheight
+
+ for _, tt in ipairs(tileset.tiles) do
+ if tt.id == lid then
+ t = tt
+ break
+ end
+ end
+
+ if t.objectGroup then
+ for _, obj in ipairs(t.objectGroup.objects) do
+ -- Every object in the tile
+ calculateObjectPosition(obj, object)
+ end
+
+ return
+ else
+ o.w = map.tiles[object.gid].width
+ o.h = map.tiles[object.gid].height
+ end
+ end
+
+ o.polygon = {
+ { x=o.x+0, y=o.y+0 },
+ { x=o.x+o.w, y=o.y+0 },
+ { x=o.x+o.w, y=o.y+o.h },
+ { x=o.x+0, y=o.y+o.h }
+ }
+
+ for _, vertex in ipairs(o.polygon) do
+ vertex.x, vertex.y = utils.rotate_vertex(map, vertex, o.x, o.y, cos, sin, oy)
+ end
+
+ local vertices = getPolygonVertices(o)
+ addObjectToWorld(o.shape, vertices, userdata, tile or object)
+ elseif o.shape == "ellipse" then
+ if not o.polygon then
+ o.polygon = utils.convert_ellipse_to_polygon(o.x, o.y, o.w, o.h)
+ end
+ local vertices = getPolygonVertices(o)
+ local triangles = love.math.triangulate(vertices)
+
+ for _, triangle in ipairs(triangles) do
+ addObjectToWorld(o.shape, triangle, userdata, tile or object)
+ end
+ elseif o.shape == "polygon" then
+ local vertices = getPolygonVertices(o)
+ local triangles = love.math.triangulate(vertices)
+
+ for _, triangle in ipairs(triangles) do
+ addObjectToWorld(o.shape, triangle, userdata, tile or object)
+ end
+ elseif o.shape == "polyline" then
+ local vertices = getPolygonVertices(o)
+ addObjectToWorld(o.shape, vertices, userdata, tile or object)
+ end
+ end
+
+ for _, tile in pairs(map.tiles) do
+ if map.tileInstances[tile.gid] then
+ for _, instance in ipairs(map.tileInstances[tile.gid]) do
+ -- Every object in every instance of a tile
+ if tile.objectGroup then
+ for _, object in ipairs(tile.objectGroup.objects) do
+ if object.properties.collidable == true then
+ object.dx = instance.x + object.x
+ object.dy = instance.y + object.y
+ calculateObjectPosition(object, instance)
+ end
+ end
+ end
+
+ -- Every instance of a tile
+ if tile.properties.collidable == true then
+ local object = {
+ shape = "rectangle",
+ x = instance.x,
+ y = instance.y,
+ width = map.tilewidth,
+ height = map.tileheight,
+ properties = tile.properties
+ }
+
+ calculateObjectPosition(object, instance)
+ end
+ end
+ end
+ end
+
+ for _, layer in ipairs(map.layers) do
+ -- Entire layer
+ if layer.properties.collidable == true then
+ if layer.type == "tilelayer" then
+ for gid, tiles in pairs(map.tileInstances) do
+ local tile = map.tiles[gid]
+ local tileset = map.tilesets[tile.tileset]
+
+ for _, instance in ipairs(tiles) do
+ if instance.layer == layer then
+ local object = {
+ shape = "rectangle",
+ x = instance.x,
+ y = instance.y,
+ width = tileset.tilewidth,
+ height = tileset.tileheight,
+ properties = tile.properties
+ }
+
+ calculateObjectPosition(object, instance)
+ end
+ end
+ end
+ elseif layer.type == "objectgroup" then
+ for _, object in ipairs(layer.objects) do
+ calculateObjectPosition(object)
+ end
+ elseif layer.type == "imagelayer" then
+ local object = {
+ shape = "rectangle",
+ x = layer.x or 0,
+ y = layer.y or 0,
+ width = layer.width,
+ height = layer.height,
+ properties = layer.properties
+ }
+
+ calculateObjectPosition(object)
+ end
+ end
+
+ -- Individual objects
+ if layer.type == "objectgroup" then
+ for _, object in ipairs(layer.objects) do
+ if object.properties.collidable == true then
+ calculateObjectPosition(object)
+ end
+ end
+ end
+ end
+
+ map.box2d_collision = collision
+ end,
+
+ --- Remove Box2D fixtures and shapes from world.
+ -- @param index The index or name of the layer being removed
+ box2d_removeLayer = function(map, index)
+ local layer = assert(map.layers[index], "Layer not found: " .. index)
+ local collision = map.box2d_collision
+
+ -- Remove collision objects
+ for i = #collision, 1, -1 do
+ local obj = collision[i]
+
+ if obj.object.layer == layer then
+ obj.fixture:destroy()
+ table.remove(collision, i)
+ end
+ end
+ end,
+
+ --- Draw Box2D physics world.
+ -- @param tx Translate on X
+ -- @param ty Translate on Y
+ -- @param sx Scale on X
+ -- @param sy Scale on Y
+ box2d_draw = function(map, tx, ty, sx, sy)
+ local collision = map.box2d_collision
+
+ lg.push()
+ lg.scale(sx or 1, sy or sx or 1)
+ lg.translate(math.floor(tx or 0), math.floor(ty or 0))
+
+ for _, obj in ipairs(collision) do
+ local points = {obj.body:getWorldPoints(obj.shape:getPoints())}
+ local shape_type = obj.shape:getType()
+
+ if shape_type == "edge" or shape_type == "chain" then
+ love.graphics.line(points)
+ elseif shape_type == "polygon" then
+ love.graphics.polygon("line", points)
+ else
+ error("sti box2d plugin does not support "..shape_type.." shapes")
+ end
+ end
+
+ lg.pop()
+ end,
+}
+
+--- Custom Properties in Tiled are used to tell this plugin what to do.
+-- @table Properties
+-- @field collidable set to true, can be used on any Layer, Tile, or Object
+-- @field sensor set to true, can be used on any Tile or Object that is also collidable
+-- @field dynamic set to true, can be used on any Tile or Object
+-- @field friction can be used to define the friction of any Object
+-- @field restitution can be used to define the restitution of any Object
+-- @field categories can be used to set the filter Category of any Object
+-- @field mask can be used to set the filter Mask of any Object
+-- @field group can be used to set the filter Group of any Object
diff --git a/bootleg.love/core/modules/world/maps/libs/sti/plugins/bump.lua b/bootleg.love/core/modules/world/maps/libs/sti/plugins/bump.lua
new file mode 100644
index 0000000..d69ff26
--- /dev/null
+++ b/bootleg.love/core/modules/world/maps/libs/sti/plugins/bump.lua
@@ -0,0 +1,194 @@
+--- Bump.lua plugin for STI
+-- @module bump.lua
+-- @author David Serrano (BobbyJones|FrenchFryLord)
+-- @copyright 2016
+-- @license MIT/X11
+
+local lg = require((...):gsub('plugins.bump', 'graphics'))
+
+return {
+ bump_LICENSE = "MIT/X11",
+ bump_URL = "https://github.com/karai17/Simple-Tiled-Implementation",
+ bump_VERSION = "3.1.6.1",
+ bump_DESCRIPTION = "Bump hooks for STI.",
+
+ --- Adds each collidable tile to the Bump world.
+ -- @param world The Bump world to add objects to.
+ -- @return collidables table containing the handles to the objects in the Bump world.
+ bump_init = function(map, world)
+ local collidables = {}
+
+ for _, tileset in ipairs(map.tilesets) do
+ for _, tile in ipairs(tileset.tiles) do
+ local gid = tileset.firstgid + tile.id
+
+ if map.tileInstances[gid] then
+ for _, instance in ipairs(map.tileInstances[gid]) do
+ -- Every object in every instance of a tile
+ if tile.objectGroup then
+ for _, object in ipairs(tile.objectGroup.objects) do
+ if object.properties.collidable == true then
+ local t = {
+ name = object.name,
+ type = object.type,
+ x = instance.x + map.offsetx + object.x,
+ y = instance.y + map.offsety + object.y,
+ width = object.width,
+ height = object.height,
+ layer = instance.layer,
+ properties = object.properties
+
+ }
+
+ world:add(t, t.x, t.y, t.width, t.height)
+ table.insert(collidables, t)
+ end
+ end
+ end
+
+ -- Every instance of a tile
+ if tile.properties and tile.properties.collidable == true then
+ local t = {
+ x = instance.x + map.offsetx,
+ y = instance.y + map.offsety,
+ width = map.tilewidth,
+ height = map.tileheight,
+ layer = instance.layer,
+ properties = tile.properties
+ }
+
+ world:add(t, t.x, t.y, t.width, t.height)
+ table.insert(collidables, t)
+ end
+ end
+ end
+ end
+ end
+
+ for _, layer in ipairs(map.layers) do
+ -- Entire layer
+ if layer.properties.collidable == true then
+ if layer.type == "tilelayer" then
+ for y, tiles in ipairs(layer.data) do
+ for x, tile in pairs(tiles) do
+
+ if tile.objectGroup then
+ for _, object in ipairs(tile.objectGroup.objects) do
+ if object.properties.collidable == true then
+ local t = {
+ name = object.name,
+ type = object.type,
+ x = ((x-1) * map.tilewidth + tile.offset.x + map.offsetx) + object.x,
+ y = ((y-1) * map.tileheight + tile.offset.y + map.offsety) + object.y,
+ width = object.width,
+ height = object.height,
+ layer = layer,
+ properties = object.properties
+ }
+
+ world:add(t, t.x, t.y, t.width, t.height)
+ table.insert(collidables, t)
+ end
+ end
+ end
+
+
+ local t = {
+ x = (x-1) * map.tilewidth + tile.offset.x + map.offsetx,
+ y = (y-1) * map.tileheight + tile.offset.y + map.offsety,
+ width = tile.width,
+ height = tile.height,
+ layer = layer,
+ properties = tile.properties
+ }
+
+ world:add(t, t.x, t.y, t.width, t.height)
+ table.insert(collidables, t)
+ end
+ end
+ elseif layer.type == "imagelayer" then
+ world:add(layer, layer.x, layer.y, layer.width, layer.height)
+ table.insert(collidables, layer)
+ end
+ end
+
+ -- individual collidable objects in a layer that is not "collidable"
+ -- or whole collidable objects layer
+ if layer.type == "objectgroup" then
+ for _, obj in ipairs(layer.objects) do
+ if layer.properties.collidable == true or obj.properties.collidable == true then
+ if obj.shape == "rectangle" then
+ local t = {
+ name = obj.name,
+ type = obj.type,
+ x = obj.x + map.offsetx,
+ y = obj.y + map.offsety,
+ width = obj.width,
+ height = obj.height,
+ layer = layer,
+ properties = obj.properties
+ }
+
+ if obj.gid then
+ t.y = t.y - obj.height
+ end
+
+ world:add(t, t.x, t.y, t.width, t.height)
+ table.insert(collidables, t)
+ end -- TODO implement other object shapes?
+ end
+ end
+ end
+
+ end
+ map.bump_collidables = collidables
+ end,
+
+ --- Remove layer
+ -- @param index to layer to be removed
+ -- @param world bump world the holds the tiles
+ -- @param tx Translate on X
+-- @param ty Translate on Y
+-- @param sx Scale on X
+-- @param sy Scale on Y
+ bump_removeLayer = function(map, index, world)
+ local layer = assert(map.layers[index], "Layer not found: " .. index)
+ local collidables = map.bump_collidables
+
+ -- Remove collision objects
+ for i = #collidables, 1, -1 do
+ local obj = collidables[i]
+
+ if obj.layer == layer
+ and (
+ layer.properties.collidable == true
+ or obj.properties.collidable == true
+ ) then
+ world:remove(obj)
+ table.remove(collidables, i)
+ end
+ end
+ end,
+
+ --- Draw bump collisions world.
+ -- @param world bump world holding the tiles geometry
+ -- @param tx Translate on X
+ -- @param ty Translate on Y
+ -- @param sx Scale on X
+ -- @param sy Scale on Y
+ bump_draw = function(map, world, tx, ty, sx, sy)
+ lg.push()
+ lg.scale(sx or 1, sy or sx or 1)
+ lg.translate(math.floor(tx or 0), math.floor(ty or 0))
+
+ for _, collidable in pairs(map.bump_collidables) do
+ lg.rectangle("line", world:getRect(collidable))
+ end
+
+ lg.pop()
+ end
+}
+
+--- Custom Properties in Tiled are used to tell this plugin what to do.
+-- @table Properties
+-- @field collidable set to true, can be used on any Layer, Tile, or Object
diff --git a/bootleg.love/core/modules/world/maps/libs/sti/utils.lua b/bootleg.love/core/modules/world/maps/libs/sti/utils.lua
new file mode 100644
index 0000000..9f8839a
--- /dev/null
+++ b/bootleg.love/core/modules/world/maps/libs/sti/utils.lua
@@ -0,0 +1,206 @@
+-- Some utility functions that shouldn't be exposed.
+local utils = {}
+
+-- https://github.com/stevedonovan/Penlight/blob/master/lua/pl/path.lua#L286
+function utils.format_path(path)
+ local np_gen1,np_gen2 = '[^SEP]+SEP%.%.SEP?','SEP+%.?SEP'
+ local np_pat1, np_pat2 = np_gen1:gsub('SEP','/'), np_gen2:gsub('SEP','/')
+ local k
+
+ repeat -- /./ -> /
+ path,k = path:gsub(np_pat2,'/')
+ until k == 0
+
+ repeat -- A/../ -> (empty)
+ path,k = path:gsub(np_pat1,'')
+ until k == 0
+
+ if path == '' then path = '.' end
+
+ return path
+end
+
+-- Compensation for scale/rotation shift
+function utils.compensate(tile, tileX, tileY, tileW, tileH)
+ local compx = 0
+ local compy = 0
+
+ if tile.sx < 0 then compx = tileW end
+ if tile.sy < 0 then compy = tileH end
+
+ if tile.r > 0 then
+ tileX = tileX + tileH - compy
+ tileY = tileY + tileH + compx - tileW
+ elseif tile.r < 0 then
+ tileX = tileX + compy
+ tileY = tileY - compx + tileH
+ else
+ tileX = tileX + compx
+ tileY = tileY + compy
+ end
+
+ return tileX, tileY
+end
+
+-- Cache images in main STI module
+function utils.cache_image(sti, path, image)
+ image = image or love.graphics.newImage(path)
+ image:setFilter("nearest", "nearest")
+ sti.cache[path] = image
+end
+
+-- We just don't know.
+function utils.get_tiles(imageW, tileW, margin, spacing)
+ imageW = imageW - margin
+ local n = 0
+
+ while imageW >= tileW do
+ imageW = imageW - tileW
+ if n ~= 0 then imageW = imageW - spacing end
+ if imageW >= 0 then n = n + 1 end
+ end
+
+ return n
+end
+
+-- Decompress tile layer data
+function utils.get_decompressed_data(data)
+ local ffi = require "ffi"
+ local d = {}
+ local decoded = ffi.cast("uint32_t*", data)
+
+ for i = 0, data:len() / ffi.sizeof("uint32_t") do
+ table.insert(d, tonumber(decoded[i]))
+ end
+
+ return d
+end
+
+-- Convert a Tiled ellipse object to a LOVE polygon
+function utils.convert_ellipse_to_polygon(x, y, w, h, max_segments)
+ local ceil = math.ceil
+ local cos = math.cos
+ local sin = math.sin
+
+ local function calc_segments(segments)
+ local function vdist(a, b)
+ local c = {
+ x = a.x - b.x,
+ y = a.y - b.y,
+ }
+
+ return c.x * c.x + c.y * c.y
+ end
+
+ segments = segments or 64
+ local vertices = {}
+
+ local v = { 1, 2, ceil(segments/4-1), ceil(segments/4) }
+
+ local m
+ if love and love.physics then
+ m = love.physics.getMeter()
+ else
+ m = 32
+ end
+
+ for _, i in ipairs(v) do
+ local angle = (i / segments) * math.pi * 2
+ local px = x + w / 2 + cos(angle) * w / 2
+ local py = y + h / 2 + sin(angle) * h / 2
+
+ table.insert(vertices, { x = px / m, y = py / m })
+ end
+
+ local dist1 = vdist(vertices[1], vertices[2])
+ local dist2 = vdist(vertices[3], vertices[4])
+
+ -- Box2D threshold
+ if dist1 < 0.0025 or dist2 < 0.0025 then
+ return calc_segments(segments-2)
+ end
+
+ return segments
+ end
+
+ local segments = calc_segments(max_segments)
+ local vertices = {}
+
+ table.insert(vertices, { x = x + w / 2, y = y + h / 2 })
+
+ for i = 0, segments do
+ local angle = (i / segments) * math.pi * 2
+ local px = x + w / 2 + cos(angle) * w / 2
+ local py = y + h / 2 + sin(angle) * h / 2
+
+ table.insert(vertices, { x = px, y = py })
+ end
+
+ return vertices
+end
+
+function utils.rotate_vertex(map, vertex, x, y, cos, sin)
+ if map.orientation == "isometric" then
+ x, y = utils.convert_isometric_to_screen(map, x, y)
+ vertex.x, vertex.y = utils.convert_isometric_to_screen(map, vertex.x, vertex.y)
+ end
+
+ vertex.x = vertex.x - x
+ vertex.y = vertex.y - y
+
+ return
+ x + cos * vertex.x - sin * vertex.y,
+ y + sin * vertex.x + cos * vertex.y
+end
+
+--- Project isometric position to cartesian position
+function utils.convert_isometric_to_screen(map, x, y)
+ local mapH = map.height
+ local tileW = map.tilewidth
+ local tileH = map.tileheight
+ local tileX = x / tileH
+ local tileY = y / tileH
+ local offsetX = mapH * tileW / 2
+
+ return
+ (tileX - tileY) * tileW / 2 + offsetX,
+ (tileX + tileY) * tileH / 2
+end
+
+function utils.hex_to_color(hex)
+ if hex:sub(1, 1) == "#" then
+ hex = hex:sub(2)
+ end
+
+ return {
+ r = tonumber(hex:sub(1, 2), 16) / 255,
+ g = tonumber(hex:sub(3, 4), 16) / 255,
+ b = tonumber(hex:sub(5, 6), 16) / 255
+ }
+end
+
+function utils.pixel_function(_, _, r, g, b, a)
+ local mask = utils._TC
+
+ if r == mask.r and
+ g == mask.g and
+ b == mask.b then
+ return r, g, b, 0
+ end
+
+ return r, g, b, a
+end
+
+function utils.fix_transparent_color(tileset, path)
+ local image_data = love.image.newImageData(path)
+ tileset.image = love.graphics.newImage(image_data)
+
+ if tileset.transparentcolor then
+ utils._TC = utils.hex_to_color(tileset.transparentcolor)
+
+ image_data:mapPixel(utils.pixel_function)
+ tileset.image = love.graphics.newImage(image_data)
+ end
+end
+
+return utils
diff --git a/bootleg.love/core/modules/world/maps/parent.lua b/bootleg.love/core/modules/world/maps/parent.lua
new file mode 100644
index 0000000..a1b445d
--- /dev/null
+++ b/bootleg.love/core/modules/world/maps/parent.lua
@@ -0,0 +1,97 @@
+local ParentMap = Object:extend()
+
+-- INIT FUNCTION
+-- Initialize the map
+
+function ParentMap:new(world, r, g, b)
+ self.world = world
+
+ local r = r or 128
+ local g = g or 128
+ local b = b or 128
+ self.backgroundColor = {r, g, b}
+
+ self:setPadding()
+ self:register()
+end
+
+function ParentMap:register()
+ self.world.map = self
+end
+
+function ParentMap:setPadding(x1, y1, x2, y2)
+ local padding = {}
+ padding.x1 = x1 or 0
+ padding.y1 = y1 or padding.x1
+ padding.x2 = x2 or padding.x1
+ padding.y2 = y2 or padding.y1
+
+ self.padding = padding
+end
+
+-- UPDATE FUNCTION
+-- Update or modify the map
+
+function ParentMap:resize(w, h)
+ -- Empty Placeholder function
+end
+
+function ParentMap:update(dt)
+ -- Empty Placeholder function
+end
+
+function ParentMap:loadObjects()
+ self:loadCollisions()
+ self:loadPlayers()
+ self:loadActors()
+end
+
+function ParentMap:loadCollisions()
+ -- Empty Placeholder function
+end
+
+function ParentMap:loadPlayers()
+ -- Empty Placeholder function
+end
+
+function ParentMap:loadActors()
+ -- Empty Placeholder function
+end
+
+function ParentMap:setBackgroundColor(r, g, b)
+ local r = r or 128
+ local g = g or 128
+ local b = b or 128
+ self.backgroundColor = {r, g, b}
+end
+
+function ParentMap:setBackgroundColorFromTable(backgroundColor)
+ self.backgroundColor = backgroundColor or {128, 128, 128}
+end
+
+function ParentMap:getBackgroundColor()
+ return self.backgroundColor[1]/256, self.backgroundColor[2]/256, self.backgroundColor[3]/256
+end
+
+function ParentMap:getPadding()
+ return self.padding.x1, self.padding.y1, self.padding.x2, self.padding.y2
+end
+
+function ParentMap:getDimensions()
+ return core.screen:getDimensions()
+end
+
+function ParentMap:getBox()
+ local x1, y1, x2, y2 = self:getPadding()
+ local w, h = self:getDimensions()
+ return -x1, -y1, w+x2, h+y2
+end
+
+function ParentMap:drawBackgroundColor()
+ local r, g, b = self.backgroundColor[1], self.backgroundColor[2], self.backgroundColor[3]
+ love.graphics.setColor(r/256, g/256, b/256)
+ love.graphics.rectangle("fill", 0, 0, 480, 272)
+ utils.graphics.resetColor()
+end
+
+return ParentMap
diff --git a/bootleg.love/core/modules/world/maps/sti.lua b/bootleg.love/core/modules/world/maps/sti.lua
new file mode 100644
index 0000000..ca10745
--- /dev/null
+++ b/bootleg.love/core/modules/world/maps/sti.lua
@@ -0,0 +1,150 @@
+local cwd = (...):gsub('%.sti$', '') .. "."
+
+local Parent = require(cwd .. "parent")
+local STI = require(cwd .. "libs.sti")
+
+local StiMap = Parent:extend()
+
+function StiMap:new(world, mapfile)
+ self.sti = STI(mapfile)
+ StiMap.super.new(self, world)
+ self:setBackgroundColorFromTable(self.sti.backgroundcolor)
+end
+
+function StiMap:getDimensions()
+ return self.sti.width * self.sti.tilewidth,
+ self.sti.height * self.sti.tileheight
+end
+
+-- UPDATE FUNCTION
+-- Update or modify the map
+
+function StiMap:resize(w, h)
+ self.sti:resize(w, h)
+end
+
+function StiMap:update(dt)
+ self.sti:update(dt)
+end
+
+-- LOADING FUNCTION
+-- Load actors directly into the world
+
+function StiMap:loadCollisions()
+ for k, objectlayer in pairs(self.sti.layers) do
+ if self.world:isCollisionIndexed(objectlayer.name) then
+ local debugstring = "loading " .. #objectlayer.objects .. " objects in " .. objectlayer.name .. " collision layer"
+ core.debug:print("map/sti", debugstring)
+ for k, object in pairs(objectlayer.objects) do
+ self:newCollision(objectlayer, object)
+ end
+ self.sti:removeLayer(objectlayer.name)
+ end
+ end
+end
+
+function StiMap:loadActors()
+ for k, objectlayer in pairs(self.sti.layers) do
+ if self.world:isActorIndexed(objectlayer.name) then
+ local debugstring = "loading " .. #objectlayer.objects .. " objects in " .. objectlayer.name .. " actor layer"
+ core.debug:print("map/sti", debugstring)
+ for k, object in pairs(objectlayer.objects) do
+ if (object.properties.batchActor) then
+ self:batchActor(objectlayer, object)
+ else
+ self:newActor(objectlayer, object)
+ end
+ end
+ self.sti:removeLayer(objectlayer.name)
+ end
+ end
+end
+
+function StiMap:loadPlayers()
+ for k, objectlayer in pairs(self.sti.layers) do
+ if (objectlayer.name == "player") then
+ local debugstring = "loading at most " .. #objectlayer.objects .. " actors in " .. objectlayer.name .. " actor layer"
+ core.debug:print("map/sti", debugstring)
+ local i = 1
+ for k, object in pairs(objectlayer.objects) do
+ self:newPlayer(object, i)
+ i = i + 1
+ end
+ self.sti:removeLayer(objectlayer.name)
+ end
+ end
+end
+
+function StiMap:batchActor(objectlayer, object)
+ local name = objectlayer.name
+ local gwidth = object.properties.gwidth or self.sti.tilewidth
+ local gheight = object.properties.gheight or self.sti.tileheight
+ local x = object.x
+ local y = object.y
+ local z = object.properties.z or 0
+ local w = object.width
+ local h = object.height
+
+ local cellHor = math.ceil(w / gwidth)
+ local cellVert = math.ceil(h / gheight)
+
+ for i=1, cellHor do
+ for j=1, cellVert do
+ self.world:newActor(name, x + (i-1)*gwidth, y + (j-1)*gheight, z)
+ end
+ end
+end
+
+function StiMap:newActor(objectlayer, object)
+ local z = object.properties.z or 0
+ local adaptPosition = object.properties.adaptPosition or false
+
+ local y = object.y
+ if (adaptPosition) then
+ y = y + z
+ end
+
+ self.world:newActor(objectlayer.name, object.x, y, z)
+end
+
+function StiMap:newCollision(objectlayer, object)
+ local z = object.properties.z or 0
+ local d = object.properties.d or 16
+ local fromTop = object.properties.fromTop or false
+
+ local y = object.y
+ if (fromTop) then
+ local poschange = z .. ";" .. y .. " => " .. z-d .. ";" .. y+z
+ core.debug:print("map/sti", "create from top, set z and y: " .. poschange)
+ y = y + z
+ z = z - d
+ end
+
+ self.world:newCollision(objectlayer.name, object.x, y, z, object.width, object.height, d)
+end
+
+function StiMap:newPlayer(object, i)
+ local z = object.properties.z or 0
+ local adaptPosition = object.properties.adaptPosition or false
+
+ local y = object.y
+ if (adaptPosition) then
+ core.debug:print("map/sti", "adapting position, set y: " .. y .. " => ", y+z)
+ y = y + z
+ end
+
+ self.world:addPlayer(object.x, y, z, i)
+end
+
+-- DRAW FUNCTIONS
+-- Draw the map
+
+function StiMap:draw()
+ for _, layer in ipairs(self.sti.layers) do
+ if layer.visible and layer.opacity > 0 and (layer.type == "tilelayer") then
+ self.sti:drawLayer(layer)
+ end
+ end
+end
+
+return StiMap
diff --git a/bootleg.love/core/modules/world/world2D.lua b/bootleg.love/core/modules/world/world2D.lua
new file mode 100644
index 0000000..11417ef
--- /dev/null
+++ b/bootleg.love/core/modules/world/world2D.lua
@@ -0,0 +1,89 @@
+-- world2D.lua :: a basic 2D world based on bump2D.
+
+--[[
+ Copyright © 2019 Kazhnuz
+
+ 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.
+]]
+
+local cwd = (...):gsub('%.world2D$', '') .. "."
+
+local BaseWorld = require(cwd .. "baseworld")
+local World2D = BaseWorld:extend()
+
+local Bump = require(cwd .. "libs.bump")
+local CameraSystem = require(cwd .. "camera")
+
+function World2D:new(scene, actorlist, mapfile, maptype)
+ World2D.super.new(self, scene, actorlist, mapfile, maptype)
+end
+
+-- ACTORS FUNCTIONS
+-- Add support for bodies in Actor functions
+
+function World2D:initActors()
+ self.currentCreationID = 0
+ self.actors = {}
+ self.bodies = Bump.newWorld(50)
+end
+
+function World2D:registerActor(actor)
+ World2D.super.registerActor(self, actor)
+end
+
+function World2D:moveActor(actor, x, y, filter)
+ return self.bodies:move(actor.mainHitbox, x, y, filter)
+end
+
+function World2D:getActorsInRect(x, y, w, h)
+ local bodies = self.bodies:queryRect(x, y, w, h)
+ local returnquery = {}
+
+ for i,body in ipairs(bodies) do
+ if (body.isMainHitBox) then
+ table.insert(returnquery, body.owner)
+ end
+ end
+
+ return returnquery
+end
+
+-- BODIES MANAGEMENT FUNCTIONS
+-- Basic function to handle bodies. Wrappers around Bump2D functions
+
+function World2D:registerBody(body)
+ return self.bodies:add(body, body.x, body.y, body.w, body.h)
+end
+
+function World2D:updateBody(body)
+ return self.bodies:update(body, body.x, body.y, body.w, body.h)
+end
+
+function World2D:removeBody(body)
+ return self.bodies:remove(body)
+end
+
+function World2D:checkCollision(body, x, y, filter)
+ return self.bodies:check(body, x, y, filter)
+end
+
+function World2D:getBodiesInRect(x, y, w, h)
+ return self.bodies:queryRect(x, y, w, h)
+end
+
+return World2D
diff --git a/bootleg.love/core/modules/world/world3D.lua b/bootleg.love/core/modules/world/world3D.lua
new file mode 100644
index 0000000..58b364c
--- /dev/null
+++ b/bootleg.love/core/modules/world/world3D.lua
@@ -0,0 +1,268 @@
+-- world3D.lua :: a basic fake3D world based on bump-2dpd.
+
+--[[
+ Copyright © 2019 Kazhnuz
+
+ 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.
+]]
+
+local cwd = (...):gsub('%.world3D$', '') .. "."
+
+local BaseWorld = require(cwd .. "baseworld")
+local World3D = BaseWorld:extend()
+
+local Bump = require(cwd .. "libs.bump")
+local Bump3D = require(cwd .. "libs.bump-3dpd")
+local Tsort = require(cwd .. "libs.tsort")
+local CameraSystem = require(cwd .. "camera")
+
+local PADDING_VALUE = 10/100
+
+function World3D:new(scene, actorlist, mapfile, maptype)
+ World3D.super.new(self, scene, actorlist, mapfile, maptype)
+end
+
+-- ACTORS FUNCTIONS
+-- Add support for bodies in Actor functions
+
+function World3D:initActors()
+ self.currentCreationID = 0
+ self.actors = {}
+ self.bodies = Bump3D.newWorld(50)
+ self:initShapes()
+ self:initTerrain()
+end
+
+function World3D:newActor(name, x, y, z)
+ self.obj.index[name](self, x, y, z)
+end
+
+function World3D:newCollision(name, x, y, z, w, h, d)
+ self.obj.collisions[name](self, x, y, z, w, h, d)
+end
+
+function World3D:moveActor(actor, x, y, z, filter)
+ return self.bodies:move(actor.mainHitbox, x, y, z, filter)
+end
+
+function World3D:getActorsInRect(x, y, w, h)
+ return self:getShapeInRect(x, y, w, h)
+end
+
+function World3D:getVisibleActors(id)
+ local actors = {}
+ if (id ~= nil) then
+ local camx, camy, camw, camh = self.cameras:getViewCoordinate(id)
+ local paddingw = camw * PADDING_VALUE
+ local paddingh = camh * PADDING_VALUE
+ local x = camx - paddingw
+ local y = camy - paddingh
+ local w = camw + paddingw * 2
+ local h = camh + paddingh * 2
+
+ actors = self:getActorsInRect(x, y, w, h)
+ else
+ actors = self:getActors()
+ end
+
+ actors = self:zSortItems(actors)
+
+ return actors
+end
+
+
+-- PLAYER FUNCTIONS
+-- Load player stuff
+
+function World3D:newPlayer(x, y, z)
+ return self.obj.Player(self, x, y, z)
+end
+
+-- BODIES MANAGEMENT FUNCTIONS
+-- Basic function to handle bodies. Wrappers around Bump2D functions
+
+function World3D:registerBody(body)
+ return self.bodies:add(body, body.x, body.y, body.z, body.w, body.h, body.d)
+end
+
+function World3D:updateBody(body)
+ return self.bodies:update(body, body.x, body.y, body.z, body.w, body.h, body.d)
+end
+
+function World3D:removeBody(body)
+ return self.bodies:remove(body)
+end
+
+function World3D:checkCollision(body, x, y, z, filter)
+ return self.bodies:check(body, x, y, z, filter)
+end
+
+function World3D:getBodiesInRect(x, y, w, h)
+ return {} --self.bodies:queryRect(x, y, w, h)
+end
+
+-- UPDATE
+-- Update everything in the world
+
+function World3D:updateActors(dt)
+ World3D.super.updateActors(self, dt)
+ local actors = self:getActors()
+ for i,v in ipairs(actors) do
+ v:redrawShadowCanvas()
+ end
+end
+
+-- SHAPE SYSTEM
+-- Handle onscreen shapes
+
+function World3D:initShapes()
+ self.shapes = Bump.newWorld(50)
+end
+
+function World3D:registerShape(actor)
+ local x, y, w, h = actor:getShape()
+ return self.shapes:add(actor, x, y, w, h)
+end
+
+function World3D:updateShape(actor)
+ local x, y, w, h = actor:getShape()
+ return self.shapes:update(actor, x, y, w, h)
+end
+
+function World3D:removeShape(actor)
+ return self.shapes:remove(actor)
+end
+
+function World3D:checkShapeIntersection(actor, x, y)
+ return self.shapes:check(actor, x, y)
+end
+
+function World3D:getShapeInRect(x, y, w, h)
+ return self.shapes:queryRect(x, y, w, h)
+end
+
+-- TERRAIN SYSTEM
+-- Handle onscreen shapes
+
+function World3D:initTerrain()
+ self.terrains = Bump.newWorld(50)
+end
+
+function World3D:registerTerrain(actor)
+ return self.terrains:add(actor, actor.x, actor.y, actor.w, actor.h)
+end
+
+function World3D:updateTerrain(actor)
+ return self.terrains:update(actor, actor.x, actor.y, actor.w, actor.h)
+end
+
+function World3D:removeTerrain(actor)
+ return self.terrains:remove(actor)
+end
+
+function World3D:getTerrainInRect(x, y, w, h)
+ return self.terrains:queryRect(x, y, w, h)
+end
+
+-- DRAW FUNCTIONS
+-- Functions to draw the world
+
+function World3D:zSortItems(items)
+ -- zSorting algorithm taken from bump3D example, adapted to gamecore.
+ local graph = Tsort.new()
+ local noOverlap = {}
+
+ -- Iterate through all visible items, and calculate ordering of all pairs
+ -- of overlapping items.
+ -- TODO: Each pair is calculated twice currently. Maybe this is slow?
+ for _, itemA in ipairs(items) do repeat
+ local x, y, w, h = self.shapes:getRect(itemA)
+ local otherItemsFilter = function(other) return other ~= itemA end
+ local overlapping, len = self.shapes:queryRect(x, y, w, h, otherItemsFilter)
+
+ if len == 0 then
+ table.insert(noOverlap, itemA)
+
+ break
+ end
+
+ local _, aY, aZ, _, aH, aD = self.bodies:getCube(itemA.mainHitbox)
+ aDepth = itemA.depth
+ aID = itemA.creationID
+ aType = itemA.type
+ aZ = math.ceil(aZ)
+ aY = math.ceil(aY)
+
+ for _, itemB in ipairs(overlapping) do
+ local _, bY, bZ, _, bH, bD = self.bodies:getCube(itemB.mainHitbox)
+ bDepth = itemB.depth
+ bID = itemB.creationID
+ bType = itemB.type
+ bZ = math.ceil(bZ)
+ bY = math.ceil(bY)
+
+ if aZ >= bZ + bD then
+ -- item A is completely above item B
+ graph:add(itemB, itemA)
+ elseif bZ >= aZ + aD then
+ -- item B is completely above item A
+ graph:add(itemA, itemB)
+ elseif aY + aH <= bY then
+ -- item A is completely behind item B
+ graph:add(itemA, itemB)
+ elseif bY + bH <= aY then
+ -- item B is completely behind item A
+ graph:add(itemB, itemA)
+ elseif (aY - aZ) + aH > (bY - bZ) + bH then
+ -- item A's forward-most point is in front of item B's forward-most point
+ graph:add(itemB, itemA)
+ elseif (aY - aZ) + aH < (bY - bZ) + bH then
+ -- item B's forward-most point is in front of item A's forward-most point
+ graph:add(itemA, itemB)
+ else
+ -- item A's forward-most point is the same than item B's forward-most point
+ if aDepth > bDepth then
+ graph:add(itemB, itemA)
+ elseif aDepth < bDepth then
+ graph:add(itemA, itemB)
+ else
+ if aID > bID then
+ graph:add(itemA, itemB)
+ elseif aID < bID then
+ graph:add(itemB, itemA)
+ else
+ err("two object can't have the same ID")
+ end
+ end
+ end
+ end
+ until true end
+
+ local sorted, err = graph:sort()
+ if err then
+ error(err)
+ end
+ for _, item in ipairs(noOverlap) do
+ table.insert(sorted, item)
+ end
+
+ return sorted
+
+end
+
+return World3D
diff --git a/bootleg.love/core/options.lua b/bootleg.love/core/options.lua
new file mode 100644
index 0000000..28b8efb
--- /dev/null
+++ b/bootleg.love/core/options.lua
@@ -0,0 +1,171 @@
+-- core/options.lua :: The options loading/saving system. Is used by the other
+-- modules to save their settings.
+
+--[[
+ Copyright © 2019 Kazhnuz
+
+ 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.
+]]
+
+local OptionsManager = Object:extend()
+
+local cwd = (...):gsub('%.options$', '') .. "."
+local binser = require(cwd .. "modules.gamesystem.libs.binser")
+
+local TRANSLATION_PATH = "datas/languages/"
+
+-- INIT FUNCTIONS
+-- Initialize and configure the game options
+
+function OptionsManager:new(controller)
+ self.controller = controller
+ -- We begin by creating an empty data table before reading the data.
+ self.data = {}
+ self:read()
+end
+
+function OptionsManager:reset()
+ -- Reset the option to the game defaults.
+ self.data.video = {}
+ self.data.video.crtfilter = false
+ self.data.video.resolution = 1
+ self.data.video.border = true
+ self.data.video.vsync = true
+ self.data.video.fullscreen = false
+
+ -- We load the default files
+ self.data.input = self:getInputDefaultData()
+
+ -- TODO: have a way to auto-load a language according to the OS ?
+ self.data.language = self:getTranslationDefaultData()
+
+ self.data.audio = {}
+ self.data.audio.music = 100
+ self.data.audio.sfx = 100
+end
+
+-- INFO FUNCTIONS
+-- Get informations from the option managers
+
+function OptionsManager:getFile(absolute)
+ local dir = ""
+ if absolute then
+ dir = love.filesystem.getSaveDirectory() .. "/"
+ if not utils.filesystem.exists(dir) then
+ love.filesystem.createDirectory( "" )
+ end
+ end
+
+ local filepath = dir .. "options.data"
+
+ return filepath
+end
+
+function OptionsManager:getInputDefaultData()
+ local _path = "datas/inputs.lua"
+ local datas = {}
+ local fileinfo = love.filesystem.getInfo(_path)
+
+ if fileinfo ~= nil then
+ datas = require "datas.inputs"
+ else
+ datas = {}
+ end
+
+ return datas
+end
+
+function OptionsManager:getPlayerInputData(id)
+ local _playerInputData = self.data.input[id]
+
+ if _playerInputData == nil then
+ _playerInputData = {}
+ _playerInputData.keys = {}
+ end
+
+ return _playerInputData
+end
+
+function OptionsManager:getInputData()
+ return self.data.input
+end
+
+function OptionsManager:setInputKey(sourceid, padkey, key)
+ if self.data.input[sourceid] ~= nil then
+ if self.data.input[sourceid].keys[padkey] ~= nil then
+ self.data.input[sourceid].keys[padkey] = key
+ end
+ end
+end
+
+-- Lang data
+
+function OptionsManager:getTranslationDefaultData()
+ local _path = TRANSLATION_PATH .. "init.lua"
+ local fileinfo = love.filesystem.getInfo(_path)
+ local datas = nil
+
+ if fileinfo ~= nil then
+ lang = require(TRANSLATION_PATH)
+ lang.current = lang.default or "en"
+ lang.path = TRANSLATION_PATH
+ end
+
+ return lang
+end
+
+function OptionsManager:setLanguage(lang)
+ if (self.controller.lang:isLangAvailable(lang)) then
+ self.data.language.current = lang
+ self.controller.lang:getTranslationData()
+ end
+end
+
+-- DATA HANDLING FUNCTIONS
+-- Save and get data from the savefile
+
+-- FIXME: maybe subclass a special module for that ?
+
+function OptionsManager:write()
+ local data = self:getData()
+
+ filepath = self:getFile(true)
+ binser.writeFile(filepath, data)
+end
+
+function OptionsManager:read()
+ filepath = self:getFile(true)
+ if utils.filesystem.exists("options.data") then
+ local loadedDatas = binser.readFile(filepath)
+ self.controller.debug:print("core/options", "data file found, loading it")
+ self:setData(loadedDatas[1])
+ else
+ self:reset()
+ self.controller.debug:print("core/options", "no data file found, reseting data")
+ end
+end
+
+function OptionsManager:getData(data)
+ return self.data
+end
+
+function OptionsManager:setData(data)
+ self.data = data
+end
+
+return OptionsManager
diff --git a/bootleg.love/core/scenemanager.lua b/bootleg.love/core/scenemanager.lua
new file mode 100644
index 0000000..69e4878
--- /dev/null
+++ b/bootleg.love/core/scenemanager.lua
@@ -0,0 +1,175 @@
+-- scene.lua :: a basic scene management system, that work by sending the different
+-- core functions to the scene, normally without the scene itself having to manage
+-- them.
+
+--[[
+ Copyright © 2019 Kazhnuz
+
+ 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.
+]]
+
+local SceneManager = Object:extend()
+
+-- INIT FUNCTIONS
+-- Initialize and configure the scene manager
+
+function SceneManager:new(controller)
+ self.controller = controller
+ self.timers = self.controller.modules.Timers(self)
+ self.currentScene = nil
+
+ self.storage = {}
+
+ self:initTransitions()
+end
+
+function SceneManager:setScene(scene)
+ if self.transition.isPrepared then
+ self:startTransition(scene)
+ else
+ self.currentScene = scene
+ self.currentScene.isActive = true
+ end
+end
+
+function SceneManager:storeCurrentScene(name)
+ self.storage[name] = self.currentScene
+end
+
+function SceneManager:setStoredScene(name)
+ local storedScene = self.storage[name]
+ if storedScene ~= nil then
+ self.currentScene = storedScene
+ self.storage[name] = nil
+ end
+end
+
+function SceneManager:clearStorage()
+ self.storage = {}
+end
+
+function SceneManager:clearScene()
+ self.currentScene = nil
+end
+
+-- UPDATE FUNCTIONS
+-- Update the current scene and its subobjects
+
+function SceneManager:update(dt)
+ self.timers:update(dt)
+ if (self.currentScene ~= nil) then
+ self.currentScene:updateStart(dt)
+ self.currentScene:setKeys()
+ self.currentScene.assets:update(dt)
+ self.currentScene.menusystem:update(dt)
+ self.currentScene:updateWorld(dt)
+ self.currentScene:update(dt)
+ self.currentScene:updateEnd(dt)
+ end
+end
+
+-- MOUSE FUNCTIONS
+-- Send pointer data to the scene
+
+function SceneManager:mousemoved(x, y, dx, dy)
+ if (self.currentScene ~= nil) then
+ self.currentScene.mouse.x,
+ self.currentScene.mouse.y = x, y
+ self.currentScene:mousemoved(x, y, dx, dy)
+ self.currentScene.menusystem:mousemoved(x, y, dx, dy)
+ end
+end
+
+function SceneManager:mousepressed( x, y, button, istouch )
+ if (self.currentScene ~= nil) then
+ self.currentScene:mousepressed( x, y, button, istouch )
+ self.currentScene.menusystem:mousepressed( x, y, button, istouch )
+ end
+end
+
+-- KEYBOARD FUNCTIONS
+-- Add send keys functions to the scene
+
+function SceneManager:keypressed( key, scancode, isrepeat )
+ self.currentScene:keypressed( key, scancode, isrepeat )
+end
+
+function SceneManager:keyreleased( key )
+ self.currentScene:keyreleased( key )
+end
+
+-- TRANSITION FUNCTIONS
+-- Prepare transitionning to the next scene
+
+function SceneManager:initTransitions()
+ self.transition = {}
+
+ self.transition.easeIn = "inQuad"
+ self.transition.easeOut = "outQuad"
+ self.transition.duration = 1
+
+ self.transition.nextScene = nil
+ self.transition.isPrepared = false
+end
+
+function SceneManager:prepareTransition(duration, easeIn, easeOut)
+ self.transition.easeIn = easeIn or self.transition.easeIn
+ self.transition.easeOut = easeOut or self.transition.easeOut
+ self.transition.duration = duration or self.transition.duration
+
+ self.transition.isPrepared = true
+end
+
+function SceneManager:startTransition(nextScene)
+ self.transition.nextScene = nextScene
+ self.currentScene:flushKeys(self.transition.duration)
+ self.currentScene.isActive = false
+ self.transition.nextScene.isActive = false
+ core.screen:fadeIn(self.transition.duration / 2.5, self.transition.easeIn)
+ self.timers:newTimer(self.transition.duration / 2, "fadeOut")
+end
+
+function SceneManager:timerResponse(timer)
+ if timer == "fadeOut" then
+ self.currentScene = self.transition.nextScene
+ self.currentScene:flushKeys(self.transition.duration / 2.5)
+ self.currentScene.isActive = false
+ core.screen:fadeOut(self.transition.duration / 2.5, self.transition.easeOut)
+ self.transition.isPrepared = false
+ self.timers:newTimer(self.transition.duration / 2.5, "activateScene")
+ elseif timer == 'activateScene' then
+ self.currentScene.isActive = true
+ end
+end
+
+-- DRAW FUNCTIONS
+-- Draw the current scene
+
+function SceneManager:draw()
+ self.controller.screen:apply()
+ if (self.currentScene ~= nil) then
+ self.currentScene:drawStart()
+ self.currentScene:drawWorld()
+ self.currentScene:draw()
+ self.currentScene.menusystem:draw()
+ self.currentScene:drawEnd()
+ end
+ self.controller.screen:cease()
+end
+
+return SceneManager
diff --git a/bootleg.love/core/screen.lua b/bootleg.love/core/screen.lua
new file mode 100644
index 0000000..7a39c22
--- /dev/null
+++ b/bootleg.love/core/screen.lua
@@ -0,0 +1,140 @@
+-- core/screen.lua :: Basic screen manager. Use CScreen as a backend, and works
+-- as an abstraction layer around CScreen.
+
+--[[
+ Copyright © 2019 Kazhnuz
+
+ 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.
+]]
+
+local ScreenManager = Object:extend()
+
+local cwd = (...):gsub('%.screen$', '') .. "."
+local CScreen = require(cwd .. "libs.cscreen")
+
+-- INIT FUNCTIONS
+-- Initialize and configure the screen manager
+
+function ScreenManager:new(controller)
+ self.controller = controller
+ self.data = self.controller.options.data.video
+ self.width, self.height = love.graphics.getDimensions()
+ self:applySettings()
+ CScreen.init(self.width, self.height, true)
+ CScreen.setColor(0, 0, 0, 1)
+
+ love.graphics.setDefaultFilter( "nearest", "nearest", 1 )
+
+ self.timers = self.controller.modules.Timers(self)
+
+ self.transitionValue = 0
+end
+
+function ScreenManager:applySettings()
+ self.data = self.controller.options.data.video
+
+ local flags = {}
+ flags.vsync = self.data.vsync
+ flags.borderless = (self.data.border == false)
+
+ love.window.setMode(self.width * self.data.resolution, self.height * self.data.resolution, flags)
+ love.window.setFullscreen( self.data.fullscreen )
+
+ local width, height = love.window.getMode()
+ CScreen.update(width, height)
+end
+
+-- POINTER FUNCTIONS
+-- Translate the pointer according to the screen coordinates
+
+function ScreenManager:project(x, y)
+ return CScreen.project(x, y)
+end
+
+function ScreenManager:getMousePosition()
+ return CScreen.project(love.mouse.getX(), love.mouse.getY())
+end
+
+function ScreenManager:getScale()
+ return CScreen.getScale()
+end
+
+function ScreenManager:getScreenCoordinate(x, y)
+ return CScreen.getScreenCoordinate(x, y)
+end
+
+-- INFO FUNCTIONS
+-- Get screen informations
+
+function ScreenManager:getDimensions()
+ return self.width, self.height
+end
+
+-- SCISSOR FUNCTIONS
+-- Simple scissor functions
+
+function ScreenManager:setScissor(x, y, width, height)
+ local x, y = self:getScreenCoordinate(x, y)
+ love.graphics.setScissor(x, y, width*self.data.resolution, height*self.data.resolution)
+end
+
+function ScreenManager:resetScissor()
+ love.graphics.setScissor( )
+end
+
+-- UPDATE FUNCTIONS
+-- Update the screen
+
+function ScreenManager:update(dt)
+ self.timers:update(dt)
+end
+
+-- TRANSITION FUNCTIONS
+-- Handle transitions
+
+function ScreenManager:fadeIn(duration, easing)
+ local duration = duration or 1
+ local easing = easing or "inExpo"
+ self.timers:newTween(0, duration, {transitionValue = 1}, easing)
+end
+
+function ScreenManager:fadeOut(duration, easing)
+ local duration = duration or 1
+ local easing = easing or "outExpo"
+ self.timers:newTween(0, duration, {transitionValue = 0}, easing)
+end
+
+function ScreenManager:drawFade()
+ local w, h = self:getDimensions()
+ love.graphics.setColor(0, 0, 0, self.transitionValue)
+ love.graphics.rectangle("fill", 0, 0, w, h)
+ utils.graphics.resetColor()
+end
+
+-- DRAW FUNCTIONS
+-- Apply draw functions to the scene
+
+function ScreenManager:apply()
+ CScreen.apply()
+end
+
+function ScreenManager:cease()
+ CScreen.cease()
+end
+
+return ScreenManager
diff --git a/bootleg.love/core/utils/filesystem.lua b/bootleg.love/core/utils/filesystem.lua
new file mode 100644
index 0000000..ce99fbb
--- /dev/null
+++ b/bootleg.love/core/utils/filesystem.lua
@@ -0,0 +1,39 @@
+-- loveutils.filesystem : functions to handle filesystem.
+
+--[[
+ Copyright © 2019 Kazhnuz
+
+ 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.
+]]
+
+local Filesystem = {}
+
+function Filesystem.exists(filepath)
+ local info = love.filesystem.getInfo( filepath )
+ local exists = false
+
+ if (info == nil) then
+ exists = false
+ else
+ exists = true
+ end
+
+ return exists
+end
+
+return Filesystem
diff --git a/bootleg.love/core/utils/graphics.lua b/bootleg.love/core/utils/graphics.lua
new file mode 100644
index 0000000..4aaac79
--- /dev/null
+++ b/bootleg.love/core/utils/graphics.lua
@@ -0,0 +1,128 @@
+-- loveutils.graphics : a set of useful functions for love2D. Aim to reduce
+-- boilerplate in love2D by creating usefull function to handle these roles.
+
+--[[
+ Copyright © 2019 Kazhnuz
+
+ 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.
+]]
+
+local Graphics = {}
+
+-- COLOR FUNCTIONS
+-- Handle colors and scene colors
+
+function Graphics.resetColor()
+ love.graphics.setColor(1,1,1,1)
+end
+
+-- PRINT TEXT
+-- Functions to draw text on screen
+
+function Graphics.print(text, x, y, align, r, sx, sy, ox, oy, kx, ky)
+ local width
+ local font = love.graphics.getFont()
+ width = font:getWidth(text)
+
+ if align == "center" then
+ width = (width/2)
+ elseif align == "right" then
+ width = width
+ else
+ width = 0
+ end
+
+ love.graphics.print(text, x - (width), y, r, sx, sy, ox, oy, kx, ky)
+end
+
+function Graphics.printWithSpacing(text, spacing, align, x, y, r, sx, sy, ox, oy, kx, ky)
+ -- DO NOT USE THIS FUNCTION IN A "UPDATE" FUNCTION !
+ -- it's pretty heavy to use as it use a loop to get every character in a text
+ local font = love.graphics.getFont()
+ local xx = 0
+ local lenght = string.len(text)
+ local basewidth = font:getWidth(text)
+ local width = basewidth + (spacing * lenght)
+
+ if align == "center" then
+ width = (width/2)
+ elseif align == "right" then
+ width = width
+ else
+ width = 0
+ end
+
+ for i=1, lenght do
+ local char = string.sub(text, i, i)
+ pos = math.floor(x + xx - width)
+ love.graphics.print(char, pos, y)
+ xx = xx + font:getWidth(char) + spacing
+ end
+end
+
+-- FILTER FUNCTIONS
+-- Basic filters to a drawable
+
+function Graphics.drawShadow(drawable, x, y, r, sx, sy, ox, oy, kx, ky)
+ local color = love.graphics.getColor()
+ love.graphics.setColor(0, 0, 0, 0)
+ love.graphics.draw(drawable, x+1, y+1, r, sx, sy, ox, oy, kx, ky)
+ love.graphics.setColor(color)
+
+ love.graphics.draw(drawable, x, y, r, sx, sy, ox, oy, kx, ky)
+end
+
+function Graphics.drawBorder(drawable, border, x, y, r, sx, sy, ox, oy, kx, ky)
+ local color = love.graphics.getColor()
+ local b = border or 1
+ love.graphics.setColor(0, 0, 0, 0)
+ love.graphics.draw(drawable, x-b, y-b, limit, align, r, sx, sy, ox, oy, kx, ky)
+ love.graphics.draw(drawable, x , y-b, limit, align, r, sx, sy, ox, oy, kx, ky)
+ love.graphics.draw(drawable, x+b, y-b, limit, align, r, sx, sy, ox, oy, kx, ky)
+
+ love.graphics.draw(drawable, x+b, y , limit, align, r, sx, sy, ox, oy, kx, ky)
+ love.graphics.draw(drawable, x-b, y , limit, align, r, sx, sy, ox, oy, kx, ky)
+
+ love.graphics.draw(drawable, x-b, y+b, limit, align, r, sx, sy, ox, oy, kx, ky)
+ love.graphics.draw(drawable, x , y+b, limit, align, r, sx, sy, ox, oy, kx, ky)
+ love.graphics.draw(drawable, x+b, y+b, limit, align, r, sx, sy, ox, oy, kx, ky)
+ love.graphics.setColor(color)
+
+ love.graphics.draw(drawable, x, y, r, sx, sy, ox, oy, kx, ky)
+end
+
+-- PLACEHOLDER GRAPHICS FUNCTIONS
+-- Ready-to-use placeolder and stuff
+
+function Graphics.box(x, y, w, h)
+ local x = math.floor(x)
+ local y = math.floor(y)
+ local w = math.floor(w)
+ local h = math.floor(h)
+ local a = a or 1
+
+ local r, g, b, a = love.graphics.getColor( )
+
+ love.graphics.setColor(r, g, b, 0.3 * a)
+ love.graphics.rectangle("fill", x, y, w, h)
+
+ love.graphics.setColor(r, g, b, a)
+ love.graphics.rectangle("line", x, y, w, h)
+end
+
+return Graphics
diff --git a/bootleg.love/core/utils/init.lua b/bootleg.love/core/utils/init.lua
new file mode 100644
index 0000000..54c4dc0
--- /dev/null
+++ b/bootleg.love/core/utils/init.lua
@@ -0,0 +1,32 @@
+-- loveutils : a set of basic functions and utility for love2D.
+
+--[[
+ Copyright © 2019 Kazhnuz
+
+ 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.
+]]
+
+local cwd = (...):gsub('%.init$', '') .. "."
+
+-- load the different elements from loveutils
+return {
+ math = require(cwd .. "math"),
+ graphics = require(cwd .. "graphics"),
+ filesystem = require(cwd .. "filesystem"),
+ table = require(cwd .. "table")
+}
diff --git a/bootleg.love/core/utils/math.lua b/bootleg.love/core/utils/math.lua
new file mode 100644
index 0000000..fea382b
--- /dev/null
+++ b/bootleg.love/core/utils/math.lua
@@ -0,0 +1,139 @@
+-- loveutils.math : easy to use functions for mathematics and geometry.
+
+--[[
+ Copyright © 2019 Kazhnuz
+
+ 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.
+]]
+
+local Math = {}
+
+-- ALGEBRA FUNCTIONS
+-- Simple yet usefull functions not supported by base love2D
+
+function Math.sign(x)
+ if (x < 0) then
+ return -1
+ elseif (x > 0) then
+ return 1
+ else
+ return 0
+ end
+end
+
+function Math.round(num)
+ return math.floor(num + 0.5)
+end
+
+function Math.toZero(num, sub)
+ local sub = math.floor(sub)
+
+ if math.abs(num) < sub then
+ return 0
+ else
+ return num - (sub * Math.sign(num))
+ end
+end
+
+function Math.between(num, value1, value2)
+ local min = math.min(value1, value2)
+ local max = math.max(value1, value2)
+ return math.min(math.max(num, min), max)
+end
+
+-- VECTOR/DIRECTION functions
+-- Easy-to-use function to handle point and motion
+
+function Math.vector(x1, y1, x2, y2)
+ local vecx, vecy
+
+ vecx = x2 - x1
+ vexy = y2 - y1
+
+ return vecx, vecy
+end
+
+function Math.getMiddlePoint(x1, y1, x2, y2)
+ local newx, newy, vecx, vecy
+
+ vecx = math.max(x1, x2) - math.min(x1, x2)
+ vecy = math.max(y1, y2) - math.min(y1, y2)
+
+ newx = math.min(x1, x2) + (vecx / 2)
+ newy = math.min(y1, y2) + (vecy / 2)
+
+ return newx, newy
+end
+
+function Math.pointDistance(x1, y1, x2, y2)
+ local vecx, vecy
+
+ vecx = math.max(x1, x2) - math.min(x1, x2)
+ vecy = math.max(y1, y2) - math.min(y1, y2)
+
+ return math.sqrt(vecx^2 + vecy^2)
+
+end
+
+function Math.pointDirection(x1,y1,x2,y2)
+ local vecx, vecy, angle
+ vecy = y2 - y1
+ vecx = x2 - x1
+ angle = math.atan2(vecy, vecx)
+
+ return angle
+end
+
+-- STRING FUNCTIONS
+-- Transform into string numbers
+
+function Math.numberToString(x, length)
+ local length = length or 1
+ local string = ""
+ local x = x
+ if (x >= math.pow(10, length)) then
+ x = unitsNumber*10 - 1
+ string = string .. x
+ else
+ for i=1, (length-1) do
+ if (x < math.pow(10, length-i)) then
+ string = string .. "0"
+ end
+ end
+ string = string .. x
+ end
+ return string
+end
+
+-- COORDINATE FUNCTIONS
+-- Easy computation on coordinate
+
+function Math.floorCoord(x, y)
+ return math.floor(x), math.floor(y)
+end
+
+function Math.pixeliseCoord(x, y, factor)
+ x, y = Math.floorCoord(x / factor, y / factor)
+
+ x = x * factor
+ y = y * factor
+
+ return x, y
+end
+
+return Math
diff --git a/bootleg.love/core/utils/table.lua b/bootleg.love/core/utils/table.lua
new file mode 100644
index 0000000..46cd624
--- /dev/null
+++ b/bootleg.love/core/utils/table.lua
@@ -0,0 +1,52 @@
+-- loveutils.table : simple functions for table manipulation and computation.
+-- TODO: could be a part of loveutils.math ?
+
+--[[
+ Copyright © 2019 Kazhnuz
+
+ 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.
+]]
+
+local Table = {}
+
+function Table.reduce(list, fn)
+ local acc
+ for k, v in ipairs(list) do
+ if 1 == k then
+ acc = v
+ else
+ acc = fn(acc, v)
+ end
+ end
+ return acc
+end
+
+function Table.sum(table)
+ local sum = 0
+ for _, v in pairs(table) do
+ sum = sum + v
+ end
+
+ return sum
+end
+
+function Table.average(table)
+ return Table.sum(table) / #table
+end
+
+return Table
diff --git a/bootleg.love/game/init.lua b/bootleg.love/game/init.lua
new file mode 100644
index 0000000..8e25e25
--- /dev/null
+++ b/bootleg.love/game/init.lua
@@ -0,0 +1,9 @@
+local GameSystem = require "core.modules.gamesystem"
+local Game = GameSystem:extend()
+
+function Game:new()
+ Game.super.new(self)
+ self.currentSlot = 1
+end
+
+return Game
diff --git a/bootleg.love/main.lua b/bootleg.love/main.lua
new file mode 100644
index 0000000..22fb285
--- /dev/null
+++ b/bootleg.love/main.lua
@@ -0,0 +1,31 @@
+-- main.lua :: the base file of the game, will load main libs and core system
+
+--[[
+ Copyright © 2019 Kazhnuz
+
+ 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.
+]]
+
+Core = require "core"
+Game = require "game"
+scenes = require "scenes"
+
+function love.load()
+ core = Core(true)
+ game = Game()
+end
diff --git a/bootleg.love/scenes/init.lua b/bootleg.love/scenes/init.lua
new file mode 100644
index 0000000..04220c4
--- /dev/null
+++ b/bootleg.love/scenes/init.lua
@@ -0,0 +1,3 @@
+return {
+
+}