├── .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 | 
8 |
9 | 
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 |
--------------------------------------------------------------------------------