├── .gitignore ├── README.md ├── assets ├── comicneuebold.ttf ├── map.ldtk ├── music │ ├── boss.mp3 │ ├── level1.mp3 │ ├── level2.mp3 │ ├── title.mp3 │ └── win.mp3 ├── shaders │ ├── bgfade.frag │ ├── pixelscale.frag │ └── white.frag ├── sounds │ ├── batfly.wav │ ├── bossimpact.wav │ ├── bosslaugh.wav │ ├── bossstart.wav │ ├── bosstp.wav │ ├── bulletCol.wav │ ├── bulletHit.wav │ ├── bulletHit2.wav │ ├── death.mp3 │ ├── death.wav │ ├── ebullet.wav │ ├── edeath.wav │ ├── gun1.wav │ ├── jump.wav │ ├── keyget.wav │ ├── land.wav │ ├── steponwarp.wav │ ├── warp.wav │ └── warpstart.wav └── sprites │ ├── backwarp.png │ ├── bat.png │ ├── boom.png │ ├── bullet.png │ ├── castle.png │ ├── crosshair.png │ ├── cursor.png │ ├── cursor2.png │ ├── cursor3.png │ ├── duck.png │ ├── eye.png │ ├── gameicon.png │ ├── glasses.png │ ├── gunarm.png │ ├── impact.png │ ├── key.png │ ├── king.png │ ├── lad.png │ ├── lock.png │ ├── star.png │ ├── star2.png │ ├── throne.png │ ├── tile.png │ ├── title.png │ ├── title_ld.png │ ├── title_outline_ld.png │ ├── title_small.png │ ├── title_small_outline.png │ └── warp.png ├── builds └── intothecastle_win.zip ├── conf.lua ├── engine ├── audio.lua ├── baton.lua ├── colors.lua ├── debugtools │ ├── console.lua │ ├── init.lua │ ├── lurker.lua │ └── menlo.ttf ├── init.lua ├── input.lua ├── inspect.lua ├── json.lua ├── lume.lua ├── oops.lua ├── scene.lua └── utils.lua ├── main.lua ├── misc ├── alarm.lua ├── cutscenes │ ├── cutscene.lua │ ├── warpcutscene.lua │ └── wincutscene.lua └── gui │ ├── button.lua │ ├── form.lua │ ├── scrollbar.lua │ └── slider.lua ├── scenes ├── scene1.lua └── title.lua ├── things ├── backwarp.lua ├── boom.lua ├── bullet.lua ├── enemies │ ├── bat.lua │ ├── enemy.lua │ ├── eye.lua │ ├── face.lua │ ├── king.lua │ ├── star.lua │ └── star2.lua ├── impact.lua ├── key.lua ├── lock.lua ├── player.lua ├── text.lua ├── thing.lua ├── throne.lua └── warp.lua └── todos.md /.gitignore: -------------------------------------------------------------------------------- 1 | *.backup.ldtk 2 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Into the Castle 2 | 3 | I originally made this game for the Ludum Dare 48 hour compo, but I've continued to do a little work on it and fix bugs. 4 | 5 | If you want to see the game as it was last submitted for the compo, check out this commit: 7f3848c1b70574af36cd9788c376a57fc59983bc 6 | 7 | ![intothecastle3](https://user-images.githubusercontent.com/19754251/120119291-c19e0a00-c14b-11eb-86ed-d336ba187531.gif) 8 | 9 | ![intothecastle6](https://user-images.githubusercontent.com/19754251/120119300-cb277200-c14b-11eb-81ac-844531d23a61.gif) 10 | -------------------------------------------------------------------------------- /assets/comicneuebold.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/groverburger/ld48/3c3cda2afb4e4dfa80587ee8bbb6b4f352d0a0ae/assets/comicneuebold.ttf -------------------------------------------------------------------------------- /assets/music/boss.mp3: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/groverburger/ld48/3c3cda2afb4e4dfa80587ee8bbb6b4f352d0a0ae/assets/music/boss.mp3 -------------------------------------------------------------------------------- /assets/music/level1.mp3: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/groverburger/ld48/3c3cda2afb4e4dfa80587ee8bbb6b4f352d0a0ae/assets/music/level1.mp3 -------------------------------------------------------------------------------- /assets/music/level2.mp3: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/groverburger/ld48/3c3cda2afb4e4dfa80587ee8bbb6b4f352d0a0ae/assets/music/level2.mp3 -------------------------------------------------------------------------------- /assets/music/title.mp3: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/groverburger/ld48/3c3cda2afb4e4dfa80587ee8bbb6b4f352d0a0ae/assets/music/title.mp3 -------------------------------------------------------------------------------- /assets/music/win.mp3: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/groverburger/ld48/3c3cda2afb4e4dfa80587ee8bbb6b4f352d0a0ae/assets/music/win.mp3 -------------------------------------------------------------------------------- /assets/shaders/bgfade.frag: -------------------------------------------------------------------------------- 1 | uniform vec4 bgcolor; 2 | 3 | vec4 effect(vec4 color, Image tex, vec2 texture_coords, vec2 screen_coords) 4 | { 5 | vec4 texturecolor = Texel(tex, texture_coords); 6 | if (texturecolor.a == 0.0) { discard; } 7 | 8 | texturecolor *= color; 9 | 10 | number r = mix(bgcolor.r, texturecolor.r, bgcolor.a); 11 | number g = mix(bgcolor.g, texturecolor.g, bgcolor.a); 12 | number b = mix(bgcolor.b, texturecolor.b, bgcolor.a); 13 | return vec4(r,g,b, texturecolor.a); 14 | } 15 | -------------------------------------------------------------------------------- /assets/shaders/pixelscale.frag: -------------------------------------------------------------------------------- 1 | // size of the texture 2 | uniform int width; 3 | uniform int height; 4 | uniform number uvmod; 5 | 6 | varying vec4 screenPosition; 7 | 8 | number d(vec4 a, vec4 b) { 9 | return abs(a.x-b.x) + abs(a.y-b.y) + abs(a.z-b.z) + abs(a.a-b.a); 10 | } 11 | 12 | vec4 effect(vec4 color, Image tex, vec2 texcoord, vec2 pixcoord) { 13 | vec4 texcolor = Texel(tex, texcoord); 14 | vec2 pos = vec2(texcoord.x*width, texcoord.y*height); 15 | vec2 frac = vec2(mod(pos.x, 1), mod(pos.y, 1)); 16 | vec2 subfrac = vec2(mod(pos.x, 0.5), mod(pos.y, 0.5)); 17 | vec2 b = vec2(floor(texcoord.x/uvmod)*uvmod, floor(texcoord.y/uvmod)*uvmod); 18 | 19 | vec4 A = Texel(tex, vec2(mod((pos.x - 1)/width, uvmod) + b.x, mod((pos.y - 1)/height, uvmod) + b.y)); 20 | vec4 B = Texel(tex, vec2(mod((pos.x )/width, uvmod) + b.x, mod((pos.y - 1)/height, uvmod) + b.y)); 21 | vec4 C = Texel(tex, vec2(mod((pos.x + 1)/width, uvmod) + b.x, mod((pos.y - 1)/height, uvmod) + b.y)); 22 | vec4 D = Texel(tex, vec2(mod((pos.x - 1)/width, uvmod) + b.x, mod((pos.y )/height, uvmod) + b.y)); 23 | vec4 E = Texel(tex, vec2(mod((pos.x )/width, uvmod) + b.x, mod((pos.y )/height, uvmod) + b.y)); 24 | vec4 F = Texel(tex, vec2(mod((pos.x + 1)/width, uvmod) + b.x, mod((pos.y )/height, uvmod) + b.y)); 25 | vec4 G = Texel(tex, vec2(mod((pos.x - 1)/width, uvmod) + b.x, mod((pos.y + 1)/height, uvmod) + b.y)); 26 | vec4 H = Texel(tex, vec2(mod((pos.x )/width, uvmod) + b.x, mod((pos.y + 1)/height, uvmod) + b.y)); 27 | vec4 I = Texel(tex, vec2(mod((pos.x + 1)/width, uvmod) + b.x, mod((pos.y + 1)/height, uvmod) + b.y)); 28 | 29 | if (d(F,H) < d(E,I)) { 30 | if (frac.x > 0.5 && frac.y > 0.5 && subfrac.x + subfrac.y >= 0.5) { 31 | //texcolor = (F+H)/2.0; 32 | texcolor = F; 33 | } 34 | } 35 | 36 | if (d(D,H) < d(E,G)) { 37 | if (frac.x <= 0.5 && frac.y > 0.5 && 0.5-subfrac.x + subfrac.y >= 0.5) { 38 | //texcolor = (D+H)/2.0; 39 | texcolor = D; 40 | } 41 | } 42 | 43 | if (d(F,B) < d(E,C)) { 44 | if (frac.x > 0.5 && frac.y <= 0.5 && subfrac.x + 0.5-subfrac.y >= 0.5) { 45 | //texcolor = (F+B)/2.0; 46 | texcolor = B; 47 | } 48 | } 49 | 50 | if (d(D,B) < d(E,A)) { 51 | if (frac.x <= 0.5 && frac.y <= 0.5 && 0.5-subfrac.x + 0.5-subfrac.y >= 0.5) { 52 | //texcolor = (D+B)/2.0; 53 | texcolor = B; 54 | } 55 | } 56 | 57 | if (texcolor.a == 0.0) { discard; } 58 | 59 | //number c = screenPosition.z/5; 60 | //return vec4(c,c,c,1) * VaryingColor; 61 | //return vec4(screenPosition.x, screenPosition.y, 1, 1) * VaryingColor; 62 | 63 | return texcolor * VaryingColor; 64 | } 65 | -------------------------------------------------------------------------------- /assets/shaders/white.frag: -------------------------------------------------------------------------------- 1 | vec4 effect(vec4 color, Image tex, vec2 texture_coords, vec2 screen_coords) 2 | { 3 | vec4 texturecolor = Texel(tex, texture_coords); 4 | if (texturecolor.a == 0.0) { discard; } 5 | return vec4(1.0, 1.0, 1.0, texturecolor.a); 6 | } 7 | -------------------------------------------------------------------------------- /assets/sounds/batfly.wav: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/groverburger/ld48/3c3cda2afb4e4dfa80587ee8bbb6b4f352d0a0ae/assets/sounds/batfly.wav -------------------------------------------------------------------------------- /assets/sounds/bossimpact.wav: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/groverburger/ld48/3c3cda2afb4e4dfa80587ee8bbb6b4f352d0a0ae/assets/sounds/bossimpact.wav -------------------------------------------------------------------------------- /assets/sounds/bosslaugh.wav: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/groverburger/ld48/3c3cda2afb4e4dfa80587ee8bbb6b4f352d0a0ae/assets/sounds/bosslaugh.wav -------------------------------------------------------------------------------- /assets/sounds/bossstart.wav: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/groverburger/ld48/3c3cda2afb4e4dfa80587ee8bbb6b4f352d0a0ae/assets/sounds/bossstart.wav -------------------------------------------------------------------------------- /assets/sounds/bosstp.wav: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/groverburger/ld48/3c3cda2afb4e4dfa80587ee8bbb6b4f352d0a0ae/assets/sounds/bosstp.wav -------------------------------------------------------------------------------- /assets/sounds/bulletCol.wav: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/groverburger/ld48/3c3cda2afb4e4dfa80587ee8bbb6b4f352d0a0ae/assets/sounds/bulletCol.wav -------------------------------------------------------------------------------- /assets/sounds/bulletHit.wav: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/groverburger/ld48/3c3cda2afb4e4dfa80587ee8bbb6b4f352d0a0ae/assets/sounds/bulletHit.wav -------------------------------------------------------------------------------- /assets/sounds/bulletHit2.wav: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/groverburger/ld48/3c3cda2afb4e4dfa80587ee8bbb6b4f352d0a0ae/assets/sounds/bulletHit2.wav -------------------------------------------------------------------------------- /assets/sounds/death.mp3: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/groverburger/ld48/3c3cda2afb4e4dfa80587ee8bbb6b4f352d0a0ae/assets/sounds/death.mp3 -------------------------------------------------------------------------------- /assets/sounds/death.wav: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/groverburger/ld48/3c3cda2afb4e4dfa80587ee8bbb6b4f352d0a0ae/assets/sounds/death.wav -------------------------------------------------------------------------------- /assets/sounds/ebullet.wav: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/groverburger/ld48/3c3cda2afb4e4dfa80587ee8bbb6b4f352d0a0ae/assets/sounds/ebullet.wav -------------------------------------------------------------------------------- /assets/sounds/edeath.wav: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/groverburger/ld48/3c3cda2afb4e4dfa80587ee8bbb6b4f352d0a0ae/assets/sounds/edeath.wav -------------------------------------------------------------------------------- /assets/sounds/gun1.wav: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/groverburger/ld48/3c3cda2afb4e4dfa80587ee8bbb6b4f352d0a0ae/assets/sounds/gun1.wav -------------------------------------------------------------------------------- /assets/sounds/jump.wav: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/groverburger/ld48/3c3cda2afb4e4dfa80587ee8bbb6b4f352d0a0ae/assets/sounds/jump.wav -------------------------------------------------------------------------------- /assets/sounds/keyget.wav: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/groverburger/ld48/3c3cda2afb4e4dfa80587ee8bbb6b4f352d0a0ae/assets/sounds/keyget.wav -------------------------------------------------------------------------------- /assets/sounds/land.wav: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/groverburger/ld48/3c3cda2afb4e4dfa80587ee8bbb6b4f352d0a0ae/assets/sounds/land.wav -------------------------------------------------------------------------------- /assets/sounds/steponwarp.wav: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/groverburger/ld48/3c3cda2afb4e4dfa80587ee8bbb6b4f352d0a0ae/assets/sounds/steponwarp.wav -------------------------------------------------------------------------------- /assets/sounds/warp.wav: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/groverburger/ld48/3c3cda2afb4e4dfa80587ee8bbb6b4f352d0a0ae/assets/sounds/warp.wav -------------------------------------------------------------------------------- /assets/sounds/warpstart.wav: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/groverburger/ld48/3c3cda2afb4e4dfa80587ee8bbb6b4f352d0a0ae/assets/sounds/warpstart.wav -------------------------------------------------------------------------------- /assets/sprites/backwarp.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/groverburger/ld48/3c3cda2afb4e4dfa80587ee8bbb6b4f352d0a0ae/assets/sprites/backwarp.png -------------------------------------------------------------------------------- /assets/sprites/bat.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/groverburger/ld48/3c3cda2afb4e4dfa80587ee8bbb6b4f352d0a0ae/assets/sprites/bat.png -------------------------------------------------------------------------------- /assets/sprites/boom.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/groverburger/ld48/3c3cda2afb4e4dfa80587ee8bbb6b4f352d0a0ae/assets/sprites/boom.png -------------------------------------------------------------------------------- /assets/sprites/bullet.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/groverburger/ld48/3c3cda2afb4e4dfa80587ee8bbb6b4f352d0a0ae/assets/sprites/bullet.png -------------------------------------------------------------------------------- /assets/sprites/castle.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/groverburger/ld48/3c3cda2afb4e4dfa80587ee8bbb6b4f352d0a0ae/assets/sprites/castle.png -------------------------------------------------------------------------------- /assets/sprites/crosshair.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/groverburger/ld48/3c3cda2afb4e4dfa80587ee8bbb6b4f352d0a0ae/assets/sprites/crosshair.png -------------------------------------------------------------------------------- /assets/sprites/cursor.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/groverburger/ld48/3c3cda2afb4e4dfa80587ee8bbb6b4f352d0a0ae/assets/sprites/cursor.png -------------------------------------------------------------------------------- /assets/sprites/cursor2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/groverburger/ld48/3c3cda2afb4e4dfa80587ee8bbb6b4f352d0a0ae/assets/sprites/cursor2.png -------------------------------------------------------------------------------- /assets/sprites/cursor3.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/groverburger/ld48/3c3cda2afb4e4dfa80587ee8bbb6b4f352d0a0ae/assets/sprites/cursor3.png -------------------------------------------------------------------------------- /assets/sprites/duck.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/groverburger/ld48/3c3cda2afb4e4dfa80587ee8bbb6b4f352d0a0ae/assets/sprites/duck.png -------------------------------------------------------------------------------- /assets/sprites/eye.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/groverburger/ld48/3c3cda2afb4e4dfa80587ee8bbb6b4f352d0a0ae/assets/sprites/eye.png -------------------------------------------------------------------------------- /assets/sprites/gameicon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/groverburger/ld48/3c3cda2afb4e4dfa80587ee8bbb6b4f352d0a0ae/assets/sprites/gameicon.png -------------------------------------------------------------------------------- /assets/sprites/glasses.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/groverburger/ld48/3c3cda2afb4e4dfa80587ee8bbb6b4f352d0a0ae/assets/sprites/glasses.png -------------------------------------------------------------------------------- /assets/sprites/gunarm.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/groverburger/ld48/3c3cda2afb4e4dfa80587ee8bbb6b4f352d0a0ae/assets/sprites/gunarm.png -------------------------------------------------------------------------------- /assets/sprites/impact.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/groverburger/ld48/3c3cda2afb4e4dfa80587ee8bbb6b4f352d0a0ae/assets/sprites/impact.png -------------------------------------------------------------------------------- /assets/sprites/key.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/groverburger/ld48/3c3cda2afb4e4dfa80587ee8bbb6b4f352d0a0ae/assets/sprites/key.png -------------------------------------------------------------------------------- /assets/sprites/king.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/groverburger/ld48/3c3cda2afb4e4dfa80587ee8bbb6b4f352d0a0ae/assets/sprites/king.png -------------------------------------------------------------------------------- /assets/sprites/lad.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/groverburger/ld48/3c3cda2afb4e4dfa80587ee8bbb6b4f352d0a0ae/assets/sprites/lad.png -------------------------------------------------------------------------------- /assets/sprites/lock.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/groverburger/ld48/3c3cda2afb4e4dfa80587ee8bbb6b4f352d0a0ae/assets/sprites/lock.png -------------------------------------------------------------------------------- /assets/sprites/star.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/groverburger/ld48/3c3cda2afb4e4dfa80587ee8bbb6b4f352d0a0ae/assets/sprites/star.png -------------------------------------------------------------------------------- /assets/sprites/star2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/groverburger/ld48/3c3cda2afb4e4dfa80587ee8bbb6b4f352d0a0ae/assets/sprites/star2.png -------------------------------------------------------------------------------- /assets/sprites/throne.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/groverburger/ld48/3c3cda2afb4e4dfa80587ee8bbb6b4f352d0a0ae/assets/sprites/throne.png -------------------------------------------------------------------------------- /assets/sprites/tile.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/groverburger/ld48/3c3cda2afb4e4dfa80587ee8bbb6b4f352d0a0ae/assets/sprites/tile.png -------------------------------------------------------------------------------- /assets/sprites/title.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/groverburger/ld48/3c3cda2afb4e4dfa80587ee8bbb6b4f352d0a0ae/assets/sprites/title.png -------------------------------------------------------------------------------- /assets/sprites/title_ld.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/groverburger/ld48/3c3cda2afb4e4dfa80587ee8bbb6b4f352d0a0ae/assets/sprites/title_ld.png -------------------------------------------------------------------------------- /assets/sprites/title_outline_ld.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/groverburger/ld48/3c3cda2afb4e4dfa80587ee8bbb6b4f352d0a0ae/assets/sprites/title_outline_ld.png -------------------------------------------------------------------------------- /assets/sprites/title_small.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/groverburger/ld48/3c3cda2afb4e4dfa80587ee8bbb6b4f352d0a0ae/assets/sprites/title_small.png -------------------------------------------------------------------------------- /assets/sprites/title_small_outline.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/groverburger/ld48/3c3cda2afb4e4dfa80587ee8bbb6b4f352d0a0ae/assets/sprites/title_small_outline.png -------------------------------------------------------------------------------- /assets/sprites/warp.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/groverburger/ld48/3c3cda2afb4e4dfa80587ee8bbb6b4f352d0a0ae/assets/sprites/warp.png -------------------------------------------------------------------------------- /builds/intothecastle_win.zip: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/groverburger/ld48/3c3cda2afb4e4dfa80587ee8bbb6b4f352d0a0ae/builds/intothecastle_win.zip -------------------------------------------------------------------------------- /conf.lua: -------------------------------------------------------------------------------- 1 | function love.conf(t) 2 | t.window.width = 1024 3 | t.window.height = 768 4 | t.window.minwidth = 1024 5 | t.window.minheight = 768 6 | t.window.title = "Into the Castle" 7 | t.window.resizable = true 8 | end 9 | -------------------------------------------------------------------------------- /engine/audio.lua: -------------------------------------------------------------------------------- 1 | local audio = {} 2 | local soundVolume = 1 3 | local musicVolume = 1 4 | 5 | local soundList = {} 6 | local musicList = {} 7 | 8 | local noise = class() 9 | noise.pitch = {0.8, 1.2} 10 | noise.volume = 1 11 | 12 | local function updateNoiseVolume(noise) 13 | noise.source:setVolume(noise.volume * (noise.isSound and soundVolume or musicVolume)) 14 | end 15 | 16 | function noise:new(path, isSound) 17 | self.source = love.audio.newSource(path, (isSound or engine.settings.web) and "static" or "stream") 18 | self.isSound = isSound 19 | end 20 | 21 | function noise:play() 22 | -- pitch can either be a range or a set value 23 | if type(self.pitch) == "table" then 24 | self.source:setPitch(utils.randomRange(self.pitch[1], self.pitch[2])) 25 | else 26 | self.source:setPitch(self.pitch) 27 | end 28 | 29 | updateNoiseVolume(self) 30 | 31 | -- reset the noise from the beginning in a web-safe way (seek(0) doesn't work) 32 | self.source:stop() 33 | self.source:play() 34 | end 35 | 36 | -------------------------------------------------------------------------------- 37 | -- public api 38 | -------------------------------------------------------------------------------- 39 | 40 | function audio.newSound(path, volume, pitch) 41 | local sound = noise(path, true) 42 | if volume then sound.volume = volume end 43 | if pitch then sound.pitch = pitch end 44 | table.insert(soundList, sound) 45 | return sound 46 | end 47 | 48 | function audio.newMusic(path, volume, pitch) 49 | local music = noise(path, false) 50 | if volume then music.volume = volume end 51 | music.pitch = pitch or 1 52 | music.source:setLooping(true) 53 | table.insert(musicList, music) 54 | return music 55 | end 56 | 57 | function audio.setSoundVolume(volume) 58 | soundVolume = volume 59 | for _, v in ipairs(soundList) do updateNoiseVolume(v) end 60 | end 61 | 62 | function audio.getSoundVolume() 63 | return soundVolume 64 | end 65 | 66 | function audio.setMusicVolume(volume) 67 | musicVolume = volume 68 | for _, v in ipairs(musicList) do updateNoiseVolume(v) end 69 | end 70 | 71 | function audio.getMusicVolume() 72 | return musicVolume 73 | end 74 | 75 | return audio 76 | -------------------------------------------------------------------------------- /engine/baton.lua: -------------------------------------------------------------------------------- 1 | local baton = { 2 | _VERSION = 'Baton v1.0.2', 3 | _DESCRIPTION = 'Input library for LÖVE.', 4 | _URL = 'https://github.com/tesselode/baton', 5 | _LICENSE = [[ 6 | MIT License 7 | 8 | Copyright (c) 2020 Andrew Minnich 9 | 10 | Permission is hereby granted, free of charge, to any person obtaining a copy 11 | of this software and associated documentation files (the "Software"), to deal 12 | in the Software without restriction, including without limitation the rights 13 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 14 | copies of the Software, and to permit persons to whom the Software is 15 | furnished to do so, subject to the following conditions: 16 | 17 | The above copyright notice and this permission notice shall be included in all 18 | copies or substantial portions of the Software. 19 | 20 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 21 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 22 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 23 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 24 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 25 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 26 | SOFTWARE. 27 | ]] 28 | } 29 | 30 | -- string parsing functions -- 31 | 32 | -- splits a source definition into type and value 33 | -- example: 'button:a' -> 'button', 'a' 34 | local function parseSource(source) 35 | return source:match '(.+):(.+)' 36 | end 37 | 38 | -- splits an axis value into axis and direction 39 | -- example: 'leftx-' -> 'leftx', '-' 40 | local function parseAxis(value) 41 | return value:match '(.+)([%+%-])' 42 | end 43 | 44 | -- splits a joystick hat value into hat number and direction 45 | -- example: '2rd' -> '2', 'rd' 46 | local function parseHat(value) 47 | return value:match '(%d)(.+)' 48 | end 49 | 50 | --[[ 51 | -- source functions -- 52 | 53 | each source function checks the state of one type of input 54 | and returns a value from 0 to 1. for binary controls, such 55 | as keyboard keys and gamepad buttons, they return 1 if the 56 | input is held down and 0 if not. for analog controls, such 57 | as "leftx+" (the left analog stick held to the right), they 58 | return a number from 0 to 1. 59 | 60 | source functions are split into keyboard/mouse functions 61 | and joystick/gamepad functions. baton treats these two 62 | categories slightly differently. 63 | ]] 64 | 65 | local sourceFunction = {keyboardMouse = {}, joystick = {}} 66 | 67 | -- checks whether a keyboard key is down or not 68 | function sourceFunction.keyboardMouse.key(key) 69 | return love.keyboard.isDown(key) and 1 or 0 70 | end 71 | 72 | -- checks whether a keyboard key is down or not, 73 | -- but it takes a scancode as an input 74 | function sourceFunction.keyboardMouse.sc(sc) 75 | return love.keyboard.isScancodeDown(sc) and 1 or 0 76 | end 77 | 78 | -- checks whether a mouse buttons is down or not. 79 | -- note that baton doesn't detect mouse movement, just the buttons 80 | function sourceFunction.keyboardMouse.mouse(button) 81 | if tonumber(button) then 82 | return love.mouse.isDown(tonumber(button)) and 1 or 0 83 | end 84 | 85 | return input.mouseCheck(button) and 1 or 0 86 | end 87 | 88 | -- checks the position of a joystick axis 89 | function sourceFunction.joystick.axis(joystick, value) 90 | local axis, direction = parseAxis(value) 91 | -- "a and b or c" is ok here because b will never be boolean 92 | value = tonumber(axis) and joystick:getAxis(tonumber(axis)) 93 | or joystick:getGamepadAxis(axis) 94 | if direction == '-' then value = -value end 95 | return value > 0 and value or 0 96 | end 97 | 98 | -- checks whether a joystick button is held down or not 99 | -- can take a number or a GamepadButton string 100 | function sourceFunction.joystick.button(joystick, button) 101 | -- i'm intentionally not using the "a and b or c" idiom here 102 | -- because joystick.isDown returns a boolean 103 | if tonumber(button) then 104 | return joystick:isDown(tonumber(button)) and 1 or 0 105 | else 106 | return joystick:isGamepadDown(button) and 1 or 0 107 | end 108 | end 109 | 110 | -- checks the direction of a joystick hat 111 | function sourceFunction.joystick.hat(joystick, value) 112 | local hat, direction = parseHat(value) 113 | return joystick:getHat(hat) == direction and 1 or 0 114 | end 115 | 116 | --[[ 117 | -- player class -- 118 | 119 | the player object takes a configuration table and handles input 120 | accordingly. it's called a "player" because it makes sense to use 121 | multiple of these for each player in a multiplayer game, but 122 | you can use separate player objects to organize inputs 123 | however you want. 124 | ]] 125 | 126 | local Player = {} 127 | Player.__index = Player 128 | 129 | -- internal functions -- 130 | 131 | -- sets the player's config to a user-defined config table 132 | -- and sets some defaults if they're not already defined 133 | function Player:_loadConfig(config) 134 | if not config then 135 | error('No config table provided', 4) 136 | end 137 | if not config.controls then 138 | error('No controls specified', 4) 139 | end 140 | config.pairs = config.pairs or {} 141 | config.deadzone = config.deadzone or .5 142 | config.squareDeadzone = config.squareDeadzone or false 143 | self.config = config 144 | end 145 | 146 | -- initializes a control object for each control defined in the config 147 | function Player:_initControls() 148 | self._controls = {} 149 | for controlName, sources in pairs(self.config.controls) do 150 | self._controls[controlName] = { 151 | sources = sources, 152 | rawValue = 0, 153 | value = 0, 154 | down = false, 155 | downPrevious = false, 156 | pressed = false, 157 | released = false, 158 | } 159 | end 160 | end 161 | 162 | -- initializes an axis pair object for each axis pair defined in the config 163 | function Player:_initPairs() 164 | self._pairs = {} 165 | for pairName, controls in pairs(self.config.pairs) do 166 | self._pairs[pairName] = { 167 | controls = controls, 168 | rawX = 0, 169 | rawY = 0, 170 | x = 0, 171 | y = 0, 172 | down = false, 173 | downPrevious = false, 174 | pressed = false, 175 | released = false, 176 | } 177 | end 178 | end 179 | 180 | function Player:_init(config) 181 | self:_loadConfig(config) 182 | self:_initControls() 183 | self:_initPairs() 184 | self._activeDevice = 'none' 185 | end 186 | 187 | --[[ 188 | detects the active device (keyboard/mouse or joystick). 189 | if the keyboard or mouse is currently being used, joystick 190 | inputs will be ignored. this is to prevent slight axis movements 191 | from adding errant inputs when someone's using the keyboard. 192 | 193 | the active device is saved to player._activeDevice, which is then 194 | used throughout the rest of the update loop to check only 195 | keyboard or joystick inputs. 196 | ]] 197 | function Player:_setActiveDevice() 198 | -- if the joystick is unset, then we should make sure _activeDevice 199 | -- isn't "joy" anymore, otherwise there will be an error later 200 | -- when we try to query a joystick that isn't there 201 | if self._activeDevice == 'joy' and not self.config.joystick then 202 | self._activeDevice = 'none' 203 | end 204 | for _, control in pairs(self._controls) do 205 | for _, source in ipairs(control.sources) do 206 | local type, value = parseSource(source) 207 | if sourceFunction.keyboardMouse[type] then 208 | if sourceFunction.keyboardMouse[type](value) > self.config.deadzone then 209 | self._activeDevice = 'kbm' 210 | return 211 | end 212 | elseif self.config.joystick and sourceFunction.joystick[type] then 213 | if sourceFunction.joystick[type](self.config.joystick, value) > self.config.deadzone then 214 | self._activeDevice = 'joy' 215 | end 216 | end 217 | end 218 | end 219 | end 220 | 221 | --[[ 222 | gets the value of a control by running the appropriate source functions 223 | for all of its sources. does not apply deadzone. 224 | ]] 225 | function Player:_getControlRawValue(control) 226 | local rawValue = 0 227 | for _, source in ipairs(control.sources) do 228 | local type, value = parseSource(source) 229 | if sourceFunction.keyboardMouse[type] and self._activeDevice == 'kbm' then 230 | if sourceFunction.keyboardMouse[type](value) == 1 then 231 | return 1 232 | end 233 | elseif sourceFunction.joystick[type] and self._activeDevice == 'joy' then 234 | rawValue = rawValue + sourceFunction.joystick[type](self.config.joystick, value) 235 | if rawValue >= 1 then 236 | return 1 237 | end 238 | end 239 | end 240 | return rawValue 241 | end 242 | 243 | --[[ 244 | updates each control in a player. saves the value with and without deadzone 245 | and the down/pressed/released state. 246 | ]] 247 | function Player:_updateControls() 248 | for _, control in pairs(self._controls) do 249 | control.rawValue = self:_getControlRawValue(control) 250 | control.value = control.rawValue >= self.config.deadzone and control.rawValue or 0 251 | control.downPrevious = control.down 252 | control.down = control.value > 0 253 | control.pressed = control.down and not control.downPrevious 254 | control.released = control.downPrevious and not control.down 255 | end 256 | end 257 | 258 | --[[ 259 | updates each axis pair in a player. saves the value with and without deadzone 260 | and the down/pressed/released state. 261 | ]] 262 | function Player:_updatePairs() 263 | for _, pair in pairs(self._pairs) do 264 | -- get raw x and y 265 | local l = self._controls[pair.controls[1]].rawValue 266 | local r = self._controls[pair.controls[2]].rawValue 267 | local u = self._controls[pair.controls[3]].rawValue 268 | local d = self._controls[pair.controls[4]].rawValue 269 | pair.rawX, pair.rawY = r - l, d - u 270 | 271 | -- limit to 1 272 | local len = math.sqrt(pair.rawX^2 + pair.rawY^2) 273 | if len > 1 then 274 | pair.rawX, pair.rawY = pair.rawX / len, pair.rawY / len 275 | end 276 | 277 | -- deadzone 278 | if self.config.squareDeadzone then 279 | pair.x = math.abs(pair.rawX) > self.config.deadzone and pair.rawX or 0 280 | pair.y = math.abs(pair.rawY) > self.config.deadzone and pair.rawY or 0 281 | else 282 | pair.x = len > self.config.deadzone and pair.rawX or 0 283 | pair.y = len > self.config.deadzone and pair.rawY or 0 284 | end 285 | 286 | -- down/pressed/released 287 | pair.downPrevious = pair.down 288 | pair.down = pair.x ~= 0 or pair.y ~= 0 289 | pair.pressed = pair.down and not pair.downPrevious 290 | pair.released = pair.downPrevious and not pair.down 291 | end 292 | end 293 | 294 | -- public API -- 295 | 296 | -- checks for changes in inputs 297 | function Player:update() 298 | self:_setActiveDevice() 299 | self:_updateControls() 300 | self:_updatePairs() 301 | end 302 | 303 | -- gets the value of a control or axis pair without deadzone applied 304 | function Player:getRaw(name) 305 | if self._pairs[name] then 306 | return self._pairs[name].rawX, self._pairs[name].rawY 307 | elseif self._controls[name] then 308 | return self._controls[name].rawValue 309 | else 310 | error('No control with name "' .. name .. '" defined', 3) 311 | end 312 | end 313 | 314 | -- gets the value of a control or axis pair with deadzone applied 315 | function Player:get(name) 316 | if self._pairs[name] then 317 | return self._pairs[name].x, self._pairs[name].y 318 | elseif self._controls[name] then 319 | return self._controls[name].value 320 | else 321 | error('No control with name "' .. name .. '" defined', 3) 322 | end 323 | end 324 | 325 | -- gets whether a control or axis pair is "held down" 326 | function Player:down(name) 327 | if self._pairs[name] then 328 | return self._pairs[name].down 329 | elseif self._controls[name] then 330 | return self._controls[name].down 331 | else 332 | error('No control with name "' .. name .. '" defined', 3) 333 | end 334 | end 335 | 336 | -- gets whether a control or axis pair was pressed this frame 337 | function Player:pressed(name) 338 | if self._pairs[name] then 339 | return self._pairs[name].pressed 340 | elseif self._controls[name] then 341 | return self._controls[name].pressed 342 | else 343 | error('No control with name "' .. name .. '" defined', 3) 344 | end 345 | end 346 | 347 | -- gets whether a control or axis pair was released this frame 348 | function Player:released(name) 349 | if self._pairs[name] then 350 | return self._pairs[name].released 351 | elseif self._controls[name] then 352 | return self._controls[name].released 353 | else 354 | error('No control with name "' .. name .. '" defined', 3) 355 | end 356 | end 357 | 358 | --[[ 359 | gets the currently active device (either "kbm", "joy", or "none"). 360 | this is useful for displaying instructional text. you may have 361 | a menu that says "press ENTER to confirm" or "press A to confirm" 362 | depending on whether the player is using their keyboard or gamepad. 363 | this function allows you to detect which they used most recently. 364 | ]] 365 | function Player:getActiveDevice() 366 | return self._activeDevice 367 | end 368 | 369 | -- main functions -- 370 | 371 | -- creates a new player with the user-provided config table 372 | function baton.new(config) 373 | local player = setmetatable({}, Player) 374 | player:_init(config) 375 | return player 376 | end 377 | 378 | return baton 379 | 380 | -------------------------------------------------------------------------------- /engine/colors.lua: -------------------------------------------------------------------------------- 1 | local hex = { 2 | white = "#FFFFFF", 3 | black = "#000000", 4 | skyblue = "#A7BFEF", 5 | blue = "#5959FF", 6 | red = "#EA2D23", 7 | yellow = "#FFCC4C", 8 | } 9 | 10 | local colors = {} 11 | colors.hex = hex 12 | 13 | for color, value in pairs(hex) do 14 | local r,g,b = lume.color(value) 15 | colors[color] = function (alpha) 16 | lg.setColor(r,g,b, alpha or 1) 17 | end 18 | end 19 | 20 | return colors 21 | -------------------------------------------------------------------------------- /engine/debugtools/console.lua: -------------------------------------------------------------------------------- 1 | -- expects lume and to exist globally 2 | local lg = love.graphics 3 | 4 | local function clamp(n, min, max) 5 | if min < max then 6 | return math.min(math.max(n, min), max) 7 | end 8 | 9 | return math.min(math.max(n, max), min) 10 | end 11 | 12 | local function map(n, start1, stop1, start2, stop2, withinBounds) 13 | local newval = (n - start1) / (stop1 - start1) * (stop2 - start2) + start2 14 | 15 | if not withinBounds then 16 | return newval 17 | end 18 | 19 | return clamp(newval, start2, stop2) 20 | end 21 | 22 | ---------------------------------------------------------------------------------------------------- 23 | -- console definition 24 | ---------------------------------------------------------------------------------------------------- 25 | 26 | local console = { 27 | enabled = false, 28 | anim = 0, 29 | text = "", 30 | wasKeyRepeat = false, 31 | font = lg.newFont((...):gsub("/console", "") .. "/menlo.ttf", 24), 32 | lines = {}, 33 | commandHistory = {}, 34 | commandIndex = 1, 35 | commands = {}, 36 | timer = 0, 37 | } 38 | 39 | function console:update(dt) 40 | local rate = 6 41 | dt = dt or 1/60 42 | if self.enabled then 43 | self.timer = self.timer + dt*4 44 | self.anim = math.min(self.anim + dt*rate, 1) 45 | else 46 | self.anim = math.max(self.anim - dt*rate, 0) 47 | end 48 | return self.enabled 49 | end 50 | 51 | function console:draw(gwidth, gheight) 52 | local r,g,b,a = lg.getColor() 53 | local anim = self.anim^2 54 | 55 | lg.setColor(0.1,0.1,0.1, 0.75) 56 | lg.push() 57 | local height = gheight and gheight/2 or lg.getHeight()/2 58 | lg.translate(0, (anim - 1)*height) 59 | lg.rectangle("fill", 0,0, gwidth or lg.getWidth(),height) 60 | 61 | local lastFont = lg.getFont() 62 | lg.setFont(self.font) 63 | lg.setColor(1,1,1) 64 | lg.print("> " .. self.text, 8, height - 8 - self.font:getHeight(self.text)) 65 | for i, line in ipairs(self.lines) do 66 | lg.setColor(1,1,1, map(i, 10,20, 1,0)) 67 | lg.print(line, 8, height - 8 - self.font:getHeight(line)*(i+1)) 68 | end 69 | lg.setColor(1,1,1, math.sin(self.timer)/2 + 0.5) 70 | lg.print(" |", 8 + self.font:getWidth(self.text) - self.font:getWidth(" ")/2, height - 8 - self.font:getHeight(self.text)) 71 | lg.setFont(lastFont) 72 | lg.pop() 73 | 74 | lg.setColor(r,g,b,a) 75 | end 76 | 77 | function console:textinput(text) 78 | if not self.enabled then return end 79 | if text ~= ":" then 80 | self.text = self.text .. text 81 | end 82 | end 83 | 84 | function console:addLine(line) 85 | table.insert(self.lines, 1, line) 86 | if #self.lines > 20 then 87 | table.remove(self.lines) 88 | end 89 | end 90 | 91 | console.log = console.addLine 92 | 93 | function console:addCommand(name, func) 94 | self.commands[name] = func 95 | end 96 | 97 | function console:execute(inputText) 98 | -- store a list of commands for arrow key shortcuts 99 | table.insert(self.commandHistory, 1, inputText) 100 | self:addLine("> " .. inputText) 101 | 102 | local args = lume.split(inputText) 103 | 104 | local function errorCatch(errormsg) 105 | self:addLine("ERROR: " .. errormsg) 106 | print(debug.traceback()) 107 | end 108 | 109 | local command = self.commands[args[1]] 110 | if command then 111 | xpcall(command, errorCatch, args) 112 | else 113 | self:addLine("command unrecognized") 114 | end 115 | end 116 | 117 | function console:keypressed(k) 118 | local function toggle(enabled) 119 | if not self.enabled and enabled then 120 | self.wasKeyRepeat = love.keyboard.hasKeyRepeat() 121 | end 122 | 123 | self.enabled = enabled 124 | self.timer = 0 125 | self.text = "" 126 | love.keyboard.setKeyRepeat(enabled or self.wasKeyRepeat) 127 | end 128 | 129 | if k == ";" and (love.keyboard.isDown("lshift") or love.keyboard.isDown("rshift")) then 130 | toggle(true) 131 | end 132 | 133 | if k == "escape" then 134 | toggle(false) 135 | end 136 | 137 | if not self.enabled then return end 138 | 139 | if k == "return" then 140 | self:execute(self.text) 141 | self.commandIndex = 0 142 | self.text = "" 143 | 144 | if love.keyboard.isDown("lshift") 145 | or love.keyboard.isDown("rshift") then 146 | toggle(false) 147 | end 148 | end 149 | 150 | -- queue up the last used command 151 | if k == "up" then 152 | self.commandIndex = math.min(self.commandIndex + 1, #self.commandHistory) 153 | local this = self.commandHistory[self.commandIndex] 154 | if this then 155 | self.text = this 156 | end 157 | end 158 | 159 | if k == "down" then 160 | self.commandIndex = math.max(self.commandIndex - 1, 1) 161 | local this = self.commandHistory[self.commandIndex] 162 | if this then 163 | self.text = this 164 | end 165 | end 166 | 167 | if k == "backspace" then 168 | if love.keyboard.isDown("lctrl") then 169 | -- delete last word 170 | local lastDeleted 171 | repeat 172 | if #self.text == 0 then break end 173 | lastDeleted = self.text:sub(#self.text, #self.text) 174 | self.text = self.text:sub(1, #self.text-1) 175 | until lastDeleted == " " 176 | else 177 | -- delete last character 178 | self.text = self.text:sub(1, #self.text-1) 179 | end 180 | end 181 | 182 | return self.enabled 183 | end 184 | 185 | console:addCommand("help", function(args) 186 | for command, _ in pairs(console.commands) do 187 | console:addLine(command) 188 | end 189 | end) 190 | 191 | console:addCommand("quit", function(args) 192 | love.event.push("quit") 193 | end) 194 | 195 | console:addCommand("q", function(args) 196 | love.event.push("quit") 197 | end) 198 | 199 | console:addLine("console initialized [" .. os.date("%c") .. "]") 200 | 201 | return console 202 | -------------------------------------------------------------------------------- /engine/debugtools/init.lua: -------------------------------------------------------------------------------- 1 | local path = ... 2 | local console = require(path .. "/console") 3 | local lurker 4 | 5 | ---------------------------------------------------------------------------------------------------- 6 | -- debug hooks 7 | ---------------------------------------------------------------------------------------------------- 8 | 9 | local debugtools = {} 10 | 11 | local showMem 12 | function debugtools.load(args) 13 | lurker = require(path .. "/lurker") 14 | 15 | console:addCommand("showmem", function (args) 16 | showMem = not showMem 17 | end) 18 | 19 | console:addCommand("reset", function (args) 20 | scene(GameScene()) 21 | end) 22 | 23 | console:addCommand("level", function (args) 24 | scene(GameScene()) 25 | local scene = scene() 26 | scene:setLevelActive(tonumber(args[2])) 27 | 28 | if args[3] and args[4] then 29 | local x, y = tonumber(args[3]), tonumber(args[4]) 30 | scene.player.x = x 31 | scene.player.y = y 32 | scene.player.spawnPoint.x = x 33 | scene.player.spawnPoint.y = y 34 | end 35 | end) 36 | 37 | -- compile all args into one long string 38 | local totalArgs = "" 39 | for i, arg in ipairs(args) do 40 | totalArgs = totalArgs .. arg .. " " 41 | end 42 | 43 | -- split totalArgs by semicolon, and pass each as a command to console 44 | local commandList = lume.split(totalArgs, ",") 45 | for _, command in ipairs(commandList) do 46 | console:execute(lume.trim(command)) 47 | end 48 | end 49 | 50 | function debugtools.update() 51 | if lurker then lurker.update() end 52 | return console:update() 53 | end 54 | 55 | function debugtools.draw() 56 | console:draw(lg.getWidth(), lg.getHeight()) 57 | if showMem then 58 | lg.setColor(0,0,0) 59 | lg.print(utils.round(collectgarbage("count"))) 60 | end 61 | end 62 | 63 | function debugtools.textinput(text) 64 | return console:textinput(text) 65 | end 66 | 67 | function debugtools.keypressed(k) 68 | console:keypressed(k) 69 | end 70 | 71 | return debugtools 72 | -------------------------------------------------------------------------------- /engine/debugtools/lurker.lua: -------------------------------------------------------------------------------- 1 | -- 2 | -- lurker 3 | -- 4 | -- Copyright (c) 2018 rxi 5 | -- 6 | -- This library is free software; you can redistribute it and/or modify it 7 | -- under the terms of the MIT license. See LICENSE for details. 8 | -- 9 | 10 | -- Assumes lume is in the same directory as this file if it does not exist 11 | -- as a global 12 | local lume = rawget(_G, "lume") or require((...):gsub("[^/.\\]+$", "lume")) 13 | 14 | local lurker = { _version = "1.0.1" } 15 | 16 | 17 | local dir = love.filesystem.enumerate or love.filesystem.getDirectoryItems 18 | local time = love.timer.getTime or os.time 19 | 20 | local function isdir(path) 21 | local info = love.filesystem.getInfo(path) 22 | return info.type == "directory" 23 | end 24 | 25 | local function lastmodified(path) 26 | local info = love.filesystem.getInfo(path, "file") 27 | return info.modtime 28 | end 29 | 30 | local lovecallbacknames = { 31 | "update", 32 | "load", 33 | "draw", 34 | "mousepressed", 35 | "mousereleased", 36 | "keypressed", 37 | "keyreleased", 38 | "focus", 39 | "quit", 40 | } 41 | 42 | 43 | function lurker.init() 44 | lurker.print("Initing lurker") 45 | lurker.path = "." 46 | lurker.preswap = function() end 47 | lurker.postswap = function() end 48 | lurker.interval = .5 49 | lurker.protected = true 50 | lurker.quiet = false 51 | lurker.lastscan = 0 52 | lurker.lasterrorfile = nil 53 | lurker.files = {} 54 | lurker.funcwrappers = {} 55 | lurker.lovefuncs = {} 56 | lurker.state = "init" 57 | lume.each(lurker.getchanged(), lurker.resetfile) 58 | return lurker 59 | end 60 | 61 | 62 | function lurker.print(...) 63 | print("[lurker] " .. lume.format(...)) 64 | end 65 | 66 | 67 | function lurker.listdir(path, recursive, skipdotfiles) 68 | path = (path == ".") and "" or path 69 | local function fullpath(x) return path .. "/" .. x end 70 | local t = {} 71 | for _, f in pairs(lume.map(dir(path), fullpath)) do 72 | if not skipdotfiles or not f:match("/%.[^/]*$") then 73 | if recursive and isdir(f) then 74 | t = lume.concat(t, lurker.listdir(f, true, true)) 75 | else 76 | table.insert(t, lume.trim(f, "/")) 77 | end 78 | end 79 | end 80 | return t 81 | end 82 | 83 | 84 | function lurker.initwrappers() 85 | for _, v in pairs(lovecallbacknames) do 86 | lurker.funcwrappers[v] = function(...) 87 | local args = {...} 88 | xpcall(function() 89 | return lurker.lovefuncs[v] and lurker.lovefuncs[v](unpack(args)) 90 | end, lurker.onerror) 91 | end 92 | lurker.lovefuncs[v] = love[v] 93 | end 94 | lurker.updatewrappers() 95 | end 96 | 97 | 98 | function lurker.updatewrappers() 99 | for _, v in pairs(lovecallbacknames) do 100 | if love[v] ~= lurker.funcwrappers[v] then 101 | lurker.lovefuncs[v] = love[v] 102 | love[v] = lurker.funcwrappers[v] 103 | end 104 | end 105 | end 106 | 107 | 108 | function lurker.onerror(e, nostacktrace) 109 | lurker.print("An error occurred; switching to error state") 110 | lurker.state = "error" 111 | 112 | -- Release mouse 113 | local setgrab = love.mouse.setGrab or love.mouse.setGrabbed 114 | setgrab(false) 115 | 116 | -- Set up callbacks 117 | for _, v in pairs(lovecallbacknames) do 118 | love[v] = function() end 119 | end 120 | 121 | love.update = lurker.update 122 | 123 | love.keypressed = function(k) 124 | if k == "escape" then 125 | lurker.print("Exiting...") 126 | love.event.quit() 127 | end 128 | end 129 | 130 | local stacktrace = nostacktrace and "" or 131 | lume.trim((debug.traceback("", 2):gsub("\t", ""))) 132 | local msg = lume.format("{1}\n\n{2}", {e, stacktrace}) 133 | print(e) 134 | local colors = { 135 | { lume.color("#1e1e2c", 256) }, 136 | { lume.color("#f0a3a3", 256) }, 137 | { lume.color("#92b5b0", 256) }, 138 | { lume.color("#66666a", 256) }, 139 | { lume.color("#cdcdcd", 256) }, 140 | } 141 | love.graphics.reset() 142 | love.graphics.setFont(love.graphics.newFont(12)) 143 | 144 | love.draw = function() 145 | local pad = 25 146 | local width = love.graphics.getWidth() 147 | 148 | local function drawhr(pos, color1, color2) 149 | local animpos = lume.smooth(pad, width - pad - 8, lume.pingpong(time())) 150 | if color1 then love.graphics.setColor(color1) end 151 | love.graphics.rectangle("fill", pad, pos, width - pad*2, 1) 152 | if color2 then love.graphics.setColor(color2) end 153 | love.graphics.rectangle("fill", animpos, pos, 8, 1) 154 | end 155 | 156 | local function drawtext(str, x, y, color, limit) 157 | love.graphics.setColor(color) 158 | love.graphics[limit and "printf" or "print"](str, x, y, limit) 159 | end 160 | 161 | love.graphics.setBackgroundColor(colors[1]) 162 | love.graphics.clear(0,0,0,1) 163 | 164 | drawtext("An error has occurred", pad, pad, colors[2]) 165 | drawtext("lurker", width - love.graphics.getFont():getWidth("lurker") - 166 | pad, pad, colors[4]) 167 | drawhr(pad + 32, colors[4], colors[5]) 168 | drawtext("If you fix the problem and update the file the program will " .. 169 | "resume", pad, pad + 46, colors[3]) 170 | drawhr(pad + 72, colors[4], colors[5]) 171 | drawtext(msg, pad, pad + 90, colors[5], width - pad * 2) 172 | 173 | love.graphics.reset() 174 | end 175 | end 176 | 177 | 178 | function lurker.exitinitstate() 179 | lurker.state = "normal" 180 | if lurker.protected then 181 | lurker.initwrappers() 182 | end 183 | end 184 | 185 | 186 | function lurker.exiterrorstate() 187 | lurker.state = "normal" 188 | for _, v in pairs(lovecallbacknames) do 189 | love[v] = lurker.funcwrappers[v] 190 | end 191 | end 192 | 193 | 194 | function lurker.update() 195 | if lurker.state == "init" then 196 | lurker.exitinitstate() 197 | end 198 | local diff = time() - lurker.lastscan 199 | if diff > lurker.interval then 200 | lurker.lastscan = lurker.lastscan + diff 201 | local changed = lurker.scan() 202 | if #changed > 0 and lurker.lasterrorfile then 203 | local f = lurker.lasterrorfile 204 | lurker.lasterrorfile = nil 205 | lurker.hotswapfile(f) 206 | end 207 | end 208 | end 209 | 210 | 211 | function lurker.getchanged() 212 | local function fn(f) 213 | return f:match("%.lua$") and lurker.files[f] ~= lastmodified(f) 214 | end 215 | return lume.filter(lurker.listdir(lurker.path, true, true), fn) 216 | end 217 | 218 | 219 | function lurker.modname(f) 220 | return (f:gsub("%.lua$", ""):gsub("[/\\]", ".")) 221 | end 222 | 223 | 224 | function lurker.resetfile(f) 225 | lurker.files[f] = lastmodified(f) 226 | end 227 | 228 | 229 | function lurker.hotswapfile(f) 230 | lurker.print("Hotswapping '{1}'...", {f}) 231 | if lurker.state == "error" then 232 | lurker.exiterrorstate() 233 | end 234 | if lurker.preswap(f) then 235 | lurker.print("Hotswap of '{1}' aborted by preswap", {f}) 236 | lurker.resetfile(f) 237 | return 238 | end 239 | local modname = lurker.modname(f) 240 | local t, ok, err = lume.time(lume.hotswap, modname) 241 | if ok then 242 | lurker.print("Swapped '{1}' in {2} secs", {f, t}) 243 | else 244 | lurker.print("Failed to swap '{1}' : {2}", {f, err}) 245 | if not lurker.quiet and lurker.protected then 246 | lurker.lasterrorfile = f 247 | lurker.onerror(err, true) 248 | lurker.resetfile(f) 249 | return 250 | end 251 | end 252 | lurker.resetfile(f) 253 | lurker.postswap(f) 254 | if lurker.protected then 255 | lurker.updatewrappers() 256 | end 257 | end 258 | 259 | 260 | function lurker.scan() 261 | if lurker.state == "init" then 262 | lurker.exitinitstate() 263 | end 264 | local changed = lurker.getchanged() 265 | lume.each(changed, lurker.hotswapfile) 266 | return changed 267 | end 268 | 269 | 270 | return lurker.init() 271 | -------------------------------------------------------------------------------- /engine/debugtools/menlo.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/groverburger/ld48/3c3cda2afb4e4dfa80587ee8bbb6b4f352d0a0ae/engine/debugtools/menlo.ttf -------------------------------------------------------------------------------- /engine/init.lua: -------------------------------------------------------------------------------- 1 | -- pinwheel engine by groverburger 2 | -- 3 | -- main.lua should contain only game-specific code 4 | -- the engine should contain all abstracted out game agnostic boilerplate 5 | 6 | local path = ... 7 | local pausedAudio = {} 8 | local debugtools 9 | 10 | local function requireAll(folder) 11 | local items = love.filesystem.getDirectoryItems(folder) 12 | for _, item in ipairs(items) do 13 | local file = folder .. '/' .. item 14 | local type = love.filesystem.getInfo(file).type 15 | if type == "file" then 16 | require(file:sub(1,-5)) 17 | elseif type == "directory" then 18 | requireAll(file) 19 | end 20 | end 21 | end 22 | 23 | return function (settings) 24 | -------------------------------------------------------------------------------- 25 | -- basic setup 26 | -------------------------------------------------------------------------------- 27 | 28 | -- make the debug console work correctly on windows 29 | io.stdout:setvbuf("no") 30 | lg = love.graphics 31 | lg.setDefaultFilter("nearest") 32 | local accumulator = 0 33 | local frametime = settings.frametime or 1/60 34 | local rollingAverage = {} 35 | local canvas = {lg.newCanvas(settings.gamewidth, settings.gameheight), depth = true} 36 | 37 | -------------------------------------------------------------------------------- 38 | -- initialize engine 39 | -------------------------------------------------------------------------------- 40 | -- settings: 41 | -- gamewidth: int 42 | -- gameheight: int 43 | -- debug: bool 44 | -- frametime: int 45 | -- web: bool 46 | -- postprocessing: shader 47 | 48 | engine = { 49 | settings = settings or {}, 50 | shake = 0, 51 | shakeSize = 1, 52 | path = path, 53 | } 54 | 55 | -------------------------------------------------------------------------------- 56 | -- load modules 57 | -------------------------------------------------------------------------------- 58 | 59 | -- make some useful libraries globally scoped 60 | -- libraries are lowercase, classes are uppercase 61 | lume = require(path .. "/lume") 62 | inspect = require(path .. "/inspect") 63 | class = require(path .. "/oops") 64 | utils = require(path .. "/utils") 65 | json = require(path .. "/json") 66 | scene = require(path .. "/scene") 67 | input = require(path .. "/input") 68 | colors = require(path .. "/colors") 69 | audio = require(path .. "/audio") 70 | 71 | -- load the components of the game 72 | requireAll("things") 73 | requireAll("scenes") 74 | requireAll("misc") 75 | 76 | -- optionally load in debugtools, make sure disable for release! 77 | debugtools = settings.debug and require(path .. "/debugtools") 78 | 79 | -- hijack love.run with a better one 80 | love.run = function () 81 | if love.load then love.load(love.arg.parseGameArguments(arg), arg) end 82 | if debugtools then debugtools.load(arg) end 83 | 84 | -- don't include load time in timestep 85 | love.timer.step() 86 | 87 | -- main loop function 88 | return function() 89 | -- process events 90 | if love.event then 91 | love.event.pump() 92 | for name, a,b,c,d,e,f in love.event.poll() do 93 | if name == "quit" then 94 | if not love.quit or not love.quit() then 95 | return a or 0 96 | end 97 | end 98 | 99 | -- debugtools gets priority 100 | if not (debugtools and debugtools[name] and debugtools[name](a,b,c,d,e,f)) then 101 | love.handlers[name](a,b,c,d,e,f) 102 | end 103 | 104 | -- resize the canvas according to engine settings 105 | local fixedCanvas = engine.settings.gamewidth or engine.settings.gameheight 106 | if name == "resize" and not fixedCanvas then 107 | love.timer.step() 108 | canvas[1] = lg.newCanvas() 109 | end 110 | 111 | -- pause and unpause audio when the window changes focus 112 | if name == "focus" then 113 | love.timer.step() 114 | if a then 115 | for _, v in ipairs(pausedAudio) do v:play() end 116 | else 117 | pausedAudio = love.audio.pause() 118 | end 119 | end 120 | 121 | -- so input can use mouse wheel as button presses 122 | if name == "wheelmoved" then 123 | input.mouse.scroll = b 124 | end 125 | 126 | -- only attach virtual mouse to real mouse when real mouse is moved 127 | -- so controllers can also move the virtual mouse 128 | if name == "mousemoved" then 129 | input.updateMouse() 130 | end 131 | end 132 | end 133 | 134 | -- don't update or draw when game window is not focused 135 | if love.window.hasFocus() and lg.isActive() then 136 | -- get the delta time 137 | local delta = love.timer.step() 138 | 139 | -- set some bounds on delta time 140 | delta = math.min(delta, 0.1) 141 | delta = math.max(delta, 0.0000001) 142 | 143 | -- fixed timestep 144 | -- update once, then draw once in a 1:1 ratio, so we don't have to worry about interpolation 145 | accumulator = accumulator + delta 146 | local iter = 0 147 | local updated = false 148 | while accumulator >= frametime and iter < 5 do 149 | accumulator = accumulator - frametime 150 | engine.shake = math.max(engine.shake - 1, 0) 151 | iter = iter + 1 152 | updated = true 153 | 154 | -- only update input if not in debug console 155 | if not (debugtools and debugtools.update()) then input.update() end 156 | 157 | -- update the game 158 | if love.update then love.update() end 159 | 160 | -- draw the game to a canvas, 161 | -- then draw the canvas scaled on the screen 162 | lg.origin() 163 | lg.clear(0,0,0,0) 164 | lg.setCanvas(canvas) 165 | lg.clear(lg.getBackgroundColor()) 166 | if love.draw then love.draw() end 167 | lg.setColor(1,1,1) 168 | lg.setCanvas() 169 | lg.setShader(engine.settings.postprocessing) 170 | local screenSize = math.min(lg.getWidth()/canvas[1]:getWidth(), lg.getHeight()/canvas[1]:getHeight()) 171 | local shake = engine.shake 172 | local shakeSize = engine.shakeSize 173 | local shakex = shake > 0 and math.sin(math.random()*2*math.pi)*shakeSize*screenSize or 0 174 | local shakey = shake > 0 and math.sin(math.random()*2*math.pi)*shakeSize*screenSize or 0 175 | lg.draw(canvas[1], lg.getWidth()/2 + shakex, lg.getHeight()/2 + shakey, 0, screenSize, screenSize, canvas[1]:getWidth()/2, canvas[1]:getHeight()/2) 176 | lg.setShader() 177 | if debugtools then debugtools.draw() end 178 | input.mouse.scroll = 0 179 | end 180 | accumulator = accumulator % frametime 181 | 182 | -- only swap buffers if the game updated, to prevent stutter 183 | if updated then 184 | lg.present() 185 | end 186 | 187 | love.timer.sleep(0.001) 188 | else 189 | -- sleep for longer and don't do anything 190 | -- when the window is not in focus 191 | love.timer.sleep(0.05) 192 | love.timer.step() 193 | end 194 | end 195 | end 196 | end 197 | -------------------------------------------------------------------------------- /engine/input.lua: -------------------------------------------------------------------------------- 1 | local input = {} 2 | local controllers = {} 3 | local baton = require(engine.path .. "/baton") 4 | local mouse = { 5 | x = love.mouse.getX(), 6 | y = love.mouse.getY(), 7 | xMove = 0, 8 | yMove = 0, 9 | xLast = love.mouse.getX(), 10 | yLast = love.mouse.getY(), 11 | scroll = 0, 12 | } 13 | input.mouse = mouse 14 | input.controllers = controllers 15 | 16 | function input.newController(name, ...) 17 | local this = baton.new(...) 18 | controllers[name] = this 19 | return this 20 | end 21 | 22 | function input.update() 23 | for _, c in pairs(controllers) do 24 | c:update() 25 | end 26 | end 27 | 28 | function input.updateMouse() 29 | mouse.xLast = mouse.x 30 | mouse.yLast = mouse.y 31 | local width, height = engine.settings.gamewidth, engine.settings.gameheight 32 | 33 | if width and height then 34 | local size = math.min(lg.getWidth()/width, lg.getHeight()/height) 35 | mouse.x = utils.map(love.mouse.getX(), lg.getWidth()/2 - width*size/2, lg.getWidth()/2 + width*size/2, 0, width) 36 | mouse.y = utils.map(love.mouse.getY(), lg.getHeight()/2 - height*size/2, lg.getHeight()/2 + height*size/2, 0, height) 37 | else 38 | mouse.x, mouse.y = love.mouse.getPosition() 39 | end 40 | 41 | mouse.x = utils.clamp(mouse.x, 0, engine.settings.gamewidth-1) 42 | mouse.y = utils.clamp(mouse.y, 0, engine.settings.gameheight-1) 43 | mouse.xMove = mouse.x - mouse.xLast 44 | mouse.yMove = mouse.y - mouse.yLast 45 | end 46 | 47 | function input.mouseCheck(button) 48 | if button == "wd" then 49 | return mouse.scroll < 0 50 | end 51 | 52 | if button == "wu" then 53 | return mouse.scroll > 0 54 | end 55 | 56 | if button == "left" then 57 | return love.mouse.isDown(1) 58 | end 59 | 60 | if button == "right" then 61 | return love.mouse.isDown(2) 62 | end 63 | 64 | if button == "middle" then 65 | return love.mouse.isDown(3) 66 | end 67 | end 68 | 69 | return input 70 | -------------------------------------------------------------------------------- /engine/inspect.lua: -------------------------------------------------------------------------------- 1 | 2 | local inspect ={ 3 | _VERSION = 'inspect.lua 3.1.0', 4 | _URL = 'http://github.com/kikito/inspect.lua', 5 | _DESCRIPTION = 'human-readable representations of tables', 6 | _LICENSE = [[ 7 | MIT LICENSE 8 | 9 | Copyright (c) 2013 Enrique García Cota 10 | 11 | Permission is hereby granted, free of charge, to any person obtaining a 12 | copy of this software and associated documentation files (the 13 | "Software"), to deal in the Software without restriction, including 14 | without limitation the rights to use, copy, modify, merge, publish, 15 | distribute, sublicense, and/or sell copies of the Software, and to 16 | permit persons to whom the Software is furnished to do so, subject to 17 | the following conditions: 18 | 19 | The above copyright notice and this permission notice shall be included 20 | in all copies or substantial portions of the Software. 21 | 22 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS 23 | OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 24 | MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. 25 | IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY 26 | CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, 27 | TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE 28 | SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 29 | ]] 30 | } 31 | 32 | local tostring = tostring 33 | 34 | inspect.KEY = setmetatable({}, {__tostring = function() return 'inspect.KEY' end}) 35 | inspect.METATABLE = setmetatable({}, {__tostring = function() return 'inspect.METATABLE' end}) 36 | 37 | local function rawpairs(t) 38 | return next, t, nil 39 | end 40 | 41 | -- Apostrophizes the string if it has quotes, but not aphostrophes 42 | -- Otherwise, it returns a regular quoted string 43 | local function smartQuote(str) 44 | if str:match('"') and not str:match("'") then 45 | return "'" .. str .. "'" 46 | end 47 | return '"' .. str:gsub('"', '\\"') .. '"' 48 | end 49 | 50 | -- \a => '\\a', \0 => '\\0', 31 => '\31' 51 | local shortControlCharEscapes = { 52 | ["\a"] = "\\a", ["\b"] = "\\b", ["\f"] = "\\f", ["\n"] = "\\n", 53 | ["\r"] = "\\r", ["\t"] = "\\t", ["\v"] = "\\v" 54 | } 55 | local longControlCharEscapes = {} -- \a => nil, \0 => \000, 31 => \031 56 | for i=0, 31 do 57 | local ch = string.char(i) 58 | if not shortControlCharEscapes[ch] then 59 | shortControlCharEscapes[ch] = "\\"..i 60 | longControlCharEscapes[ch] = string.format("\\%03d", i) 61 | end 62 | end 63 | 64 | local function escape(str) 65 | return (str:gsub("\\", "\\\\") 66 | :gsub("(%c)%f[0-9]", longControlCharEscapes) 67 | :gsub("%c", shortControlCharEscapes)) 68 | end 69 | 70 | local function isIdentifier(str) 71 | return type(str) == 'string' and str:match( "^[_%a][_%a%d]*$" ) 72 | end 73 | 74 | local function isSequenceKey(k, sequenceLength) 75 | return type(k) == 'number' 76 | and 1 <= k 77 | and k <= sequenceLength 78 | and math.floor(k) == k 79 | end 80 | 81 | local defaultTypeOrders = { 82 | ['number'] = 1, ['boolean'] = 2, ['string'] = 3, ['table'] = 4, 83 | ['function'] = 5, ['userdata'] = 6, ['thread'] = 7 84 | } 85 | 86 | local function sortKeys(a, b) 87 | local ta, tb = type(a), type(b) 88 | 89 | -- strings and numbers are sorted numerically/alphabetically 90 | if ta == tb and (ta == 'string' or ta == 'number') then return a < b end 91 | 92 | local dta, dtb = defaultTypeOrders[ta], defaultTypeOrders[tb] 93 | -- Two default types are compared according to the defaultTypeOrders table 94 | if dta and dtb then return defaultTypeOrders[ta] < defaultTypeOrders[tb] 95 | elseif dta then return true -- default types before custom ones 96 | elseif dtb then return false -- custom types after default ones 97 | end 98 | 99 | -- custom types are sorted out alphabetically 100 | return ta < tb 101 | end 102 | 103 | -- For implementation reasons, the behavior of rawlen & # is "undefined" when 104 | -- tables aren't pure sequences. So we implement our own # operator. 105 | local function getSequenceLength(t) 106 | local len = 1 107 | local v = rawget(t,len) 108 | while v ~= nil do 109 | len = len + 1 110 | v = rawget(t,len) 111 | end 112 | return len - 1 113 | end 114 | 115 | local function getNonSequentialKeys(t) 116 | local keys, keysLength = {}, 0 117 | local sequenceLength = getSequenceLength(t) 118 | for k,_ in rawpairs(t) do 119 | if not isSequenceKey(k, sequenceLength) then 120 | keysLength = keysLength + 1 121 | keys[keysLength] = k 122 | end 123 | end 124 | table.sort(keys, sortKeys) 125 | return keys, keysLength, sequenceLength 126 | end 127 | 128 | local function countTableAppearances(t, tableAppearances) 129 | tableAppearances = tableAppearances or {} 130 | 131 | if type(t) == 'table' then 132 | if not tableAppearances[t] then 133 | tableAppearances[t] = 1 134 | for k,v in rawpairs(t) do 135 | countTableAppearances(k, tableAppearances) 136 | countTableAppearances(v, tableAppearances) 137 | end 138 | countTableAppearances(getmetatable(t), tableAppearances) 139 | else 140 | tableAppearances[t] = tableAppearances[t] + 1 141 | end 142 | end 143 | 144 | return tableAppearances 145 | end 146 | 147 | local copySequence = function(s) 148 | local copy, len = {}, #s 149 | for i=1, len do copy[i] = s[i] end 150 | return copy, len 151 | end 152 | 153 | local function makePath(path, ...) 154 | local keys = {...} 155 | local newPath, len = copySequence(path) 156 | for i=1, #keys do 157 | newPath[len + i] = keys[i] 158 | end 159 | return newPath 160 | end 161 | 162 | local function processRecursive(process, item, path, visited) 163 | if item == nil then return nil end 164 | if visited[item] then return visited[item] end 165 | 166 | local processed = process(item, path) 167 | if type(processed) == 'table' then 168 | local processedCopy = {} 169 | visited[item] = processedCopy 170 | local processedKey 171 | 172 | for k,v in rawpairs(processed) do 173 | processedKey = processRecursive(process, k, makePath(path, k, inspect.KEY), visited) 174 | if processedKey ~= nil then 175 | processedCopy[processedKey] = processRecursive(process, v, makePath(path, processedKey), visited) 176 | end 177 | end 178 | 179 | local mt = processRecursive(process, getmetatable(processed), makePath(path, inspect.METATABLE), visited) 180 | if type(mt) ~= 'table' then mt = nil end -- ignore not nil/table __metatable field 181 | setmetatable(processedCopy, mt) 182 | processed = processedCopy 183 | end 184 | return processed 185 | end 186 | 187 | 188 | 189 | ------------------------------------------------------------------- 190 | 191 | local Inspector = {} 192 | local Inspector_mt = {__index = Inspector} 193 | 194 | function Inspector:puts(...) 195 | local args = {...} 196 | local buffer = self.buffer 197 | local len = #buffer 198 | for i=1, #args do 199 | len = len + 1 200 | buffer[len] = args[i] 201 | end 202 | end 203 | 204 | function Inspector:down(f) 205 | self.level = self.level + 1 206 | f() 207 | self.level = self.level - 1 208 | end 209 | 210 | function Inspector:tabify() 211 | self:puts(self.newline, string.rep(self.indent, self.level)) 212 | end 213 | 214 | function Inspector:alreadyVisited(v) 215 | return self.ids[v] ~= nil 216 | end 217 | 218 | function Inspector:getId(v) 219 | local id = self.ids[v] 220 | if not id then 221 | local tv = type(v) 222 | id = (self.maxIds[tv] or 0) + 1 223 | self.maxIds[tv] = id 224 | self.ids[v] = id 225 | end 226 | return tostring(id) 227 | end 228 | 229 | function Inspector:putKey(k) 230 | if isIdentifier(k) then return self:puts(k) end 231 | self:puts("[") 232 | self:putValue(k) 233 | self:puts("]") 234 | end 235 | 236 | function Inspector:putTable(t) 237 | if t == inspect.KEY or t == inspect.METATABLE then 238 | self:puts(tostring(t)) 239 | elseif self:alreadyVisited(t) then 240 | self:puts('') 241 | elseif self.level >= self.depth then 242 | self:puts('{...}') 243 | else 244 | if self.tableAppearances[t] > 1 then self:puts('<', self:getId(t), '>') end 245 | 246 | local nonSequentialKeys, nonSequentialKeysLength, sequenceLength = getNonSequentialKeys(t) 247 | local mt = getmetatable(t) 248 | 249 | self:puts('{') 250 | self:down(function() 251 | local count = 0 252 | for i=1, sequenceLength do 253 | if count > 0 then self:puts(',') end 254 | self:puts(' ') 255 | self:putValue(t[i]) 256 | count = count + 1 257 | end 258 | 259 | for i=1, nonSequentialKeysLength do 260 | local k = nonSequentialKeys[i] 261 | if count > 0 then self:puts(',') end 262 | self:tabify() 263 | self:putKey(k) 264 | self:puts(' = ') 265 | self:putValue(t[k]) 266 | count = count + 1 267 | end 268 | 269 | if type(mt) == 'table' then 270 | if count > 0 then self:puts(',') end 271 | self:tabify() 272 | self:puts(' = ') 273 | self:putValue(mt) 274 | end 275 | end) 276 | 277 | if nonSequentialKeysLength > 0 or type(mt) == 'table' then -- result is multi-lined. Justify closing } 278 | self:tabify() 279 | elseif sequenceLength > 0 then -- array tables have one extra space before closing } 280 | self:puts(' ') 281 | end 282 | 283 | self:puts('}') 284 | end 285 | end 286 | 287 | function Inspector:putValue(v) 288 | local tv = type(v) 289 | 290 | if tv == 'string' then 291 | self:puts(smartQuote(escape(v))) 292 | elseif tv == 'number' or tv == 'boolean' or tv == 'nil' or 293 | tv == 'cdata' or tv == 'ctype' then 294 | self:puts(tostring(v)) 295 | elseif tv == 'table' then 296 | self:putTable(v) 297 | else 298 | self:puts('<', tv, ' ', self:getId(v), '>') 299 | end 300 | end 301 | 302 | ------------------------------------------------------------------- 303 | 304 | function inspect.inspect(root, options) 305 | options = options or {} 306 | 307 | local depth = options.depth or math.huge 308 | local newline = options.newline or '\n' 309 | local indent = options.indent or ' ' 310 | local process = options.process 311 | 312 | if process then 313 | root = processRecursive(process, root, {}, {}) 314 | end 315 | 316 | local inspector = setmetatable({ 317 | depth = depth, 318 | level = 0, 319 | buffer = {}, 320 | ids = {}, 321 | maxIds = {}, 322 | newline = newline, 323 | indent = indent, 324 | tableAppearances = countTableAppearances(root) 325 | }, Inspector_mt) 326 | 327 | inspector:putValue(root) 328 | 329 | return table.concat(inspector.buffer) 330 | end 331 | 332 | setmetatable(inspect, { __call = function(_, ...) return inspect.inspect(...) end }) 333 | 334 | return inspect 335 | 336 | -------------------------------------------------------------------------------- /engine/json.lua: -------------------------------------------------------------------------------- 1 | -- 2 | -- json.lua 3 | -- 4 | -- Copyright (c) 2020 rxi 5 | -- 6 | -- Permission is hereby granted, free of charge, to any person obtaining a copy of 7 | -- this software and associated documentation files (the "Software"), to deal in 8 | -- the Software without restriction, including without limitation the rights to 9 | -- use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies 10 | -- of the Software, and to permit persons to whom the Software is furnished to do 11 | -- so, subject to the following conditions: 12 | -- 13 | -- The above copyright notice and this permission notice shall be included in all 14 | -- copies or substantial portions of the Software. 15 | -- 16 | -- THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 17 | -- IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 18 | -- FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 19 | -- AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 20 | -- LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 21 | -- OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 22 | -- SOFTWARE. 23 | -- 24 | 25 | local json = { _version = "0.1.2" } 26 | 27 | ------------------------------------------------------------------------------- 28 | -- Encode 29 | ------------------------------------------------------------------------------- 30 | 31 | local encode 32 | 33 | local escape_char_map = { 34 | [ "\\" ] = "\\", 35 | [ "\"" ] = "\"", 36 | [ "\b" ] = "b", 37 | [ "\f" ] = "f", 38 | [ "\n" ] = "n", 39 | [ "\r" ] = "r", 40 | [ "\t" ] = "t", 41 | } 42 | 43 | local escape_char_map_inv = { [ "/" ] = "/" } 44 | for k, v in pairs(escape_char_map) do 45 | escape_char_map_inv[v] = k 46 | end 47 | 48 | 49 | local function escape_char(c) 50 | return "\\" .. (escape_char_map[c] or string.format("u%04x", c:byte())) 51 | end 52 | 53 | 54 | local function encode_nil(val) 55 | return "null" 56 | end 57 | 58 | 59 | local function encode_table(val, stack) 60 | local res = {} 61 | stack = stack or {} 62 | 63 | -- Circular reference? 64 | if stack[val] then error("circular reference") end 65 | 66 | stack[val] = true 67 | 68 | if rawget(val, 1) ~= nil or next(val) == nil then 69 | -- Treat as array -- check keys are valid and it is not sparse 70 | local n = 0 71 | for k in pairs(val) do 72 | if type(k) ~= "number" then 73 | error("invalid table: mixed or invalid key types") 74 | end 75 | n = n + 1 76 | end 77 | if n ~= #val then 78 | error("invalid table: sparse array") 79 | end 80 | -- Encode 81 | for i, v in ipairs(val) do 82 | table.insert(res, encode(v, stack)) 83 | end 84 | stack[val] = nil 85 | return "[" .. table.concat(res, ",") .. "]" 86 | 87 | else 88 | -- Treat as an object 89 | for k, v in pairs(val) do 90 | if type(k) ~= "string" then 91 | error("invalid table: mixed or invalid key types") 92 | end 93 | table.insert(res, encode(k, stack) .. ":" .. encode(v, stack)) 94 | end 95 | stack[val] = nil 96 | return "{" .. table.concat(res, ",") .. "}" 97 | end 98 | end 99 | 100 | 101 | local function encode_string(val) 102 | return '"' .. val:gsub('[%z\1-\31\\"]', escape_char) .. '"' 103 | end 104 | 105 | 106 | local function encode_number(val) 107 | -- Check for NaN, -inf and inf 108 | if val ~= val or val <= -math.huge or val >= math.huge then 109 | error("unexpected number value '" .. tostring(val) .. "'") 110 | end 111 | return string.format("%.14g", val) 112 | end 113 | 114 | 115 | local type_func_map = { 116 | [ "nil" ] = encode_nil, 117 | [ "table" ] = encode_table, 118 | [ "string" ] = encode_string, 119 | [ "number" ] = encode_number, 120 | [ "boolean" ] = tostring, 121 | } 122 | 123 | 124 | encode = function(val, stack) 125 | local t = type(val) 126 | local f = type_func_map[t] 127 | if f then 128 | return f(val, stack) 129 | end 130 | error("unexpected type '" .. t .. "'") 131 | end 132 | 133 | 134 | function json.encode(val) 135 | return ( encode(val) ) 136 | end 137 | 138 | 139 | ------------------------------------------------------------------------------- 140 | -- Decode 141 | ------------------------------------------------------------------------------- 142 | 143 | local parse 144 | 145 | local function create_set(...) 146 | local res = {} 147 | for i = 1, select("#", ...) do 148 | res[ select(i, ...) ] = true 149 | end 150 | return res 151 | end 152 | 153 | local space_chars = create_set(" ", "\t", "\r", "\n") 154 | local delim_chars = create_set(" ", "\t", "\r", "\n", "]", "}", ",") 155 | local escape_chars = create_set("\\", "/", '"', "b", "f", "n", "r", "t", "u") 156 | local literals = create_set("true", "false", "null") 157 | 158 | local literal_map = { 159 | [ "true" ] = true, 160 | [ "false" ] = false, 161 | [ "null" ] = nil, 162 | } 163 | 164 | 165 | local function next_char(str, idx, set, negate) 166 | for i = idx, #str do 167 | if set[str:sub(i, i)] ~= negate then 168 | return i 169 | end 170 | end 171 | return #str + 1 172 | end 173 | 174 | 175 | local function decode_error(str, idx, msg) 176 | local line_count = 1 177 | local col_count = 1 178 | for i = 1, idx - 1 do 179 | col_count = col_count + 1 180 | if str:sub(i, i) == "\n" then 181 | line_count = line_count + 1 182 | col_count = 1 183 | end 184 | end 185 | error( string.format("%s at line %d col %d", msg, line_count, col_count) ) 186 | end 187 | 188 | 189 | local function codepoint_to_utf8(n) 190 | -- http://scripts.sil.org/cms/scripts/page.php?site_id=nrsi&id=iws-appendixa 191 | local f = math.floor 192 | if n <= 0x7f then 193 | return string.char(n) 194 | elseif n <= 0x7ff then 195 | return string.char(f(n / 64) + 192, n % 64 + 128) 196 | elseif n <= 0xffff then 197 | return string.char(f(n / 4096) + 224, f(n % 4096 / 64) + 128, n % 64 + 128) 198 | elseif n <= 0x10ffff then 199 | return string.char(f(n / 262144) + 240, f(n % 262144 / 4096) + 128, 200 | f(n % 4096 / 64) + 128, n % 64 + 128) 201 | end 202 | error( string.format("invalid unicode codepoint '%x'", n) ) 203 | end 204 | 205 | 206 | local function parse_unicode_escape(s) 207 | local n1 = tonumber( s:sub(1, 4), 16 ) 208 | local n2 = tonumber( s:sub(7, 10), 16 ) 209 | -- Surrogate pair? 210 | if n2 then 211 | return codepoint_to_utf8((n1 - 0xd800) * 0x400 + (n2 - 0xdc00) + 0x10000) 212 | else 213 | return codepoint_to_utf8(n1) 214 | end 215 | end 216 | 217 | 218 | local function parse_string(str, i) 219 | local res = "" 220 | local j = i + 1 221 | local k = j 222 | 223 | while j <= #str do 224 | local x = str:byte(j) 225 | 226 | if x < 32 then 227 | decode_error(str, j, "control character in string") 228 | 229 | elseif x == 92 then -- `\`: Escape 230 | res = res .. str:sub(k, j - 1) 231 | j = j + 1 232 | local c = str:sub(j, j) 233 | if c == "u" then 234 | local hex = str:match("^[dD][89aAbB]%x%x\\u%x%x%x%x", j + 1) 235 | or str:match("^%x%x%x%x", j + 1) 236 | or decode_error(str, j - 1, "invalid unicode escape in string") 237 | res = res .. parse_unicode_escape(hex) 238 | j = j + #hex 239 | else 240 | if not escape_chars[c] then 241 | decode_error(str, j - 1, "invalid escape char '" .. c .. "' in string") 242 | end 243 | res = res .. escape_char_map_inv[c] 244 | end 245 | k = j + 1 246 | 247 | elseif x == 34 then -- `"`: End of string 248 | res = res .. str:sub(k, j - 1) 249 | return res, j + 1 250 | end 251 | 252 | j = j + 1 253 | end 254 | 255 | decode_error(str, i, "expected closing quote for string") 256 | end 257 | 258 | 259 | local function parse_number(str, i) 260 | local x = next_char(str, i, delim_chars) 261 | local s = str:sub(i, x - 1) 262 | local n = tonumber(s) 263 | if not n then 264 | decode_error(str, i, "invalid number '" .. s .. "'") 265 | end 266 | return n, x 267 | end 268 | 269 | 270 | local function parse_literal(str, i) 271 | local x = next_char(str, i, delim_chars) 272 | local word = str:sub(i, x - 1) 273 | if not literals[word] then 274 | decode_error(str, i, "invalid literal '" .. word .. "'") 275 | end 276 | return literal_map[word], x 277 | end 278 | 279 | 280 | local function parse_array(str, i) 281 | local res = {} 282 | local n = 1 283 | i = i + 1 284 | while 1 do 285 | local x 286 | i = next_char(str, i, space_chars, true) 287 | -- Empty / end of array? 288 | if str:sub(i, i) == "]" then 289 | i = i + 1 290 | break 291 | end 292 | -- Read token 293 | x, i = parse(str, i) 294 | res[n] = x 295 | n = n + 1 296 | -- Next token 297 | i = next_char(str, i, space_chars, true) 298 | local chr = str:sub(i, i) 299 | i = i + 1 300 | if chr == "]" then break end 301 | if chr ~= "," then decode_error(str, i, "expected ']' or ','") end 302 | end 303 | return res, i 304 | end 305 | 306 | 307 | local function parse_object(str, i) 308 | local res = {} 309 | i = i + 1 310 | while 1 do 311 | local key, val 312 | i = next_char(str, i, space_chars, true) 313 | -- Empty / end of object? 314 | if str:sub(i, i) == "}" then 315 | i = i + 1 316 | break 317 | end 318 | -- Read key 319 | if str:sub(i, i) ~= '"' then 320 | decode_error(str, i, "expected string for key") 321 | end 322 | key, i = parse(str, i) 323 | -- Read ':' delimiter 324 | i = next_char(str, i, space_chars, true) 325 | if str:sub(i, i) ~= ":" then 326 | decode_error(str, i, "expected ':' after key") 327 | end 328 | i = next_char(str, i + 1, space_chars, true) 329 | -- Read value 330 | val, i = parse(str, i) 331 | -- Set 332 | res[key] = val 333 | -- Next token 334 | i = next_char(str, i, space_chars, true) 335 | local chr = str:sub(i, i) 336 | i = i + 1 337 | if chr == "}" then break end 338 | if chr ~= "," then decode_error(str, i, "expected '}' or ','") end 339 | end 340 | return res, i 341 | end 342 | 343 | 344 | local char_func_map = { 345 | [ '"' ] = parse_string, 346 | [ "0" ] = parse_number, 347 | [ "1" ] = parse_number, 348 | [ "2" ] = parse_number, 349 | [ "3" ] = parse_number, 350 | [ "4" ] = parse_number, 351 | [ "5" ] = parse_number, 352 | [ "6" ] = parse_number, 353 | [ "7" ] = parse_number, 354 | [ "8" ] = parse_number, 355 | [ "9" ] = parse_number, 356 | [ "-" ] = parse_number, 357 | [ "t" ] = parse_literal, 358 | [ "f" ] = parse_literal, 359 | [ "n" ] = parse_literal, 360 | [ "[" ] = parse_array, 361 | [ "{" ] = parse_object, 362 | } 363 | 364 | 365 | parse = function(str, idx) 366 | local chr = str:sub(idx, idx) 367 | local f = char_func_map[chr] 368 | if f then 369 | return f(str, idx) 370 | end 371 | decode_error(str, idx, "unexpected character '" .. chr .. "'") 372 | end 373 | 374 | 375 | function json.decode(str) 376 | if type(str) ~= "string" then 377 | error("expected argument of type string, got " .. type(str)) 378 | end 379 | local res, idx = parse(str, next_char(str, 1, space_chars, true)) 380 | idx = next_char(str, idx, space_chars, true) 381 | if idx <= #str then 382 | decode_error(str, idx, "trailing garbage") 383 | end 384 | return res 385 | end 386 | 387 | 388 | return json 389 | -------------------------------------------------------------------------------- /engine/lume.lua: -------------------------------------------------------------------------------- 1 | -- 2 | -- lume 3 | -- 4 | -- Copyright (c) 2020 rxi 5 | -- 6 | -- Permission is hereby granted, free of charge, to any person obtaining a copy of 7 | -- this software and associated documentation files (the "Software"), to deal in 8 | -- the Software without restriction, including without limitation the rights to 9 | -- use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies 10 | -- of the Software, and to permit persons to whom the Software is furnished to do 11 | -- so, subject to the following conditions: 12 | -- 13 | -- The above copyright notice and this permission notice shall be included in all 14 | -- copies or substantial portions of the Software. 15 | -- 16 | -- THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 17 | -- IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 18 | -- FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 19 | -- AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 20 | -- LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 21 | -- OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 22 | -- SOFTWARE. 23 | -- 24 | 25 | local lume = { _version = "2.3.0" } 26 | 27 | local pairs, ipairs = pairs, ipairs 28 | local type, assert, unpack = type, assert, unpack or table.unpack 29 | local tostring, tonumber = tostring, tonumber 30 | local math_floor = math.floor 31 | local math_ceil = math.ceil 32 | local math_atan2 = math.atan2 or math.atan 33 | local math_sqrt = math.sqrt 34 | local math_abs = math.abs 35 | 36 | local noop = function() 37 | end 38 | 39 | local identity = function(x) 40 | return x 41 | end 42 | 43 | local patternescape = function(str) 44 | return str:gsub("[%(%)%.%%%+%-%*%?%[%]%^%$]", "%%%1") 45 | end 46 | 47 | local absindex = function(len, i) 48 | return i < 0 and (len + i + 1) or i 49 | end 50 | 51 | local iscallable = function(x) 52 | if type(x) == "function" then return true end 53 | local mt = getmetatable(x) 54 | return mt and mt.__call ~= nil 55 | end 56 | 57 | local getiter = function(x) 58 | if lume.isarray(x) then 59 | return ipairs 60 | elseif type(x) == "table" then 61 | return pairs 62 | end 63 | error("expected table", 3) 64 | end 65 | 66 | local iteratee = function(x) 67 | if x == nil then return identity end 68 | if iscallable(x) then return x end 69 | if type(x) == "table" then 70 | return function(z) 71 | for k, v in pairs(x) do 72 | if z[k] ~= v then return false end 73 | end 74 | return true 75 | end 76 | end 77 | return function(z) return z[x] end 78 | end 79 | 80 | 81 | 82 | function lume.clamp(x, min, max) 83 | return x < min and min or (x > max and max or x) 84 | end 85 | 86 | 87 | function lume.round(x, increment) 88 | if increment then return lume.round(x / increment) * increment end 89 | return x >= 0 and math_floor(x + .5) or math_ceil(x - .5) 90 | end 91 | 92 | 93 | function lume.sign(x) 94 | return x < 0 and -1 or 1 95 | end 96 | 97 | 98 | function lume.lerp(a, b, amount) 99 | return a + (b - a) * lume.clamp(amount, 0, 1) 100 | end 101 | 102 | 103 | function lume.smooth(a, b, amount) 104 | local t = lume.clamp(amount, 0, 1) 105 | local m = t * t * (3 - 2 * t) 106 | return a + (b - a) * m 107 | end 108 | 109 | 110 | function lume.pingpong(x) 111 | return 1 - math_abs(1 - x % 2) 112 | end 113 | 114 | 115 | function lume.distance(x1, y1, x2, y2, squared) 116 | local dx = x1 - x2 117 | local dy = y1 - y2 118 | local s = dx * dx + dy * dy 119 | return squared and s or math_sqrt(s) 120 | end 121 | 122 | 123 | function lume.angle(x1, y1, x2, y2) 124 | return math_atan2(y2 - y1, x2 - x1) 125 | end 126 | 127 | 128 | function lume.vector(angle, magnitude) 129 | return math.cos(angle) * magnitude, math.sin(angle) * magnitude 130 | end 131 | 132 | 133 | function lume.random(a, b) 134 | if not a then a, b = 0, 1 end 135 | if not b then b = 0 end 136 | return a + math.random() * (b - a) 137 | end 138 | 139 | 140 | function lume.randomchoice(t) 141 | return t[math.random(#t)] 142 | end 143 | 144 | 145 | function lume.weightedchoice(t) 146 | local sum = 0 147 | for _, v in pairs(t) do 148 | assert(v >= 0, "weight value less than zero") 149 | sum = sum + v 150 | end 151 | assert(sum ~= 0, "all weights are zero") 152 | local rnd = lume.random(sum) 153 | for k, v in pairs(t) do 154 | if rnd < v then return k end 155 | rnd = rnd - v 156 | end 157 | end 158 | 159 | 160 | function lume.isarray(x) 161 | return type(x) == "table" and x[1] ~= nil 162 | end 163 | 164 | 165 | function lume.push(t, ...) 166 | local n = select("#", ...) 167 | for i = 1, n do 168 | t[#t + 1] = select(i, ...) 169 | end 170 | return ... 171 | end 172 | 173 | 174 | function lume.remove(t, x) 175 | local iter = getiter(t) 176 | for i, v in iter(t) do 177 | if v == x then 178 | if lume.isarray(t) then 179 | table.remove(t, i) 180 | break 181 | else 182 | t[i] = nil 183 | break 184 | end 185 | end 186 | end 187 | return x 188 | end 189 | 190 | 191 | function lume.clear(t) 192 | local iter = getiter(t) 193 | for k in iter(t) do 194 | t[k] = nil 195 | end 196 | return t 197 | end 198 | 199 | 200 | function lume.extend(t, ...) 201 | for i = 1, select("#", ...) do 202 | local x = select(i, ...) 203 | if x then 204 | for k, v in pairs(x) do 205 | t[k] = v 206 | end 207 | end 208 | end 209 | return t 210 | end 211 | 212 | 213 | function lume.shuffle(t) 214 | local rtn = {} 215 | for i = 1, #t do 216 | local r = math.random(i) 217 | if r ~= i then 218 | rtn[i] = rtn[r] 219 | end 220 | rtn[r] = t[i] 221 | end 222 | return rtn 223 | end 224 | 225 | 226 | function lume.sort(t, comp) 227 | local rtn = lume.clone(t) 228 | if comp then 229 | if type(comp) == "string" then 230 | table.sort(rtn, function(a, b) return a[comp] < b[comp] end) 231 | else 232 | table.sort(rtn, comp) 233 | end 234 | else 235 | table.sort(rtn) 236 | end 237 | return rtn 238 | end 239 | 240 | 241 | function lume.array(...) 242 | local t = {} 243 | for x in ... do t[#t + 1] = x end 244 | return t 245 | end 246 | 247 | 248 | function lume.each(t, fn, ...) 249 | local iter = getiter(t) 250 | if type(fn) == "string" then 251 | for _, v in iter(t) do v[fn](v, ...) end 252 | else 253 | for _, v in iter(t) do fn(v, ...) end 254 | end 255 | return t 256 | end 257 | 258 | 259 | function lume.map(t, fn) 260 | fn = iteratee(fn) 261 | local iter = getiter(t) 262 | local rtn = {} 263 | for k, v in iter(t) do rtn[k] = fn(v) end 264 | return rtn 265 | end 266 | 267 | 268 | function lume.all(t, fn) 269 | fn = iteratee(fn) 270 | local iter = getiter(t) 271 | for _, v in iter(t) do 272 | if not fn(v) then return false end 273 | end 274 | return true 275 | end 276 | 277 | 278 | function lume.any(t, fn) 279 | fn = iteratee(fn) 280 | local iter = getiter(t) 281 | for _, v in iter(t) do 282 | if fn(v) then return true end 283 | end 284 | return false 285 | end 286 | 287 | 288 | function lume.reduce(t, fn, first) 289 | local started = first ~= nil 290 | local acc = first 291 | local iter = getiter(t) 292 | for _, v in iter(t) do 293 | if started then 294 | acc = fn(acc, v) 295 | else 296 | acc = v 297 | started = true 298 | end 299 | end 300 | assert(started, "reduce of an empty table with no first value") 301 | return acc 302 | end 303 | 304 | 305 | function lume.unique(t) 306 | local rtn = {} 307 | for k in pairs(lume.invert(t)) do 308 | rtn[#rtn + 1] = k 309 | end 310 | return rtn 311 | end 312 | 313 | 314 | function lume.filter(t, fn, retainkeys) 315 | fn = iteratee(fn) 316 | local iter = getiter(t) 317 | local rtn = {} 318 | if retainkeys then 319 | for k, v in iter(t) do 320 | if fn(v) then rtn[k] = v end 321 | end 322 | else 323 | for _, v in iter(t) do 324 | if fn(v) then rtn[#rtn + 1] = v end 325 | end 326 | end 327 | return rtn 328 | end 329 | 330 | 331 | function lume.reject(t, fn, retainkeys) 332 | fn = iteratee(fn) 333 | local iter = getiter(t) 334 | local rtn = {} 335 | if retainkeys then 336 | for k, v in iter(t) do 337 | if not fn(v) then rtn[k] = v end 338 | end 339 | else 340 | for _, v in iter(t) do 341 | if not fn(v) then rtn[#rtn + 1] = v end 342 | end 343 | end 344 | return rtn 345 | end 346 | 347 | 348 | function lume.merge(...) 349 | local rtn = {} 350 | for i = 1, select("#", ...) do 351 | local t = select(i, ...) 352 | local iter = getiter(t) 353 | for k, v in iter(t) do 354 | rtn[k] = v 355 | end 356 | end 357 | return rtn 358 | end 359 | 360 | 361 | function lume.concat(...) 362 | local rtn = {} 363 | for i = 1, select("#", ...) do 364 | local t = select(i, ...) 365 | if t ~= nil then 366 | local iter = getiter(t) 367 | for _, v in iter(t) do 368 | rtn[#rtn + 1] = v 369 | end 370 | end 371 | end 372 | return rtn 373 | end 374 | 375 | 376 | function lume.find(t, value) 377 | local iter = getiter(t) 378 | for k, v in iter(t) do 379 | if v == value then return k end 380 | end 381 | return nil 382 | end 383 | 384 | 385 | function lume.match(t, fn) 386 | fn = iteratee(fn) 387 | local iter = getiter(t) 388 | for k, v in iter(t) do 389 | if fn(v) then return v, k end 390 | end 391 | return nil 392 | end 393 | 394 | 395 | function lume.count(t, fn) 396 | local count = 0 397 | local iter = getiter(t) 398 | if fn then 399 | fn = iteratee(fn) 400 | for _, v in iter(t) do 401 | if fn(v) then count = count + 1 end 402 | end 403 | else 404 | if lume.isarray(t) then 405 | return #t 406 | end 407 | for _ in iter(t) do count = count + 1 end 408 | end 409 | return count 410 | end 411 | 412 | 413 | function lume.slice(t, i, j) 414 | i = i and absindex(#t, i) or 1 415 | j = j and absindex(#t, j) or #t 416 | local rtn = {} 417 | for x = i < 1 and 1 or i, j > #t and #t or j do 418 | rtn[#rtn + 1] = t[x] 419 | end 420 | return rtn 421 | end 422 | 423 | 424 | function lume.first(t, n) 425 | if not n then return t[1] end 426 | return lume.slice(t, 1, n) 427 | end 428 | 429 | 430 | function lume.last(t, n) 431 | if not n then return t[#t] end 432 | return lume.slice(t, -n, -1) 433 | end 434 | 435 | 436 | function lume.invert(t) 437 | local rtn = {} 438 | for k, v in pairs(t) do rtn[v] = k end 439 | return rtn 440 | end 441 | 442 | 443 | function lume.pick(t, ...) 444 | local rtn = {} 445 | for i = 1, select("#", ...) do 446 | local k = select(i, ...) 447 | rtn[k] = t[k] 448 | end 449 | return rtn 450 | end 451 | 452 | 453 | function lume.keys(t) 454 | local rtn = {} 455 | local iter = getiter(t) 456 | for k in iter(t) do rtn[#rtn + 1] = k end 457 | return rtn 458 | end 459 | 460 | 461 | function lume.clone(t) 462 | local rtn = {} 463 | for k, v in pairs(t) do rtn[k] = v end 464 | return rtn 465 | end 466 | 467 | 468 | function lume.fn(fn, ...) 469 | assert(iscallable(fn), "expected a function as the first argument") 470 | local args = { ... } 471 | return function(...) 472 | local a = lume.concat(args, { ... }) 473 | return fn(unpack(a)) 474 | end 475 | end 476 | 477 | 478 | function lume.once(fn, ...) 479 | local f = lume.fn(fn, ...) 480 | local done = false 481 | return function(...) 482 | if done then return end 483 | done = true 484 | return f(...) 485 | end 486 | end 487 | 488 | 489 | local memoize_fnkey = {} 490 | local memoize_nil = {} 491 | 492 | function lume.memoize(fn) 493 | local cache = {} 494 | return function(...) 495 | local c = cache 496 | for i = 1, select("#", ...) do 497 | local a = select(i, ...) or memoize_nil 498 | c[a] = c[a] or {} 499 | c = c[a] 500 | end 501 | c[memoize_fnkey] = c[memoize_fnkey] or {fn(...)} 502 | return unpack(c[memoize_fnkey]) 503 | end 504 | end 505 | 506 | 507 | function lume.combine(...) 508 | local n = select('#', ...) 509 | if n == 0 then return noop end 510 | if n == 1 then 511 | local fn = select(1, ...) 512 | if not fn then return noop end 513 | assert(iscallable(fn), "expected a function or nil") 514 | return fn 515 | end 516 | local funcs = {} 517 | for i = 1, n do 518 | local fn = select(i, ...) 519 | if fn ~= nil then 520 | assert(iscallable(fn), "expected a function or nil") 521 | funcs[#funcs + 1] = fn 522 | end 523 | end 524 | return function(...) 525 | for _, f in ipairs(funcs) do f(...) end 526 | end 527 | end 528 | 529 | 530 | function lume.call(fn, ...) 531 | if fn then 532 | return fn(...) 533 | end 534 | end 535 | 536 | 537 | function lume.time(fn, ...) 538 | local start = os.clock() 539 | local rtn = {fn(...)} 540 | return (os.clock() - start), unpack(rtn) 541 | end 542 | 543 | 544 | local lambda_cache = {} 545 | 546 | function lume.lambda(str) 547 | if not lambda_cache[str] then 548 | local args, body = str:match([[^([%w,_ ]-)%->(.-)$]]) 549 | assert(args and body, "bad string lambda") 550 | local s = "return function(" .. args .. ")\nreturn " .. body .. "\nend" 551 | lambda_cache[str] = lume.dostring(s) 552 | end 553 | return lambda_cache[str] 554 | end 555 | 556 | 557 | local serialize 558 | 559 | local serialize_map = { 560 | [ "boolean" ] = tostring, 561 | [ "nil" ] = tostring, 562 | [ "string" ] = function(v) return string.format("%q", v) end, 563 | [ "number" ] = function(v) 564 | if v ~= v then return "0/0" -- nan 565 | elseif v == 1 / 0 then return "1/0" -- inf 566 | elseif v == -1 / 0 then return "-1/0" end -- -inf 567 | return tostring(v) 568 | end, 569 | [ "table" ] = function(t, stk) 570 | stk = stk or {} 571 | if stk[t] then error("circular reference") end 572 | local rtn = {} 573 | stk[t] = true 574 | for k, v in pairs(t) do 575 | rtn[#rtn + 1] = "[" .. serialize(k, stk) .. "]=" .. serialize(v, stk) 576 | end 577 | stk[t] = nil 578 | return "{" .. table.concat(rtn, ",") .. "}" 579 | end 580 | } 581 | 582 | setmetatable(serialize_map, { 583 | __index = function(_, k) error("unsupported serialize type: " .. k) end 584 | }) 585 | 586 | serialize = function(x, stk) 587 | return serialize_map[type(x)](x, stk) 588 | end 589 | 590 | function lume.serialize(x) 591 | return serialize(x) 592 | end 593 | 594 | 595 | function lume.deserialize(str) 596 | return lume.dostring("return " .. str) 597 | end 598 | 599 | 600 | function lume.split(str, sep) 601 | if not sep then 602 | return lume.array(str:gmatch("([%S]+)")) 603 | else 604 | assert(sep ~= "", "empty separator") 605 | local psep = patternescape(sep) 606 | return lume.array((str..sep):gmatch("(.-)("..psep..")")) 607 | end 608 | end 609 | 610 | 611 | function lume.trim(str, chars) 612 | if not chars then return str:match("^[%s]*(.-)[%s]*$") end 613 | chars = patternescape(chars) 614 | return str:match("^[" .. chars .. "]*(.-)[" .. chars .. "]*$") 615 | end 616 | 617 | 618 | function lume.wordwrap(str, limit) 619 | limit = limit or 72 620 | local check 621 | if type(limit) == "number" then 622 | check = function(s) return #s >= limit end 623 | else 624 | check = limit 625 | end 626 | local rtn = {} 627 | local line = "" 628 | for word, spaces in str:gmatch("(%S+)(%s*)") do 629 | local s = line .. word 630 | if check(s) then 631 | table.insert(rtn, line .. "\n") 632 | line = word 633 | else 634 | line = s 635 | end 636 | for c in spaces:gmatch(".") do 637 | if c == "\n" then 638 | table.insert(rtn, line .. "\n") 639 | line = "" 640 | else 641 | line = line .. c 642 | end 643 | end 644 | end 645 | table.insert(rtn, line) 646 | return table.concat(rtn) 647 | end 648 | 649 | 650 | function lume.format(str, vars) 651 | if not vars then return str end 652 | local f = function(x) 653 | return tostring(vars[x] or vars[tonumber(x)] or "{" .. x .. "}") 654 | end 655 | return (str:gsub("{(.-)}", f)) 656 | end 657 | 658 | 659 | function lume.trace(...) 660 | local info = debug.getinfo(2, "Sl") 661 | local t = { info.short_src .. ":" .. info.currentline .. ":" } 662 | for i = 1, select("#", ...) do 663 | local x = select(i, ...) 664 | if type(x) == "number" then 665 | x = string.format("%g", lume.round(x, .01)) 666 | end 667 | t[#t + 1] = tostring(x) 668 | end 669 | print(table.concat(t, " ")) 670 | end 671 | 672 | 673 | function lume.dostring(str) 674 | return assert((loadstring or load)(str))() 675 | end 676 | 677 | 678 | function lume.uuid() 679 | local fn = function(x) 680 | local r = math.random(16) - 1 681 | r = (x == "x") and (r + 1) or (r % 4) + 9 682 | return ("0123456789abcdef"):sub(r, r) 683 | end 684 | return (("xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx"):gsub("[xy]", fn)) 685 | end 686 | 687 | 688 | function lume.hotswap(modname) 689 | local oldglobal = lume.clone(_G) 690 | local updated = {} 691 | local function update(old, new) 692 | if updated[old] then return end 693 | updated[old] = true 694 | local oldmt, newmt = getmetatable(old), getmetatable(new) 695 | if oldmt and newmt then update(oldmt, newmt) end 696 | for k, v in pairs(new) do 697 | if type(v) == "table" then update(old[k], v) else old[k] = v end 698 | end 699 | end 700 | local err = nil 701 | local function onerror(e) 702 | for k in pairs(_G) do _G[k] = oldglobal[k] end 703 | err = lume.trim(e) 704 | end 705 | local ok, oldmod = pcall(require, modname) 706 | oldmod = ok and oldmod or nil 707 | xpcall(function() 708 | package.loaded[modname] = nil 709 | local newmod = require(modname) 710 | if type(oldmod) == "table" then update(oldmod, newmod) end 711 | for k, v in pairs(oldglobal) do 712 | if v ~= _G[k] and type(v) == "table" then 713 | update(v, _G[k]) 714 | _G[k] = v 715 | end 716 | end 717 | end, onerror) 718 | package.loaded[modname] = oldmod 719 | if err then return nil, err end 720 | return oldmod 721 | end 722 | 723 | 724 | local ripairs_iter = function(t, i) 725 | i = i - 1 726 | local v = t[i] 727 | if v ~= nil then 728 | return i, v 729 | end 730 | end 731 | 732 | function lume.ripairs(t) 733 | return ripairs_iter, t, (#t + 1) 734 | end 735 | 736 | 737 | function lume.color(str, mul) 738 | mul = mul or 1 739 | local r, g, b, a 740 | r, g, b = str:match("#(%x%x)(%x%x)(%x%x)") 741 | if r then 742 | r = tonumber(r, 16) / 0xff 743 | g = tonumber(g, 16) / 0xff 744 | b = tonumber(b, 16) / 0xff 745 | a = 1 746 | elseif str:match("rgba?%s*%([%d%s%.,]+%)") then 747 | local f = str:gmatch("[%d.]+") 748 | r = (f() or 0) / 0xff 749 | g = (f() or 0) / 0xff 750 | b = (f() or 0) / 0xff 751 | a = f() or 1 752 | else 753 | error(("bad color string '%s'"):format(str)) 754 | end 755 | return r * mul, g * mul, b * mul, a * mul 756 | end 757 | 758 | 759 | local chain_mt = {} 760 | chain_mt.__index = lume.map(lume.filter(lume, iscallable, true), 761 | function(fn) 762 | return function(self, ...) 763 | self._value = fn(self._value, ...) 764 | return self 765 | end 766 | end) 767 | chain_mt.__index.result = function(x) return x._value end 768 | 769 | function lume.chain(value) 770 | return setmetatable({ _value = value }, chain_mt) 771 | end 772 | 773 | setmetatable(lume, { 774 | __call = function(_, ...) 775 | return lume.chain(...) 776 | end 777 | }) 778 | 779 | 780 | return lume 781 | -------------------------------------------------------------------------------- /engine/oops.lua: -------------------------------------------------------------------------------- 1 | -- object oriented programming system (oops) 2 | -- by groverburger, february 2021 3 | -- MIT License 4 | 5 | return function(parent) 6 | local class = {super = parent} 7 | local classMetatable = {__index = parent} 8 | local instanceMetatable = {__index = class} 9 | 10 | -- instantiate a class by calling it like a function 11 | function classMetatable:__call(...) 12 | local instance = setmetatable({}, instanceMetatable) 13 | if class.new then 14 | instance:new(...) 15 | end 16 | return instance 17 | end 18 | 19 | -- a class's metatable contains __call to instantiate it 20 | -- as well as a __index pointing to its parent class if it has one 21 | setmetatable(class, classMetatable) 22 | 23 | -- get the class that this instance is derived from 24 | function class:getClass() 25 | return class 26 | end 27 | 28 | -- check if this instance is a derived from this class - this checks the parent classes as well 29 | function class:instanceOf(someClass) 30 | return class == someClass or (parent and parent:instanceOf(someClass)) 31 | end 32 | 33 | function class:implement(otherClass) 34 | for i, v in pairs(otherClass) do 35 | if not self[i] and type(v) == "function" then 36 | self[i] = v 37 | end 38 | end 39 | end 40 | 41 | return class 42 | end 43 | -------------------------------------------------------------------------------- /engine/scene.lua: -------------------------------------------------------------------------------- 1 | local scene 2 | 3 | return function (newscene) 4 | if newscene then 5 | scene = newscene 6 | love.audio.stop() 7 | if scene.init then scene:init() end 8 | love.timer.step() 9 | end 10 | 11 | return scene 12 | end 13 | -------------------------------------------------------------------------------- /engine/utils.lua: -------------------------------------------------------------------------------- 1 | ---------------------------------------------------------------------------------------------------- 2 | -- useful math utility functions 3 | ---------------------------------------------------------------------------------------------------- 4 | 5 | local utils = {} 6 | 7 | ---------------------------------------------------------------------------------------------------- 8 | -- wave and conversion functions 9 | ---------------------------------------------------------------------------------------------------- 10 | 11 | function utils.lerp(a,b,t) 12 | return (1-t)*a + t*b 13 | end 14 | 15 | -- decimal determines to what decimal point it should round 16 | -- different case for negative numbers to round them correctly 17 | function utils.round(n, decimal) 18 | decimal = decimal or 0 19 | local pow = 10^decimal 20 | return (n >= 0 and math.floor(n*pow + 0.5) or math.ceil(n*pow - 0.5))/pow 21 | end 22 | 23 | function utils.sigmoid(n) 24 | return 1/(1+2.71828^(-1*n)) 25 | end 26 | 27 | function utils.sign(n) 28 | return (n > 0 and 1) or (n < 0 and -1) or 0 29 | end 30 | 31 | function utils.clamp(n, min, max) 32 | if min < max then 33 | return math.min(math.max(n, min), max) 34 | end 35 | 36 | return math.min(math.max(n, max), min) 37 | end 38 | 39 | function utils.map(n, start1, stop1, start2, stop2, withinBounds) 40 | local newval = (n - start1) / (stop1 - start1) * (stop2 - start2) + start2 41 | 42 | if not withinBounds then 43 | return newval 44 | end 45 | 46 | return utils.clamp(newval, start2, stop2) 47 | end 48 | 49 | ---------------------------------------------------------------------------------------------------- 50 | -- RNG 51 | ---------------------------------------------------------------------------------------------------- 52 | 53 | function utils.randomRange(low,high) 54 | return math.random()*(high-low) + low 55 | end 56 | 57 | -- returns a random element from the given table's array part 58 | function utils.choose(...) 59 | local count = select("#", ...) 60 | assert(count > 0, "Nothing provided to choose from!") 61 | local first = select(1, ...) 62 | 63 | -- if there's only one argument and it's a table, choose from that instead 64 | if count == 1 and type(first) == "table" then 65 | return first[math.random(#first)] 66 | end 67 | 68 | -- store in a local so we don't multi-return 69 | local ret = select(math.random(count), ...) 70 | return ret 71 | end 72 | 73 | function utils.randomish(number) 74 | return (math.sin(number*37.8)*1001 % 70 - 35) / 35 75 | end 76 | 77 | ---------------------------------------------------------------------------------------------------- 78 | -- vector functions 79 | ---------------------------------------------------------------------------------------------------- 80 | 81 | function utils.distance3d(x1,y1,z1, x2,y2,z2) 82 | return ((x2-x1)^2+(y2-y1)^2+(z2-z1)^2)^0.5 83 | end 84 | 85 | function utils.distance(x1,y1, x2,y2) 86 | return ((x2-x1)^2+(y2-y1)^2)^0.5 87 | end 88 | 89 | function utils.lengthdir(angle, length) 90 | return math.cos(angle)*length, math.sin(angle)*length 91 | end 92 | 93 | -- returns the angle between two points 94 | function utils.angle(x1,y1, x2,y2) 95 | if x2 and y2 then 96 | return math.atan2(y2-y1, x2-x1) 97 | end 98 | 99 | return math.atan2(y1,x1) 100 | end 101 | 102 | ---------------------------------------------------------------------------------------------------- 103 | -- misc 104 | ---------------------------------------------------------------------------------------------------- 105 | 106 | -- creates an animation from a horizontal animation strip 107 | -- assume all elements are squares based on the height of the strip 108 | function utils.newAnimation(path) 109 | local anim = {} 110 | anim.source = love.graphics.newImage(path) 111 | local width, height = anim.source:getWidth(), anim.source:getHeight() 112 | anim.size = height 113 | 114 | for i=0, math.floor(width/height) do 115 | local x = i*height 116 | anim[i+1] = love.graphics.newQuad(x,0, height,height, width,height) 117 | end 118 | 119 | return anim 120 | end 121 | 122 | -- unpacks a table like in lua 5.2 123 | function utils.unpack(tab, start, stop) 124 | if not start then start = math.min(1, #tab) end 125 | if not stop then stop = #tab end 126 | 127 | if start == stop then 128 | return tab[start] 129 | else 130 | return tab[start], utils.unpack(tab, start + 1, stop) 131 | end 132 | end 133 | 134 | return utils 135 | -------------------------------------------------------------------------------- /main.lua: -------------------------------------------------------------------------------- 1 | love.window.setIcon(love.image.newImageData("assets/sprites/gameicon.png")) 2 | 3 | require "engine" { 4 | gamewidth = 1024, 5 | gameheight = 768, 6 | debug = true, 7 | --postprocessing = love.graphics.newShader("assets/shaders/pixelscale.frag") 8 | } 9 | 10 | local showPauseMenu, showAudioMenu, paused 11 | local pauseMenu, audioMenu 12 | local volumes = {} 13 | 14 | local uifont = lg.newFont("assets/comicneuebold.ttf", 20) 15 | 16 | input.newController("menu", {controls = {ok = {"mouse:left"}, scrolldown = {"mouse:wd"}, scrollup = {"mouse:wu"}}}) 17 | local button = "ok" 18 | local controller = "menu" 19 | 20 | pauseMenu = GuiForm(1024/2 - 250/2, 768/2 - 238/2, 250, 238):setFont(uifont) 21 | pauseMenu:cut("top", 70) 22 | :setContent("paused") 23 | :setAlign("center") 24 | :setMargin(10) 25 | pauseMenu:cut("top", 36) 26 | :undercut("left", 20) 27 | :undercut("right", 20) 28 | :attach(GuiButton(function() showPauseMenu = false; paused = false end)) 29 | :setContent("resume") 30 | :setAlign("center") 31 | :setBorder(true) 32 | pauseMenu:cut("top", 20) 33 | pauseMenu:cut("top", 36) 34 | :undercut("left", 20) 35 | :undercut("right", 20) 36 | :attach(GuiButton(function() showAudioMenu = true; showPauseMenu = false end)) 37 | :setContent("audio settings") 38 | :setAlign("center") 39 | :setBorder(true) 40 | pauseMenu:cut("top", 20) 41 | pauseMenu:cut("top", 36) 42 | :undercut("left", 20) 43 | :undercut("right", 20) 44 | :attach(GuiButton(function() love.event.push("quit") end)) 45 | :setContent("quit game") 46 | :setAlign("center") 47 | :setBorder(true) 48 | 49 | audioMenu = GuiForm(1024/2 - 400/2, 768/2 - 270/2, 400, 270):setFont(uifont) 50 | audioMenu = audioMenu:undercut("right", 16):undercut("left", 16) 51 | audioMenu:cut("bottom", 16) 52 | audioMenu:cut("bottom", 36) 53 | :undercut("left", 140-16) 54 | :undercut("right", 140-16) 55 | :setContent("ok") 56 | :setAlign("center") 57 | :attach(GuiButton(function() showAudioMenu = false; showPauseMenu = true end)) 58 | :setBorder(true) 59 | audioMenu:cut("top", 70) 60 | :setContent("audio settings") 61 | :setAlign("center") 62 | :setMargin(10) 63 | local label = audioMenu:cut("left", 200) 64 | local h = 32 65 | label:cut("top", h):setContent("master volume:") 66 | audioMenu:cut("top", h):attach(GuiSlider(volumes, "master", 1)) 67 | audioMenu:cut("top", 16) 68 | label:cut("top", 16) 69 | label:cut("top", h):setContent("music volume:") 70 | audioMenu:cut("top", h):attach(GuiSlider(volumes, "music", 1)) 71 | audioMenu:cut("top", 16) 72 | label:cut("top", 16) 73 | label:cut("top", h):setContent("sound volume:") 74 | audioMenu:cut("top", h):attach(GuiSlider(volumes, "sound", 1)) 75 | 76 | function love.load(args) 77 | --engine.settings.postprocessing:send("width", 1024) 78 | --engine.settings.postprocessing:send("height", 768) 79 | --engine.settings.postprocessing:send("uvmod", 1) 80 | scene(TitleScene()) 81 | love.mouse.setVisible(false) 82 | end 83 | 84 | function love.update() 85 | local scene = scene() 86 | if scene.update and not paused then 87 | scene:update() 88 | end 89 | 90 | if love.keyboard.isDown("escape") then 91 | paused = true 92 | showPauseMenu = true 93 | end 94 | end 95 | 96 | local mouseCursor = lg.newImage("assets/sprites/cursor3.png") 97 | 98 | function love.draw() 99 | local scene = scene() 100 | if scene.draw then 101 | scene:draw() 102 | end 103 | 104 | if showPauseMenu then 105 | pauseMenu:draw() 106 | end 107 | if showAudioMenu then 108 | audioMenu:draw() 109 | 110 | love.audio.setVolume(volumes.master and volumes.master^2 or 1) 111 | audio.setSoundVolume(volumes.sound and volumes.sound^2 or 1) 112 | audio.setMusicVolume(volumes.music and volumes.music^2 or 1) 113 | end 114 | 115 | colors.white() 116 | lg.draw(mouseCursor, input.mouse.x, input.mouse.y, 0, 1, 1, 16, 16) 117 | end 118 | -------------------------------------------------------------------------------- /misc/alarm.lua: -------------------------------------------------------------------------------- 1 | Alarm = class() 2 | 3 | local function unpack(tab, start, stop) 4 | if not start then start = math.min(1, #tab) end 5 | if not stop then stop = #tab end 6 | 7 | if start == stop then 8 | return tab[start] 9 | else 10 | return tab[start], unpack(tab, start + 1, stop) 11 | end 12 | end 13 | 14 | function Alarm:new(callback, ...) 15 | self.callback = callback 16 | self.time = math.huge 17 | self.lastTime = math.huge 18 | self.args = {...} 19 | end 20 | 21 | function Alarm:set(timeLower, timeUpper) 22 | self.settingLower = timeLower 23 | assert(not timeUpper or timeUpper > timeLower, 24 | "Alarm upper bound must be greater than its lower bound! (" .. tostring(timeLower) .. ", " .. tostring(timeUpper) .. ")") 25 | self.settingUpper = timeUpper 26 | 27 | return self:reset() 28 | end 29 | 30 | function Alarm:reset() 31 | assert(self.settingLower, "Alarm must be set before it is reset!") 32 | 33 | if self.settingUpper then 34 | self.time = self.settingLower + math.random()*(self.settingUpper-self.settingLower) 35 | else 36 | self.time = self.settingLower 37 | end 38 | 39 | self.lastTime = self.time 40 | 41 | return self 42 | end 43 | 44 | function Alarm:unset() 45 | self.time = math.huge 46 | end 47 | 48 | function Alarm:isActive() 49 | return self.time ~= math.huge 50 | end 51 | 52 | function Alarm:getProgress() 53 | return math.min(1 - self.time / self.lastTime, 1) 54 | end 55 | 56 | function Alarm:update(dt) 57 | self.time = self.time - (dt or 1) 58 | if self.time <= 0 then 59 | self.time = math.huge 60 | if self.callback then 61 | self.callback(unpack(self.args)) 62 | end 63 | end 64 | end 65 | 66 | return Alarm 67 | -------------------------------------------------------------------------------- /misc/cutscenes/cutscene.lua: -------------------------------------------------------------------------------- 1 | Cutscene = class() 2 | 3 | function Cutscene:new() 4 | self.drawcalls = {} 5 | end 6 | 7 | function Cutscene:update() 8 | if coroutine.status(self.routine) ~= "dead" then 9 | lume.clear(self.drawcalls) 10 | local success, value = coroutine.resume(self.routine) 11 | assert(success, value) 12 | else 13 | self.dead = true 14 | end 15 | end 16 | 17 | -- unpack from the second place in the table 18 | local function unpack2(tab, start, stop) 19 | if not start then start = math.min(2, #tab) end 20 | if not stop then stop = #tab end 21 | 22 | if start == stop then 23 | return tab[start] 24 | elseif start < stop then 25 | return tab[start], unpack(tab, start + 1, stop) 26 | end 27 | end 28 | 29 | -- this is absolutely atrocious 30 | -- but necessary because lua treats nil as an argument in varargs 31 | function Cutscene:draw() 32 | for _, drawcall in ipairs(self.drawcalls) do 33 | if drawcall[1] then 34 | if #drawcall > 1 then 35 | drawcall[1](unpack2(drawcall)) 36 | else 37 | drawcall[1]() 38 | end 39 | end 40 | end 41 | end 42 | -------------------------------------------------------------------------------- /misc/cutscenes/warpcutscene.lua: -------------------------------------------------------------------------------- 1 | require "misc/cutscenes/cutscene" 2 | 3 | WarpCutscene = class(Cutscene) 4 | 5 | local warpSound = audio.newSound("assets/sounds/warp.wav", 1, 1) 6 | local warpStartSound = audio.newSound("assets/sounds/warpstart.wav", 1, 1) 7 | 8 | function WarpCutscene:new(warpDir) 9 | WarpCutscene.super.new(self) 10 | 11 | local function draw(...) 12 | table.insert(self.drawcalls, {...}) 13 | end 14 | 15 | self.routine = coroutine.create(function () 16 | local scene = scene() 17 | local player = scene.player 18 | local warpThing = player.currentWarp 19 | player.currentWarp = nil 20 | player.stretch.x = 1 21 | player.stretch.y = 1 22 | player.speed.x = 0 23 | player.speed.y = 15 24 | player.coyoteFrames = 0 25 | player.animIndex = 3 26 | 27 | local py = player.y 28 | local time = 80 29 | local jumpHeight = 180 30 | warpStartSound:play() 31 | for i=1, time do 32 | scene:pauseFrame() 33 | local value = utils.map(i, 1,time, 0,1) 34 | player.x = utils.lerp(player.x, warpThing.x, 0.1) 35 | 36 | if i == 20 then 37 | warpSound:play() 38 | end 39 | 40 | if i <= time/4 then 41 | player.y = py - (1-utils.map(i, 1,time/4, 1,0)^2)*jumpHeight 42 | end 43 | if i >= time*3/4 then 44 | player.y = py - (1-utils.map(i, time*3/4,time, 0,1)^2)*jumpHeight 45 | end 46 | 47 | scene.depthOffset = math.sin(utils.map(value, 0.25, 0.75, 0, math.pi/2, true)) * warpDir 48 | coroutine.yield() 49 | end 50 | 51 | player.spawnPoint.x = player.x 52 | player.spawnPoint.y = player.y 53 | 54 | scene.levelIndex = scene.levelIndex + warpDir 55 | scene:setLevelActive(scene.levelIndex) 56 | end) 57 | end 58 | -------------------------------------------------------------------------------- /misc/cutscenes/wincutscene.lua: -------------------------------------------------------------------------------- 1 | require "misc/cutscenes/cutscene" 2 | 3 | WinCutscene = class(Cutscene) 4 | 5 | local music = audio.newMusic("assets/music/win.mp3") 6 | 7 | function WinCutscene:new(throne) 8 | WinCutscene.super.new(self) 9 | 10 | love.audio.stop() 11 | music:play() 12 | 13 | local function draw(...) 14 | table.insert(self.drawcalls, {...}) 15 | end 16 | 17 | self.routine = coroutine.create(function () 18 | local scene = scene() 19 | local player = scene.player 20 | local i = 0 21 | scene.cameraTracking = false 22 | 23 | while i < 200 do 24 | i = i + 1 25 | scene:pauseFrame() 26 | player.x = utils.lerp(player.x, throne.x, 0.025) 27 | player.y = utils.lerp(player.y, throne.y, 0.025) 28 | scene.camera.x = utils.lerp(scene.camera.x, player.x, 0.1) 29 | scene.camera.y = utils.lerp(scene.camera.y, player.y, 0.1) 30 | player.speed.x = 0 31 | player.speed.y = 0 32 | player.stretch.x = 1 33 | player.stretch.y = 1 34 | 35 | if i > 30 then 36 | local str = "you win!" 37 | draw(colors.white,utils.map(i, 30,60, 0,1, true)) 38 | draw(lg.print, str, 1024/2 - lg.getFont():getWidth(str)/2, 768/2 - 100) 39 | end 40 | 41 | coroutine.yield() 42 | end 43 | 44 | while true do 45 | i = i + 1 46 | scene:pauseFrame() 47 | 48 | draw(colors.black, utils.map(i, 200,260, 0,1, true)) 49 | draw(lg.rectangle, "fill", 0, 0, 1024, 768) 50 | local str = "created by groverburger (zach b)" 51 | draw(colors.white) 52 | draw(lg.print, str, 1024/2 - lg.getFont():getWidth(str)/2, 768/2 - 100) 53 | 54 | local str = "music by juhani junkala (opengameart.org)" 55 | draw(lg.print, str, 1024/2 - lg.getFont():getWidth(str)/2, 768/2) 56 | 57 | local str = "thanks for playing!" 58 | draw(lg.print, str, 1024/2 - lg.getFont():getWidth(str)/2, 768/2 + 100) 59 | 60 | coroutine.yield() 61 | end 62 | end) 63 | end 64 | -------------------------------------------------------------------------------- /misc/gui/button.lua: -------------------------------------------------------------------------------- 1 | GuiButton = class() 2 | GuiButton.controller = "menu" 3 | GuiButton.button = "ok" 4 | 5 | function GuiButton:new(callback, controller, button) 6 | self.callback = callback 7 | end 8 | 9 | function GuiButton:draw(x,y,w,h) 10 | local hovered = input.mouse.x >= x and input.mouse.y >= y 11 | and input.mouse.x <= x + w and input.mouse.y <= y + h 12 | 13 | -- restricts clicks to only the visible part of the button 14 | local sx,sy,sw,sh = lg.getScissor() 15 | local scissorhovered = not sx or (input.mouse.x >= sx and input.mouse.y >= sy 16 | and input.mouse.x <= sx + sw and input.mouse.y <= sy + sh) 17 | 18 | if hovered and scissorhovered then 19 | lg.setColor(1,1,1, 0.25) 20 | if input.controllers[self.controller]:pressed(self.button) then 21 | lg.setColor(1,1,1, 0.5) 22 | self.callback() 23 | end 24 | lg.rectangle("fill", x,y,w,h) 25 | end 26 | end 27 | -------------------------------------------------------------------------------- /misc/gui/form.lua: -------------------------------------------------------------------------------- 1 | GuiForm = class() 2 | GuiForm.controller = "menu" 3 | GuiForm.scrollup = "scrollup" 4 | GuiForm.scrolldown = "scrolldown" 5 | GuiForm.freeScroll = true 6 | 7 | function GuiForm:new(x,y,w,h) 8 | self.x = x 9 | self.y = y 10 | self.w = w 11 | self.h = h 12 | -- the view dimensions for scrolling relative to current position 13 | self.vx = 0 14 | self.vy = 0 15 | self.vw = w 16 | self.vh = h 17 | -- the original dimensions for graphical scissor 18 | self.ox = x 19 | self.oy = y 20 | self.ow = w 21 | self.oh = h 22 | -- the total width and height that gets added when setScrollable adds more 23 | self.totalw = w 24 | self.totalh = h 25 | 26 | self.margin = 4 27 | self.children = {} 28 | self.attached = {} 29 | self.align = "left" 30 | end 31 | 32 | function GuiForm:cut(side, amount) 33 | local class = self:getClass() 34 | 35 | if side == "right" then 36 | self.w = math.max(self.w - amount, 0) 37 | local n = class(self.x + self.w, self.y, amount, self.h) 38 | n.showingBorder = self.showingBorder 39 | n.font = self.font 40 | table.insert(self.children, n) 41 | return n 42 | end 43 | 44 | if side == "left" then 45 | self.x = self.x + amount 46 | self.w = math.max(self.w - amount, 0) 47 | local n = class(self.x - amount, self.y, amount, self.h) 48 | n.showingBorder = self.showingBorder 49 | n.font = self.font 50 | table.insert(self.children, n) 51 | return n 52 | end 53 | 54 | if side == "bottom" then 55 | self.h = math.max(self.h - amount, 0) 56 | local n = class(self.x, self.y + self.h, self.w, amount) 57 | n.showingBorder = self.showingBorder 58 | n.font = self.font 59 | table.insert(self.children, n) 60 | return n 61 | end 62 | 63 | if side == "top" then 64 | self.y = self.y + amount 65 | self.h = math.max(self.h - amount, 0) 66 | local n = class(self.x, self.y - amount, self.w, amount) 67 | n.showingBorder = self.showingBorder 68 | n.font = self.font 69 | table.insert(self.children, n) 70 | return n 71 | end 72 | 73 | error("GuiForm side " .. side .. " does not exist!") 74 | end 75 | 76 | function GuiForm:undercut(...) 77 | self:cut(...) 78 | return self 79 | end 80 | 81 | function GuiForm:setContent(what) 82 | self.content = what 83 | return self 84 | end 85 | 86 | function GuiForm:attach(...) 87 | local count = select("#", ...) 88 | for i=1, count do 89 | local item = select(i, ...) 90 | table.insert(self.attached, item) 91 | end 92 | return self 93 | end 94 | 95 | function GuiForm:setAlign(what) 96 | self.align = what 97 | return self 98 | end 99 | 100 | function GuiForm:setMargin(what) 101 | self.margin = what 102 | return self 103 | end 104 | 105 | function GuiForm:setBorder(what) 106 | self.showingBorder = what 107 | return self 108 | end 109 | 110 | function GuiForm:setFont(what) 111 | self.font = what 112 | return self 113 | end 114 | 115 | function GuiForm:setScrollable(dir, amount, scrollbar) 116 | if dir == "down" then 117 | if scrollbar then 118 | local scroll = self:cut("left", self.w-(tonumber(scrollbar) or 10)):setScrollable(dir, amount) 119 | self:attach(GuiScrollbarV(scroll)) 120 | return scroll, self 121 | end 122 | self.scrolldir = dir 123 | self.h = self.h + amount 124 | self.totalh = self.totalh + amount 125 | return self 126 | end 127 | 128 | if dir == "right" then 129 | if scrollbar then 130 | local scroll = self:cut("top", self.h-(tonumber(scrollbar) or 10)):setScrollable(dir, amount) 131 | self:attach(GuiScrollbarH(scroll)) 132 | return scroll, self 133 | end 134 | self.scrolldir = dir 135 | self.w = self.w + amount 136 | self.totalw = self.totalw + amount 137 | return self 138 | end 139 | 140 | error(dir .. " is not a valid GuiForm scroll direction!") 141 | end 142 | 143 | function GuiForm:scroll(dx,dy) 144 | assert(self.scrolldir, "This GuiForm is not scrollable!") 145 | self.vx = self.vx + dx 146 | self.vy = self.vy + dy 147 | return self 148 | end 149 | 150 | function GuiForm:setScrollAmount(x,y) 151 | assert(self.scrolldir, "This GuiForm is not scrollable!") 152 | self.vx = utils.lerp(0, self.totalw - self.vw, x or 0) 153 | self.vy = utils.lerp(0, self.totalh - self.vh, y or 0) 154 | return self 155 | end 156 | 157 | function GuiForm:draw(xoff,yoff) 158 | if not self.scrolldir then 159 | -- keep all children in view when not scrolling 160 | self.vx = 0 161 | self.vy = 0 162 | self.vw = self.ow 163 | self.vh = self.oh 164 | else 165 | -- keep scroll in bounds when scrolling 166 | self.vx = utils.clamp(self.vx, 0, self.totalw - self.vw) 167 | self.vy = utils.clamp(self.vy, 0, self.totalh - self.vh) 168 | end 169 | 170 | -- if this is the top-level form, save the previous graphics transform 171 | -- in case there was a scissor going on up there 172 | local original = xoff == nil 173 | if original then lg.push("all") end 174 | 175 | local xoff = xoff and utils.round(xoff) or 0 176 | local yoff = yoff and utils.round(yoff) or 0 177 | 178 | -- get previous scissor, and intersect with it 179 | local sx,sy,sw,sh = lg.getScissor() 180 | if sx then 181 | lg.intersectScissor(xoff + self.ox, yoff + self.oy, self.vw, self.vh) 182 | else 183 | lg.setScissor(xoff + self.ox, yoff + self.oy, self.ow, self.oh) 184 | end 185 | 186 | -- let the user use their scroll buttons when hovering 187 | local _sx,_sy,_sw,_sh = lg.getScissor() 188 | if input.mouse.x >= _sx and input.mouse.y >= _sy 189 | and input.mouse.y <= _sx+_sw and input.mouse.y <= _sy+_sw and self.freeScroll then 190 | if input.controllers[self.controller]:down(self.scrollup) then 191 | self.vy = self.vy - 8 192 | end 193 | if input.controllers[self.controller]:down(self.scrolldown) then 194 | self.vy = self.vy + 8 195 | end 196 | 197 | self.vx = utils.clamp(self.vx, 0, self.totalw - self.vw) 198 | self.vy = utils.clamp(self.vy, 0, self.totalh - self.vh) 199 | end 200 | 201 | local xoff = xoff - self.vx 202 | local yoff = yoff - self.vy 203 | local dx, dy = xoff + self.x, yoff + self.y 204 | 205 | -- draw background 206 | lg.setColor(0,0,0, 0.85) 207 | lg.rectangle("fill", dx,dy,self.w,self.h) 208 | 209 | -- draw content 210 | local prevFont = lg.getFont() 211 | if self.font then lg.setFont(self.font) end 212 | self:drawContent(xoff,yoff) 213 | lg.setFont(prevFont) 214 | 215 | -- draw border, if this form has one 216 | if self.showingBorder then 217 | local w = lg.getLineWidth() 218 | lg.setColor(1,1,1) 219 | lg.setLineWidth(2) 220 | lg.rectangle("line", dx,dy,self.w,self.h) 221 | lg.setLineWidth(w) 222 | end 223 | 224 | -- reset previous scissor 225 | lg.setScissor(sx,sy,sw,sh) 226 | if original then lg.pop("all") end 227 | end 228 | 229 | function GuiForm:drawContent(xoff,yoff) 230 | lg.setColor(1,1,1) 231 | local dx, dy = (xoff or 0) + self.x, (yoff or 0) + self.y 232 | 233 | -- draw the children 234 | for _, child in ipairs(self.children) do 235 | child:draw(xoff,yoff) 236 | end 237 | 238 | -- draw attached element only when it is visible 239 | -- this prevents elements out of scrollview from being interacted with 240 | local sx,sy,sw,sh = lg.getScissor() 241 | if #self.attached > 0 and sw > 0 and sh > 0 then 242 | for _, attached in ipairs(self.attached) do 243 | attached:draw(dx,dy,self.w,self.h) 244 | end 245 | end 246 | 247 | -- draw content of this guiform 248 | if not self.content then return end 249 | 250 | local m = self.margin 251 | lg.setColor(1,1,1) 252 | 253 | -- if content is a string, then print it and wrap it 254 | if type(self.content) == "string" then 255 | lg.printf(self.content, dx + m, dy + m, self.w - m*2, self.align) 256 | end 257 | 258 | -- if content is an image, then draw it to fill the rect 259 | if type(self.content) == "userdata" and self.content.typeOf and self.content:typeOf("Image") then 260 | local sx,sy = self.w/self.content:getWidth(), self.h/self.content:getHeight() 261 | lg.draw(self.content, dx, dy, 0, sx, sy) 262 | end 263 | end 264 | -------------------------------------------------------------------------------- /misc/gui/scrollbar.lua: -------------------------------------------------------------------------------- 1 | GuiScrollbarV = class() 2 | GuiScrollbarV.controller = "menu" 3 | GuiScrollbarV.button = "ok" 4 | 5 | function GuiScrollbarV:new(scrollform) 6 | self.scrollform = scrollform 7 | end 8 | 9 | function GuiScrollbarV:draw(ox,oy,ow,oh) 10 | local form = self.scrollform 11 | local h = (form.vh / form.totalh) * oh 12 | local w = ow 13 | local y = oy + utils.map(form.vy, 0, form.totalh - form.vh, 0, oh - h) 14 | local x = ox 15 | 16 | if input.controllers[self.controller]:pressed(self.button) then 17 | if input.mouse.x >= x and input.mouse.y >= y 18 | and input.mouse.x <= x+w and input.mouse.y <= y+h then 19 | self.grabbed = true 20 | self.ox = x - input.mouse.x 21 | self.oy = y - input.mouse.y 22 | end 23 | end 24 | 25 | self.grabbed = self.grabbed and input.controllers[self.controller]:down(self.button) 26 | 27 | if self.grabbed then 28 | y = utils.clamp(input.mouse.y + self.oy, oy, oy+oh-h) 29 | local frac = utils.map(y, oy, oy+oh-h, 0, 1) 30 | form:setScrollAmount(0, frac) 31 | end 32 | 33 | lg.setColor(1,1,1) 34 | lg.rectangle("fill", x,y,w,h) 35 | end 36 | 37 | GuiScrollbarH = class() 38 | GuiScrollbarH.controller = "menu" 39 | GuiScrollbarH.button = "ok" 40 | 41 | function GuiScrollbarH:new(scrollform) 42 | self.scrollform = scrollform 43 | end 44 | 45 | function GuiScrollbarH:draw(ox,oy,ow,oh) 46 | local form = self.scrollform 47 | local h = oh 48 | local w = (form.vw / form.totalw) * ow 49 | local y = oy 50 | local x = ox + utils.map(form.vx, 0, form.totalw - form.vw, 0, ow - w) 51 | 52 | if input.controllers[self.controller]:pressed(self.button) then 53 | if input.mouse.x >= x and input.mouse.y >= y 54 | and input.mouse.x <= x+w and input.mouse.y <= y+h then 55 | self.grabbed = true 56 | self.ox = x - input.mouse.x 57 | self.oy = y - input.mouse.y 58 | end 59 | end 60 | 61 | self.grabbed = self.grabbed and input.controllers[self.controller]:down(self.button) 62 | 63 | if self.grabbed then 64 | x = utils.clamp(input.mouse.x + self.ox, ox, ox+ow-w) 65 | local frac = utils.map(x, ox, ox+ow-w, 0, 1) 66 | form:setScrollAmount(frac, 0) 67 | end 68 | 69 | lg.setColor(1,1,1) 70 | lg.rectangle("fill", x,y,w,h) 71 | end 72 | -------------------------------------------------------------------------------- /misc/gui/slider.lua: -------------------------------------------------------------------------------- 1 | GuiSlider = class() 2 | GuiSlider.controller = "menu" 3 | GuiSlider.button = "ok" 4 | 5 | function GuiSlider:new(table, key, value) 6 | self.value = value or 0.5 7 | self.grabbed = false 8 | self.table = table 9 | self.key = key or 1 10 | end 11 | 12 | function GuiSlider:draw(x,y,w,h) 13 | lg.setColor(1,1,1, 0.75) 14 | local r = 8 15 | local x = x + r+1 16 | local w = w - (r+1)*2 17 | local y = y + r+1+8 18 | local vx = utils.lerp(x, x + w, self.value) 19 | 20 | -- do the behavior 21 | if input.controllers[self.controller]:pressed(self.button) 22 | and not self.grabbed then 23 | if utils.distance(input.mouse.x, input.mouse.y, vx, y) <= r then 24 | self.grabbed = true 25 | self.offset = vx - input.mouse.x 26 | elseif math.abs(y - input.mouse.y) <= r and math.abs((x+x+w)/2 - input.mouse.x) <= w/2 then 27 | self.grabbed = true 28 | self.offset = 0 29 | end 30 | end 31 | self.grabbed = self.grabbed and input.controllers[self.controller]:down(self.button) 32 | if self.grabbed then 33 | self.value = utils.map(input.mouse.x + self.offset, x, x+w, 0, 1, true) 34 | self.table[self.key] = self.value 35 | end 36 | 37 | -- draw line and circle 38 | local vx = utils.lerp(x, x + w, self.value) 39 | local lw = lg.getLineWidth() 40 | lg.setLineWidth(lw + 3) 41 | lg.line(x, y, vx, y) 42 | lg.setLineWidth(lw) 43 | lg.line(vx, y, x + w, y) 44 | lg.setColor(1,1,1) 45 | lg.circle("fill", vx, y, r) 46 | end 47 | -------------------------------------------------------------------------------- /scenes/scene1.lua: -------------------------------------------------------------------------------- 1 | GameScene = class() 2 | 3 | local music1 = audio.newMusic("assets/music/level1.mp3", 0.35) 4 | local music2 = audio.newMusic("assets/music/level2.mp3", 0.35) 5 | local bossMusic = audio.newMusic("assets/music/boss.mp3", 0.5) 6 | 7 | ---------------------------------------------------------------------------------------------------- 8 | -- load the game map 9 | ---------------------------------------------------------------------------------------------------- 10 | 11 | local map = json.decode(love.filesystem.read("assets/map.ldtk")) 12 | local levels = {} 13 | local levelTexture = lg.newImage("assets/sprites/tile.png") 14 | for levelIndex, level in ipairs(map.levels) do 15 | local currentLevel = {} 16 | levels[levelIndex] = currentLevel 17 | 18 | for _, layer in ipairs(level.layerInstances) do 19 | if layer.__identifier == "IntGrid" then 20 | local width, height = layer.__cWid, layer.__cHei 21 | 22 | local sb = lg.newSpriteBatch(levelTexture) 23 | 24 | -- load the data itself 25 | currentLevel.width = width 26 | currentLevel.height = height 27 | currentLevel.sprite = sb 28 | for i, v in ipairs(layer.intGridCsv) do 29 | local x, y = (i-1)%width + 1, math.floor((i-1)/width) + 1 30 | if not currentLevel[x] then currentLevel[x] = {} end 31 | currentLevel[x][y] = v 32 | 33 | -- add tiles to the spritebatch 34 | if v == 1 then 35 | sb:add((x-1)*64 - 8, (y-1)*64 - 8) 36 | 37 | if y == height then 38 | sb:add((x-1)*64 - 8, (y)*64 - 8) 39 | sb:add((x-1)*64 - 8, (y+1)*64 - 8) 40 | sb:add((x-1)*64 - 8, (y+2)*64 - 8) 41 | sb:add((x-1)*64 - 8, (y+3)*64 - 8) 42 | sb:add((x-1)*64 - 8, (y+4)*64 - 8) 43 | sb:add((x-1)*64 - 8, (y+5)*64 - 8) 44 | 45 | if x == width then 46 | for xx=1, 5 do 47 | for yy=0, 7 do 48 | sb:add((x+xx)*64 - 8, (y+yy)*64 - 8) 49 | end 50 | end 51 | end 52 | 53 | if x == 1 then 54 | for xx=1, 5 do 55 | for yy=0, 7 do 56 | sb:add((x-xx)*64 - 8, (y+yy)*64 - 8) 57 | end 58 | end 59 | end 60 | end 61 | 62 | if x == width then 63 | sb:add((x-1)*64 - 8, (y-1)*64 - 8) 64 | sb:add((x)*64 - 8, (y-1)*64 - 8) 65 | sb:add((x+1)*64 - 8, (y-1)*64 - 8) 66 | sb:add((x+2)*64 - 8, (y-1)*64 - 8) 67 | sb:add((x+3)*64 - 8, (y-1)*64 - 8) 68 | sb:add((x+4)*64 - 8, (y-1)*64 - 8) 69 | end 70 | 71 | if x == 1 then 72 | sb:add((x-1)*64 - 8, (y-1)*64 - 8) 73 | sb:add((x-2)*64 - 8, (y-1)*64 - 8) 74 | sb:add((x-3)*64 - 8, (y-1)*64 - 8) 75 | sb:add((x-4)*64 - 8, (y-1)*64 - 8) 76 | sb:add((x-5)*64 - 8, (y-1)*64 - 8) 77 | sb:add((x-6)*64 - 8, (y-1)*64 - 8) 78 | end 79 | end 80 | end 81 | end 82 | 83 | if layer.__identifier == "Entities" then 84 | currentLevel.entities = layer.entityInstances 85 | end 86 | end 87 | end 88 | 89 | local bgFadeShader = lg.newShader("assets/shaders/bgfade.frag") 90 | 91 | local castle = 9 92 | function GameScene:new() 93 | self.camera = {x=640,y=7.5*64} 94 | self.cutscene = nil 95 | self.depthOffset = 0 96 | self.levelThings = {} 97 | self.cameraTracking = true 98 | self.lastLevelIndex = 1 99 | 100 | self.depthProps = { 101 | [castle] = { 102 | scale = 10, 103 | xoff = 0, 104 | yoff = -500, 105 | sprite = lg.newImage("assets/sprites/castle.png"), 106 | }, 107 | } 108 | end 109 | 110 | local castlePoint = castle - 0.9 111 | 112 | function GameScene:init() 113 | self.levelIndex = 1 114 | for i, level in ipairs(levels) do 115 | self:loadLevel(i, level) 116 | end 117 | self:setLevelActive(1) 118 | music1:play() 119 | end 120 | 121 | function GameScene:createThing(thing, levelIndex) 122 | assert(levelIndex, "no level index given!") 123 | 124 | thing.levelIndex = levelIndex 125 | table.insert(self.levelThings[levelIndex], thing) 126 | 127 | if thing:instanceOf(Enemy) and levelIndex == self.levelIndex then 128 | table.insert(self.enemyList, thing) 129 | end 130 | 131 | if thing.init then 132 | thing:init() 133 | end 134 | 135 | return thing 136 | end 137 | 138 | function GameScene:nextLevel() 139 | self.levelIndex = self.levelIndex + 1 140 | self:setLevelActive(self.levelIndex) 141 | end 142 | 143 | function GameScene:resetLevel() 144 | self:loadLevel(self.levelIndex, levels[self.levelIndex]) 145 | self:setLevelActive(self.levelIndex) 146 | end 147 | 148 | function GameScene:setLevelActive(index) 149 | if self.player and self.levelThings[self.levelIndex] then 150 | lume.remove(self.levelThings[self.levelIndex], self.player) 151 | end 152 | 153 | self.levelIndex = index 154 | self.depthOffset = 0 155 | self.thingList = self.levelThings[index] 156 | 157 | -- put all the enemies in their own list 158 | self.enemyList = {} 159 | for _, thing in ipairs(self.thingList) do 160 | thing.levelIndex = index 161 | if thing:instanceOf(Enemy) then 162 | table.insert(self.enemyList, thing) 163 | end 164 | end 165 | 166 | if self.player then 167 | table.insert(self.thingList, self.player) 168 | self.player.levelIndex = self.levelIndex 169 | end 170 | end 171 | 172 | function GameScene:loadLevel(index, level) 173 | local thingList = {} 174 | self.levelThings[index] = thingList 175 | 176 | for _, entity in ipairs(level.entities) do 177 | -- try to get the class from the global table, and make sure it exists 178 | local class = _G[entity.__identifier] 179 | if class and type(class) == "table" and class.getClass then 180 | local instance = class(entity.px[1], entity.px[2]) 181 | 182 | -- set the instance's level index so it knows what level it's in 183 | instance.levelIndex = index 184 | 185 | for _, field in ipairs(entity.fieldInstances) do 186 | if field.__identifier == "message" and class == Text then 187 | instance.message = field.__value 188 | end 189 | 190 | if field.__identifier == "keycolor" then 191 | instance.keycolor = field.__value 192 | end 193 | end 194 | 195 | -- save a reference to the player 196 | if class == Player then 197 | if not self.player then 198 | self.player = instance 199 | end 200 | else 201 | table.insert(thingList, instance) 202 | 203 | if instance.init then 204 | instance:init() 205 | end 206 | end 207 | else 208 | print("class " .. entity.__identifier .. " not found!") 209 | end 210 | end 211 | end 212 | 213 | function GameScene:getLevel(index) 214 | return levels[index] 215 | end 216 | 217 | function GameScene:pauseFrame() 218 | self.pausedThisFrame = true 219 | end 220 | 221 | local neighbors = {1,0,2,-1} 222 | 223 | function GameScene:update() 224 | if self.cutscene then 225 | self.cutscene:update() 226 | if self.cutscene.dead then 227 | self.cutscene = nil 228 | end 229 | end 230 | 231 | self.wasPausedThisFrame = self.pausedThisFrame 232 | if self.pausedThisFrame then 233 | self.pausedThisFrame = false 234 | return 235 | end 236 | 237 | if self.levelIndex >= castlePoint and self.lastLevelIndex < castlePoint then 238 | music1.source:stop() 239 | music2:play() 240 | end 241 | if self.levelIndex == #levels and self.lastLevelIndex < #levels then 242 | music2.source:stop() 243 | bossMusic:play() 244 | end 245 | self.lastLevelIndex = self.levelIndex 246 | 247 | -- update all things in the scene, cull the dead ones 248 | for _, v in pairs(neighbors) do 249 | local thingList = self.levelThings[self.levelIndex+v] 250 | if thingList then 251 | local i = 1 252 | while i <= #thingList do 253 | local thing = thingList[i] 254 | 255 | local canupdate = v >= 1 and thing ~= self.player 256 | canupdate = canupdate or v == 0 257 | canupdate = canupdate or v == -1 and thing:instanceOf(Bullet) 258 | 259 | if canupdate then 260 | if thing.dead then 261 | table.remove(thingList, i) 262 | if thing.onDeath then 263 | thing:onDeath() 264 | end 265 | else 266 | thing:update() 267 | i = i + 1 268 | end 269 | else 270 | i = i + 1 271 | end 272 | end 273 | end 274 | end 275 | 276 | -- camera tracking player and staying centered on level 277 | if self.cameraTracking then 278 | local currentLevel = self:getLevel(self.levelIndex) 279 | local px, py = self.player.x, self.player.y 280 | local cx, cy = currentLevel.width*32, currentLevel.height*32 281 | self.camera.x = utils.round(utils.lerp(self.camera.x, utils.clamp((px+cx)/2, 1024/2, currentLevel.width*64 - 1024/2), 0.2)) 282 | self.camera.y = utils.round(utils.lerp(self.camera.y, utils.clamp((py+cy)/2, 768/2, currentLevel.height*64 - 768/2), 0.2)) 283 | end 284 | end 285 | 286 | local furthest = 20 287 | 288 | local function getDepth(i) 289 | local scene = scene() 290 | return utils.lerp(0.1, 1, utils.map(i - scene.depthOffset, scene.levelIndex, scene.levelIndex+furthest, 1, 0)^5) 291 | end 292 | 293 | function GameScene:draw() 294 | if self.levelIndex + self.depthOffset >= castlePoint then 295 | lg.clear(lume.color("#555555")) 296 | else 297 | lg.clear(lume.color(colors.hex.skyblue)) 298 | end 299 | 300 | local nearestDepth = 1 + 10*self.depthOffset^2 301 | local furthestLevel = self.levelIndex+furthest 302 | local currentLevel = self:getLevel(self.levelIndex) 303 | 304 | -- draw the level and the levels further back 305 | -- in painter's order 306 | for i=furthestLevel, math.max(self.levelIndex-1, 1), -1 do 307 | lg.push() 308 | colors.white() 309 | 310 | -- higher depth is closer 311 | local depth = getDepth(i) 312 | local r,g,b = lume.color("#7D95C4") 313 | if self.levelIndex + self.depthOffset >= castlePoint then 314 | r,g,b = lume.color("#444444") 315 | end 316 | local alpha = 1--utils.map(depth, 0.18,0.2, 0,1) 317 | 318 | if i <= self.levelIndex then 319 | alpha = utils.map(depth, 1,1.035, 1,0.1) 320 | colors.white(alpha) 321 | end 322 | 323 | bgFadeShader:send("bgcolor", {r,g,b,depth^8}) 324 | lg.setShader(bgFadeShader) 325 | lg.translate(-self.camera.x*depth, -self.camera.y*depth) 326 | lg.translate(1024/2, 768/2) 327 | lg.scale(depth) 328 | 329 | if levels[i] then 330 | local sprite = levels[i].sprite 331 | colors.white(alpha) 332 | lg.draw(sprite) 333 | local things = self.levelThings[i] 334 | if things and i >= self.levelIndex then 335 | for _, thing in ipairs(things) do 336 | if thing ~= self.player then 337 | colors.white(alpha) 338 | thing:draw() 339 | end 340 | end 341 | end 342 | end 343 | 344 | local prop = self.depthProps[i] 345 | if prop and i ~= self.levelIndex then 346 | bgFadeShader:send("bgcolor", {r,g,b,getDepth(i-0.8)^8}) 347 | lg.translate(currentLevel.width*32, currentLevel.height*32) 348 | local r,g,b = lg.getColor() 349 | if i == self.levelIndex+1 then 350 | lg.setColor(r,g,b, utils.map(depth, 0.8,1, 1,0)) 351 | end 352 | 353 | lg.draw(prop.sprite, prop.xoff or 0, prop.yoff or 0, 0, prop.scale, prop.scale, prop.sprite:getWidth()/2, prop.sprite:getHeight()/2) 354 | end 355 | 356 | lg.pop() 357 | lg.setShader() 358 | end 359 | 360 | -- draw the player seperately 361 | -- because the player is not affected by depth 362 | lg.push() 363 | lg.translate(-self.camera.x, -self.camera.y) 364 | lg.translate(1024/2, 768/2) 365 | if self.player then 366 | colors.white() 367 | self.player:draw() 368 | end 369 | lg.pop() 370 | 371 | if self.cutscene then 372 | self.cutscene:draw() 373 | end 374 | 375 | local i = 0 376 | local player = self.player 377 | for color, _ in pairs(player.keys) do 378 | colors[color](0.8) 379 | lg.draw(Key.sprite.source, i*80 + 32, 32) 380 | i = i + 1 381 | end 382 | end 383 | -------------------------------------------------------------------------------- /scenes/title.lua: -------------------------------------------------------------------------------- 1 | TitleScene = class() 2 | 3 | local bg = lg.newImage("assets/sprites/title_small_outline.png") 4 | local font = lg.newFont("assets/comicneuebold.ttf", 64) 5 | local smallfont = lg.newFont("assets/comicneuebold.ttf", 48) 6 | local tinyfont = lg.newFont("assets/comicneuebold.ttf", 24) 7 | local music = audio.newMusic("assets/music/title.mp3", 0.35) 8 | local time = 0 9 | 10 | local controller = input.controllers.player 11 | 12 | local uifont = lg.newFont("assets/comicneuebold.ttf", 20) 13 | local menu, hmenu, hmenuscroll, submenu, scroll, scrollbar 14 | 15 | function TitleScene:init() 16 | menu = GuiForm(1024/2 - 150, 768/2 - 175, 300,350) 17 | :setBorder(true) 18 | :setFont(uifont) 19 | menu:cut("top", 50) 20 | :setContent("this is a menu") 21 | :setAlign("center") 22 | :setMargin(10) 23 | 24 | scroll = menu:cut("top", 250):setBorder(false):setScrollable("down", 200, true) 25 | --scrollbar = menu:cut("top", 250):setBorder(false) 26 | --scroll = scrollbar:cut("left", 290):setScrollable("down", 200) 27 | --scrollbar:attach(GuiScrollbarV(scroll)) 28 | scroll:cut("top", 50):setContent("part 1") 29 | scroll:cut("top", 50):setContent("part 2") 30 | scroll:cut("top", 50):setContent("part 3") 31 | scroll:cut("top", 50):setContent("part 4") 32 | scroll:cut("top", 50):setContent("part 5") 33 | scroll:cut("top", 50):setContent("part 6") 34 | scroll:cut("top", 50):setContent("part 7"):attach(GuiButton(function () print "test" end)) 35 | menu:setContent("footer!") 36 | 37 | hmenu = GuiForm(1024/2 - 100, 768/2 - 100, 200, 200) 38 | :setBorder(true) 39 | :setFont(uifont) 40 | hmenuscroll = hmenu:setScrollable("right", 100, true) 41 | submenu = hmenuscroll:cut("left", 50):setContent("part 2") 42 | :setScrollable("down", 200, true) 43 | :undercut("top", 25) 44 | :undercut("top", 25) 45 | :undercut("top", 25) 46 | :undercut("top", 25) 47 | :undercut("top", 25) 48 | :undercut("top", 25) 49 | submenu:cut("top", 25):setContent "swing your arms from side to side do the mario" 50 | submenu:undercut("top", 25) 51 | :undercut("top", 25) 52 | hmenuscroll:cut("left", 50):setContent("part 1") 53 | hmenuscroll:cut("left", 50):setContent("part 3") 54 | hmenuscroll:cut("left", 50):setContent(lg.newImage("assets/sprites/bullet.png")) 55 | hmenuscroll:cut("left", 50):setContent("part 5") 56 | hmenuscroll:cut("left", 50):setContent("part 6") 57 | 58 | --music:play() 59 | end 60 | 61 | function TitleScene:update() 62 | if controller:released("shoot") then 63 | scene(GameScene()) 64 | end 65 | time = time + 1 66 | end 67 | 68 | local function drawtext(str,dx,dy) 69 | colors.black() 70 | lg.print(str, dx,dy) 71 | end 72 | 73 | function TitleScene:draw() 74 | lg.draw(bg) 75 | lg.setFont(font) 76 | 77 | --menu:draw() 78 | --hmenu:setScrollAmount(time/300, nil) 79 | --scroll:scroll(0,1) 80 | --submenu:setScrollAmount(nil, time/300) 81 | 82 | lg.setFont(smallfont) 83 | drawtext("Click to start!", 100, 150 + math.sin(time*0.05)*4) 84 | lg.setFont(tinyfont) 85 | drawtext("Created by groverburger for Ludum Dare 48 in 48 hours\n@grover_burger on Twitter", 64,768-70) 86 | end 87 | -------------------------------------------------------------------------------- /things/backwarp.lua: -------------------------------------------------------------------------------- 1 | require "things/warp" 2 | 3 | BackWarp = class(Warp) 4 | BackWarp.sprite = utils.newAnimation("assets/sprites/backwarp.png") 5 | -------------------------------------------------------------------------------- /things/boom.lua: -------------------------------------------------------------------------------- 1 | require "things/thing" 2 | 3 | Boom = class(Thing) 4 | Boom.sprite = utils.newAnimation("assets/sprites/boom.png") 5 | 6 | local anim = {1,2,3, speed=0.4} 7 | 8 | function Boom:new(...) 9 | Boom.super.new(self, ...) 10 | self.size = utils.lerp(0.75,2,math.random()) 11 | self.wait = Alarm():set(utils.randomRange(0,8)) 12 | end 13 | 14 | function Boom:update() 15 | self.wait:update() 16 | if self.wait:isActive() then return end 17 | Boom.super.update(self) 18 | self:animate(anim) 19 | self.dead = self.dead or self.animTimer > #anim 20 | end 21 | 22 | function Boom:draw() 23 | if self.wait:isActive() then return end 24 | if self.sprite and not self.dead then 25 | self:subdraw(nil,nil,nil,self.size,self.size) 26 | end 27 | end 28 | -------------------------------------------------------------------------------- /things/bullet.lua: -------------------------------------------------------------------------------- 1 | require "things/thing" 2 | 3 | Bullet = class(Thing) 4 | Bullet.sprite = utils.newAnimation("assets/sprites/bullet.png") 5 | local sound = audio.newSound("assets/sounds/gun1.wav", 0.7) 6 | local esound = audio.newSound("assets/sounds/ebullet.wav", 0.4) 7 | local colSound = audio.newSound("assets/sounds/bulletcol.wav", 0.3) 8 | 9 | function Bullet:new(x,y,angle,owner,speed,time) 10 | Bullet.super.new(self, x,y) 11 | 12 | self.speed.mag = speed or 30 13 | self.speed.x, self.speed.y = utils.lengthdir(angle, self.speed.mag) 14 | self.life = Alarm():set(time or 18) 15 | self.animIndex = 0 16 | self.size = 1 17 | local player = scene().player 18 | self.owner = owner or player 19 | self.firstFrame = true 20 | 21 | if self.owner == player then 22 | sound:play() 23 | end 24 | end 25 | 26 | function Bullet:update() 27 | local scene = scene() 28 | self.x = self.x + self.speed.x 29 | self.y = self.y + self.speed.y 30 | 31 | if self.firstFrame and self:isLevelActive() and self.owner ~= scene.player then 32 | esound:play() 33 | self.firstFrame = false 34 | end 35 | 36 | for i=1, self.speed.mag, 2 do 37 | local angle = utils.angle(0,0,self.speed.x,self.speed.y) 38 | local hit = self:isSolid(self.x + math.cos(angle)*i, self.y + math.sin(angle)*i, true) 39 | if hit and not self.dead then 40 | if self:isLevelActive() then colSound:play() end 41 | self:createThing(Impact(self.x,self.y)) 42 | end 43 | self.dead = self.dead or hit 44 | end 45 | 46 | self.life:update() 47 | self.dead = self.dead or not self.life:isActive() 48 | 49 | if self.animIndex == 0 then 50 | self.animIndex = 1 51 | else 52 | self.animIndex = 2 53 | end 54 | 55 | if not self.owner:instanceOf(Enemy) then 56 | for _, enemy in ipairs(scene.enemyList) do 57 | if enemy:collisionAt(self.x,self.y) 58 | and enemy.levelIndex == self.levelIndex 59 | and enemy ~= self.owner then 60 | enemy:hit(self) 61 | self.dead = true 62 | self:createThing(Impact(self.x,self.y)) 63 | end 64 | end 65 | end 66 | 67 | local player = scene.player 68 | if math.abs(self.x - player.x) <= 12 69 | and math.abs(self.y - player.y) <= 32 70 | and player ~= self.owner 71 | and self:isLevelActive() then 72 | player:hit(self) 73 | self.dead = true 74 | self:createThing(Impact(self.x,self.y)) 75 | end 76 | end 77 | 78 | function Bullet:draw() 79 | if self.animIndex == 0 then self.animIndex = 1 end 80 | 81 | local r,g,b,a = lg.getColor() 82 | 83 | self.size = 1 84 | if self.animIndex == 2 then 85 | colors.red(a) 86 | self:subdraw(self.x + self.speed.x*-1, self.y + self.speed.y*-1) 87 | self:subdraw(self.x + self.speed.x*-0.5, self.y + self.speed.y*-0.5) 88 | colors.white(a) 89 | self:subdraw() 90 | else 91 | self.size = 1.5 92 | self:subdraw() 93 | end 94 | end 95 | -------------------------------------------------------------------------------- /things/enemies/bat.lua: -------------------------------------------------------------------------------- 1 | require "things/enemies/enemy" 2 | 3 | local batfly = audio.newSound("assets/sounds/batfly.wav") 4 | 5 | Bat = class(Enemy) 6 | Bat.sprite = utils.newAnimation("assets/sprites/bat.png") 7 | Bat.animIndex = 3 8 | Bat.state = 1 9 | 10 | local anims = { 11 | {3,4}, 12 | {1,2, speed=0.075}, 13 | } 14 | 15 | function Bat:new(x,y) 16 | Bat.super.new(self,x+32,y+48) 17 | self.hoverTime = 0 18 | self.oy = y+48 19 | self.alarms = { 20 | wakeup = Alarm(), 21 | } 22 | end 23 | 24 | function Bat:hit() 25 | Bat.super.hit(self) 26 | 27 | if self.state == 1 then 28 | self.state = 2 29 | self.alarms.wakeup:set(60) 30 | batfly:play() 31 | end 32 | end 33 | 34 | function Bat:update() 35 | Bat.super.update(self) 36 | 37 | self:animate(anims[self.state]) 38 | if self.alarms.wakeup:isActive() then 39 | local p = self.alarms.wakeup:getProgress() 40 | if p < 0.8 then 41 | self.y = self.oy + utils.map((1-p)^2, 1,0, 0,150) 42 | return 43 | end 44 | end 45 | 46 | local scene = scene() 47 | local player = scene.player 48 | if self.state == 1 then 49 | if math.abs(player.x-self.x) < 250 50 | and player.y > self.y + 64 51 | and self:isLevelActive() then 52 | self.state = 2 53 | self.alarms.wakeup:set(60) 54 | batfly:play() 55 | end 56 | elseif utils.distance(self.x,self.y,player.x,player.y) > 24 then 57 | local angle = utils.angle(self.x,self.y,player.x,player.y) 58 | self.speed.x, self.speed.y = utils.lengthdir(angle, 1.5) 59 | self.speed.y = self.speed.y + math.sin(self.hoverTime)*1.2 60 | self.hoverTime = self.hoverTime + 0.05 61 | 62 | if not self:isSolid(self.x + self.speed.x, self.y, true, true, true) then 63 | self.x = self.x + self.speed.x 64 | end 65 | 66 | if not self:isSolid(self.x, self.y + self.speed.y, true, true, true) then 67 | self.y = self.y + self.speed.y 68 | end 69 | end 70 | end 71 | 72 | function Bat:draw() 73 | local scene = scene() 74 | local dir = self.state == 2 and scene.player.x < self.x and -1 or 1 75 | 76 | self:drawKey() 77 | self:subdraw(nil,nil,nil,dir,nil) 78 | end 79 | -------------------------------------------------------------------------------- /things/enemies/enemy.lua: -------------------------------------------------------------------------------- 1 | require "things/thing" 2 | 3 | Enemy = class(Thing) 4 | 5 | local hitShader = lg.newShader("assets/shaders/white.frag") 6 | local deathSound = audio.newSound("assets/sounds/edeath.wav", 0.7) 7 | local hitSound = audio.newSound("assets/sounds/bullethit.wav", 0.5) 8 | local hitSound2 = audio.newSound("assets/sounds/bullethit2.wav", 0.5) 9 | 10 | function Enemy:new(x,y) 11 | Enemy.super.new(self, x,y) 12 | self.hp = 3 13 | self.hitflash = 0 14 | end 15 | 16 | function Enemy:hit(bullet) 17 | self.hp = self.hp - 1 18 | self.hitflash = 4 19 | utils.choose(hitSound, hitSound2):play() 20 | love.timer.sleep(0.02) 21 | end 22 | 23 | function Enemy:update() 24 | Enemy.super.update(self) 25 | self.hitflash = math.max(self.hitflash-1, 0) 26 | self.dead = self.dead or self.hp <= 0 27 | 28 | if self:isLevelActive() then 29 | local player = scene().player 30 | if utils.distance(player.x, player.y, self.x, self.y) <= 40 then 31 | if player.speed.y > 0.25 then 32 | self:hit(player) 33 | player:jump() 34 | elseif player.speed.y < 0 and player.y < self.y then 35 | -- let the player jump away from enemies after they've jumped on them 36 | else 37 | player:hit(self) 38 | end 39 | end 40 | end 41 | end 42 | 43 | function Enemy:collisionAt(x,y) 44 | return math.abs(x - self.x) <= 32 and math.abs(y - self.y) <= 32 45 | end 46 | 47 | function Enemy:onDeath() 48 | local scene = scene() 49 | for i, enemy in ipairs(scene.enemyList) do 50 | if enemy == self then 51 | table.remove(scene.enemyList, i) 52 | end 53 | end 54 | 55 | deathSound:play() 56 | for i=1, 3 do 57 | local x, y = utils.lengthdir(math.random()*2*math.pi, utils.randomRange(10,20)) 58 | x, y = x + self.x, y + self.y 59 | self:createThing(Boom(x,y)) 60 | end 61 | 62 | if self.keycolor then 63 | local key = Key(self.x,self.y) 64 | key.keycolor = self.keycolor 65 | self:createThing(key) 66 | end 67 | end 68 | 69 | function Enemy:drawKey() 70 | if not self.keycolor then return end 71 | 72 | local r,g,b,a = lg.getColor() 73 | colors[self.keycolor](a) 74 | Key.subdraw(Key, self.x+32,self.y+32,1,1,1,0) 75 | lg.setColor(1,1,1,a) 76 | end 77 | 78 | function Enemy:subdraw(...) 79 | if self.hitflash > 0 then lg.setShader(hitShader) end 80 | Enemy.super.subdraw(self, ...) 81 | if self.hitflash > 0 then lg.setShader() end 82 | end 83 | 84 | function Enemy:draw() 85 | self:drawKey() 86 | Enemy.super.draw(self) 87 | end 88 | -------------------------------------------------------------------------------- /things/enemies/eye.lua: -------------------------------------------------------------------------------- 1 | require "things/enemies/enemy" 2 | 3 | Eye = class(Enemy) 4 | Eye.sprite = utils.newAnimation("assets/sprites/eye.png") 5 | 6 | local anim = {1,2} 7 | 8 | local function shoot(self) 9 | self.alarms.shoot:reset() 10 | self.alarms.blink:set(8) 11 | local scene = scene() 12 | local angle = math.acos(self.xdir) 13 | local bullet = self:createThing(Bullet(self.x + self.xdir*32,self.y,angle,self,15,70)) 14 | end 15 | 16 | function Eye:new(x,y) 17 | Eye.super.new(self, x + 32, y + 32) 18 | 19 | self.alarms = { 20 | shoot = Alarm(shoot, self):set(60), 21 | blink = Alarm(), 22 | } 23 | end 24 | 25 | function Eye:init() 26 | self.xdir = self:isSolid(self.x+64, self.y, true,true,true) and -1 or 1 27 | self.x = self.x + self.xdir*48 28 | end 29 | 30 | function Eye:update() 31 | Eye.super.update(self) 32 | self.animIndex = self.alarms.blink:isActive() and 2 or 1 33 | end 34 | 35 | function Eye:draw() 36 | self:drawKey() 37 | self:subdraw(nil,nil,nil,self.xdir*1.2,1.2) 38 | end 39 | -------------------------------------------------------------------------------- /things/enemies/face.lua: -------------------------------------------------------------------------------- 1 | require "things/enemies/enemy" 2 | 3 | Glasses = class(Enemy) 4 | Glasses.sprite = utils.newAnimation("assets/sprites/glasses.png") 5 | 6 | local function shoot(self) 7 | self.alarms.shoot:set(120) 8 | if not self:isLevelActive() then return end 9 | self.alarms.blink:set(20) 10 | self.alarms.shot1:set(5) 11 | self.alarms.shot2:set(10) 12 | self.alarms.shot3:set(15) 13 | end 14 | 15 | local function shot(self) 16 | local scene = scene() 17 | local player = scene.player 18 | local angle = utils.angle(self.x,self.y,player.x,player.y) 19 | local bullet = self:createThing(Bullet(self.x,self.y,angle,self,15,70)) 20 | end 21 | 22 | function Glasses:new(x,y) 23 | Glasses.super.new(self, x + 32, y + 32) 24 | 25 | self.hp = 5 26 | self.alarms = { 27 | shoot = Alarm(shoot, self):set((x*y + x + y)%120), 28 | shot1 = Alarm(shot, self), 29 | shot2 = Alarm(shot, self), 30 | shot3 = Alarm(shot, self), 31 | blink = Alarm(), 32 | } 33 | end 34 | 35 | function Glasses:update() 36 | Glasses.super.update(self) 37 | self.animIndex = self.alarms.blink:isActive() and 2 or 1 38 | end 39 | -------------------------------------------------------------------------------- /things/enemies/king.lua: -------------------------------------------------------------------------------- 1 | require "things/enemies/enemy" 2 | 3 | King = class(Enemy) 4 | King.sprite = utils.newAnimation("assets/sprites/king.png") 5 | King.state = 1 6 | 7 | local startSound = audio.newSound("assets/sounds/bossstart.wav", 1, 1) 8 | local laughSound = audio.newSound("assets/sounds/bosslaugh.wav") 9 | local teleportSound = audio.newSound("assets/sounds/bosstp.wav") 10 | local impactSound = audio.newSound("assets/sounds/bossimpact.wav", 1, 1) 11 | 12 | local function randomize(self) 13 | local choices = { 14 | self.shoot, 15 | self.radialShoot, 16 | self.teleport, 17 | self.summon, 18 | } 19 | 20 | local scene = scene() 21 | if #scene.enemyList > 15 then 22 | lume.remove(choices, self.summon) 23 | end 24 | 25 | self.index = (self.index%#choices) + 1 26 | self.alarms.action.callback = choices[self.index] 27 | self.alarms.action:set(60) 28 | end 29 | 30 | function King:shoot() 31 | randomize(self) 32 | self.bulletStream = 35 33 | end 34 | 35 | function King:radialShoot() 36 | randomize(self) 37 | 38 | for i=0, math.pi*2, math.pi*2/16 do 39 | self:createThing(Bullet(self.x,self.y,i,self,8,100)) 40 | end 41 | end 42 | 43 | function King:teleport() 44 | randomize(self) 45 | teleportSound:play() 46 | local angle = math.random()*2*math.pi 47 | local r = utils.randomRange(0,500) 48 | self.x = self.ox + math.cos(angle)*r 49 | self.y = self.oy + math.abs(math.sin(angle))*r*-1 50 | end 51 | 52 | local summonball = class(Thing) 53 | 54 | function summonball:new(x,y,type) 55 | summonball.super.new(self,x,y) 56 | self.type = type 57 | self.angle = math.random()*2*math.pi 58 | self.speed = math.random() < 0.5 and 5 or 3 59 | self.alarms = { 60 | main = Alarm(self.summon, self):set(utils.randomRange(40,60)), 61 | } 62 | end 63 | 64 | function summonball:update() 65 | summonball.super.update(self) 66 | local spd = self.speed 67 | local c, s = math.cos(self.angle)*spd, math.sin(self.angle)*spd 68 | if not self:isSolid(self.x + c, self.y, true,true,true) then 69 | self.x = self.x + c 70 | end 71 | if not self:isSolid(self.x, self.y + s, true,true,true) then 72 | self.y = self.y + s 73 | end 74 | end 75 | 76 | function summonball:summon() 77 | local x, y = self.x, self.y 78 | if self.type == StillStar then 79 | x = x - 32 80 | y = y - 32 81 | end 82 | self:createThing(self.type(x,y)) 83 | self.dead = true 84 | end 85 | 86 | function summonball:draw() 87 | local r,g,b,a = lg.getColor() 88 | colors.blue(a) 89 | lg.circle("fill",self.x,self.y,24) 90 | end 91 | 92 | function King:summon() 93 | randomize(self) 94 | 95 | local type = utils.choose(Star, StillStar) 96 | 97 | local amount = math.random() < 0.5 and 3 or 2 98 | 99 | for i=1, amount do 100 | self:createThing(summonball(self.x,self.y,type)) 101 | end 102 | end 103 | 104 | local function wakeup(self) 105 | self.state = 2 106 | scene().cameraTracking = true 107 | randomize(self) 108 | impactSound:play() 109 | end 110 | 111 | local function laugh(self) 112 | laughSound:play() 113 | self.alarms.laugh:set(utils.randomRange(60*6,60*10)*3) 114 | end 115 | 116 | function King:new(x,y) 117 | King.super.new(self, x,y) 118 | self.hp = 70 119 | self.ohp = self.hp 120 | self.ox = x+128 121 | self.oy = y+128 122 | self.bulletStream = 0 123 | self.index = 1 124 | 125 | self.alarms = { 126 | wakeup = Alarm(wakeup, self), 127 | action = Alarm(randomize, self), 128 | laugh = Alarm(laugh, self):set(200), 129 | } 130 | end 131 | 132 | function King:update() 133 | King.super.update(self) 134 | local scene = scene() 135 | local player = scene.player 136 | 137 | if self.state == 1 and self:isLevelActive() and not self.alarms.wakeup:isActive() and player.onGround then 138 | self.alarms.wakeup:set(130) 139 | scene.cameraTracking = false 140 | startSound:play() 141 | end 142 | 143 | if self.alarms.wakeup:isActive() then 144 | scene.camera.x = utils.lerp(scene.camera.x, self.x, 0.1) 145 | scene.camera.y = utils.lerp(scene.camera.y, self.y, 0.1) 146 | end 147 | 148 | -- shotgun bullets 149 | if self.bulletStream > 0 then 150 | self.bulletStream = self.bulletStream - 1 151 | if self.bulletStream%7 == 0 then 152 | local angle = utils.angle(self.x,self.y, player.x,player.y) 153 | local off = 0.075 154 | self:createThing(Bullet(self.x,self.y,angle,self,8,100)) 155 | self:createThing(Bullet(self.x,self.y,angle+off,self,8,100)) 156 | self:createThing(Bullet(self.x,self.y,angle-off,self,8,100)) 157 | end 158 | end 159 | 160 | self.animIndex = self.state 161 | end 162 | 163 | function King:collisionAt(x,y) 164 | return math.abs(x - self.x) <= 64 and math.abs(y - self.y) <= 64 165 | end 166 | 167 | function King:onDeath() 168 | local scene = scene() 169 | scene.win = true 170 | 171 | scene.enemyList = {} 172 | local i = 1 173 | while i <= #scene.thingList do 174 | local thing = scene.thingList[i] 175 | 176 | if thing:instanceOf(Enemy) 177 | or thing:instanceOf(Bullet) 178 | or thing:instanceOf(summonball) then 179 | thing.dead = true 180 | if thing.onDeath then 181 | thing:onDeath() 182 | end 183 | table.remove(scene.thingList, i) 184 | else 185 | i = i + 1 186 | end 187 | end 188 | end 189 | 190 | function King:hit(attacker) 191 | if self.state == 2 then 192 | King.super.hit(self, attacker) 193 | end 194 | end 195 | 196 | function King:draw() 197 | local wakeshake = self.alarms.wakeup:isActive() and self.alarms.wakeup:getProgress()*math.cos(math.random()*2*math.pi)*4 or 0 198 | local dx = self.x + wakeshake 199 | local dy = self.y 200 | self:subdraw(dx,dy) 201 | 202 | if self.state == 2 then 203 | local w = 200 204 | local h = 10 205 | lg.setColor(0.1,0.1,0.1) 206 | lg.rectangle("fill", self.x-w/2,self.y-100,w,h) 207 | colors.red() 208 | lg.rectangle("fill", self.x-w/2,self.y-100,w*self.hp/self.ohp,h) 209 | end 210 | end 211 | -------------------------------------------------------------------------------- /things/enemies/star.lua: -------------------------------------------------------------------------------- 1 | require "things/enemies/enemy" 2 | 3 | Star = class(Enemy) 4 | Star.sprite = utils.newAnimation("assets/sprites/star.png") 5 | 6 | local anim = {1,2} 7 | 8 | local function shoot(self) 9 | self.alarms.shoot:set(60) 10 | if not self:isLevelActive() then return end 11 | 12 | local scene = scene() 13 | local player = scene.player 14 | local angle = utils.angle(self.x, self.y, player.x, player.y) 15 | --scene:createThing(Bullet(self.x,self.y,angle,self,15,25)) 16 | end 17 | 18 | function Star:new(x,y) 19 | Star.super.new(self, x,y) 20 | local spd = 3 21 | self.speed.x = x%128 == 0 and spd or -spd 22 | self.speed.y = y%128 == 0 and spd or -spd 23 | self.rot = math.random()*4 24 | 25 | self.alarms = { 26 | shoot = Alarm(shoot, self):set((x*y + x + y)%60), 27 | } 28 | end 29 | 30 | function Star:update() 31 | Star.super.update(self) 32 | 33 | local scene = scene() 34 | if self:isSolid(self.x + self.speed.x, self.y, true,true,true) then 35 | self.speed.x = self.speed.x * -1 36 | end 37 | self.x = self.x + self.speed.x 38 | 39 | if self:isSolid(self.x, self.y + self.speed.y, true,true,true) then 40 | self.speed.y = self.speed.y * -1 41 | end 42 | self.y = self.y + self.speed.y 43 | 44 | self.rot = (self.rot + 0.1)%(math.pi*2) 45 | 46 | self:animate(anim) 47 | end 48 | 49 | function Star:draw() 50 | self:drawKey() 51 | self:subdraw(nil,nil,nil,nil,nil,self.rot) 52 | end 53 | -------------------------------------------------------------------------------- /things/enemies/star2.lua: -------------------------------------------------------------------------------- 1 | require "things/enemies/enemy" 2 | 3 | StillStar = class(Enemy) 4 | StillStar.sprite = utils.newAnimation("assets/sprites/star2.png") 5 | 6 | local anim = {1,2} 7 | 8 | local function shoot(self) 9 | self.alarms.shoot:set(60) 10 | if not self:isLevelActive() then return end 11 | 12 | local scene = scene() 13 | local player = scene.player 14 | local angle = utils.angle(self.x, self.y, player.x, player.y) 15 | --self:createThing(Bullet(self.x,self.y,angle,self,15,25)) 16 | end 17 | 18 | function StillStar:new(x,y) 19 | StillStar.super.new(self, x+32,y+32) 20 | self.rot = math.random()*4 21 | self.hp = 8 22 | 23 | self.alarms = { 24 | shoot = Alarm(shoot, self):set((x*y + x + y)%60), 25 | } 26 | end 27 | 28 | function StillStar:update() 29 | StillStar.super.update(self) 30 | 31 | self.rot = (self.rot + 0.1)%(math.pi*2) 32 | 33 | self:animate(anim) 34 | end 35 | 36 | function StillStar:draw() 37 | self:drawKey() 38 | self:subdraw(nil,nil,nil,nil,nil,self.rot) 39 | end 40 | -------------------------------------------------------------------------------- /things/impact.lua: -------------------------------------------------------------------------------- 1 | require "things/thing" 2 | 3 | Impact = class(Thing) 4 | Impact.sprite = utils.newAnimation("assets/sprites/impact.png") 5 | 6 | function Impact:update() 7 | Impact.super.update(self) 8 | 9 | self.animIndex = self.animIndex + 0.4 10 | if self.animIndex >= 3 then 11 | self.dead = true 12 | end 13 | end 14 | -------------------------------------------------------------------------------- /things/key.lua: -------------------------------------------------------------------------------- 1 | Key = class(Thing) 2 | Key.sprite = utils.newAnimation("assets/sprites/key.png") 3 | Key.keycolor = "#ffffff" 4 | 5 | local sound = audio.newSound("assets/sounds/keyget.wav", 0.5) 6 | 7 | function Key:new(x,y) 8 | Key.super.new(self, x,y) 9 | self.time = 0 10 | end 11 | 12 | function Key:update() 13 | Key.super.update(self) 14 | self.y = self.y + math.sin(self.time) 15 | self.time = self.time + 0.1 16 | 17 | local scene = scene() 18 | local player = scene.player 19 | if self:isLevelActive() 20 | and utils.distance(self.x,self.y, player.x,player.y) <= 64 21 | and self.keycolor then 22 | self.dead = true 23 | player.keys[self.keycolor] = true 24 | sound:play() 25 | end 26 | end 27 | 28 | function Key:draw() 29 | local r,g,b,a = lg.getColor() 30 | colors[self.keycolor](a) 31 | self:subdraw() 32 | end 33 | -------------------------------------------------------------------------------- /things/lock.lua: -------------------------------------------------------------------------------- 1 | Lock = class(Thing) 2 | Lock.sprite = utils.newAnimation("assets/sprites/lock.png") 3 | 4 | local deathSound = audio.newSound("assets/sounds/edeath.wav", 0.3) 5 | 6 | function Lock:new(x,y) 7 | Lock.super.new(self, x,y) 8 | end 9 | 10 | function Lock:init() 11 | local scene = scene() 12 | local level = scene:getLevel(self.levelIndex) 13 | level[math.floor(self.x/64) + 1][math.floor(self.y/64) + 1] = 1 14 | end 15 | 16 | function Lock:update() 17 | Lock.super.update(self) 18 | 19 | local scene = scene() 20 | local player = scene.player 21 | if self:isLevelActive() 22 | and utils.distance(self.x,self.y, player.x,player.y) <= 100 23 | and player.keys[self.keycolor] then 24 | self.dead = true 25 | --player.keys[self.keycolor] = nil 26 | end 27 | end 28 | 29 | function Lock:onDeath() 30 | local scene = scene() 31 | local level = scene:getLevel(self.levelIndex) 32 | level[math.floor(self.x/64) + 1][math.floor(self.y/64) + 1] = 0 33 | 34 | deathSound:play() 35 | for i=1, 3 do 36 | local x, y = utils.lengthdir(math.random()*2*math.pi, utils.randomRange(10,20)) 37 | x, y = x + self.x, y + self.y 38 | self:createThing(Boom(x+32,y+32)) 39 | end 40 | end 41 | 42 | function Lock:draw() 43 | local r,g,b,a = lg.getColor() 44 | colors[self.keycolor](a) 45 | self:subdraw(self.x + 32, self.y + 32) 46 | end 47 | -------------------------------------------------------------------------------- /things/player.lua: -------------------------------------------------------------------------------- 1 | require "things/thing" 2 | 3 | Player = class(Thing) 4 | 5 | local sprite = utils.newAnimation("assets/sprites/lad.png") 6 | local gunarm = lg.newImage("assets/sprites/gunarm.png") 7 | 8 | local jumpSound = audio.newSound("assets/sounds/jump.wav", 0.5) 9 | local landSound = audio.newSound("assets/sounds/land.wav", 0.25) 10 | local deathSound = audio.newSound("assets/sounds/death.wav") 11 | 12 | local animations = { 13 | idle = {1,2}, 14 | walk = {1,3, speed=0.15}, 15 | } 16 | 17 | local controllerData = { 18 | controls = { 19 | left = {"key:a"}, 20 | right = {"key:d"}, 21 | jump = {"key:w", "key:space"}, 22 | shoot = {"mouse:1", "mouse:2", "mouse:3"}, 23 | }, 24 | } 25 | local controller = input.newController("player", controllerData) 26 | 27 | local function reload(self) 28 | self.reloaded = true 29 | end 30 | 31 | local function respawn(self) 32 | for i, v in pairs(self.spawnPoint) do 33 | self[i] = v 34 | self.speed[i] = 0 35 | end 36 | self.currentWarp = nil 37 | scene():resetLevel() 38 | end 39 | 40 | function Player:new(x,y) 41 | Player.super.new(self, x,y) 42 | self.x = x 43 | self.y = y 44 | self.speed = {x=0,y=0} 45 | self.spawnPoint = {x=x,y=y} 46 | self.stretch = {x=1,y=1} 47 | 48 | self.onWall = 0 49 | self.onGround = false 50 | self.coyoteFrames = 7 51 | self.wannaJumpFrames = 0 52 | self.disabledAirControl = 0 53 | 54 | self.animIndex = 1 55 | self.animTimer = 0 56 | 57 | self.keys = {} 58 | self.currentWarp = nil 59 | self.gunAngle = 0 60 | self.newGunAngle = 0 61 | self.gx, self.gy = x, y 62 | self.reloaded = true 63 | self.alarms = { 64 | gun = Alarm(reload, self):set(8), 65 | respawn = Alarm(respawn, self), 66 | } 67 | end 68 | 69 | local walkSpeed = 1.1 70 | local airSpeed = 0.7 71 | local walkFriction = 0.9 72 | local stopFriction = 0.65 73 | local maxWalkSpeed = 8--walkSpeed / (1-walkFriction) 74 | local width, height = 12, 32 75 | 76 | function Player:update() 77 | -- update all my alarms 78 | for _, alarm in pairs(self.alarms) do 79 | alarm:update() 80 | end 81 | 82 | if self.alarms.respawn:isActive() then return end 83 | 84 | -------------------------------------------------------------------------------- 85 | -- vertical physics 86 | -------------------------------------------------------------------------------- 87 | 88 | if self.onWall ~= 0 then 89 | -- slide on wall 90 | local slideSpeed = 3 91 | 92 | -- change speed more gradually when going up compared to going down 93 | if self.speed.y < 0 then 94 | self.speed.y = utils.lerp(self.speed.y, slideSpeed, 0.1) 95 | else 96 | self.speed.y = utils.lerp(self.speed.y, slideSpeed, 0.25) 97 | end 98 | else 99 | -- add gravity 100 | -- gravity is halved when going up, makes jumps feel better 101 | if self.speed.y <= 0 then 102 | self.speed.y = self.speed.y + 0.75 103 | else 104 | self.speed.y = self.speed.y + 1.5 105 | end 106 | 107 | self.speed.y = math.min(self.speed.y, 30) 108 | end 109 | 110 | -- hit ground 111 | local wasOnGround = self.onGround 112 | local iter = 0 113 | self.onGround = false 114 | if self:isSolid(self.x-width,self.y+self.speed.y+height, true,true) 115 | or self:isSolid(self.x+width,self.y+self.speed.y+height, true,true) then 116 | while not self:isSolid(self.x-width,self.y+height+1, true,true) 117 | and not self:isSolid(self.x+width,self.y+height+1, true,true) 118 | and iter < 32 do 119 | self.y = self.y + 1 120 | iter = iter + 1 121 | end 122 | 123 | -- squash on first hit 124 | if not wasOnGround and self.speed.y > 8 then 125 | self.stretch.x = 1.5 126 | self.stretch.y = 0.4 127 | landSound:play() 128 | end 129 | 130 | self.speed.y = 0 131 | self.onGround = true 132 | self.onWall = 0 133 | self.coyoteFrames = 7 134 | end 135 | 136 | -- start the jump 137 | self.wannaJumpFrames = math.max(self.wannaJumpFrames - 1, 0) 138 | if controller:pressed("jump") then 139 | self.wannaJumpFrames = 7 140 | end 141 | 142 | if self.coyoteFrames > 0 and self.wannaJumpFrames > 0 then 143 | self:jump() 144 | end 145 | 146 | -- hit ceiling 147 | local iter = 0 148 | if self:isSolid(self.x-width,self.y+self.speed.y-height, true,true) 149 | or self:isSolid(self.x+width,self.y+self.speed.y-height, true,true) then 150 | while not self:isSolid(self.x-width,self.y-height-1, true,true) 151 | and not self:isSolid(self.x+width,self.y-height-1, true,true) 152 | and iter < 32 do 153 | self.y = self.y - 1 154 | iter = iter + 1 155 | end 156 | self.speed.y = 0 157 | end 158 | 159 | self.coyoteFrames = math.max(self.coyoteFrames - 1, 0) 160 | if self.coyoteFrames <= 0 then self.onWall = 0 end 161 | self.disabledAirControl = math.max(self.disabledAirControl - 1, 0) 162 | 163 | -- variable jump height 164 | if not controller:down("jump") and self.speed.y < 0 then 165 | self.speed.y = self.speed.y * 0.7 166 | end 167 | 168 | -- integrate y 169 | self.y = self.y + self.speed.y 170 | 171 | -------------------------------------------------------------------------------- 172 | -- horizontal physics 173 | -------------------------------------------------------------------------------- 174 | 175 | -- walk left and right 176 | local walking = false 177 | local speed = self.onGround and walkSpeed or airSpeed 178 | if self.onGround or self.disabledAirControl <= 0 then 179 | if controller:down("right") then 180 | self.speed.x = self.speed.x + speed 181 | self.speed.x = math.min(self.speed.x, maxWalkSpeed) 182 | walking = true 183 | elseif controller:down("left") then 184 | self.speed.x = self.speed.x - speed 185 | self.speed.x = math.max(self.speed.x, -maxWalkSpeed) 186 | walking = true 187 | end 188 | end 189 | 190 | if self.onGround then 191 | self.speed.x = self.speed.x * (walking and walkFriction or stopFriction) 192 | end 193 | 194 | -- hit left wall 195 | local iter = 0 196 | if self:isSolid(self.x+self.speed.x-width,self.y+height-1, true,true) 197 | or self:isSolid(self.x+self.speed.x-width,self.y-height+1, true,true) then 198 | while not self:isSolid(self.x-1-width,self.y+height-1, true,true) 199 | and not self:isSolid(self.x-1-width,self.y-height+1, true,true) 200 | and iter < 32 do 201 | self.x = self.x - 1 202 | iter = iter + 1 203 | end 204 | self.speed.x = 0 205 | end 206 | 207 | -- hit right wall 208 | local iter = 0 209 | if self:isSolid(self.x+self.speed.x+width,self.y+height-1, true,true) 210 | or self:isSolid(self.x+self.speed.x+width,self.y-height+1, true,true) then 211 | while not self:isSolid(self.x+1+width,self.y+height-1, true,true) 212 | and not self:isSolid(self.x+1+width,self.y-height+1, true,true) 213 | and iter < 32 do 214 | self.x = self.x + 1 215 | iter = iter + 1 216 | end 217 | self.speed.x = 0 218 | end 219 | 220 | -- wall slide collision testing 221 | if not self.onGround then 222 | if (self:isSolid(self.x+width+2, self.y+height-4, true) 223 | or self:isSolid(self.x+width+2, self.y-height+4, true)) 224 | and self:isSolid(self.x+width+2, self.y, true) then 225 | self.onWall = 1 226 | self.coyoteFrames = 7 227 | end 228 | if (self:isSolid(self.x-width-2, self.y+height-4, true) 229 | or self:isSolid(self.x-width-2, self.y-height+4, true)) 230 | and self:isSolid(self.x-width-2, self.y, true) then 231 | self.onWall = -1 232 | self.coyoteFrames = 7 233 | end 234 | end 235 | 236 | -- integrate x 237 | self.x = self.x + self.speed.x 238 | 239 | -- death plane 240 | if self.y >= 15*64 then self:die() end 241 | 242 | -------------------------------------------------------------------------------- 243 | -- animation 244 | -------------------------------------------------------------------------------- 245 | 246 | -- control walking animation 247 | if math.abs(self.speed.x) > 0.1 then 248 | self:animate(animations.walk) 249 | else 250 | self:animate(animations.idle) 251 | end 252 | 253 | -- in air and wall sliding animations 254 | if not self.onGround then 255 | self.animIndex = 3 256 | 257 | if self.onWall ~= 0 then 258 | self.animIndex = 4 259 | end 260 | end 261 | 262 | -- unsquash and unstretch 263 | for i, v in pairs(self.stretch) do 264 | self.stretch[i] = utils.lerp(v, 1, 0.25) 265 | end 266 | 267 | -- shoot! 268 | self.gunAngle = self.newGunAngle 269 | if controller:down("shoot") and self.reloaded then 270 | self.reloaded = false 271 | self.alarms.gun:reset() 272 | local x = self.x + self.gx + math.cos(self.gunAngle)*20 273 | local y = self.y + self.gy + math.sin(self.gunAngle)*20 274 | local angle = self.gunAngle + utils.lerp(-0.1,0.1, math.random()) 275 | self:createThing(Bullet(x,y,angle)) 276 | engine.shake = 5 277 | self.speed.x = self.speed.x - math.cos(angle)*2 278 | self.speed.y = self.speed.y - math.sin(angle)*2 279 | end 280 | end 281 | 282 | function Player:jump() 283 | self.wannaJumpFrames = 0 284 | self.coyoteFrames = 0 285 | 286 | if self.currentWarp and self.onGround then 287 | self.spawnPoint.x = self.x 288 | self.spawnPoint.y = self.y 289 | scene().cutscene = WarpCutscene(self.currentWarp:instanceOf(BackWarp) and -1 or 1) 290 | return 291 | end 292 | 293 | self.speed.y = -15 294 | self.stretch.x = 0.4 295 | self.stretch.y = 1.5 296 | 297 | if self.onWall ~= 0 then 298 | self.speed.x = self.onWall*-0.8*maxWalkSpeed 299 | self.onWall = 0 300 | self.disabledAirControl = 7 301 | end 302 | 303 | jumpSound:play() 304 | end 305 | 306 | function Player:die() 307 | if self.alarms.respawn:isActive() then return end 308 | deathSound:play() 309 | self.alarms.respawn:set(60) 310 | 311 | for i=1, 3 do 312 | local x, y = utils.lengthdir(math.random()*2*math.pi, utils.randomRange(10,20)) 313 | x, y = x + self.x, y + self.y 314 | self:createThing(Boom(x,y)) 315 | end 316 | 317 | engine.shake = 10 318 | end 319 | 320 | -- one hit kills, the hit function is just a reference to the die function 321 | Player.hit = Player.die 322 | 323 | function Player:draw() 324 | if self.alarms.respawn:isActive() then return end 325 | 326 | colors.white() 327 | local dx, dy = self.x, self.y 328 | local sx, sy = lg.transformPoint(dx, dy) 329 | self.newGunAngle = utils.angle(sx, sy, input.mouse.x, input.mouse.y) 330 | local gunflip = utils.sign(math.cos(self.newGunAngle)) 331 | local gx, gy = 7*gunflip, 10 332 | if self.onWall ~= 0 then 333 | if gunflip == self.onWall then 334 | if math.sin(self.newGunAngle) < 0 then 335 | self.newGunAngle = math.pi*1.5 336 | else 337 | self.newGunAngle = math.pi*0.5 338 | end 339 | end 340 | 341 | gunflip = -1*self.onWall 342 | gx = 0 343 | end 344 | 345 | -- store this calculation for reals here because this is the easiest place to do it 346 | self.gx, self.gy = gx, gy 347 | lg.draw(sprite.source, sprite[self.animIndex], dx, dy + 32*math.max(1-self.stretch.y, 0), 0, gunflip*self.stretch.x, self.stretch.y, 24, 32) 348 | lg.draw(gunarm, dx + gx, dy + gy, self.gunAngle, 1, gunflip, 0, 16) 349 | end 350 | -------------------------------------------------------------------------------- /things/text.lua: -------------------------------------------------------------------------------- 1 | require "things/thing" 2 | 3 | Text = class(Thing) 4 | 5 | local font = lg.newFont("assets/comicneuebold.ttf", 32) 6 | 7 | function Text:new(x, y) 8 | Text.super.new(self, x, y) 9 | end 10 | 11 | function Text:draw() 12 | lg.setFont(font) 13 | local scene = scene() 14 | local alpha = 1 - math.abs(self.levelIndex - scene.levelIndex - scene.depthOffset) 15 | self.message = self.message and string.gsub(self.message, "#", "\n") or "message wasn't loaded!" 16 | 17 | local dx, dy = self.x - font:getWidth(self.message)/2, self.y 18 | 19 | colors.white(alpha) 20 | local r = 3 21 | for i=0, math.pi*2, math.pi*2/10 do 22 | local dx, dy = dx + math.cos(i)*r, dy + math.sin(i)*r 23 | lg.print(self.message, dx,dy) 24 | end 25 | 26 | colors.black(alpha) 27 | lg.print(self.message, dx,dy) 28 | 29 | colors.white() 30 | end 31 | -------------------------------------------------------------------------------- /things/thing.lua: -------------------------------------------------------------------------------- 1 | Thing = class() 2 | 3 | function Thing:new(x, y) 4 | self.x = x 5 | self.y = y 6 | self.speed = {x=0,y=0} 7 | self.animIndex = 1 8 | self.animTimer = 0 9 | self.levelIndex = 1 -- updated by the scene 10 | end 11 | 12 | function Thing:update() 13 | if self.alarms then 14 | for _, alarm in pairs(self.alarms) do 15 | alarm:update() 16 | end 17 | end 18 | end 19 | 20 | function Thing:draw() 21 | if self.sprite and not self.dead then 22 | self:subdraw() 23 | end 24 | end 25 | 26 | function Thing:subdraw(x, y, frame, xs, ys, rot) 27 | lg.draw(self.sprite.source, self.sprite[frame or math.floor(self.animIndex)], x or self.x, y or self.y, rot or 0, xs or 1, ys or 1, self.sprite.size/2, self.sprite.size/2) 28 | end 29 | 30 | function Thing:animate(anim) 31 | self.animTimer = self.animTimer + (anim.speed or 0.1) 32 | self.animIndex = anim[math.floor(self.animTimer % #anim) + 1] 33 | end 34 | 35 | function Thing:isLevelActive() 36 | local scene = scene() 37 | return scene.levelIndex == self.levelIndex 38 | end 39 | 40 | function Thing:createThing(thing) 41 | return scene():createThing(thing, self.levelIndex) 42 | end 43 | 44 | function Thing:isSolid(x,y, tile,hoob,voob) 45 | local scene = scene() 46 | local level = scene:getLevel(self.levelIndex) 47 | 48 | if hoob and (x <= 0 or x >= level.width*64) then return true end 49 | if voob and (y <= 0 or y >= level.height*64) then return true end 50 | 51 | if tile then 52 | local x, y = math.floor(x/64)+1, math.max(math.floor(y/64)+1, 1) 53 | if level[x] and level[x][y] then 54 | return level[x][y] == 1 55 | end 56 | end 57 | 58 | return false 59 | end 60 | -------------------------------------------------------------------------------- /things/throne.lua: -------------------------------------------------------------------------------- 1 | require "things/thing" 2 | 3 | Throne = class(Thing) 4 | Throne.sprite = utils.newAnimation("assets/sprites/throne.png") 5 | 6 | function Throne:new(x,y) 7 | Throne.super.new(self, x+128,y+128) 8 | end 9 | 10 | function Throne:init() 11 | self.king = King(self.x,self.y) 12 | self:createThing(self.king) 13 | end 14 | 15 | function Throne:update() 16 | Throne.super.update(self) 17 | 18 | local scene = scene() 19 | local player = scene.player 20 | if self.king.dead 21 | and utils.distance(self.x,self.y,player.x,player.y) <= 200 then 22 | scene.cutscene = WinCutscene(self) 23 | end 24 | end 25 | -------------------------------------------------------------------------------- /things/warp.lua: -------------------------------------------------------------------------------- 1 | require "things/thing" 2 | 3 | Warp = class(Thing) 4 | Warp.sprite = utils.newAnimation("assets/sprites/warp.png") 5 | 6 | local stepSound = audio.newSound("assets/sounds/steponwarp.wav", 0.25) 7 | 8 | local ready = {2} 9 | local notready = {1} 10 | 11 | function Warp:new(x, y) 12 | Warp.super.new(self, x+64, y+56) 13 | self.oy = y+56 14 | end 15 | 16 | function Warp:update() 17 | local scene = scene() 18 | local player = scene.player 19 | if math.abs(player.x - self.x) <= 80 20 | and self.y - player.y < 128 21 | and player.y < self.y 22 | and self.levelIndex == scene.levelIndex then 23 | if player.currentWarp ~= self then 24 | stepSound:play() 25 | end 26 | 27 | player.currentWarp = self 28 | elseif player.currentWarp == self then 29 | player.currentWarp = nil 30 | end 31 | 32 | if player.currentWarp == self then 33 | self:animate(ready) 34 | self.y = self.oy - 8 35 | else 36 | self:animate(notready) 37 | self.y = self.oy 38 | end 39 | end 40 | -------------------------------------------------------------------------------- /todos.md: -------------------------------------------------------------------------------- 1 | # todos 2 | 3 | - locked doors 4 | - to not make each level feel like the same sorta runaround 5 | - king boss battle 6 | - foot boss battle 7 | - coin collectables! 8 | 9 | # enemy types 10 | 11 | - ghosts which run away from your crosshair 12 | --------------------------------------------------------------------------------