From 830de3f2d27db0b973d103288d0df1e7d1f48977 Mon Sep 17 00:00:00 2001 From: Kazhnuz Date: Sat, 28 Sep 2019 14:25:29 +0200 Subject: [PATCH] chore: add minimal gamecore-based game files --- CREDITS.md | 21 + bootleg.love/conf.lua | 41 + bootleg.love/core/callbacks.lua | 46 + bootleg.love/core/debug.lua | 61 + bootleg.love/core/init.lua | 122 ++ bootleg.love/core/input.lua | 206 +++ bootleg.love/core/lang.lua | 129 ++ bootleg.love/core/libs/classic.lua | 68 + bootleg.love/core/libs/cscreen.lua | 107 ++ bootleg.love/core/libs/lovebird.lua | 737 ++++++++ bootleg.love/core/modules/assets/animator.lua | 149 ++ bootleg.love/core/modules/assets/autotile.lua | 117 ++ .../core/modules/assets/background.lua | 54 + bootleg.love/core/modules/assets/fonts.lua | 191 +++ .../core/modules/assets/imagefonts.lua | 43 + bootleg.love/core/modules/assets/init.lua | 294 ++++ bootleg.love/core/modules/assets/sprites.lua | 125 ++ bootleg.love/core/modules/assets/texture.lua | 74 + bootleg.love/core/modules/assets/tileset.lua | 120 ++ bootleg.love/core/modules/gamesystem/init.lua | 145 ++ .../core/modules/gamesystem/libs/binser.lua | 687 ++++++++ .../core/modules/gamesystem/submodule.lua | 23 + bootleg.love/core/modules/init.lua | 33 + .../core/modules/menusystem/flowbox.lua | 230 +++ bootleg.love/core/modules/menusystem/grid.lua | 276 +++ .../core/modules/menusystem/hlistbox.lua | 148 ++ bootleg.love/core/modules/menusystem/init.lua | 285 ++++ .../core/modules/menusystem/listbox.lua | 148 ++ .../core/modules/menusystem/parent.lua | 291 ++++ .../core/modules/menusystem/widgets/init.lua | 144 ++ .../core/modules/menusystem/widgets/utils.lua | 57 + bootleg.love/core/modules/scenes.lua | 173 ++ bootleg.love/core/modules/timers/init.lua | 131 ++ .../core/modules/timers/libs/timer.lua | 45 + .../core/modules/timers/libs/tween.lua | 367 ++++ .../core/modules/world/actors/actor2D.lua | 207 +++ .../core/modules/world/actors/actor3D.lua | 297 ++++ .../core/modules/world/actors/baseactor.lua | 479 ++++++ .../core/modules/world/actors/gfx2D.lua | 41 + .../core/modules/world/actors/gfx3D.lua | 41 + .../modules/world/actors/utils/boxes/init.lua | 32 + .../world/actors/utils/boxes/mapped.lua | 51 + .../world/actors/utils/boxes/parent.lua | 164 ++ .../world/actors/utils/boxes/textured.lua | 53 + .../modules/world/actors/utils/hitbox2D.lua | 123 ++ .../modules/world/actors/utils/hitbox3D.lua | 129 ++ .../core/modules/world/actors/utils/timer.lua | 44 + bootleg.love/core/modules/world/baseworld.lua | 413 +++++ .../core/modules/world/camera/init.lua | 401 +++++ .../core/modules/world/camera/utils.lua | 83 + .../core/modules/world/libs/bump-3dpd.lua | 947 +++++++++++ bootleg.love/core/modules/world/libs/bump.lua | 769 +++++++++ .../core/modules/world/libs/tsort.lua | 84 + bootleg.love/core/modules/world/maps/init.lua | 7 + .../modules/world/maps/libs/sti/graphics.lua | 128 ++ .../core/modules/world/maps/libs/sti/init.lua | 1485 +++++++++++++++++ .../world/maps/libs/sti/plugins/box2d.lua | 303 ++++ .../world/maps/libs/sti/plugins/bump.lua | 194 +++ .../modules/world/maps/libs/sti/utils.lua | 206 +++ .../core/modules/world/maps/parent.lua | 97 ++ bootleg.love/core/modules/world/maps/sti.lua | 150 ++ bootleg.love/core/modules/world/world2D.lua | 89 + bootleg.love/core/modules/world/world3D.lua | 268 +++ bootleg.love/core/options.lua | 171 ++ bootleg.love/core/scenemanager.lua | 175 ++ bootleg.love/core/screen.lua | 140 ++ bootleg.love/core/utils/filesystem.lua | 39 + bootleg.love/core/utils/graphics.lua | 128 ++ bootleg.love/core/utils/init.lua | 32 + bootleg.love/core/utils/math.lua | 139 ++ bootleg.love/core/utils/table.lua | 52 + bootleg.love/game/init.lua | 9 + bootleg.love/main.lua | 31 + bootleg.love/scenes/init.lua | 3 + 74 files changed, 14092 insertions(+) create mode 100644 CREDITS.md create mode 100644 bootleg.love/conf.lua create mode 100644 bootleg.love/core/callbacks.lua create mode 100644 bootleg.love/core/debug.lua create mode 100644 bootleg.love/core/init.lua create mode 100644 bootleg.love/core/input.lua create mode 100644 bootleg.love/core/lang.lua create mode 100644 bootleg.love/core/libs/classic.lua create mode 100644 bootleg.love/core/libs/cscreen.lua create mode 100644 bootleg.love/core/libs/lovebird.lua create mode 100644 bootleg.love/core/modules/assets/animator.lua create mode 100644 bootleg.love/core/modules/assets/autotile.lua create mode 100644 bootleg.love/core/modules/assets/background.lua create mode 100644 bootleg.love/core/modules/assets/fonts.lua create mode 100644 bootleg.love/core/modules/assets/imagefonts.lua create mode 100644 bootleg.love/core/modules/assets/init.lua create mode 100644 bootleg.love/core/modules/assets/sprites.lua create mode 100644 bootleg.love/core/modules/assets/texture.lua create mode 100644 bootleg.love/core/modules/assets/tileset.lua create mode 100644 bootleg.love/core/modules/gamesystem/init.lua create mode 100644 bootleg.love/core/modules/gamesystem/libs/binser.lua create mode 100644 bootleg.love/core/modules/gamesystem/submodule.lua create mode 100644 bootleg.love/core/modules/init.lua create mode 100644 bootleg.love/core/modules/menusystem/flowbox.lua create mode 100644 bootleg.love/core/modules/menusystem/grid.lua create mode 100644 bootleg.love/core/modules/menusystem/hlistbox.lua create mode 100644 bootleg.love/core/modules/menusystem/init.lua create mode 100644 bootleg.love/core/modules/menusystem/listbox.lua create mode 100644 bootleg.love/core/modules/menusystem/parent.lua create mode 100644 bootleg.love/core/modules/menusystem/widgets/init.lua create mode 100644 bootleg.love/core/modules/menusystem/widgets/utils.lua create mode 100644 bootleg.love/core/modules/scenes.lua create mode 100644 bootleg.love/core/modules/timers/init.lua create mode 100644 bootleg.love/core/modules/timers/libs/timer.lua create mode 100644 bootleg.love/core/modules/timers/libs/tween.lua create mode 100644 bootleg.love/core/modules/world/actors/actor2D.lua create mode 100644 bootleg.love/core/modules/world/actors/actor3D.lua create mode 100644 bootleg.love/core/modules/world/actors/baseactor.lua create mode 100644 bootleg.love/core/modules/world/actors/gfx2D.lua create mode 100644 bootleg.love/core/modules/world/actors/gfx3D.lua create mode 100644 bootleg.love/core/modules/world/actors/utils/boxes/init.lua create mode 100644 bootleg.love/core/modules/world/actors/utils/boxes/mapped.lua create mode 100644 bootleg.love/core/modules/world/actors/utils/boxes/parent.lua create mode 100644 bootleg.love/core/modules/world/actors/utils/boxes/textured.lua create mode 100644 bootleg.love/core/modules/world/actors/utils/hitbox2D.lua create mode 100644 bootleg.love/core/modules/world/actors/utils/hitbox3D.lua create mode 100644 bootleg.love/core/modules/world/actors/utils/timer.lua create mode 100644 bootleg.love/core/modules/world/baseworld.lua create mode 100644 bootleg.love/core/modules/world/camera/init.lua create mode 100644 bootleg.love/core/modules/world/camera/utils.lua create mode 100644 bootleg.love/core/modules/world/libs/bump-3dpd.lua create mode 100644 bootleg.love/core/modules/world/libs/bump.lua create mode 100644 bootleg.love/core/modules/world/libs/tsort.lua create mode 100644 bootleg.love/core/modules/world/maps/init.lua create mode 100644 bootleg.love/core/modules/world/maps/libs/sti/graphics.lua create mode 100644 bootleg.love/core/modules/world/maps/libs/sti/init.lua create mode 100644 bootleg.love/core/modules/world/maps/libs/sti/plugins/box2d.lua create mode 100644 bootleg.love/core/modules/world/maps/libs/sti/plugins/bump.lua create mode 100644 bootleg.love/core/modules/world/maps/libs/sti/utils.lua create mode 100644 bootleg.love/core/modules/world/maps/parent.lua create mode 100644 bootleg.love/core/modules/world/maps/sti.lua create mode 100644 bootleg.love/core/modules/world/world2D.lua create mode 100644 bootleg.love/core/modules/world/world3D.lua create mode 100644 bootleg.love/core/options.lua create mode 100644 bootleg.love/core/scenemanager.lua create mode 100644 bootleg.love/core/screen.lua create mode 100644 bootleg.love/core/utils/filesystem.lua create mode 100644 bootleg.love/core/utils/graphics.lua create mode 100644 bootleg.love/core/utils/init.lua create mode 100644 bootleg.love/core/utils/math.lua create mode 100644 bootleg.love/core/utils/table.lua create mode 100644 bootleg.love/game/init.lua create mode 100644 bootleg.love/main.lua create mode 100644 bootleg.love/scenes/init.lua 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 { + +}