├── assets ├── images │ └── icon.png └── fonts │ ├── Roboto-Bold.ttf │ ├── Roboto-Thin.ttf │ ├── Roboto-Italic.ttf │ ├── Roboto-Light.ttf │ ├── Roboto-Regular.ttf │ ├── Roboto-BoldItalic.ttf │ ├── Roboto-LightItalic.ttf │ ├── Roboto-ThinItalic.ttf │ ├── RobotoMono-Regular.ttf │ └── LICENSE.txt ├── play.bat ├── states └── game.lua ├── .gitattributes ├── LICENSE ├── libs ├── signal.lua ├── class.lua ├── state.lua ├── vector.lua ├── camera.lua ├── timer.lua ├── husl.lua ├── inspect.lua └── lume.lua ├── globals.lua ├── README.md ├── conf.lua └── main.lua /assets/images/icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/camchenry/Love2D-Template/HEAD/assets/images/icon.png -------------------------------------------------------------------------------- /assets/fonts/Roboto-Bold.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/camchenry/Love2D-Template/HEAD/assets/fonts/Roboto-Bold.ttf -------------------------------------------------------------------------------- /assets/fonts/Roboto-Thin.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/camchenry/Love2D-Template/HEAD/assets/fonts/Roboto-Thin.ttf -------------------------------------------------------------------------------- /assets/fonts/Roboto-Italic.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/camchenry/Love2D-Template/HEAD/assets/fonts/Roboto-Italic.ttf -------------------------------------------------------------------------------- /assets/fonts/Roboto-Light.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/camchenry/Love2D-Template/HEAD/assets/fonts/Roboto-Light.ttf -------------------------------------------------------------------------------- /assets/fonts/Roboto-Regular.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/camchenry/Love2D-Template/HEAD/assets/fonts/Roboto-Regular.ttf -------------------------------------------------------------------------------- /assets/fonts/Roboto-BoldItalic.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/camchenry/Love2D-Template/HEAD/assets/fonts/Roboto-BoldItalic.ttf -------------------------------------------------------------------------------- /assets/fonts/Roboto-LightItalic.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/camchenry/Love2D-Template/HEAD/assets/fonts/Roboto-LightItalic.ttf -------------------------------------------------------------------------------- /assets/fonts/Roboto-ThinItalic.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/camchenry/Love2D-Template/HEAD/assets/fonts/Roboto-ThinItalic.ttf -------------------------------------------------------------------------------- /assets/fonts/RobotoMono-Regular.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/camchenry/Love2D-Template/HEAD/assets/fonts/RobotoMono-Regular.ttf -------------------------------------------------------------------------------- /play.bat: -------------------------------------------------------------------------------- 1 | @echo OFF 2 | set mydir="%~p0" 3 | SET mydir=%mydir:\=;% 4 | 5 | for /F "tokens=* delims=;" %%i IN (%mydir%) DO call :LAST_FOLDER %%i 6 | goto :EOF 7 | 8 | :LAST_FOLDER 9 | if "%1"=="" ( 10 | @echo Running %LAST% project 11 | "%PROGRAMFILES%\LOVE\love" ..\%LAST% 12 | exit 13 | ) 14 | 15 | set LAST=%1 16 | SHIFT 17 | 18 | goto :LAST_FOLDER -------------------------------------------------------------------------------- /states/game.lua: -------------------------------------------------------------------------------- 1 | local game = {} 2 | 3 | function game:init() 4 | 5 | end 6 | 7 | function game:enter() 8 | 9 | end 10 | 11 | function game:update(dt) 12 | 13 | end 14 | 15 | function game:keypressed(key, code) 16 | 17 | end 18 | 19 | function game:mousepressed(x, y, mbutton) 20 | 21 | end 22 | 23 | function game:draw() 24 | 25 | end 26 | 27 | return game 28 | -------------------------------------------------------------------------------- /.gitattributes: -------------------------------------------------------------------------------- 1 | # Auto detect text files and perform LF normalization 2 | * text=auto 3 | 4 | # Custom for Visual Studio 5 | *.cs diff=csharp 6 | 7 | # Standard to msysgit 8 | *.doc diff=astextplain 9 | *.DOC diff=astextplain 10 | *.docx diff=astextplain 11 | *.DOCX diff=astextplain 12 | *.dot diff=astextplain 13 | *.DOT diff=astextplain 14 | *.pdf diff=astextplain 15 | *.PDF diff=astextplain 16 | *.rtf diff=astextplain 17 | *.RTF diff=astextplain 18 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2016 Cameron McHenry 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /libs/signal.lua: -------------------------------------------------------------------------------- 1 | --[[ 2 | Copyright (c) 2012-2013 Matthias Richter 3 | 4 | Permission is hereby granted, free of charge, to any person obtaining a copy 5 | of this software and associated documentation files (the "Software"), to deal 6 | in the Software without restriction, including without limitation the rights 7 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 8 | copies of the Software, and to permit persons to whom the Software is 9 | furnished to do so, subject to the following conditions: 10 | 11 | The above copyright notice and this permission notice shall be included in 12 | all copies or substantial portions of the Software. 13 | 14 | Except as contained in this notice, the name(s) of the above copyright holders 15 | shall not be used in advertising or otherwise to promote the sale, use or 16 | other dealings in this Software without prior written authorization. 17 | 18 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 19 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 20 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 21 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 22 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 23 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 24 | THE SOFTWARE. 25 | ]]-- 26 | 27 | local Registry = {} 28 | Registry.__index = function(self, key) 29 | return Registry[key] or (function() 30 | local t = {} 31 | rawset(self, key, t) 32 | return t 33 | end)() 34 | end 35 | 36 | function Registry:register(s, f) 37 | self[s][f] = f 38 | return f 39 | end 40 | 41 | function Registry:emit(s, ...) 42 | for f in pairs(self[s]) do 43 | f(...) 44 | end 45 | end 46 | 47 | function Registry:remove(s, ...) 48 | local f = {...} 49 | for i = 1,select('#', ...) do 50 | self[s][f[i]] = nil 51 | end 52 | end 53 | 54 | function Registry:clear(...) 55 | local s = {...} 56 | for i = 1,select('#', ...) do 57 | self[s[i]] = {} 58 | end 59 | end 60 | 61 | function Registry:emitPattern(p, ...) 62 | for s in pairs(self) do 63 | if s:match(p) then self:emit(s, ...) end 64 | end 65 | end 66 | 67 | function Registry:removePattern(p, ...) 68 | for s in pairs(self) do 69 | if s:match(p) then self:remove(s, ...) end 70 | end 71 | end 72 | 73 | function Registry:clearPattern(p) 74 | for s in pairs(self) do 75 | if s:match(p) then self[s] = {} end 76 | end 77 | end 78 | 79 | -- the module 80 | local function new() 81 | local registry = setmetatable({}, Registry) 82 | 83 | return setmetatable({ 84 | new = new, 85 | register = function(...) return registry:register(...) end, 86 | emit = function(...) registry:emit(...) end, 87 | remove = function(...) registry:remove(...) end, 88 | clear = function(...) registry:clear(...) end, 89 | emitPattern = function(...) registry:emitPattern(...) end, 90 | removePattern = function(...) registry:removePattern(...) end, 91 | clearPattern = function(...) registry:clearPattern(...) end, 92 | }, {__call = new}) 93 | end 94 | 95 | return new() 96 | -------------------------------------------------------------------------------- /libs/class.lua: -------------------------------------------------------------------------------- 1 | --[[ 2 | Copyright (c) 2010-2013 Matthias Richter 3 | 4 | Permission is hereby granted, free of charge, to any person obtaining a copy 5 | of this software and associated documentation files (the "Software"), to deal 6 | in the Software without restriction, including without limitation the rights 7 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 8 | copies of the Software, and to permit persons to whom the Software is 9 | furnished to do so, subject to the following conditions: 10 | 11 | The above copyright notice and this permission notice shall be included in 12 | all copies or substantial portions of the Software. 13 | 14 | Except as contained in this notice, the name(s) of the above copyright holders 15 | shall not be used in advertising or otherwise to promote the sale, use or 16 | other dealings in this Software without prior written authorization. 17 | 18 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 19 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 20 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 21 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 22 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 23 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 24 | THE SOFTWARE. 25 | ]]-- 26 | 27 | local function include_helper(to, from, seen) 28 | if from == nil then 29 | return to 30 | elseif type(from) ~= 'table' then 31 | return from 32 | elseif seen[from] then 33 | return seen[from] 34 | end 35 | 36 | seen[from] = to 37 | for k,v in pairs(from) do 38 | k = include_helper({}, k, seen) -- keys might also be tables 39 | if to[k] == nil then 40 | to[k] = include_helper({}, v, seen) 41 | end 42 | end 43 | return to 44 | end 45 | 46 | -- deeply copies `other' into `class'. keys in `other' that are already 47 | -- defined in `class' are omitted 48 | local function include(class, other) 49 | return include_helper(class, other, {}) 50 | end 51 | 52 | -- returns a deep copy of `other' 53 | local function clone(other) 54 | return setmetatable(include({}, other), getmetatable(other)) 55 | end 56 | 57 | local function new(class) 58 | -- mixins 59 | class = class or {} -- class can be nil 60 | local inc = class.__includes or {} 61 | if getmetatable(inc) then inc = {inc} end 62 | 63 | for _, other in ipairs(inc) do 64 | if type(other) == "string" then 65 | other = _G[other] 66 | end 67 | include(class, other) 68 | end 69 | 70 | -- class implementation 71 | class.__index = class 72 | class.init = class.init or class[1] or function() end 73 | class.include = class.include or include 74 | class.clone = class.clone or clone 75 | 76 | -- constructor call 77 | return setmetatable(class, {__call = function(c, ...) 78 | local o = setmetatable({}, c) 79 | o:init(...) 80 | return o 81 | end}) 82 | end 83 | 84 | -- interface for cross class-system compatibility (see https://github.com/bartbes/Class-Commons). 85 | if class_commons ~= false and not common then 86 | common = {} 87 | function common.class(name, prototype, parent) 88 | return new{__includes = {prototype, parent}} 89 | end 90 | function common.instance(class, ...) 91 | return class(...) 92 | end 93 | end 94 | 95 | 96 | -- the module 97 | return setmetatable({new = new, include = include, clone = clone}, 98 | {__call = function(_,...) return new(...) end}) 99 | -------------------------------------------------------------------------------- /globals.lua: -------------------------------------------------------------------------------- 1 | -- !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! 2 | -- !! This flag controls the ability to toggle the debug view. !! 3 | -- !! You will want to turn this to 'true' when you publish your game. !! 4 | -- !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! 5 | RELEASE = false 6 | 7 | -- Enables the debug stats 8 | DEBUG = not RELEASE 9 | 10 | CONFIG = { 11 | graphics = { 12 | filter = { 13 | -- FilterModes: linear (blurry) / nearest (blocky) 14 | -- Default filter used when scaling down 15 | down = "nearest", 16 | 17 | -- Default filter used when scaling up 18 | up = "nearest", 19 | 20 | -- Amount of anisotropic filter performed 21 | anisotropy = 1, 22 | } 23 | }, 24 | 25 | window = { 26 | icon = 'assets/images/icon.png' 27 | }, 28 | 29 | debug = { 30 | -- The key (scancode) that will toggle the debug state. 31 | -- Scancodes are independent of keyboard layout so it will always be in the same 32 | -- position on the keyboard. The positions are based on an American layout. 33 | key = '`', 34 | 35 | stats = { 36 | font = nil, -- set after fonts are created 37 | fontSize = 16, 38 | lineHeight = 18, 39 | foreground = {1, 1, 1, 1}, 40 | shadow = {0, 0, 0, 1}, 41 | shadowOffset = {x = 1, y = 1}, 42 | position = {x = 8, y = 6}, 43 | 44 | kilobytes = false, 45 | }, 46 | 47 | -- Error screen config 48 | error = { 49 | font = nil, -- set after fonts are created 50 | fontSize = 16, 51 | background = {.1, .31, .5}, 52 | foreground = {1, 1, 1}, 53 | shadow = {0, 0, 0, .88}, 54 | shadowOffset = {x = 1, y = 1}, 55 | position = {x = 70, y = 70}, 56 | }, 57 | } 58 | } 59 | 60 | local function makeFont(path) 61 | return setmetatable({}, { 62 | __index = function(t, size) 63 | local f = love.graphics.newFont(path, size) 64 | rawset(t, size, f) 65 | return f 66 | end 67 | }) 68 | end 69 | 70 | Fonts = { 71 | default = nil, 72 | 73 | regular = makeFont 'assets/fonts/Roboto-Regular.ttf', 74 | bold = makeFont 'assets/fonts/Roboto-Bold.ttf', 75 | light = makeFont 'assets/fonts/Roboto-Light.ttf', 76 | thin = makeFont 'assets/fonts/Roboto-Thin.ttf', 77 | regularItalic = makeFont 'assets/fonts/Roboto-Italic.ttf', 78 | boldItalic = makeFont 'assets/fonts/Roboto-BoldItalic.ttf', 79 | lightItalic = makeFont 'assets/fonts/Roboto-LightItalic.ttf', 80 | thinItalic = makeFont 'assets/fonts/Roboto-Italic.ttf', 81 | 82 | monospace = makeFont 'assets/fonts/RobotoMono-Regular.ttf', 83 | } 84 | Fonts.default = Fonts.regular 85 | 86 | CONFIG.debug.stats.font = Fonts.monospace 87 | CONFIG.debug.error.font = Fonts.monospace 88 | 89 | Lume = require 'libs.lume' 90 | Husl = require 'libs.husl' 91 | Class = require 'libs.class' 92 | Vector = require 'libs.vector' 93 | State = require 'libs.state' 94 | Signal = require 'libs.signal' 95 | Inspect = require 'libs.inspect' 96 | Camera = require 'libs.camera' 97 | Timer = require 'libs.timer' 98 | 99 | States = { 100 | game = require 'states.game', 101 | } 102 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | 2 | 3 | LÖVE Template 4 | ============= 5 | 6 | This is a simple template for making games with LÖVE. It tries to do as little as possible while still being useful for most types of games (or tools). 7 | 8 | ## Should you use this template? 9 | 10 | This template is primarily for my own personal use and the way that things are setup matches how I tend to write projects. 11 | 12 | If you are an absolute novice to LÖVE, it would be in your best interest to become familiar with the basics first before using any templates. 13 | 14 | If you are a beginner/intermediate user of LÖVE and you are not sure what libraries to use or how to layout a project, then this may be what you need. 15 | 16 | If you are experienced with LÖVE, you probably know enough to make your own template, and you don't need this anyway. On the other hand, it should also be trivial to modify the code to suit your own needs. It is your choice. 17 | 18 | ## General project guidelines 19 | 20 | There are a few guidelines that I follow to keep projects consistent and maintainable. 21 | 22 | 1. **Modules should be local by default.** 23 | 24 | This makes it easy to alias the module name. It also keeps the global namespace clean. For further reading: http://kiki.to/blog/2014/03/31/rule-2-return-a-local-table/ 25 | 26 | ```lua 27 | -- theModule.lua 28 | local theModule = {} 29 | ... 30 | return theModule 31 | ``` 32 | 33 | ```lua 34 | -- Other lua file 35 | local theModule = require 'theModule' 36 | ``` 37 | 38 | 2. **Common libraries should be global.** 39 | 40 | If a library is used everywhere, then it might be a good idea to require it globally. 41 | 42 | ```lua 43 | Class = require 'libs.class' 44 | ``` 45 | 46 | 3. **Global variables should be put into a file called globals.lua.** 47 | 48 | Ideally, you do not want many global variables in the global namespace, because it makes it hard to keep track of state. To make things even easier, putting global variables in a single file makes it easier to find where they are defined. 49 | 50 | 4. **Directories should be plural, where applicable.** 51 | 52 | 5. **Directories should not be abbreviated.** 53 | 54 | There is no technical justification for these two guidelines, but it makes names easier to remember. 55 | 56 | ## Included libraries 57 | 58 | * [inspect.lua](https://github.com/kikito/inspect.lua) (Used as `Inspect`) - A useful debug utility for printing out the contents of tables. 59 | * [HUSL 1.0 (now called HSLuv)](https://github.com/hsluv/hsluv-lua) - Provides an easier and more aesthetically pleasing alternative to RGB for specifying colors. 60 | * [HUMP](https://github.com/vrld/hump) 61 | * [Camera](http://hump.readthedocs.io/en/latest/camera.html) - Handles translations, rotation, and scaling. 62 | * [Class](http://hump.readthedocs.io/en/latest/class.html) - Allows lightweight classes, not a full OOP implementation. 63 | * [Gamestate](http://hump.readthedocs.io/en/latest/gamestate.html) (Used as `State`) - Creates different states in your game that you can switch between, e.g. menu, game, pause screen, etc. 64 | * [Signal](http://hump.readthedocs.io/en/latest/signal.html) - Used for broadcasting events, "player killed an enemy," "an item was dropped," "the game has started," which is useful for triggering sounds and effects without tying it to your gameplay code. 65 | * [Timer](http://hump.readthedocs.io/en/latest/timer.html) - Allows you to create code that will run after N seconds, every N seconds, and during N seconds, and other useful timing utilities. 66 | * [Vector](http://hump.readthedocs.io/en/latest/vector.html) - Represents a 2D vector with a full set of the needed functions (normalization, dot product, cross product, etc.). 67 | * [Lume](https://github.com/rxi/lume) - A set of utility functions that are incredibly useful for gamedev. 68 | -------------------------------------------------------------------------------- /conf.lua: -------------------------------------------------------------------------------- 1 | function love.conf(t) 2 | t.identity = nil -- The name of the save directory (string) 3 | t.appendidentity = false -- Search files in source directory before save directory (boolean) 4 | t.version = "11.1" -- The LÖVE version this game was made for (string) 5 | t.console = false -- Attach a console (boolean, Windows only) 6 | t.accelerometerjoystick = true -- Enable the accelerometer on iOS and Android by exposing it as a Joystick (boolean) 7 | t.externalstorage = false -- True to save files (and read from the save directory) in external storage on Android (boolean) 8 | t.gammacorrect = false -- Enable gamma-correct rendering, when supported by the system (boolean) 9 | 10 | t.audio.mixwithsystem = true -- Keep background music playing when opening LOVE (boolean, iOS and Android only) 11 | 12 | t.window.title = "Untitled" -- The window title (string) 13 | t.window.icon = nil -- Filepath to an image to use as the window's icon (string) 14 | t.window.width = 1280 -- The window width (number) 15 | t.window.height = 720 -- The window height (number) 16 | t.window.borderless = false -- Remove all border visuals from the window (boolean) 17 | t.window.resizable = false -- Let the window be user-resizable (boolean) 18 | t.window.minwidth = 1 -- Minimum window width if the window is resizable (number) 19 | t.window.minheight = 1 -- Minimum window height if the window is resizable (number) 20 | t.window.fullscreen = false -- Enable fullscreen (boolean) 21 | t.window.fullscreentype = "desktop" -- Choose between "desktop" fullscreen or "exclusive" fullscreen mode (string) 22 | t.window.vsync = 0 -- Vertical sync mode (number) 23 | t.window.msaa = 0 -- The number of samples to use with multi-sampled antialiasing (number) 24 | t.window.display = 1 -- Index of the monitor to show the window in (number) 25 | t.window.highdpi = false -- Enable high-dpi mode for the window on a Retina display (boolean) 26 | t.window.x = nil -- The x-coordinate of the window's position in the specified display (number) 27 | t.window.y = nil -- The y-coordinate of the window's position in the specified display (number) 28 | 29 | t.modules.audio = true -- Enable the audio module (boolean) 30 | t.modules.data = true -- Enable the data module (boolean) 31 | t.modules.event = true -- Enable the event module (boolean) 32 | t.modules.font = true -- Enable the font module (boolean) 33 | t.modules.graphics = true -- Enable the graphics module (boolean) 34 | t.modules.image = true -- Enable the image module (boolean) 35 | t.modules.joystick = true -- Enable the joystick module (boolean) 36 | t.modules.keyboard = true -- Enable the keyboard module (boolean) 37 | t.modules.math = true -- Enable the math module (boolean) 38 | t.modules.mouse = true -- Enable the mouse module (boolean) 39 | t.modules.physics = true -- Enable the physics module (boolean) 40 | t.modules.sound = true -- Enable the sound module (boolean) 41 | t.modules.system = true -- Enable the system module (boolean) 42 | t.modules.thread = true -- Enable the thread module (boolean) 43 | t.modules.timer = true -- Enable the timer module (boolean), Disabling it will result 0 delta time in love.update 44 | t.modules.touch = true -- Enable the touch module (boolean) 45 | t.modules.video = true -- Enable the video module (boolean) 46 | t.modules.window = true -- Enable the window module (boolean) 47 | end 48 | -------------------------------------------------------------------------------- /libs/state.lua: -------------------------------------------------------------------------------- 1 | --[[ 2 | Copyright (c) 2010-2013 Matthias Richter 3 | 4 | Permission is hereby granted, free of charge, to any person obtaining a copy 5 | of this software and associated documentation files (the "Software"), to deal 6 | in the Software without restriction, including without limitation the rights 7 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 8 | copies of the Software, and to permit persons to whom the Software is 9 | furnished to do so, subject to the following conditions: 10 | 11 | The above copyright notice and this permission notice shall be included in 12 | all copies or substantial portions of the Software. 13 | 14 | Except as contained in this notice, the name(s) of the above copyright holders 15 | shall not be used in advertising or otherwise to promote the sale, use or 16 | other dealings in this Software without prior written authorization. 17 | 18 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 19 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 20 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 21 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 22 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 23 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 24 | THE SOFTWARE. 25 | ]]-- 26 | 27 | local function __NULL__() end 28 | 29 | -- default gamestate produces error on every callback 30 | local state_init = setmetatable({leave = __NULL__}, 31 | {__index = function() error("Gamestate not initialized. Use Gamestate.switch()") end}) 32 | local stack = {state_init} 33 | local initialized_states = setmetatable({}, {__mode = "k"}) 34 | local state_is_dirty = true 35 | 36 | local GS = {} 37 | function GS.new(t) return t or {} end -- constructor - deprecated! 38 | 39 | local function change_state(stack_offset, to, ...) 40 | local pre = stack[#stack] 41 | 42 | -- initialize only on first call 43 | ;(initialized_states[to] or to.init or __NULL__)(to) 44 | initialized_states[to] = __NULL__ 45 | 46 | stack[#stack+stack_offset] = to 47 | state_is_dirty = true 48 | return (to.enter or __NULL__)(to, pre, ...) 49 | end 50 | 51 | function GS.switch(to, ...) 52 | assert(to, "Missing argument: Gamestate to switch to") 53 | assert(to ~= GS, "Can't call switch with colon operator") 54 | ;(stack[#stack].leave or __NULL__)(stack[#stack]) 55 | return change_state(0, to, ...) 56 | end 57 | 58 | function GS.push(to, ...) 59 | assert(to, "Missing argument: Gamestate to switch to") 60 | assert(to ~= GS, "Can't call push with colon operator") 61 | return change_state(1, to, ...) 62 | end 63 | 64 | function GS.pop(...) 65 | assert(#stack > 1, "No more states to pop!") 66 | local pre, to = stack[#stack], stack[#stack-1] 67 | stack[#stack] = nil 68 | ;(pre.leave or __NULL__)(pre) 69 | state_is_dirty = true 70 | return (to.resume or __NULL__)(to, pre, ...) 71 | end 72 | 73 | function GS.current() 74 | return stack[#stack] 75 | end 76 | 77 | -- fetch event callbacks from love.handlers 78 | local all_callbacks = { 'draw', 'errorhandler', 'update' } 79 | for k in pairs(love.handlers) do 80 | all_callbacks[#all_callbacks+1] = k 81 | end 82 | 83 | function GS.registerEvents(callbacks) 84 | local registry = {} 85 | callbacks = callbacks or all_callbacks 86 | for _, f in ipairs(callbacks) do 87 | registry[f] = love[f] or __NULL__ 88 | love[f] = function(...) 89 | registry[f](...) 90 | return GS[f](...) 91 | end 92 | end 93 | end 94 | 95 | -- forward any undefined functions 96 | setmetatable(GS, {__index = function(_, func) 97 | -- call function only if at least one 'update' was called beforehand 98 | -- (see issue #46) 99 | if not state_is_dirty or func == 'update' then 100 | state_is_dirty = false 101 | return function(...) 102 | return (stack[#stack][func] or __NULL__)(stack[#stack], ...) 103 | end 104 | end 105 | return __NULL__ 106 | end}) 107 | 108 | return GS 109 | -------------------------------------------------------------------------------- /libs/vector.lua: -------------------------------------------------------------------------------- 1 | --[[ 2 | Copyright (c) 2010-2013 Matthias Richter 3 | 4 | Permission is hereby granted, free of charge, to any person obtaining a copy 5 | of this software and associated documentation files (the "Software"), to deal 6 | in the Software without restriction, including without limitation the rights 7 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 8 | copies of the Software, and to permit persons to whom the Software is 9 | furnished to do so, subject to the following conditions: 10 | 11 | The above copyright notice and this permission notice shall be included in 12 | all copies or substantial portions of the Software. 13 | 14 | Except as contained in this notice, the name(s) of the above copyright holders 15 | shall not be used in advertising or otherwise to promote the sale, use or 16 | other dealings in this Software without prior written authorization. 17 | 18 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 19 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 20 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 21 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 22 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 23 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 24 | THE SOFTWARE. 25 | ]]-- 26 | 27 | local assert = assert 28 | local sqrt, cos, sin, atan2 = math.sqrt, math.cos, math.sin, math.atan2 29 | 30 | local vector = {} 31 | vector.__index = vector 32 | 33 | local function new(x,y) 34 | return setmetatable({x = x or 0, y = y or 0}, vector) 35 | end 36 | local zero = new(0,0) 37 | 38 | local function isvector(v) 39 | return type(v) == 'table' and type(v.x) == 'number' and type(v.y) == 'number' 40 | end 41 | 42 | function vector:clone() 43 | return new(self.x, self.y) 44 | end 45 | 46 | function vector:unpack() 47 | return self.x, self.y 48 | end 49 | 50 | function vector:__tostring() 51 | return "("..tonumber(self.x)..","..tonumber(self.y)..")" 52 | end 53 | 54 | function vector.__unm(a) 55 | return new(-a.x, -a.y) 56 | end 57 | 58 | function vector.__add(a,b) 59 | assert(isvector(a) and isvector(b), "Add: wrong argument types ( expected)") 60 | return new(a.x+b.x, a.y+b.y) 61 | end 62 | 63 | function vector.__sub(a,b) 64 | assert(isvector(a) and isvector(b), "Sub: wrong argument types ( expected)") 65 | return new(a.x-b.x, a.y-b.y) 66 | end 67 | 68 | function vector.__mul(a,b) 69 | if type(a) == "number" then 70 | return new(a*b.x, a*b.y) 71 | elseif type(b) == "number" then 72 | return new(b*a.x, b*a.y) 73 | else 74 | assert(isvector(a) and isvector(b), "Mul: wrong argument types ( or expected)") 75 | return a.x*b.x + a.y*b.y 76 | end 77 | end 78 | 79 | function vector.__div(a,b) 80 | assert(isvector(a) and type(b) == "number", "wrong argument types (expected / )") 81 | return new(a.x / b, a.y / b) 82 | end 83 | 84 | function vector.__eq(a,b) 85 | return a.x == b.x and a.y == b.y 86 | end 87 | 88 | function vector.__lt(a,b) 89 | return a.x < b.x or (a.x == b.x and a.y < b.y) 90 | end 91 | 92 | function vector.__le(a,b) 93 | return a.x <= b.x and a.y <= b.y 94 | end 95 | 96 | function vector.permul(a,b) 97 | assert(isvector(a) and isvector(b), "permul: wrong argument types ( expected)") 98 | return new(a.x*b.x, a.y*b.y) 99 | end 100 | 101 | function vector:len2() 102 | return self.x * self.x + self.y * self.y 103 | end 104 | 105 | function vector:len() 106 | return sqrt(self.x * self.x + self.y * self.y) 107 | end 108 | 109 | function vector.dist(a, b) 110 | assert(isvector(a) and isvector(b), "dist: wrong argument types ( expected)") 111 | local dx = a.x - b.x 112 | local dy = a.y - b.y 113 | return sqrt(dx * dx + dy * dy) 114 | end 115 | 116 | function vector.dist2(a, b) 117 | assert(isvector(a) and isvector(b), "dist: wrong argument types ( expected)") 118 | local dx = a.x - b.x 119 | local dy = a.y - b.y 120 | return (dx * dx + dy * dy) 121 | end 122 | 123 | function vector:normalizeInplace() 124 | local l = self:len() 125 | if l > 0 then 126 | self.x, self.y = self.x / l, self.y / l 127 | end 128 | return self 129 | end 130 | 131 | function vector:normalized() 132 | return self:clone():normalizeInplace() 133 | end 134 | 135 | function vector:rotateInplace(phi) 136 | local c, s = cos(phi), sin(phi) 137 | self.x, self.y = c * self.x - s * self.y, s * self.x + c * self.y 138 | return self 139 | end 140 | 141 | function vector:rotated(phi) 142 | local c, s = cos(phi), sin(phi) 143 | return new(c * self.x - s * self.y, s * self.x + c * self.y) 144 | end 145 | 146 | function vector:perpendicular() 147 | return new(-self.y, self.x) 148 | end 149 | 150 | function vector:projectOn(v) 151 | assert(isvector(v), "invalid argument: cannot project vector on " .. type(v)) 152 | -- (self * v) * v / v:len2() 153 | local s = (self.x * v.x + self.y * v.y) / (v.x * v.x + v.y * v.y) 154 | return new(s * v.x, s * v.y) 155 | end 156 | 157 | function vector:mirrorOn(v) 158 | assert(isvector(v), "invalid argument: cannot mirror vector on " .. type(v)) 159 | -- 2 * self:projectOn(v) - self 160 | local s = 2 * (self.x * v.x + self.y * v.y) / (v.x * v.x + v.y * v.y) 161 | return new(s * v.x - self.x, s * v.y - self.y) 162 | end 163 | 164 | function vector:cross(v) 165 | assert(isvector(v), "cross: wrong argument types ( expected)") 166 | return self.x * v.y - self.y * v.x 167 | end 168 | 169 | -- ref.: http://blog.signalsondisplay.com/?p=336 170 | function vector:trimInplace(maxLen) 171 | local s = maxLen * maxLen / self:len2() 172 | s = (s > 1 and 1) or math.sqrt(s) 173 | self.x, self.y = self.x * s, self.y * s 174 | return self 175 | end 176 | 177 | function vector:angleTo(other) 178 | if other then 179 | return atan2(self.y, self.x) - atan2(other.y, other.x) 180 | end 181 | return atan2(self.y, self.x) 182 | end 183 | 184 | function vector:trimmed(maxLen) 185 | return self:clone():trimInplace(maxLen) 186 | end 187 | 188 | 189 | -- the module 190 | return setmetatable({new = new, isvector = isvector, zero = zero}, 191 | {__call = function(_, ...) return new(...) end}) 192 | -------------------------------------------------------------------------------- /libs/camera.lua: -------------------------------------------------------------------------------- 1 | --[[ 2 | Copyright (c) 2010-2015 Matthias Richter 3 | 4 | Permission is hereby granted, free of charge, to any person obtaining a copy 5 | of this software and associated documentation files (the "Software"), to deal 6 | in the Software without restriction, including without limitation the rights 7 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 8 | copies of the Software, and to permit persons to whom the Software is 9 | furnished to do so, subject to the following conditions: 10 | 11 | The above copyright notice and this permission notice shall be included in 12 | all copies or substantial portions of the Software. 13 | 14 | Except as contained in this notice, the name(s) of the above copyright holders 15 | shall not be used in advertising or otherwise to promote the sale, use or 16 | other dealings in this Software without prior written authorization. 17 | 18 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 19 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 20 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 21 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 22 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 23 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 24 | THE SOFTWARE. 25 | ]]-- 26 | 27 | local _PATH = (...):match('^(.*[%./])[^%.%/]+$') or '' 28 | local cos, sin = math.cos, math.sin 29 | 30 | local camera = {} 31 | camera.__index = camera 32 | 33 | -- Movement interpolators (for camera locking/windowing) 34 | camera.smooth = {} 35 | 36 | function camera.smooth.none() 37 | return function(dx,dy) return dx,dy end 38 | end 39 | 40 | function camera.smooth.linear(speed) 41 | assert(type(speed) == "number", "Invalid parameter: speed = "..tostring(speed)) 42 | return function(dx,dy, s) 43 | -- normalize direction 44 | local d = math.sqrt(dx*dx+dy*dy) 45 | local dts = math.min((s or speed) * love.timer.getDelta(), d) -- prevent overshooting the goal 46 | if d > 0 then 47 | dx,dy = dx/d, dy/d 48 | end 49 | 50 | return dx*dts, dy*dts 51 | end 52 | end 53 | 54 | function camera.smooth.damped(stiffness) 55 | assert(type(stiffness) == "number", "Invalid parameter: stiffness = "..tostring(stiffness)) 56 | return function(dx,dy, s) 57 | local dts = love.timer.getDelta() * (s or stiffness) 58 | return dx*dts, dy*dts 59 | end 60 | end 61 | 62 | 63 | local function new(x,y, zoom, rot, smoother) 64 | x,y = x or love.graphics.getWidth()/2, y or love.graphics.getHeight()/2 65 | zoom = zoom or 1 66 | rot = rot or 0 67 | smoother = smoother or camera.smooth.none() -- for locking, see below 68 | return setmetatable({x = x, y = y, scale = zoom, rot = rot, smoother = smoother}, camera) 69 | end 70 | 71 | function camera:lookAt(x,y) 72 | self.x, self.y = x, y 73 | return self 74 | end 75 | 76 | function camera:move(dx,dy) 77 | self.x, self.y = self.x + dx, self.y + dy 78 | return self 79 | end 80 | 81 | function camera:position() 82 | return self.x, self.y 83 | end 84 | 85 | function camera:rotate(phi) 86 | self.rot = self.rot + phi 87 | return self 88 | end 89 | 90 | function camera:rotateTo(phi) 91 | self.rot = phi 92 | return self 93 | end 94 | 95 | function camera:zoom(mul) 96 | self.scale = self.scale * mul 97 | return self 98 | end 99 | 100 | function camera:zoomTo(zoom) 101 | self.scale = zoom 102 | return self 103 | end 104 | 105 | function camera:attach(x,y,w,h, noclip) 106 | x,y = x or 0, y or 0 107 | w,h = w or love.graphics.getWidth(), h or love.graphics.getHeight() 108 | 109 | self._sx,self._sy,self._sw,self._sh = love.graphics.getScissor() 110 | if not noclip then 111 | love.graphics.setScissor(x,y,w,h) 112 | end 113 | 114 | local cx,cy = x+w/2, y+h/2 115 | love.graphics.push() 116 | love.graphics.translate(cx, cy) 117 | love.graphics.scale(self.scale) 118 | love.graphics.rotate(self.rot) 119 | love.graphics.translate(-self.x, -self.y) 120 | end 121 | 122 | function camera:detach() 123 | love.graphics.pop() 124 | love.graphics.setScissor(self._sx,self._sy,self._sw,self._sh) 125 | end 126 | 127 | function camera:draw(...) 128 | local x,y,w,h,noclip,func 129 | local nargs = select("#", ...) 130 | if nargs == 1 then 131 | func = ... 132 | elseif nargs == 5 then 133 | x,y,w,h,func = ... 134 | elseif nargs == 6 then 135 | x,y,w,h,noclip,func = ... 136 | else 137 | error("Invalid arguments to camera:draw()") 138 | end 139 | 140 | self:attach(x,y,w,h,noclip) 141 | func() 142 | self:detach() 143 | end 144 | 145 | -- world coordinates to camera coordinates 146 | function camera:cameraCoords(x,y, ox,oy,w,h) 147 | ox, oy = ox or 0, oy or 0 148 | w,h = w or love.graphics.getWidth(), h or love.graphics.getHeight() 149 | 150 | -- x,y = ((x,y) - (self.x, self.y)):rotated(self.rot) * self.scale + center 151 | local c,s = cos(self.rot), sin(self.rot) 152 | x,y = x - self.x, y - self.y 153 | x,y = c*x - s*y, s*x + c*y 154 | return x*self.scale + w/2 + ox, y*self.scale + h/2 + oy 155 | end 156 | 157 | -- camera coordinates to world coordinates 158 | function camera:worldCoords(x,y, ox,oy,w,h) 159 | ox, oy = ox or 0, oy or 0 160 | w,h = w or love.graphics.getWidth(), h or love.graphics.getHeight() 161 | 162 | -- x,y = (((x,y) - center) / self.scale):rotated(-self.rot) + (self.x,self.y) 163 | local c,s = cos(-self.rot), sin(-self.rot) 164 | x,y = (x - w/2 - ox) / self.scale, (y - h/2 - oy) / self.scale 165 | x,y = c*x - s*y, s*x + c*y 166 | return x+self.x, y+self.y 167 | end 168 | 169 | function camera:mousePosition(ox,oy,w,h) 170 | local mx,my = love.mouse.getPosition() 171 | return self:worldCoords(mx,my, ox,oy,w,h) 172 | end 173 | 174 | -- camera scrolling utilities 175 | function camera:lockX(x, smoother, ...) 176 | local dx, dy = (smoother or self.smoother)(x - self.x, self.y, ...) 177 | self.x = self.x + dx 178 | return self 179 | end 180 | 181 | function camera:lockY(y, smoother, ...) 182 | local dx, dy = (smoother or self.smoother)(self.x, y - self.y, ...) 183 | self.y = self.y + dy 184 | return self 185 | end 186 | 187 | function camera:lockPosition(x,y, smoother, ...) 188 | return self:move((smoother or self.smoother)(x - self.x, y - self.y, ...)) 189 | end 190 | 191 | function camera:lockWindow(x, y, x_min, x_max, y_min, y_max, smoother, ...) 192 | -- figure out displacement in camera coordinates 193 | x,y = self:cameraCoords(x,y) 194 | local dx, dy = 0,0 195 | if x < x_min then 196 | dx = x - x_min 197 | elseif x > x_max then 198 | dx = x - x_max 199 | end 200 | if y < y_min then 201 | dy = y - y_min 202 | elseif y > y_max then 203 | dy = y - y_max 204 | end 205 | 206 | -- transform displacement to movement in world coordinates 207 | local c,s = cos(-self.rot), sin(-self.rot) 208 | dx,dy = (c*dx - s*dy) / self.scale, (s*dx + c*dy) / self.scale 209 | 210 | -- move 211 | self:move((smoother or self.smoother)(dx,dy,...)) 212 | end 213 | 214 | -- the module 215 | return setmetatable({new = new, smooth = camera.smooth}, 216 | {__call = function(_, ...) return new(...) end}) 217 | -------------------------------------------------------------------------------- /libs/timer.lua: -------------------------------------------------------------------------------- 1 | --[[ 2 | Copyright (c) 2010-2013 Matthias Richter 3 | 4 | Permission is hereby granted, free of charge, to any person obtaining a copy 5 | of this software and associated documentation files (the "Software"), to deal 6 | in the Software without restriction, including without limitation the rights 7 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 8 | copies of the Software, and to permit persons to whom the Software is 9 | furnished to do so, subject to the following conditions: 10 | 11 | The above copyright notice and this permission notice shall be included in 12 | all copies or substantial portions of the Software. 13 | 14 | Except as contained in this notice, the name(s) of the above copyright holders 15 | shall not be used in advertising or otherwise to promote the sale, use or 16 | other dealings in this Software without prior written authorization. 17 | 18 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 19 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 20 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 21 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 22 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 23 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 24 | THE SOFTWARE. 25 | ]]-- 26 | 27 | local Timer = {} 28 | Timer.__index = Timer 29 | 30 | local function _nothing_() end 31 | 32 | function Timer:update(dt) 33 | local to_remove = {} 34 | 35 | for handle in pairs(self.functions) do 36 | -- handle: { 37 | -- time = , 38 | -- after = , 39 | -- during = , 40 | -- limit = , 41 | -- count = , 42 | -- } 43 | 44 | handle.time = handle.time + dt 45 | handle.during(dt, math.max(handle.limit - handle.time, 0)) 46 | 47 | while handle.time >= handle.limit and handle.count > 0 do 48 | if handle.after(handle.after) == false then 49 | handle.count = 0 50 | break 51 | end 52 | handle.time = handle.time - handle.limit 53 | handle.count = handle.count - 1 54 | end 55 | 56 | if handle.count == 0 then 57 | table.insert(to_remove, handle) 58 | end 59 | end 60 | 61 | for i = 1, #to_remove do 62 | self.functions[to_remove[i]] = nil 63 | end 64 | end 65 | 66 | function Timer:during(delay, during, after) 67 | local handle = { time = 0, during = during, after = after or _nothing_, limit = delay, count = 1 } 68 | self.functions[handle] = true 69 | return handle 70 | end 71 | 72 | function Timer:after(delay, func) 73 | return self:during(delay, _nothing_, func) 74 | end 75 | 76 | function Timer:every(delay, after, count) 77 | local count = count or math.huge -- exploit below: math.huge - 1 = math.huge 78 | local handle = { time = 0, during = _nothing_, after = after, limit = delay, count = count } 79 | self.functions[handle] = true 80 | return handle 81 | end 82 | 83 | function Timer:cancel(handle) 84 | self.functions[handle] = nil 85 | end 86 | 87 | function Timer:clear() 88 | self.functions = {} 89 | end 90 | 91 | function Timer:script(f) 92 | local co = coroutine.wrap(f) 93 | co(function(t) 94 | self:after(t, co) 95 | coroutine.yield() 96 | end) 97 | end 98 | 99 | Timer.tween = setmetatable({ 100 | -- helper functions 101 | out = function(f) -- 'rotates' a function 102 | return function(s, ...) return 1 - f(1-s, ...) end 103 | end, 104 | chain = function(f1, f2) -- concatenates two functions 105 | return function(s, ...) return (s < .5 and f1(2*s, ...) or 1 + f2(2*s-1, ...)) * .5 end 106 | end, 107 | 108 | -- useful tweening functions 109 | linear = function(s) return s end, 110 | quad = function(s) return s*s end, 111 | cubic = function(s) return s*s*s end, 112 | quart = function(s) return s*s*s*s end, 113 | quint = function(s) return s*s*s*s*s end, 114 | sine = function(s) return 1-math.cos(s*math.pi/2) end, 115 | expo = function(s) return 2^(10*(s-1)) end, 116 | circ = function(s) return 1 - math.sqrt(1-s*s) end, 117 | 118 | back = function(s,bounciness) 119 | bounciness = bounciness or 1.70158 120 | return s*s*((bounciness+1)*s - bounciness) 121 | end, 122 | 123 | bounce = function(s) -- magic numbers ahead 124 | local a,b = 7.5625, 1/2.75 125 | return math.min(a*s^2, a*(s-1.5*b)^2 + .75, a*(s-2.25*b)^2 + .9375, a*(s-2.625*b)^2 + .984375) 126 | end, 127 | 128 | elastic = function(s, amp, period) 129 | amp, period = amp and math.max(1, amp) or 1, period or .3 130 | return (-amp * math.sin(2*math.pi/period * (s-1) - math.asin(1/amp))) * 2^(10*(s-1)) 131 | end, 132 | }, { 133 | 134 | -- register new tween 135 | __call = function(tween, self, len, subject, target, method, after, ...) 136 | -- recursively collects fields that are defined in both subject and target into a flat list 137 | local function tween_collect_payload(subject, target, out) 138 | for k,v in pairs(target) do 139 | local ref = subject[k] 140 | assert(type(v) == type(ref), 'Type mismatch in field "'..k..'".') 141 | if type(v) == 'table' then 142 | tween_collect_payload(ref, v, out) 143 | else 144 | local ok, delta = pcall(function() return (v-ref)*1 end) 145 | assert(ok, 'Field "'..k..'" does not support arithmetic operations') 146 | out[#out+1] = {subject, k, delta} 147 | end 148 | end 149 | return out 150 | end 151 | 152 | method = tween[method or 'linear'] -- see __index 153 | local payload, t, args = tween_collect_payload(subject, target, {}), 0, {...} 154 | 155 | local last_s = 0 156 | return self:during(len, function(dt) 157 | t = t + dt 158 | local s = method(math.min(1, t/len), unpack(args)) 159 | local ds = s - last_s 160 | last_s = s 161 | for _, info in ipairs(payload) do 162 | local ref, key, delta = unpack(info) 163 | ref[key] = ref[key] + delta * ds 164 | end 165 | end, after) 166 | end, 167 | 168 | -- fetches function and generated compositions for method `key` 169 | __index = function(tweens, key) 170 | if type(key) == 'function' then return key end 171 | 172 | assert(type(key) == 'string', 'Method must be function or string.') 173 | if rawget(tweens, key) then return rawget(tweens, key) end 174 | 175 | local function construct(pattern, f) 176 | local method = rawget(tweens, key:match(pattern)) 177 | if method then return f(method) end 178 | return nil 179 | end 180 | 181 | local out, chain = rawget(tweens,'out'), rawget(tweens,'chain') 182 | return construct('^in%-([^-]+)$', function(...) return ... end) 183 | or construct('^out%-([^-]+)$', out) 184 | or construct('^in%-out%-([^-]+)$', function(f) return chain(f, out(f)) end) 185 | or construct('^out%-in%-([^-]+)$', function(f) return chain(out(f), f) end) 186 | or error('Unknown interpolation method: ' .. key) 187 | end}) 188 | 189 | -- Timer instancing 190 | function Timer.new() 191 | return setmetatable({functions = {}, tween = Timer.tween}, Timer) 192 | end 193 | 194 | -- default instance 195 | local default = Timer.new() 196 | 197 | -- module forwards calls to default instance 198 | local module = {} 199 | for k in pairs(Timer) do 200 | if k ~= "__index" then 201 | module[k] = function(...) return default[k](default, ...) end 202 | end 203 | end 204 | module.tween = setmetatable({}, { 205 | __index = Timer.tween, 206 | __newindex = function(k,v) Timer.tween[k] = v end, 207 | __call = function(t, ...) return default:tween(...) end, 208 | }) 209 | 210 | return setmetatable(module, {__call = Timer.new}) 211 | -------------------------------------------------------------------------------- /libs/husl.lua: -------------------------------------------------------------------------------- 1 | -- Version: 1.0.0 2 | 3 | local m = { 4 | { 3.240454162114103, -1.537138512797715, -0.49853140955601}, 5 | {-0.96926603050518, 1.876010845446694, 0.041556017530349}, 6 | { 0.055643430959114, -0.20402591351675, 1.057225188223179} 7 | } 8 | 9 | local m_inv = { 10 | {0.41245643908969, 0.3575760776439, 0.18043748326639 }, 11 | {0.21267285140562, 0.71515215528781, 0.072174993306559}, 12 | {0.019333895582329, 0.1191920258813, 0.95030407853636 } 13 | } 14 | 15 | -- hard-coded d65 illuminant 16 | local refX = 0.95047 17 | local refY = 1.00000 18 | local refZ = 1.08883 19 | local refU = (4 * refX) / (refX + (15 * refY) + (3 * refZ)) 20 | local refV = (9 * refY) / (refX + (15 * refY) + (3 * refZ)) 21 | 22 | local kappa = 24389 / 27 23 | local epsilon = 216 / 24389 24 | 25 | -- public api 26 | 27 | local husl = {} 28 | 29 | function husl.husl_to_rgb(h, s, l) 30 | return husl.lch_to_rgb(husl.husl_to_lch(h, s, l)) 31 | end 32 | 33 | function husl.husl_to_hex(h, s, l) 34 | return husl.rgb_to_hex(husl.husl_to_rgb(h, s, l)) 35 | end 36 | 37 | function husl.rgb_to_husl(r, g, b) 38 | return husl.lch_to_husl(husl.rgb_to_lch(r, g, b)) 39 | end 40 | 41 | function husl.hex_to_husl(hex) 42 | return husl.rgb_to_husl(husl.hex_to_rgb(hex)) 43 | end 44 | 45 | function husl.huslp_to_rgb(h, s, l) 46 | return husl.lch_to_rgb(husl.huslp_to_lch(h, s, l)) 47 | end 48 | 49 | function husl.huslp_to_hex(h, s, l) 50 | return husl.rgb_to_hex(husl.huslp_to_rgb(h, s, l)) 51 | end 52 | 53 | function husl.rgb_to_huslp(r, g, b) 54 | return husl.lch_to_huslp(husl.rgb_to_lch(r, g, b)) 55 | end 56 | 57 | function husl.hex_to_huslp(hex) 58 | return husl.rgb_to_huslp(husl.hex_to_rgb(hex)) 59 | end 60 | 61 | function husl.lch_to_rgb(l, c, h) 62 | return husl.xyz_to_rgb(husl.luv_to_xyz(husl.lch_to_luv(l, c, h))) 63 | end 64 | 65 | function husl.rgb_to_lch(r, g, b) 66 | return husl.luv_to_lch(husl.xyz_to_luv(husl.rgb_to_xyz(r, g, b))) 67 | end 68 | 69 | function husl.max_chroma(L, H) 70 | local hrad = math.rad(H) 71 | local sinH = math.sin(hrad) 72 | local cosH = math.cos(hrad) 73 | local sub1 = math.pow(L + 16, 3) / 1560896 74 | local sub2 = (sub1 > epsilon) and sub1 or (L / kappa) 75 | 76 | local result = math.huge 77 | 78 | for _, row in ipairs(m) do 79 | local m1, m2, m3 = unpack(row) 80 | 81 | local top = (12739311 * m3 + 11700000 * m2 + 11120499 * m1) * sub2 82 | local rbottom = 9608480 * m3 - 1921696 * m2 83 | local lbottom = 1441272 * m3 - 4323816 * m1 84 | 85 | local bottom = (rbottom * sinH + lbottom * cosH) * sub2 86 | 87 | local C0 = L * top / bottom 88 | local C1 = L * (top - 11700000) / (bottom + 1921696 * sinH) 89 | 90 | if C0 > 0 and C0 < result then 91 | result = C0 92 | end 93 | 94 | if C1 > 0 and C1 < result then 95 | result = C1 96 | end 97 | end 98 | 99 | return result 100 | end 101 | 102 | local function hrad_extremum(L) 103 | local lhs = (math.pow(L, 3) + 48 * math.pow(L, 2) + 768 * L + 4096) / 1560896 104 | local rhs = epsilon 105 | local sub = (lhs > rhs) and lhs or L / kappa 106 | 107 | local chroma = math.huge 108 | local result = nil 109 | 110 | for _, row in ipairs(m) do 111 | local m1, m2, m3 = unpack(row) 112 | local bottom = (3 * m3 - 9 * m1) * sub 113 | 114 | for limit=0,1 do 115 | local top = (20 * m3 - 4 * m2) * sub + 4 * limit 116 | local hrad = math.atan2(top, bottom) 117 | -- This is a math hack to deal with tan quadrants, I'm too lazy to figure 118 | -- out how to do this properly 119 | if limit == 1 then 120 | hrad = hrad + math.pi 121 | end 122 | 123 | local test = husl.max_chroma(L, math.deg(hrad)) 124 | 125 | if test < chroma then 126 | chroma = test 127 | result = hrad 128 | end 129 | end 130 | end 131 | 132 | return result 133 | end 134 | 135 | function husl.max_chroma_pastel(L) 136 | local H = math.deg(hrad_extremum(L)) 137 | return husl.max_chroma(L, H) 138 | end 139 | 140 | local function dot_product(a, b) 141 | local sum = 0 142 | 143 | for i=1,#a do 144 | sum = sum + a[i] * b[i] 145 | end 146 | 147 | return sum 148 | end 149 | 150 | function husl.f(t) 151 | if t > epsilon then 152 | return 116 * math.pow((t / refY), 1 / 3) - 16 153 | else 154 | return t / refY * kappa 155 | end 156 | end 157 | 158 | function husl.f_inv(t) 159 | if t > 8 then 160 | return refY * math.pow((t + 16) / 116, 3) 161 | else 162 | return refY * t / kappa 163 | end 164 | end 165 | 166 | function husl.from_linear(c) 167 | if c <= 0.0031308 then 168 | return 12.92 * c 169 | else 170 | return 1.055 * math.pow(c, 1 / 2.4) - 0.055 171 | end 172 | end 173 | 174 | function husl.to_linear(c) 175 | local a = 0.055 176 | 177 | if c > 0.04045 then 178 | return math.pow((c + a) / (1 + a), 2.4) 179 | else 180 | return c / 12.92 181 | end 182 | end 183 | 184 | local function round(number, digits) 185 | local f = math.pow(10, digits or 0) 186 | 187 | return math.floor(number * f + 0.5) / f 188 | end 189 | 190 | function husl.rgb_prepare(r, g, b) 191 | local prepared = {} 192 | 193 | for i, component in ipairs{r, g, b} do 194 | component = round(component, 3) 195 | 196 | assert(component >= -0.0001 and component <= 1.0001, "illegal rgb value " .. component) 197 | 198 | component = math.min(1, math.max(component, 0)) 199 | 200 | prepared[i] = round(component * 255) 201 | end 202 | 203 | return unpack(prepared) 204 | end 205 | 206 | function husl.hex_to_rgb(hex) 207 | hex = hex:gsub("#", "") 208 | 209 | local r = tonumber(hex:sub(1,2), 16) / 255 210 | local g = tonumber(hex:sub(3,4), 16) / 255 211 | local b = tonumber(hex:sub(5,6), 16) / 255 212 | 213 | return r, g, b 214 | end 215 | 216 | function husl.rgb_to_hex(r, g, b) 217 | return string.format("#%02x%02x%02x", husl.rgb_prepare(r, g, b)) 218 | end 219 | 220 | function husl.xyz_to_rgb(x, y, z) 221 | local rgb = {} 222 | 223 | for i, row in ipairs(m) do 224 | rgb[i] = husl.from_linear(dot_product(row, {x, y, z})) 225 | end 226 | 227 | return unpack(rgb) 228 | end 229 | 230 | function husl.rgb_to_xyz(r, g, b) 231 | local rgb = { 232 | husl.to_linear(r), 233 | husl.to_linear(g), 234 | husl.to_linear(b), 235 | } 236 | 237 | local xyz = {} 238 | 239 | for i, row in ipairs(m_inv) do 240 | xyz[i] = dot_product(row, rgb) 241 | end 242 | 243 | return unpack(xyz) 244 | end 245 | 246 | function husl.xyz_to_luv(X, Y, Z) 247 | if X == 0 and Y == 0 and Z == 0 then 248 | return 0, 0, 0 249 | end 250 | 251 | local varU = (4 * X) / (X + (15 * Y) + (3 * Z)) 252 | local varV = (9 * Y) / (X + (15 * Y) + (3 * Z)) 253 | local L = husl.f(Y) 254 | 255 | -- Black will create a divide-by-zero error 256 | if L == 0.0 then 257 | return 0, 0, 0 258 | end 259 | 260 | local U = 13 * L * (varU - refU) 261 | local V = 13 * L * (varV - refV) 262 | 263 | return L, U, V 264 | end 265 | 266 | function husl.luv_to_xyz(L, U, V) 267 | if L == 0 then 268 | return 0, 0, 0 269 | end 270 | 271 | local varY = husl.f_inv(L) 272 | local varU = U / (13 * L) + refU 273 | local varV = V / (13 * L) + refV 274 | local Y = varY * refY 275 | local X = -(9 * Y * varU) / ((varU - 4) * varV - varU * varV) 276 | local Z = (9 * Y - (15 * varV * Y) - (varV * X)) / (3 * varV) 277 | 278 | return X, Y, Z 279 | end 280 | 281 | function husl.luv_to_lch(L, U, V) 282 | local C = (math.pow(math.pow(U, 2) + math.pow(V, 2), 0.5)) 283 | local hrad = (math.atan2(V, U)) 284 | local H = math.deg(hrad) 285 | 286 | if H < 0 then 287 | H = 360 + H 288 | end 289 | 290 | return L, C, H 291 | end 292 | 293 | function husl.lch_to_luv(L, C, H) 294 | local Hrad = math.rad(H) 295 | local U = math.cos(Hrad) * C 296 | local V = math.sin(Hrad) * C 297 | 298 | return L, U, V 299 | end 300 | 301 | function husl.husl_to_lch(H, S, L) 302 | if L > 99.9999999 then 303 | return 100, 0, H 304 | elseif L < 0.00000001 then 305 | return 0, 0, H 306 | end 307 | 308 | local mx = husl.max_chroma(L, H) 309 | local C = mx / 100 * S 310 | 311 | return L, C, H 312 | end 313 | 314 | function husl.lch_to_husl(L, C, H) 315 | if L > 99.9999999 then 316 | return H, 0, 100 317 | elseif L < 0.00000001 then 318 | return H, 0, 0 319 | end 320 | 321 | local mx = husl.max_chroma(L, H) 322 | local S = C / mx * 100 323 | 324 | return H, S, L 325 | end 326 | 327 | function husl.huslp_to_lch(H, S, L) 328 | if L > 99.9999999 then 329 | return 100, 0, H 330 | elseif L < 0.00000001 then 331 | return 0, 0, H 332 | end 333 | 334 | local mx = husl.max_chroma_pastel(L) 335 | local C = mx / 100 * S 336 | 337 | return L, C, H 338 | end 339 | 340 | function husl.lch_to_huslp(L, C, H) 341 | if L > 99.9999999 then 342 | return H, 0, 100 343 | elseif L < 0.00000001 then 344 | return H, 0, 0 345 | end 346 | 347 | local mx = husl.max_chroma_pastel(L) 348 | local S = C / mx * 100 349 | 350 | return H, S, L 351 | end 352 | 353 | return husl 354 | -------------------------------------------------------------------------------- /main.lua: -------------------------------------------------------------------------------- 1 | local loadTimeStart = love.timer.getTime() 2 | 3 | require 'globals' 4 | 5 | function love.load() 6 | love.window.setIcon(love.image.newImageData(CONFIG.window.icon)) 7 | love.graphics.setDefaultFilter(CONFIG.graphics.filter.down, 8 | CONFIG.graphics.filter.up, 9 | CONFIG.graphics.filter.anisotropy) 10 | 11 | -- Draw is left out so we can override it ourselves 12 | local callbacks = {'update'} 13 | for k in pairs(love.handlers) do 14 | callbacks[#callbacks+1] = k 15 | end 16 | 17 | State.registerEvents(callbacks) 18 | State.switch(States.game) 19 | 20 | if DEBUG then 21 | local loadTimeEnd = love.timer.getTime() 22 | local loadTime = (loadTimeEnd - loadTimeStart) 23 | print(("Loaded game in %.3f seconds."):format(loadTime)) 24 | end 25 | end 26 | 27 | function love.update(dt) 28 | 29 | end 30 | 31 | function love.draw() 32 | local drawTimeStart = love.timer.getTime() 33 | State.current():draw() 34 | local drawTimeEnd = love.timer.getTime() 35 | local drawTime = drawTimeEnd - drawTimeStart 36 | 37 | if DEBUG then 38 | love.graphics.push() 39 | local x, y = CONFIG.debug.stats.position.x, CONFIG.debug.stats.position.y 40 | local dy = CONFIG.debug.stats.lineHeight 41 | local stats = love.graphics.getStats() 42 | local memoryUnit = "KB" 43 | local ram = collectgarbage("count") 44 | local vram = stats.texturememory / 1024 45 | if not CONFIG.debug.stats.kilobytes then 46 | ram = ram / 1024 47 | vram = vram / 1024 48 | memoryUnit = "MB" 49 | end 50 | local info = { 51 | "FPS: " .. ("%3d"):format(love.timer.getFPS()), 52 | "DRAW: " .. ("%7.3fms"):format(Lume.round(drawTime * 1000, .001)), 53 | "RAM: " .. string.format("%7.2f", Lume.round(ram, .01)) .. memoryUnit, 54 | "VRAM: " .. string.format("%6.2f", Lume.round(vram, .01)) .. memoryUnit, 55 | "Draw calls: " .. stats.drawcalls, 56 | "Images: " .. stats.images, 57 | "Canvases: " .. stats.canvases, 58 | "\tSwitches: " .. stats.canvasswitches, 59 | "Shader switches: " .. stats.shaderswitches, 60 | "Fonts: " .. stats.fonts, 61 | } 62 | love.graphics.setFont(CONFIG.debug.stats.font[CONFIG.debug.stats.fontSize]) 63 | for i, text in ipairs(info) do 64 | local sx, sy = CONFIG.debug.stats.shadowOffset.x, CONFIG.debug.stats.shadowOffset.y 65 | love.graphics.setColor(CONFIG.debug.stats.shadow) 66 | love.graphics.print(text, x + sx, y + sy + (i-1)*dy) 67 | love.graphics.setColor(CONFIG.debug.stats.foreground) 68 | love.graphics.print(text, x, y + (i-1)*dy) 69 | end 70 | love.graphics.pop() 71 | end 72 | end 73 | 74 | function love.keypressed(key, code, isRepeat) 75 | if not RELEASE and code == CONFIG.debug.key then 76 | DEBUG = not DEBUG 77 | end 78 | end 79 | 80 | function love.threaderror(thread, errorMessage) 81 | print("Thread error!\n" .. errorMessage) 82 | end 83 | 84 | ----------------------------------------------------------- 85 | -- Error screen. 86 | ----------------------------------------------------------- 87 | 88 | local utf8 = require("utf8") 89 | 90 | local function error_printer(msg, layer) 91 | print((debug.traceback("Error: " .. tostring(msg), 1+(layer or 1)):gsub("\n[^\n]+$", ""))) 92 | end 93 | 94 | function love.errorhandler(msg) 95 | msg = tostring(msg) 96 | 97 | error_printer(msg, 2) 98 | 99 | if not love.window or not love.graphics or not love.event then 100 | return 101 | end 102 | 103 | if not love.graphics.isCreated() or not love.window.isOpen() then 104 | local success, status = pcall(love.window.setMode, 800, 600) 105 | if not success or not status then 106 | return 107 | end 108 | end 109 | 110 | -- Reset state. 111 | if love.mouse then 112 | love.mouse.setVisible(true) 113 | love.mouse.setGrabbed(false) 114 | love.mouse.setRelativeMode(false) 115 | if love.mouse.isCursorSupported() then 116 | love.mouse.setCursor() 117 | end 118 | end 119 | if love.joystick then 120 | -- Stop all joystick vibrations. 121 | for i,v in ipairs(love.joystick.getJoysticks()) do 122 | v:setVibration() 123 | end 124 | end 125 | if love.audio then love.audio.stop() end 126 | 127 | love.graphics.reset() 128 | local size = math.floor(love.window.toPixels(CONFIG.debug.error.fontSize)) 129 | local font = CONFIG.debug.error.font[size] 130 | love.graphics.setFont(font) 131 | 132 | love.graphics.setColor(CONFIG.debug.error.foreground) 133 | 134 | local trace = debug.traceback() 135 | 136 | love.graphics.origin() 137 | 138 | local sanitizedmsg = {} 139 | for char in msg:gmatch(utf8.charpattern) do 140 | table.insert(sanitizedmsg, char) 141 | end 142 | sanitizedmsg = table.concat(sanitizedmsg) 143 | 144 | local err = {} 145 | 146 | table.insert(err, "Error\n") 147 | table.insert(err, sanitizedmsg) 148 | 149 | if #sanitizedmsg ~= #msg then 150 | table.insert(err, "Invalid UTF-8 string in error message.") 151 | end 152 | 153 | table.insert(err, "\n") 154 | 155 | for l in trace:gmatch("(.-)\n") do 156 | if not l:match("boot.lua") then 157 | l = l:gsub("stack traceback:", "Traceback\n") 158 | table.insert(err, l) 159 | end 160 | end 161 | 162 | local p = table.concat(err, "\n") 163 | 164 | p = p:gsub("\t", "") 165 | p = p:gsub("%[string \"(.-)\"%]", "%1") 166 | 167 | local max_text_width = love.graphics.getWidth() - love.window.toPixels(CONFIG.debug.error.position.x) 168 | local _, lines = font:getWrap(p, max_text_width) 169 | local max_translate_y = (#lines + 8) * font:getHeight() 170 | local translate_y = 0 171 | local translate_vy = 0 172 | local translate_ay = 0 173 | 174 | local function draw() 175 | local x, y = love.window.toPixels(CONFIG.debug.error.position.x), love.window.toPixels(CONFIG.debug.error.position.y) 176 | love.graphics.clear(CONFIG.debug.error.background) 177 | local sx, sy = CONFIG.debug.error.shadowOffset.x, CONFIG.debug.error.shadowOffset.y 178 | love.graphics.setColor(CONFIG.debug.error.shadow) 179 | love.graphics.printf(p, x + sx, y + sy, love.graphics.getWidth() - x) 180 | love.graphics.setColor(CONFIG.debug.error.foreground) 181 | love.graphics.printf(p, x, y, love.graphics.getWidth() - x) 182 | 183 | if math.abs(translate_vy) > 4 then 184 | love.graphics.push() 185 | love.graphics.translate(0, -translate_y) 186 | local bar_width = 8 187 | local bar_height = love.graphics.getHeight()^2 / max_translate_y 188 | local bar_y = -translate_y / max_translate_y * love.graphics.getHeight() 189 | love.graphics.setColor(1, 1, 1, 0.5) 190 | love.graphics.rectangle('fill', love.graphics.getWidth() - bar_width - 1, bar_y, bar_width, bar_height, 2, 2) 191 | love.graphics.pop() 192 | else 193 | love.graphics.push() 194 | love.graphics.translate(0, -translate_y) 195 | local bar_width = 8 196 | local bar_height = love.graphics.getHeight()^2 / max_translate_y 197 | local bar_y = -translate_y / max_translate_y * love.graphics.getHeight() 198 | love.graphics.setColor(1, 1, 1, Lume.clamp(math.abs(translate_vy)/4, 0, 0.5)) 199 | love.graphics.rectangle('fill', love.graphics.getWidth() - bar_width - 1, bar_y, bar_width, bar_height, 2, 2) 200 | love.graphics.pop() 201 | end 202 | 203 | love.graphics.present() 204 | end 205 | 206 | local fullErrorText = p 207 | local function copyToClipboard() 208 | if not love.system then return end 209 | love.system.setClipboardText(fullErrorText) 210 | p = p .. "\nCopied to clipboard!" 211 | draw() 212 | end 213 | 214 | if love.system then 215 | p = p .. "\n\nPress Ctrl+C or tap to copy this error" 216 | end 217 | 218 | local is_clicked = false 219 | 220 | return function() 221 | love.event.pump() 222 | 223 | for e, a, b, c, d in love.event.poll() do 224 | if e == "quit" then 225 | return 1 226 | elseif e == "keypressed" and a == "escape" then 227 | return 1 228 | elseif e == "keypressed" and a == "c" and love.keyboard.isDown("lctrl", "rctrl") then 229 | copyToClipboard() 230 | elseif e == "mousepressed" and c == 1 then 231 | is_clicked = true 232 | elseif e == "mousereleased" and c == 1 then 233 | is_clicked = false 234 | elseif e == "touchpressed" then 235 | local name = love.window.getTitle() 236 | if #name == 0 or name == "Untitled" then name = "Game" end 237 | local buttons = {"OK", "Cancel"} 238 | if love.system then 239 | buttons[3] = "Copy to clipboard" 240 | end 241 | local pressed = love.window.showMessageBox("Quit "..name.."?", "", buttons) 242 | if pressed == 1 then 243 | return 1 244 | elseif pressed == 3 then 245 | copyToClipboard() 246 | end 247 | elseif e == "mousemoved" and is_clicked then 248 | translate_ay = d * 100 * math.sqrt((1+ math.abs(translate_vy))) 249 | elseif e == "wheelmoved" then 250 | translate_ay = b * 2000 * math.sqrt((1+ math.abs(translate_vy))) 251 | end 252 | end 253 | 254 | if max_translate_y > love.graphics.getHeight() then 255 | local dt = 0.016 256 | translate_vy = translate_vy + translate_ay * dt 257 | translate_vy = translate_vy * 0.9 258 | translate_y = translate_y + translate_vy * dt 259 | if translate_y > 0 then 260 | translate_y = 0 261 | end 262 | local max_scroll_y = -max_translate_y + love.graphics.getHeight() 263 | if translate_y < max_scroll_y then 264 | translate_y = max_scroll_y 265 | end 266 | translate_ay = 0 267 | end 268 | 269 | love.graphics.translate(0, translate_y) 270 | draw() 271 | love.graphics.origin() 272 | 273 | if love.timer then 274 | love.timer.sleep(0.016) 275 | end 276 | end 277 | end 278 | -------------------------------------------------------------------------------- /libs/inspect.lua: -------------------------------------------------------------------------------- 1 | local inspect ={ 2 | _VERSION = 'inspect.lua 3.1.0', 3 | _URL = 'http://github.com/kikito/inspect.lua', 4 | _DESCRIPTION = 'human-readable representations of tables', 5 | _LICENSE = [[ 6 | MIT LICENSE 7 | 8 | Copyright (c) 2013 Enrique García Cota 9 | 10 | Permission is hereby granted, free of charge, to any person obtaining a 11 | copy of this software and associated documentation files (the 12 | "Software"), to deal in the Software without restriction, including 13 | without limitation the rights to use, copy, modify, merge, publish, 14 | distribute, sublicense, and/or sell copies of the Software, and to 15 | permit persons to whom the Software is furnished to do so, subject to 16 | the following conditions: 17 | 18 | The above copyright notice and this permission notice shall be included 19 | in all copies or substantial portions of the Software. 20 | 21 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS 22 | OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 23 | MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. 24 | IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY 25 | CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, 26 | TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE 27 | SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 28 | ]] 29 | } 30 | 31 | local tostring = tostring 32 | 33 | inspect.KEY = setmetatable({}, {__tostring = function() return 'inspect.KEY' end}) 34 | inspect.METATABLE = setmetatable({}, {__tostring = function() return 'inspect.METATABLE' end}) 35 | 36 | -- Apostrophizes the string if it has quotes, but not aphostrophes 37 | -- Otherwise, it returns a regular quoted string 38 | local function smartQuote(str) 39 | if str:match('"') and not str:match("'") then 40 | return "'" .. str .. "'" 41 | end 42 | return '"' .. str:gsub('"', '\\"') .. '"' 43 | end 44 | 45 | -- \a => '\\a', \0 => '\\0', 31 => '\31' 46 | local shortControlCharEscapes = { 47 | ["\a"] = "\\a", ["\b"] = "\\b", ["\f"] = "\\f", ["\n"] = "\\n", 48 | ["\r"] = "\\r", ["\t"] = "\\t", ["\v"] = "\\v" 49 | } 50 | local longControlCharEscapes = {} -- \a => nil, \0 => \000, 31 => \031 51 | for i=0, 31 do 52 | local ch = string.char(i) 53 | if not shortControlCharEscapes[ch] then 54 | shortControlCharEscapes[ch] = "\\"..i 55 | longControlCharEscapes[ch] = string.format("\\%03d", i) 56 | end 57 | end 58 | 59 | local function escape(str) 60 | return (str:gsub("\\", "\\\\") 61 | :gsub("(%c)%f[0-9]", longControlCharEscapes) 62 | :gsub("%c", shortControlCharEscapes)) 63 | end 64 | 65 | local function isIdentifier(str) 66 | return type(str) == 'string' and str:match( "^[_%a][_%a%d]*$" ) 67 | end 68 | 69 | local function isSequenceKey(k, sequenceLength) 70 | return type(k) == 'number' 71 | and 1 <= k 72 | and k <= sequenceLength 73 | and math.floor(k) == k 74 | end 75 | 76 | local defaultTypeOrders = { 77 | ['number'] = 1, ['boolean'] = 2, ['string'] = 3, ['table'] = 4, 78 | ['function'] = 5, ['userdata'] = 6, ['thread'] = 7 79 | } 80 | 81 | local function sortKeys(a, b) 82 | local ta, tb = type(a), type(b) 83 | 84 | -- strings and numbers are sorted numerically/alphabetically 85 | if ta == tb and (ta == 'string' or ta == 'number') then return a < b end 86 | 87 | local dta, dtb = defaultTypeOrders[ta], defaultTypeOrders[tb] 88 | -- Two default types are compared according to the defaultTypeOrders table 89 | if dta and dtb then return defaultTypeOrders[ta] < defaultTypeOrders[tb] 90 | elseif dta then return true -- default types before custom ones 91 | elseif dtb then return false -- custom types after default ones 92 | end 93 | 94 | -- custom types are sorted out alphabetically 95 | return ta < tb 96 | end 97 | 98 | -- For implementation reasons, the behavior of rawlen & # is "undefined" when 99 | -- tables aren't pure sequences. So we implement our own # operator. 100 | local function getSequenceLength(t) 101 | local len = 1 102 | local v = rawget(t,len) 103 | while v ~= nil do 104 | len = len + 1 105 | v = rawget(t,len) 106 | end 107 | return len - 1 108 | end 109 | 110 | local function getNonSequentialKeys(t) 111 | local keys = {} 112 | local sequenceLength = getSequenceLength(t) 113 | for k,_ in pairs(t) do 114 | if not isSequenceKey(k, sequenceLength) then table.insert(keys, k) end 115 | end 116 | table.sort(keys, sortKeys) 117 | return keys, sequenceLength 118 | end 119 | 120 | local function getToStringResultSafely(t, mt) 121 | local __tostring = type(mt) == 'table' and rawget(mt, '__tostring') 122 | local str, ok 123 | if type(__tostring) == 'function' then 124 | ok, str = pcall(__tostring, t) 125 | str = ok and str or 'error: ' .. tostring(str) 126 | end 127 | if type(str) == 'string' and #str > 0 then return str end 128 | end 129 | 130 | local function countTableAppearances(t, tableAppearances) 131 | tableAppearances = tableAppearances or {} 132 | 133 | if type(t) == 'table' then 134 | if not tableAppearances[t] then 135 | tableAppearances[t] = 1 136 | for k,v in pairs(t) do 137 | countTableAppearances(k, tableAppearances) 138 | countTableAppearances(v, tableAppearances) 139 | end 140 | countTableAppearances(getmetatable(t), tableAppearances) 141 | else 142 | tableAppearances[t] = tableAppearances[t] + 1 143 | end 144 | end 145 | 146 | return tableAppearances 147 | end 148 | 149 | local copySequence = function(s) 150 | local copy, len = {}, #s 151 | for i=1, len do copy[i] = s[i] end 152 | return copy, len 153 | end 154 | 155 | local function makePath(path, ...) 156 | local keys = {...} 157 | local newPath, len = copySequence(path) 158 | for i=1, #keys do 159 | newPath[len + i] = keys[i] 160 | end 161 | return newPath 162 | end 163 | 164 | local function processRecursive(process, item, path, visited) 165 | 166 | if item == nil then return nil end 167 | if visited[item] then return visited[item] end 168 | 169 | local processed = process(item, path) 170 | if type(processed) == 'table' then 171 | local processedCopy = {} 172 | visited[item] = processedCopy 173 | local processedKey 174 | 175 | for k,v in pairs(processed) do 176 | processedKey = processRecursive(process, k, makePath(path, k, inspect.KEY), visited) 177 | if processedKey ~= nil then 178 | processedCopy[processedKey] = processRecursive(process, v, makePath(path, processedKey), visited) 179 | end 180 | end 181 | 182 | local mt = processRecursive(process, getmetatable(processed), makePath(path, inspect.METATABLE), visited) 183 | setmetatable(processedCopy, mt) 184 | processed = processedCopy 185 | end 186 | return processed 187 | end 188 | 189 | 190 | 191 | ------------------------------------------------------------------- 192 | 193 | local Inspector = {} 194 | local Inspector_mt = {__index = Inspector} 195 | 196 | function Inspector:puts(...) 197 | local args = {...} 198 | local buffer = self.buffer 199 | local len = #buffer 200 | for i=1, #args do 201 | len = len + 1 202 | buffer[len] = args[i] 203 | end 204 | end 205 | 206 | function Inspector:down(f) 207 | self.level = self.level + 1 208 | f() 209 | self.level = self.level - 1 210 | end 211 | 212 | function Inspector:tabify() 213 | self:puts(self.newline, string.rep(self.indent, self.level)) 214 | end 215 | 216 | function Inspector:alreadyVisited(v) 217 | return self.ids[v] ~= nil 218 | end 219 | 220 | function Inspector:getId(v) 221 | local id = self.ids[v] 222 | if not id then 223 | local tv = type(v) 224 | id = (self.maxIds[tv] or 0) + 1 225 | self.maxIds[tv] = id 226 | self.ids[v] = id 227 | end 228 | return tostring(id) 229 | end 230 | 231 | function Inspector:putKey(k) 232 | if isIdentifier(k) then return self:puts(k) end 233 | self:puts("[") 234 | self:putValue(k) 235 | self:puts("]") 236 | end 237 | 238 | function Inspector:putTable(t) 239 | if t == inspect.KEY or t == inspect.METATABLE then 240 | self:puts(tostring(t)) 241 | elseif self:alreadyVisited(t) then 242 | self:puts('') 243 | elseif self.level >= self.depth then 244 | self:puts('{...}') 245 | else 246 | if self.tableAppearances[t] > 1 then self:puts('<', self:getId(t), '>') end 247 | 248 | local nonSequentialKeys, sequenceLength = getNonSequentialKeys(t) 249 | local mt = getmetatable(t) 250 | local toStringResult = getToStringResultSafely(t, mt) 251 | 252 | self:puts('{') 253 | self:down(function() 254 | if toStringResult then 255 | self:puts(' -- ', escape(toStringResult)) 256 | if sequenceLength >= 1 then self:tabify() end 257 | end 258 | 259 | local count = 0 260 | for i=1, sequenceLength do 261 | if count > 0 then self:puts(',') end 262 | self:puts(' ') 263 | self:putValue(t[i]) 264 | count = count + 1 265 | end 266 | 267 | for _,k in ipairs(nonSequentialKeys) do 268 | if count > 0 then self:puts(',') end 269 | self:tabify() 270 | self:putKey(k) 271 | self:puts(' = ') 272 | self:putValue(t[k]) 273 | count = count + 1 274 | end 275 | 276 | if mt then 277 | if count > 0 then self:puts(',') end 278 | self:tabify() 279 | self:puts(' = ') 280 | self:putValue(mt) 281 | end 282 | end) 283 | 284 | if #nonSequentialKeys > 0 or mt then -- result is multi-lined. Justify closing } 285 | self:tabify() 286 | elseif sequenceLength > 0 then -- array tables have one extra space before closing } 287 | self:puts(' ') 288 | end 289 | 290 | self:puts('}') 291 | end 292 | end 293 | 294 | function Inspector:putValue(v) 295 | local tv = type(v) 296 | 297 | if tv == 'string' then 298 | self:puts(smartQuote(escape(v))) 299 | elseif tv == 'number' or tv == 'boolean' or tv == 'nil' then 300 | self:puts(tostring(v)) 301 | elseif tv == 'table' then 302 | self:putTable(v) 303 | else 304 | self:puts('<',tv,' ',self:getId(v),'>') 305 | end 306 | end 307 | 308 | ------------------------------------------------------------------- 309 | 310 | function inspect.inspect(root, options) 311 | options = options or {} 312 | 313 | local depth = options.depth or math.huge 314 | local newline = options.newline or '\n' 315 | local indent = options.indent or ' ' 316 | local process = options.process 317 | 318 | if process then 319 | root = processRecursive(process, root, {}, {}) 320 | end 321 | 322 | local inspector = setmetatable({ 323 | depth = depth, 324 | level = 0, 325 | buffer = {}, 326 | ids = {}, 327 | maxIds = {}, 328 | newline = newline, 329 | indent = indent, 330 | tableAppearances = countTableAppearances(root) 331 | }, Inspector_mt) 332 | 333 | inspector:putValue(root) 334 | 335 | return table.concat(inspector.buffer) 336 | end 337 | 338 | setmetatable(inspect, { __call = function(_, ...) return inspect.inspect(...) end }) 339 | 340 | return inspect 341 | 342 | -------------------------------------------------------------------------------- /assets/fonts/LICENSE.txt: -------------------------------------------------------------------------------- 1 | 2 | Apache License 3 | Version 2.0, January 2004 4 | http://www.apache.org/licenses/ 5 | 6 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 7 | 8 | 1. Definitions. 9 | 10 | "License" shall mean the terms and conditions for use, reproduction, 11 | and distribution as defined by Sections 1 through 9 of this document. 12 | 13 | "Licensor" shall mean the copyright owner or entity authorized by 14 | the copyright owner that is granting the License. 15 | 16 | "Legal Entity" shall mean the union of the acting entity and all 17 | other entities that control, are controlled by, or are under common 18 | control with that entity. For the purposes of this definition, 19 | "control" means (i) the power, direct or indirect, to cause the 20 | direction or management of such entity, whether by contract or 21 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 22 | outstanding shares, or (iii) beneficial ownership of such entity. 23 | 24 | "You" (or "Your") shall mean an individual or Legal Entity 25 | exercising permissions granted by this License. 26 | 27 | "Source" form shall mean the preferred form for making modifications, 28 | including but not limited to software source code, documentation 29 | source, and configuration files. 30 | 31 | "Object" form shall mean any form resulting from mechanical 32 | transformation or translation of a Source form, including but 33 | not limited to compiled object code, generated documentation, 34 | and conversions to other media types. 35 | 36 | "Work" shall mean the work of authorship, whether in Source or 37 | Object form, made available under the License, as indicated by a 38 | copyright notice that is included in or attached to the work 39 | (an example is provided in the Appendix below). 40 | 41 | "Derivative Works" shall mean any work, whether in Source or Object 42 | form, that is based on (or derived from) the Work and for which the 43 | editorial revisions, annotations, elaborations, or other modifications 44 | represent, as a whole, an original work of authorship. For the purposes 45 | of this License, Derivative Works shall not include works that remain 46 | separable from, or merely link (or bind by name) to the interfaces of, 47 | the Work and Derivative Works thereof. 48 | 49 | "Contribution" shall mean any work of authorship, including 50 | the original version of the Work and any modifications or additions 51 | to that Work or Derivative Works thereof, that is intentionally 52 | submitted to Licensor for inclusion in the Work by the copyright owner 53 | or by an individual or Legal Entity authorized to submit on behalf of 54 | the copyright owner. For the purposes of this definition, "submitted" 55 | means any form of electronic, verbal, or written communication sent 56 | to the Licensor or its representatives, including but not limited to 57 | communication on electronic mailing lists, source code control systems, 58 | and issue tracking systems that are managed by, or on behalf of, the 59 | Licensor for the purpose of discussing and improving the Work, but 60 | excluding communication that is conspicuously marked or otherwise 61 | designated in writing by the copyright owner as "Not a Contribution." 62 | 63 | "Contributor" shall mean Licensor and any individual or Legal Entity 64 | on behalf of whom a Contribution has been received by Licensor and 65 | subsequently incorporated within the Work. 66 | 67 | 2. Grant of Copyright License. Subject to the terms and conditions of 68 | this License, each Contributor hereby grants to You a perpetual, 69 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 70 | copyright license to reproduce, prepare Derivative Works of, 71 | publicly display, publicly perform, sublicense, and distribute the 72 | Work and such Derivative Works in Source or Object form. 73 | 74 | 3. Grant of Patent License. Subject to the terms and conditions of 75 | this License, each Contributor hereby grants to You a perpetual, 76 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 77 | (except as stated in this section) patent license to make, have made, 78 | use, offer to sell, sell, import, and otherwise transfer the Work, 79 | where such license applies only to those patent claims licensable 80 | by such Contributor that are necessarily infringed by their 81 | Contribution(s) alone or by combination of their Contribution(s) 82 | with the Work to which such Contribution(s) was submitted. If You 83 | institute patent litigation against any entity (including a 84 | cross-claim or counterclaim in a lawsuit) alleging that the Work 85 | or a Contribution incorporated within the Work constitutes direct 86 | or contributory patent infringement, then any patent licenses 87 | granted to You under this License for that Work shall terminate 88 | as of the date such litigation is filed. 89 | 90 | 4. Redistribution. You may reproduce and distribute copies of the 91 | Work or Derivative Works thereof in any medium, with or without 92 | modifications, and in Source or Object form, provided that You 93 | meet the following conditions: 94 | 95 | (a) You must give any other recipients of the Work or 96 | Derivative Works a copy of this License; and 97 | 98 | (b) You must cause any modified files to carry prominent notices 99 | stating that You changed the files; and 100 | 101 | (c) You must retain, in the Source form of any Derivative Works 102 | that You distribute, all copyright, patent, trademark, and 103 | attribution notices from the Source form of the Work, 104 | excluding those notices that do not pertain to any part of 105 | the Derivative Works; and 106 | 107 | (d) If the Work includes a "NOTICE" text file as part of its 108 | distribution, then any Derivative Works that You distribute must 109 | include a readable copy of the attribution notices contained 110 | within such NOTICE file, excluding those notices that do not 111 | pertain to any part of the Derivative Works, in at least one 112 | of the following places: within a NOTICE text file distributed 113 | as part of the Derivative Works; within the Source form or 114 | documentation, if provided along with the Derivative Works; or, 115 | within a display generated by the Derivative Works, if and 116 | wherever such third-party notices normally appear. The contents 117 | of the NOTICE file are for informational purposes only and 118 | do not modify the License. You may add Your own attribution 119 | notices within Derivative Works that You distribute, alongside 120 | or as an addendum to the NOTICE text from the Work, provided 121 | that such additional attribution notices cannot be construed 122 | as modifying the License. 123 | 124 | You may add Your own copyright statement to Your modifications and 125 | may provide additional or different license terms and conditions 126 | for use, reproduction, or distribution of Your modifications, or 127 | for any such Derivative Works as a whole, provided Your use, 128 | reproduction, and distribution of the Work otherwise complies with 129 | the conditions stated in this License. 130 | 131 | 5. Submission of Contributions. Unless You explicitly state otherwise, 132 | any Contribution intentionally submitted for inclusion in the Work 133 | by You to the Licensor shall be under the terms and conditions of 134 | this License, without any additional terms or conditions. 135 | Notwithstanding the above, nothing herein shall supersede or modify 136 | the terms of any separate license agreement you may have executed 137 | with Licensor regarding such Contributions. 138 | 139 | 6. Trademarks. This License does not grant permission to use the trade 140 | names, trademarks, service marks, or product names of the Licensor, 141 | except as required for reasonable and customary use in describing the 142 | origin of the Work and reproducing the content of the NOTICE file. 143 | 144 | 7. Disclaimer of Warranty. Unless required by applicable law or 145 | agreed to in writing, Licensor provides the Work (and each 146 | Contributor provides its Contributions) on an "AS IS" BASIS, 147 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 148 | implied, including, without limitation, any warranties or conditions 149 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 150 | PARTICULAR PURPOSE. You are solely responsible for determining the 151 | appropriateness of using or redistributing the Work and assume any 152 | risks associated with Your exercise of permissions under this License. 153 | 154 | 8. Limitation of Liability. In no event and under no legal theory, 155 | whether in tort (including negligence), contract, or otherwise, 156 | unless required by applicable law (such as deliberate and grossly 157 | negligent acts) or agreed to in writing, shall any Contributor be 158 | liable to You for damages, including any direct, indirect, special, 159 | incidental, or consequential damages of any character arising as a 160 | result of this License or out of the use or inability to use the 161 | Work (including but not limited to damages for loss of goodwill, 162 | work stoppage, computer failure or malfunction, or any and all 163 | other commercial damages or losses), even if such Contributor 164 | has been advised of the possibility of such damages. 165 | 166 | 9. Accepting Warranty or Additional Liability. While redistributing 167 | the Work or Derivative Works thereof, You may choose to offer, 168 | and charge a fee for, acceptance of support, warranty, indemnity, 169 | or other liability obligations and/or rights consistent with this 170 | License. However, in accepting such obligations, You may act only 171 | on Your own behalf and on Your sole responsibility, not on behalf 172 | of any other Contributor, and only if You agree to indemnify, 173 | defend, and hold each Contributor harmless for any liability 174 | incurred by, or claims asserted against, such Contributor by reason 175 | of your accepting any such warranty or additional liability. 176 | 177 | END OF TERMS AND CONDITIONS 178 | 179 | APPENDIX: How to apply the Apache License to your work. 180 | 181 | To apply the Apache License to your work, attach the following 182 | boilerplate notice, with the fields enclosed by brackets "[]" 183 | replaced with your own identifying information. (Don't include 184 | the brackets!) The text should be enclosed in the appropriate 185 | comment syntax for the file format. We also recommend that a 186 | file or class name and description of purpose be included on the 187 | same "printed page" as the copyright notice for easier 188 | identification within third-party archives. 189 | 190 | Copyright [yyyy] [name of copyright owner] 191 | 192 | Licensed under the Apache License, Version 2.0 (the "License"); 193 | you may not use this file except in compliance with the License. 194 | You may obtain a copy of the License at 195 | 196 | http://www.apache.org/licenses/LICENSE-2.0 197 | 198 | Unless required by applicable law or agreed to in writing, software 199 | distributed under the License is distributed on an "AS IS" BASIS, 200 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 201 | See the License for the specific language governing permissions and 202 | limitations under the License. 203 | -------------------------------------------------------------------------------- /libs/lume.lua: -------------------------------------------------------------------------------- 1 | 2 | -- 3 | -- lume 4 | -- 5 | -- Copyright (c) 2016 rxi 6 | -- 7 | -- This library is free software; you can redistribute it and/or modify it 8 | -- under the terms of the MIT license. See LICENSE for details. 9 | -- 10 | 11 | local lume = { _version = "2.2.3" } 12 | 13 | local pairs, ipairs = pairs, ipairs 14 | local type, assert, unpack = type, assert, unpack or table.unpack 15 | local tostring, tonumber = tostring, tonumber 16 | local math_floor = math.floor 17 | local math_ceil = math.ceil 18 | local math_atan2 = math.atan2 or math.atan 19 | local math_sqrt = math.sqrt 20 | local math_abs = math.abs 21 | 22 | local noop = function() 23 | end 24 | 25 | local identity = function(x) 26 | return x 27 | end 28 | 29 | local patternescape = function(str) 30 | return str:gsub("[%(%)%.%%%+%-%*%?%[%]%^%$]", "%%%1") 31 | end 32 | 33 | local absindex = function(len, i) 34 | return i < 0 and (len + i + 1) or i 35 | end 36 | 37 | local iscallable = function(x) 38 | if type(x) == "function" then return true end 39 | local mt = getmetatable(x) 40 | return mt and mt.__call ~= nil 41 | end 42 | 43 | local getiter = function(x) 44 | if lume.isarray(x) then 45 | return ipairs 46 | elseif type(x) == "table" then 47 | return pairs 48 | end 49 | error("expected table", 3) 50 | end 51 | 52 | local iteratee = function(x) 53 | if x == nil then return identity end 54 | if iscallable(x) then return x end 55 | if type(x) == "table" then 56 | return function(z) 57 | for k, v in pairs(x) do 58 | if z[k] ~= v then return false end 59 | end 60 | return true 61 | end 62 | end 63 | return function(z) return z[x] end 64 | end 65 | 66 | 67 | 68 | function lume.clamp(x, min, max) 69 | return x < min and min or (x > max and max or x) 70 | end 71 | 72 | 73 | function lume.round(x, increment) 74 | if increment then return lume.round(x / increment) * increment end 75 | return x >= 0 and math_floor(x + .5) or math_ceil(x - .5) 76 | end 77 | 78 | 79 | function lume.sign(x) 80 | return x < 0 and -1 or 1 81 | end 82 | 83 | 84 | function lume.lerp(a, b, amount) 85 | return a + (b - a) * lume.clamp(amount, 0, 1) 86 | end 87 | 88 | 89 | function lume.smooth(a, b, amount) 90 | local t = lume.clamp(amount, 0, 1) 91 | local m = t * t * (3 - 2 * t) 92 | return a + (b - a) * m 93 | end 94 | 95 | 96 | function lume.pingpong(x) 97 | return 1 - math_abs(1 - x % 2) 98 | end 99 | 100 | 101 | function lume.distance(x1, y1, x2, y2, squared) 102 | local dx = x1 - x2 103 | local dy = y1 - y2 104 | local s = dx * dx + dy * dy 105 | return squared and s or math_sqrt(s) 106 | end 107 | 108 | 109 | function lume.angle(x1, y1, x2, y2) 110 | return math_atan2(y2 - y1, x2 - x1) 111 | end 112 | 113 | 114 | function lume.vector(angle, magnitude) 115 | return math.cos(angle) * magnitude, math.sin(angle) * magnitude 116 | end 117 | 118 | 119 | function lume.random(a, b) 120 | if not a then a, b = 0, 1 end 121 | if not b then b = 0 end 122 | return a + math.random() * (b - a) 123 | end 124 | 125 | 126 | function lume.randomchoice(t) 127 | return t[math.random(#t)] 128 | end 129 | 130 | 131 | function lume.weightedchoice(t) 132 | local sum = 0 133 | for _, v in pairs(t) do 134 | assert(v >= 0, "weight value less than zero") 135 | sum = sum + v 136 | end 137 | assert(sum ~= 0, "all weights are zero") 138 | local rnd = lume.random(sum) 139 | for k, v in pairs(t) do 140 | if rnd < v then return k end 141 | rnd = rnd - v 142 | end 143 | end 144 | 145 | 146 | function lume.isarray(x) 147 | return (type(x) == "table" and x[1] ~= nil) and true or false 148 | end 149 | 150 | 151 | function lume.push(t, ...) 152 | local n = select("#", ...) 153 | for i = 1, n do 154 | t[#t + 1] = select(i, ...) 155 | end 156 | return ... 157 | end 158 | 159 | 160 | function lume.remove(t, x) 161 | local iter = getiter(t) 162 | for i, v in iter(t) do 163 | if v == x then 164 | if lume.isarray(t) then 165 | table.remove(t, i) 166 | break 167 | else 168 | t[i] = nil 169 | break 170 | end 171 | end 172 | end 173 | return x 174 | end 175 | 176 | 177 | function lume.clear(t) 178 | local iter = getiter(t) 179 | for k in iter(t) do 180 | t[k] = nil 181 | end 182 | return t 183 | end 184 | 185 | 186 | function lume.extend(t, ...) 187 | for i = 1, select("#", ...) do 188 | local x = select(i, ...) 189 | if x then 190 | for k, v in pairs(x) do 191 | t[k] = v 192 | end 193 | end 194 | end 195 | return t 196 | end 197 | 198 | 199 | function lume.shuffle(t) 200 | local rtn = {} 201 | for i = 1, #t do 202 | local r = math.random(i) 203 | if r ~= i then 204 | rtn[i] = rtn[r] 205 | end 206 | rtn[r] = t[i] 207 | end 208 | return rtn 209 | end 210 | 211 | 212 | function lume.sort(t, comp) 213 | local rtn = lume.clone(t) 214 | if comp then 215 | if type(comp) == "string" then 216 | table.sort(rtn, function(a, b) return a[comp] < b[comp] end) 217 | else 218 | table.sort(rtn, comp) 219 | end 220 | else 221 | table.sort(rtn) 222 | end 223 | return rtn 224 | end 225 | 226 | 227 | function lume.array(...) 228 | local t = {} 229 | for x in ... do t[#t + 1] = x end 230 | return t 231 | end 232 | 233 | 234 | function lume.each(t, fn, ...) 235 | local iter = getiter(t) 236 | if type(fn) == "string" then 237 | for _, v in iter(t) do v[fn](v, ...) end 238 | else 239 | for _, v in iter(t) do fn(v, ...) end 240 | end 241 | return t 242 | end 243 | 244 | 245 | function lume.map(t, fn) 246 | fn = iteratee(fn) 247 | local iter = getiter(t) 248 | local rtn = {} 249 | for k, v in iter(t) do rtn[k] = fn(v) end 250 | return rtn 251 | end 252 | 253 | 254 | function lume.all(t, fn) 255 | fn = iteratee(fn) 256 | local iter = getiter(t) 257 | for _, v in iter(t) do 258 | if not fn(v) then return false end 259 | end 260 | return true 261 | end 262 | 263 | 264 | function lume.any(t, fn) 265 | fn = iteratee(fn) 266 | local iter = getiter(t) 267 | for _, v in iter(t) do 268 | if fn(v) then return true end 269 | end 270 | return false 271 | end 272 | 273 | 274 | function lume.reduce(t, fn, first) 275 | local acc = first 276 | local started = first and true or false 277 | local iter = getiter(t) 278 | for _, v in iter(t) do 279 | if started then 280 | acc = fn(acc, v) 281 | else 282 | acc = v 283 | started = true 284 | end 285 | end 286 | assert(started, "reduce of an empty table with no first value") 287 | return acc 288 | end 289 | 290 | 291 | function lume.set(t) 292 | local rtn = {} 293 | for k in pairs(lume.invert(t)) do 294 | rtn[#rtn + 1] = k 295 | end 296 | return rtn 297 | end 298 | 299 | 300 | function lume.filter(t, fn, retainkeys) 301 | fn = iteratee(fn) 302 | local iter = getiter(t) 303 | local rtn = {} 304 | if retainkeys then 305 | for k, v in iter(t) do 306 | if fn(v) then rtn[k] = v end 307 | end 308 | else 309 | for _, v in iter(t) do 310 | if fn(v) then rtn[#rtn + 1] = v end 311 | end 312 | end 313 | return rtn 314 | end 315 | 316 | 317 | function lume.reject(t, fn, retainkeys) 318 | fn = iteratee(fn) 319 | local iter = getiter(t) 320 | local rtn = {} 321 | if retainkeys then 322 | for k, v in iter(t) do 323 | if not fn(v) then rtn[k] = v end 324 | end 325 | else 326 | for _, v in iter(t) do 327 | if not fn(v) then rtn[#rtn + 1] = v end 328 | end 329 | end 330 | return rtn 331 | end 332 | 333 | 334 | function lume.merge(...) 335 | local rtn = {} 336 | for i = 1, select("#", ...) do 337 | local t = select(i, ...) 338 | local iter = getiter(t) 339 | for k, v in iter(t) do 340 | rtn[k] = v 341 | end 342 | end 343 | return rtn 344 | end 345 | 346 | 347 | function lume.concat(...) 348 | local rtn = {} 349 | for i = 1, select("#", ...) do 350 | local t = select(i, ...) 351 | if t ~= nil then 352 | local iter = getiter(t) 353 | for _, v in iter(t) do 354 | rtn[#rtn + 1] = v 355 | end 356 | end 357 | end 358 | return rtn 359 | end 360 | 361 | 362 | function lume.find(t, value) 363 | local iter = getiter(t) 364 | for k, v in iter(t) do 365 | if v == value then return k end 366 | end 367 | return nil 368 | end 369 | 370 | 371 | function lume.match(t, fn) 372 | fn = iteratee(fn) 373 | local iter = getiter(t) 374 | for k, v in iter(t) do 375 | if fn(v) then return v, k end 376 | end 377 | return nil 378 | end 379 | 380 | 381 | function lume.count(t, fn) 382 | local count = 0 383 | local iter = getiter(t) 384 | if fn then 385 | fn = iteratee(fn) 386 | for _, v in iter(t) do 387 | if fn(v) then count = count + 1 end 388 | end 389 | else 390 | if lume.isarray(t) then 391 | return #t 392 | end 393 | for _ in iter(t) do count = count + 1 end 394 | end 395 | return count 396 | end 397 | 398 | 399 | function lume.slice(t, i, j) 400 | i = i and absindex(#t, i) or 1 401 | j = j and absindex(#t, j) or #t 402 | local rtn = {} 403 | for x = i < 1 and 1 or i, j > #t and #t or j do 404 | rtn[#rtn + 1] = t[x] 405 | end 406 | return rtn 407 | end 408 | 409 | 410 | function lume.first(t, n) 411 | if not n then return t[1] end 412 | return lume.slice(t, 1, n) 413 | end 414 | 415 | 416 | function lume.last(t, n) 417 | if not n then return t[#t] end 418 | return lume.slice(t, -n, -1) 419 | end 420 | 421 | 422 | function lume.invert(t) 423 | local rtn = {} 424 | for k, v in pairs(t) do rtn[v] = k end 425 | return rtn 426 | end 427 | 428 | 429 | function lume.pick(t, ...) 430 | local rtn = {} 431 | for i = 1, select("#", ...) do 432 | local k = select(i, ...) 433 | rtn[k] = t[k] 434 | end 435 | return rtn 436 | end 437 | 438 | 439 | function lume.keys(t) 440 | local rtn = {} 441 | local iter = getiter(t) 442 | for k in iter(t) do rtn[#rtn + 1] = k end 443 | return rtn 444 | end 445 | 446 | 447 | function lume.clone(t) 448 | local rtn = {} 449 | for k, v in pairs(t) do rtn[k] = v end 450 | return rtn 451 | end 452 | 453 | 454 | function lume.fn(fn, ...) 455 | assert(iscallable(fn), "expected a function as the first argument") 456 | local args = { ... } 457 | return function(...) 458 | local a = lume.concat(args, { ... }) 459 | return fn(unpack(a)) 460 | end 461 | end 462 | 463 | 464 | function lume.once(fn, ...) 465 | local f = lume.fn(fn, ...) 466 | local done = false 467 | return function(...) 468 | if done then return end 469 | done = true 470 | return f(...) 471 | end 472 | end 473 | 474 | 475 | local memoize_fnkey = {} 476 | local memoize_nil = {} 477 | 478 | function lume.memoize(fn) 479 | local cache = {} 480 | return function(...) 481 | local c = cache 482 | for i = 1, select("#", ...) do 483 | local a = select(i, ...) or memoize_nil 484 | c[a] = c[a] or {} 485 | c = c[a] 486 | end 487 | c[memoize_fnkey] = c[memoize_fnkey] or {fn(...)} 488 | return unpack(c[memoize_fnkey]) 489 | end 490 | end 491 | 492 | 493 | function lume.combine(...) 494 | local n = select('#', ...) 495 | if n == 0 then return noop end 496 | if n == 1 then 497 | local fn = select(1, ...) 498 | if not fn then return noop end 499 | assert(iscallable(fn), "expected a function or nil") 500 | return fn 501 | end 502 | local funcs = {} 503 | for i = 1, n do 504 | local fn = select(i, ...) 505 | if fn ~= nil then 506 | assert(iscallable(fn), "expected a function or nil") 507 | funcs[#funcs + 1] = fn 508 | end 509 | end 510 | return function(...) 511 | for _, f in ipairs(funcs) do f(...) end 512 | end 513 | end 514 | 515 | 516 | function lume.call(fn, ...) 517 | if fn then 518 | return fn(...) 519 | end 520 | end 521 | 522 | 523 | function lume.time(fn, ...) 524 | local start = os.clock() 525 | local rtn = {fn(...)} 526 | return (os.clock() - start), unpack(rtn) 527 | end 528 | 529 | 530 | local lambda_cache = {} 531 | 532 | function lume.lambda(str) 533 | if not lambda_cache[str] then 534 | local args, body = str:match([[^([%w,_ ]-)%->(.-)$]]) 535 | assert(args and body, "bad string lambda") 536 | local s = "return function(" .. args .. ")\nreturn " .. body .. "\nend" 537 | lambda_cache[str] = lume.dostring(s) 538 | end 539 | return lambda_cache[str] 540 | end 541 | 542 | 543 | local serialize 544 | 545 | local serialize_map = { 546 | [ "boolean" ] = tostring, 547 | [ "nil" ] = tostring, 548 | [ "string" ] = function(v) return string.format("%q", v) end, 549 | [ "number" ] = function(v) 550 | if v ~= v then return "0/0" -- nan 551 | elseif v == 1 / 0 then return "1/0" -- inf 552 | elseif v == -1 / 0 then return "-1/0" end -- -inf 553 | return tostring(v) 554 | end, 555 | [ "table" ] = function(t, stk) 556 | stk = stk or {} 557 | if stk[t] then error("circular reference") end 558 | local rtn = {} 559 | stk[t] = true 560 | for k, v in pairs(t) do 561 | rtn[#rtn + 1] = "[" .. serialize(k, stk) .. "]=" .. serialize(v, stk) 562 | end 563 | stk[t] = nil 564 | return "{" .. table.concat(rtn, ",") .. "}" 565 | end 566 | } 567 | 568 | setmetatable(serialize_map, { 569 | __index = function(_, k) error("unsupported serialize type: " .. k) end 570 | }) 571 | 572 | serialize = function(x, stk) 573 | return serialize_map[type(x)](x, stk) 574 | end 575 | 576 | function lume.serialize(x) 577 | return serialize(x) 578 | end 579 | 580 | 581 | function lume.deserialize(str) 582 | return lume.dostring("return " .. str) 583 | end 584 | 585 | 586 | function lume.split(str, sep) 587 | if not sep then 588 | return lume.array(str:gmatch("([%S]+)")) 589 | else 590 | assert(sep ~= "", "empty separator") 591 | local psep = patternescape(sep) 592 | return lume.array((str..sep):gmatch("(.-)("..psep..")")) 593 | end 594 | end 595 | 596 | 597 | function lume.trim(str, chars) 598 | if not chars then return str:match("^[%s]*(.-)[%s]*$") end 599 | chars = patternescape(chars) 600 | return str:match("^[" .. chars .. "]*(.-)[" .. chars .. "]*$") 601 | end 602 | 603 | 604 | function lume.wordwrap(str, limit) 605 | limit = limit or 72 606 | local check 607 | if type(limit) == "number" then 608 | check = function(s) return #s >= limit end 609 | else 610 | check = limit 611 | end 612 | local rtn = {} 613 | local line = "" 614 | for word, spaces in str:gmatch("(%S+)(%s*)") do 615 | local s = line .. word 616 | if check(s) then 617 | table.insert(rtn, line .. "\n") 618 | line = word 619 | else 620 | line = s 621 | end 622 | for c in spaces:gmatch(".") do 623 | if c == "\n" then 624 | table.insert(rtn, line .. "\n") 625 | line = "" 626 | else 627 | line = line .. c 628 | end 629 | end 630 | end 631 | table.insert(rtn, line) 632 | return table.concat(rtn) 633 | end 634 | 635 | 636 | function lume.format(str, vars) 637 | if not vars then return str end 638 | local f = function(x) 639 | return tostring(vars[x] or vars[tonumber(x)] or "{" .. x .. "}") 640 | end 641 | return (str:gsub("{(.-)}", f)) 642 | end 643 | 644 | 645 | function lume.trace(...) 646 | local info = debug.getinfo(2, "Sl") 647 | local t = { info.short_src .. ":" .. info.currentline .. ":" } 648 | for i = 1, select("#", ...) do 649 | local x = select(i, ...) 650 | if type(x) == "number" then 651 | x = string.format("%g", lume.round(x, .01)) 652 | end 653 | t[#t + 1] = tostring(x) 654 | end 655 | print(table.concat(t, " ")) 656 | end 657 | 658 | 659 | function lume.dostring(str) 660 | return assert((loadstring or load)(str))() 661 | end 662 | 663 | 664 | function lume.uuid() 665 | local fn = function(x) 666 | local r = math.random(16) - 1 667 | r = (x == "x") and (r + 1) or (r % 4) + 9 668 | return ("0123456789abcdef"):sub(r, r) 669 | end 670 | return (("xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx"):gsub("[xy]", fn)) 671 | end 672 | 673 | 674 | function lume.hotswap(modname) 675 | local oldglobal = lume.clone(_G) 676 | local updated = {} 677 | local function update(old, new) 678 | if updated[old] then return end 679 | if not old then 680 | old = {} 681 | end 682 | updated[old] = true 683 | local oldmt, newmt = getmetatable(old), getmetatable(new) 684 | if oldmt and newmt then update(oldmt, newmt) end 685 | for k, v in pairs(new) do 686 | print(string.format("%20s", k), string.format("%20s", v), string.format("%10s", old[k] == nil)) 687 | if type(v) == "table" and not old[k] == nil then 688 | update(old[k], v) 689 | else 690 | old[k] = v 691 | end 692 | end 693 | end 694 | local err = nil 695 | local function onerror(e) 696 | for k in pairs(_G) do _G[k] = oldglobal[k] end 697 | err = lume.trim(e) 698 | end 699 | local ok, oldmod = pcall(require, modname) 700 | oldmod = ok and oldmod or nil 701 | xpcall(function() 702 | package.loaded[modname] = nil 703 | local newmod = require(modname) 704 | if type(oldmod) == "table" then update(oldmod, newmod) end 705 | for k, v in pairs(oldglobal) do 706 | if v ~= _G[k] and type(v) == "table" then 707 | update(v, _G[k]) 708 | _G[k] = v 709 | end 710 | end 711 | end, onerror) 712 | package.loaded[modname] = oldmod 713 | if err then return nil, err end 714 | return oldmod 715 | end 716 | 717 | 718 | local ripairs_iter = function(t, i) 719 | i = i - 1 720 | local v = t[i] 721 | if v then return i, v end 722 | end 723 | 724 | function lume.ripairs(t) 725 | return ripairs_iter, t, (#t + 1) 726 | end 727 | 728 | 729 | function lume.color(str, mul) 730 | mul = mul or 1 731 | local r, g, b, a 732 | r, g, b = str:match("#(%x%x)(%x%x)(%x%x)") 733 | if r then 734 | r = tonumber(r, 16) / 0xff 735 | g = tonumber(g, 16) / 0xff 736 | b = tonumber(b, 16) / 0xff 737 | a = 1 738 | elseif str:match("rgba?%s*%([%d%s%.,]+%)") then 739 | local f = str:gmatch("[%d.]+") 740 | r = (f() or 0) / 0xff 741 | g = (f() or 0) / 0xff 742 | b = (f() or 0) / 0xff 743 | a = f() or 1 744 | else 745 | error(("bad color string '%s'"):format(str)) 746 | end 747 | return r * mul, g * mul, b * mul, a * mul 748 | end 749 | 750 | 751 | function lume.rgba(color) 752 | local a = math_floor((color / 16777216) % 256) 753 | local r = math_floor((color / 65536) % 256) 754 | local g = math_floor((color / 256) % 256) 755 | local b = math_floor((color) % 256) 756 | return r, g, b, a 757 | end 758 | 759 | 760 | local chain_mt = {} 761 | chain_mt.__index = lume.map(lume.filter(lume, iscallable, true), 762 | function(fn) 763 | return function(self, ...) 764 | self._value = fn(self._value, ...) 765 | return self 766 | end 767 | end) 768 | chain_mt.__index.result = function(x) return x._value end 769 | 770 | function lume.chain(value) 771 | return setmetatable({ _value = value }, chain_mt) 772 | end 773 | 774 | setmetatable(lume, { 775 | __call = function(_, ...) 776 | return lume.chain(...) 777 | end 778 | }) 779 | 780 | 781 | return lume 782 | --------------------------------------------------------------------------------