Compare commits
316 Commits
v0.1.5
...
highscore_
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
9f570306f5 | ||
|
|
b22f671409 | ||
|
|
0b6f62d50e | ||
|
|
086f327371 | ||
|
|
3c83ae0bf4 | ||
|
|
450833b246 | ||
|
|
8e7a5418dc | ||
|
|
6609b642dc | ||
|
|
452879ebab | ||
|
|
70a827b477 | ||
|
|
d281a732db | ||
|
|
01e91fbd93 | ||
|
|
ece853c9d3 | ||
|
|
ea8d008370 | ||
|
|
e20eb048c8 | ||
|
|
a33ca1af24 | ||
|
|
664bca2282 | ||
|
|
fc8fb8b66f | ||
|
|
fc58e6e908 | ||
|
|
061f6f5164 | ||
|
|
4e9cea7dda | ||
|
|
fa97216167 | ||
|
|
3f8d68cc9d | ||
|
|
6639d73c1c | ||
|
|
668f061077 | ||
|
|
cb70967b82 | ||
|
|
0c2ba5f0cc | ||
|
|
6d07a3b820 | ||
|
|
2de13a97f0 | ||
|
|
512c2149f0 | ||
|
|
6fb19220b7 | ||
|
|
08da67c434 | ||
|
|
2d63ca8ee1 | ||
|
|
0f09d47e60 | ||
|
|
9d44d1e771 | ||
|
|
5d022f9037 | ||
|
|
818743fe77 | ||
|
|
f22424d671 | ||
|
|
dd6baf1fe6 | ||
|
|
11cf5a9d55 | ||
|
|
5642ed1326 | ||
|
|
c0888c484f | ||
|
|
3ef3b193fd | ||
|
|
0c2e3efd1a | ||
|
|
5076adf022 | ||
|
|
1a75d983dc | ||
|
|
5b8e9586bd | ||
|
|
7d7dd8c3c2 | ||
|
|
29afdcecfc | ||
|
|
8b09833ae6 | ||
|
|
64047eaf9c | ||
|
|
125488b4d9 | ||
|
|
1fdd091456 | ||
|
|
ced40297cc | ||
|
|
32f2a0b3e7 | ||
|
|
dd5347ad8d | ||
|
|
b732ebb213 | ||
|
|
84634d6933 | ||
|
|
0d13a9f236 | ||
|
|
45120bc9f7 | ||
|
|
57c7d9c4c3 | ||
|
|
9f52d8bf10 | ||
|
|
57bd6a8286 | ||
|
|
1a68cd8fce | ||
|
|
56baf46839 | ||
|
|
305d07e10a | ||
|
|
8d954cabc2 | ||
|
|
0281220ea0 | ||
|
|
aef5d88d3f | ||
|
|
3676f7697c | ||
|
|
acb0eb1a71 | ||
|
|
a89bf05cab | ||
|
|
8008315994 | ||
|
|
90f62cb7dd | ||
|
|
eaee5fc7f0 | ||
|
|
e3b038b5a7 | ||
|
|
083693496e | ||
|
|
ba576dfc77 | ||
|
|
e195ccd721 | ||
|
|
70f703eb2f | ||
|
|
dc4d4a8259 | ||
|
|
565510c7b2 | ||
|
|
c26a3f37de | ||
|
|
0c1ce2f717 | ||
|
|
f14ab2a328 | ||
|
|
042dbd220b | ||
|
|
548612123a | ||
|
|
f4675da0b0 | ||
|
|
511e9592bc | ||
|
|
5f3990ff58 | ||
|
|
50ff4adf27 | ||
|
|
87b88f4b42 | ||
|
|
130c2ea403 | ||
|
|
1ea304916e | ||
|
|
e26b094830 | ||
|
|
bcb44725bf | ||
|
|
2990844c52 | ||
|
|
c343014d6f | ||
|
|
605add7e94 | ||
|
|
d3b647ca71 | ||
|
|
1101aa467d | ||
|
|
ce27a7ed18 | ||
|
|
f31beffab8 | ||
|
|
2ff8fb5edc | ||
|
|
1bf8f91ef2 | ||
|
|
ba5f78d5f1 | ||
|
|
f7c4908062 | ||
|
|
3aa5bae7be | ||
|
|
40a2e78280 | ||
|
|
696da3fa3f | ||
|
|
4afe9f2bd4 | ||
|
|
1f686fb5d4 | ||
|
|
f4779c9847 | ||
|
|
06cbec4bc8 | ||
|
|
668564ffb0 | ||
|
|
e6edeea3d1 | ||
|
|
513cd6ba90 | ||
|
|
1beef8f157 | ||
|
|
d3b2b4c2d9 | ||
|
|
2b8b9d5084 | ||
|
|
2728780c45 | ||
|
|
ca592a3bcf | ||
|
|
b6f4158d70 | ||
|
|
e43f5c470a | ||
|
|
7bcdc517c0 | ||
|
|
1d30987f9a | ||
|
|
1dd46a11ef | ||
|
|
935c7aa14c | ||
|
|
aea115d953 | ||
|
|
3d5b33f41a | ||
|
|
29f07bb6ab | ||
|
|
891f96e814 | ||
|
|
36837a3af5 | ||
|
|
01b0f9f618 | ||
|
|
7c8c5bb11d | ||
|
|
acaa6bdbbf | ||
|
|
c37757f592 | ||
|
|
905e4bcc77 | ||
|
|
d956647678 | ||
|
|
10f032b49b | ||
|
|
5590e6c89b | ||
|
|
0393396d74 | ||
|
|
8c1eaec1aa | ||
|
|
957802a78e | ||
|
|
169a4e4d2f | ||
|
|
48aee18340 | ||
|
|
7b496d9412 | ||
|
|
7abb861446 | ||
|
|
21f8769228 | ||
|
|
44423fd2e8 | ||
|
|
351fb4cfe9 | ||
|
|
103f04ceaa | ||
|
|
88d2f0d8d1 | ||
|
|
e100289c82 | ||
|
|
e38da49180 | ||
|
|
b03473d2fe | ||
|
|
cf6e0be4e7 | ||
|
|
2bc9dc179c | ||
|
|
d626926d5a | ||
|
|
721acefea0 | ||
|
|
b9b71e90bb | ||
|
|
9f61b139fd | ||
|
|
3b0fdba27d | ||
|
|
d8fad3dc37 | ||
|
|
6d326a142c | ||
|
|
b6f1072587 | ||
|
|
eef04ebf05 | ||
|
|
e24737a3b8 | ||
|
|
f9368fa806 | ||
|
|
189feb1802 | ||
|
|
dc09dabacb | ||
|
|
e13278c6a8 | ||
|
|
f7f11b0e22 | ||
|
|
869a0f7ec5 | ||
|
|
10a9d97848 | ||
|
|
a470b40def | ||
|
|
fd739dcfdf | ||
|
|
e1dc01d0d0 | ||
|
|
7228707241 | ||
|
|
af86ce3a98 | ||
|
|
78ae0ae671 | ||
|
|
c614e9c4cd | ||
|
|
b27ef0e9f4 | ||
|
|
843b1e108a | ||
|
|
a8d697064c | ||
|
|
cf32474898 | ||
|
|
6a295cad59 | ||
|
|
2d80e20c82 | ||
|
|
2279c24d11 | ||
|
|
8510ad9bea | ||
|
|
6b77ad8547 | ||
|
|
6834e92674 | ||
|
|
3479374686 | ||
|
|
863c614a4c | ||
|
|
49e52c6a39 | ||
|
|
a105086ca6 | ||
|
|
1b381c4bf3 | ||
|
|
91a87fea73 | ||
|
|
28b455fcc0 | ||
|
|
2e3eff025f | ||
|
|
4670cb7c15 | ||
|
|
9b04e14388 | ||
|
|
f52da36bf7 | ||
|
|
76142c1dff | ||
|
|
a3458e2413 | ||
|
|
7eba9c012f | ||
|
|
4d2868b7b6 | ||
|
|
2e6fcd232b | ||
|
|
10833f2ec1 | ||
|
|
abb2b9491e | ||
|
|
062ab2005e | ||
|
|
468025fc80 | ||
|
|
c8544975d6 | ||
|
|
6776229bfb | ||
|
|
84b4dc5073 | ||
|
|
35dafb6615 | ||
|
|
3641d85fcb | ||
|
|
9b89c4d1de | ||
|
|
2dba120919 | ||
|
|
9224f271b1 | ||
|
|
febb5d546c | ||
|
|
c6482c423e | ||
|
|
6beb313c6b | ||
|
|
eb70f55b6e | ||
|
|
0badcde9ad | ||
|
|
6f39b591d3 | ||
|
|
129237f0b0 | ||
|
|
741c246244 | ||
|
|
b5937af8b2 | ||
|
|
33b8533d8e | ||
|
|
69959ff687 | ||
|
|
f91cd99dfd | ||
|
|
be59727ca5 | ||
|
|
cca295066c | ||
|
|
f2862b4d93 | ||
|
|
2aafd30253 | ||
|
|
b27ba335ba | ||
|
|
33244736b8 | ||
|
|
a324e0015a | ||
|
|
285108ca08 | ||
|
|
4b1fed727c | ||
|
|
d38168ca00 | ||
|
|
b0ce0f17f5 | ||
|
|
9fca272e8d | ||
|
|
5a21c8244b | ||
|
|
4923b2e2d4 | ||
|
|
8810f24e7a | ||
|
|
57a9f6ef55 | ||
|
|
342036bc28 | ||
|
|
78dcfe43c4 | ||
|
|
cdf6b5cf33 | ||
|
|
e6a60b0021 | ||
|
|
5f29c987f2 | ||
|
|
608d75b1ac | ||
|
|
1427c0d19e | ||
|
|
e221a91d73 | ||
|
|
bdcd25b82c | ||
|
|
a5158e0994 | ||
|
|
d946b17e13 | ||
|
|
69a5c0a21a | ||
|
|
b6423c3335 | ||
|
|
5b960d7291 | ||
|
|
54f4b0b890 | ||
|
|
8c62f321a0 | ||
|
|
fdffd2cd9a | ||
|
|
8ddf468121 | ||
|
|
8e77407ff2 | ||
|
|
92c852d178 | ||
|
|
f658ed63f2 | ||
|
|
c2d1c1183c | ||
|
|
36c568feaf | ||
|
|
bf30fcefbd | ||
|
|
d9f5bd16d7 | ||
|
|
d978ff8d87 | ||
|
|
b47d0f36b9 | ||
|
|
abc210c69c | ||
|
|
436e4ac861 | ||
|
|
a48d7c67b5 | ||
|
|
f6ca79ff91 | ||
|
|
4eb3901610 | ||
|
|
3f7fc4b622 | ||
|
|
ac7ae91c39 | ||
|
|
0c8e910245 | ||
|
|
6233ffb12d | ||
|
|
1f78bb9e99 | ||
|
|
a125c09106 | ||
|
|
090ffa5126 | ||
|
|
12a6f42198 | ||
|
|
0c317d9ce1 | ||
|
|
eddfee566d | ||
|
|
7fe366a8de | ||
|
|
55be30c99f | ||
|
|
36ceef8488 | ||
|
|
b59edb5e8e | ||
|
|
5d32b6a3e7 | ||
|
|
05230ac046 | ||
|
|
f28dc08ae2 | ||
|
|
ecd958bdc5 | ||
|
|
43f59cfde8 | ||
|
|
00c46961f9 | ||
|
|
0a0053276b | ||
|
|
d0f1d869a8 | ||
|
|
29ee000998 | ||
|
|
995fd7fee9 | ||
|
|
67abf35a28 | ||
|
|
9982613e26 | ||
|
|
629beb7240 | ||
|
|
4cb20101b0 | ||
|
|
63c0721978 | ||
|
|
1366451a3d | ||
|
|
6b624b9853 | ||
|
|
b0bda25466 | ||
|
|
2bde9d1378 | ||
|
|
6178b2cee9 | ||
|
|
4e8a237de3 | ||
|
|
5606251ea7 |
2
.gitignore
vendored
@@ -2,3 +2,5 @@
|
||||
*.love
|
||||
dist/*.zip
|
||||
dist/**/cambridge.exe
|
||||
dist/**/libs
|
||||
dist/**/*.md
|
||||
72
README.md
@@ -1,56 +1,94 @@
|
||||

|
||||
|
||||
Cambridge
|
||||
=========
|
||||
|
||||
Welcome to Cambridge, the next open-source falling-block game engine!
|
||||
|
||||
This fork is written and maintained exclusively by [SashLilac](https://github.com/SashLilac) and [Oshisaure](https://github.com/oshisaure)!
|
||||
This fork is written and maintained exclusively by [SashLilac](https://github.com/SashLilac), [joezeng](https://github.com/joezeng) and [Oshisaure](https://github.com/oshisaure)!
|
||||
|
||||
Join our Discord server for help and a welcoming community! https://discord.gg/mteMJw4
|
||||
|
||||
Credits
|
||||
-------
|
||||
|
||||
- [Lilla Oshisaure](https://www.youtube.com/user/LeSpyroshisaure) for their amazing contributions to my life in general!
|
||||
- [Lilla Oshisaure](https://www.youtube.com/user/LeSpyroshisaure) for being my co-dev!
|
||||
- [joezeng](https://github.com/joezeng) for the original project, and for offering to help with the expansion!
|
||||
- [The Tetra Legends Discord](http://discord.com/invite/7hMx5r2) for supporting me and playtesting!
|
||||
- [joezeng](https://github.com/joezeng) for the original project.
|
||||
- [The Absolute Plus](https://discord.gg/6Gf2awJ) for being another source of motivation!
|
||||
|
||||
Installation instructions
|
||||
-------------------------
|
||||
The following people in no particular order also helped with the project:
|
||||
- [Hailey](https://github.com/haileylgbt)
|
||||
- CylinderKnot
|
||||
- MarkGamed7794
|
||||
- [Mizu](https://github.com/rexxt)
|
||||
- MattMayuga
|
||||
- Kitaru
|
||||
- switchpalacecorner
|
||||
- [sinefuse](https://github.com/sinefuse)
|
||||
- [2Tie](https://github.com/2Tie)
|
||||
- [nightmareci](https://github.com/nightmareci)
|
||||
- [MyPasswordIsWeak](https://github.com/MyPasswordIsWeak)
|
||||
- [Dr Ocelot](https://github.com/Dr-Ocelot)
|
||||
|
||||
Pre-built releases are available on the releases page.
|
||||

|
||||
|
||||
Playing the game
|
||||
----------------
|
||||
|
||||
### Windows
|
||||
|
||||
Unzip the exe file and run it directly. All assets are currently bundled inside the executable.
|
||||
You do not need LÖVE on Windows, as it comes bundled with the program.
|
||||
|
||||
### macOS
|
||||
To get the stable release, simply download the ZIP in the latest release. All assets needed are bundled with the executable.
|
||||
|
||||
For the time being, the file `cambridge.love` only works on the command line. Install `love` with [Homebrew](https://brew.sh), and run:
|
||||
If you want the bleeding edge version, download [this](https://github.com/SashLilac/cambridge/archive/master.zip).
|
||||
|
||||
$ love cambridge.love
|
||||
Extract the ZIP, open a Command Prompt at the folder you extracted Cambridge to, then run this command:
|
||||
|
||||
### Linux
|
||||
dist\windows\love.exe .
|
||||
|
||||
Same as macOS, except install `love` with your favourite package manager.
|
||||
Alternatively, if you're on a 32-bit system, run this instead:
|
||||
|
||||
dist\win32\love.exe .
|
||||
|
||||
Running from source
|
||||
-------------------
|
||||
32-bit systems do not support rich presence integration.
|
||||
|
||||
If you want the bleeding-edge release, you can also clone the code straight from this repository.
|
||||
Then, check the mod pack section at the bottom of this page.
|
||||
|
||||
### macOS, Linux
|
||||
|
||||
If you haven't already, install `love` with your favourite package manager (Homebrew on macOS, your system's default on Linux). **Make sure you're using LÖVE 11, because it won't work with earlier versions!**
|
||||
|
||||
#### Downloading a release
|
||||
|
||||
You can download the .love file in the latest release, and run it with:
|
||||
|
||||
love cambridge.love
|
||||
|
||||
#### Installing from source
|
||||
|
||||
Clone the repository in git:
|
||||
|
||||
git clone https://github.com/SashLilac/cambridge
|
||||
git clone https://github.com/SashLilac/cambridge
|
||||
|
||||
Alternatively, download the source code ZIP in the latest release.
|
||||
|
||||
Then, navigate to the root directory that you just cloned, and type:
|
||||
|
||||
love .
|
||||
love .
|
||||
|
||||
It should run automatically!
|
||||
|
||||
## Installing modpacks
|
||||
|
||||
Simply drag your mode, ruleset, and randomizer Lua files into their respective [directory](https://love2d.org/wiki/love.filesystem), and they should appear automatically.
|
||||
|
||||
You can also load custom assets through this way, assuming you preserve the directory structure.
|
||||
|
||||
**WARNING:** The .exe / .love files and the bleeding edge releases have different save directories. Read the above link carefully!
|
||||
|
||||
For more detailed instructions, install [this](https://github.com/SashLilac/cambridge-modpack) mod pack to get a taste of the mod potential.
|
||||
|
||||
License
|
||||
-------
|
||||
|
||||
68
SOURCES.md
@@ -8,7 +8,7 @@ Some of the assets are used without proper licenses. We aim to have fully licens
|
||||
Backgrounds
|
||||
-----------
|
||||
|
||||
1. Title: "Motus Glacies." Contributed by Daniel "Explo" McCarthy.
|
||||
1. Title: Original picrute found on the Wikipedia article for Cambridge
|
||||
|
||||
1. *Gameplay level 0: "Quantum foam." Alex Sukontsev. https://www.flickr.com/photos/control9/14957509814/
|
||||
2. *Gameplay level 1: No name. http://www.onekind.tv/univision-mqb/q5mqh5brlvuuj2nhdx7ch7eum183uu
|
||||
@@ -34,10 +34,18 @@ Backgrounds
|
||||
Backgrounds marked with a * are placeholders that will be replaced in later versions due to incompatible licenses. We are generally aiming for public domain background images, but will also accept backgrounds given proper licenses to be included within Cambridge.
|
||||
|
||||
|
||||
Sounds
|
||||
------
|
||||
|
||||
All piece sounds are (c) 2020 Damian Yerrick.
|
||||
Other sounds from:
|
||||
- NullpoMino
|
||||
- DTET, (c) 2003 Mihys.
|
||||
|
||||
Music
|
||||
-----
|
||||
|
||||
1. TGM3 credit roll music.
|
||||
1. Second Reality opening scene music (1993).
|
||||
2. The FitnessGram™ Pacer Test.
|
||||
|
||||
All background music is (currently) only unofficially included. In later releases they may be replaced with specifically licensed music as applicable.
|
||||
@@ -106,3 +114,59 @@ Permission is hereby granted, free of charge, to any person obtaining a copy of
|
||||
The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||
|
||||
simple-slider (https://love2d.org/forums/viewtopic.php?t=80711)
|
||||
--------------------
|
||||
|
||||
Copyright (c) 2016 George Prosser
|
||||
|
||||
Permission is hereby granted, free of charge, to any person
|
||||
obtaining a copy of this software and associated documentation
|
||||
files (the "Software"), to deal in the Software without
|
||||
restriction, including without limitation the rights to use,
|
||||
copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the
|
||||
Software is furnished to do so, subject to the following
|
||||
conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be
|
||||
included in all copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
||||
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES
|
||||
OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
|
||||
NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT
|
||||
HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
|
||||
WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
|
||||
FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR
|
||||
OTHER DEALINGS IN THE SOFTWARE.
|
||||
|
||||
bigint.lua (https://github.com/empyreuma/bigint.lua)
|
||||
--------------------
|
||||
|
||||
3-Clause BSD License
|
||||
|
||||
Copyright (c) Emily "empyreuma" 2016
|
||||
All rights reserved.
|
||||
|
||||
Redistribution and use in source and binary forms, with or without
|
||||
modification, are permitted provided that the following conditions are met:
|
||||
* Redistributions of source code must retain the above copyright
|
||||
notice, this list of conditions and the following disclaimer.
|
||||
* Redistributions in binary form must reproduce the above copyright
|
||||
notice, this list of conditions and the following disclaimer in the
|
||||
documentation and/or other materials provided with the distribution.
|
||||
* Neither the name of the <organization> nor the
|
||||
names of its contributors may be used to endorse or promote products
|
||||
derived from this software without specific prior written permission.
|
||||
|
||||
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
|
||||
ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
|
||||
WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
|
||||
DISCLAIMED. IN NO EVENT SHALL <COPYRIGHT HOLDER> BE LIABLE FOR ANY
|
||||
DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
|
||||
(INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
|
||||
LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
|
||||
ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
|
||||
(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
|
||||
SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
||||
11
clean.bat
Normal file
@@ -0,0 +1,11 @@
|
||||
@del cambridge.love
|
||||
@del dist\windows\cambridge.exe
|
||||
@del dist\windows\SOURCES.md
|
||||
@del dist\windows\LICENSE.md
|
||||
@rmdir /Q /S dist\windows\libs
|
||||
@del dist\win32\cambridge.exe
|
||||
@del dist\win32\SOURCES.md
|
||||
@del dist\win32\LICENSE.md
|
||||
@rmdir /Q /S dist\win32\libs
|
||||
@del dist\cambridge-windows.zip
|
||||
@del dist\cambridge-win32.zip
|
||||
@@ -4,17 +4,17 @@ Game modes
|
||||
There are several classes of game modes. The modes that originate from other games are organized by suffix:
|
||||
|
||||
* The "C" series stand for "Classic" games, games that were produced before around 1992-1993 and generally have no wallkicks or lock delay.
|
||||
* C84 - The original version from the Electronika 60.
|
||||
* C88 - Sega Tetris.
|
||||
* C89 - Nintendo / NES Tetris.
|
||||
* C84 - The original version from the Electronika 60.
|
||||
* C88 - Sega Tetris.
|
||||
* C89 - Nintendo / NES Tetris.
|
||||
* The "A" series stand for "Arika" games, or games in the Tetris the Grand Master series.
|
||||
* A1 - Tetris The Grand Master (the original from 1998).
|
||||
* A2 - Tetris The Absolute The Grand Master 2 PLUS.
|
||||
* A3 - Tetris The Grand Master 3 Terror-Instinct.
|
||||
* AX - Tetris The Grand Master ACE (X for Xbox).
|
||||
* A1 - Tetris The Grand Master (the original from 1998).
|
||||
* A2 - Tetris The Absolute The Grand Master 2 PLUS.
|
||||
* A3 - Tetris The Grand Master 3 Terror-Instinct.
|
||||
* AX - Tetris The Grand Master ACE (X for Xbox).
|
||||
* The "G" series stand for "Guideline" games, or games that follow the Tetris Guideline.
|
||||
* GF - Tetris Friends (2007-2019)
|
||||
* GJ - Tetris Online Japan (2005-2011)
|
||||
* GF - Tetris Friends (2007-2019)
|
||||
* GJ - Tetris Online Japan (2005-2011)
|
||||
* N stands for Nullpomino, only used for Phantom Mania N.
|
||||
|
||||
MARATHON
|
||||
|
||||
42
funcs.lua
@@ -1,11 +1,11 @@
|
||||
function copy(t)
|
||||
-- returns deep copy of t (as opposed to the shallow copy you get from var = t)
|
||||
if type(t) ~= "table" then return t end
|
||||
local meta = getmetatable(t)
|
||||
local target = {}
|
||||
for k, v in pairs(t) do target[k] = v end
|
||||
setmetatable(target, meta)
|
||||
return target
|
||||
local meta = getmetatable(t)
|
||||
local target = {}
|
||||
for k, v in pairs(t) do target[k] = v end
|
||||
setmetatable(target, meta)
|
||||
return target
|
||||
end
|
||||
|
||||
function strTrueValues(tbl)
|
||||
@@ -56,9 +56,33 @@ end
|
||||
|
||||
function formatBigNum(number)
|
||||
-- returns a string representing a number with commas as thousands separator (e.g. 12,345,678)
|
||||
local s = string.format("%d", number)
|
||||
local pos = string.len(s) % 3
|
||||
if pos == 0 then pos = 3 end
|
||||
local s
|
||||
if type(number) == "number" then
|
||||
s = string.format("%d", number)
|
||||
elseif type(number) == "string" then
|
||||
if not tonumber(number) then
|
||||
return
|
||||
else
|
||||
s = number
|
||||
end
|
||||
else
|
||||
return
|
||||
end
|
||||
local pos = Mod1(string.len(s), 3)
|
||||
return string.sub(s, 1, pos)
|
||||
.. string.gsub(string.sub(s, pos+1), "(...)", ",%1")
|
||||
end
|
||||
end
|
||||
|
||||
function Mod1(n, m)
|
||||
-- returns a number congruent to n modulo m in the range [1;m] (as opposed to [0;m-1])
|
||||
return ((n-1) % m) + 1
|
||||
end
|
||||
|
||||
function table.contains(table, element)
|
||||
for _, value in pairs(table) do
|
||||
if value == element then
|
||||
return true
|
||||
end
|
||||
end
|
||||
return false
|
||||
end
|
||||
558
libs/bigint/bigint.lua
Normal file
@@ -0,0 +1,558 @@
|
||||
#!/usr/bin/env lua
|
||||
-- If this variable is true, then strict type checking is performed for all
|
||||
-- operations. This may result in slower code, but it will allow you to catch
|
||||
-- errors and bugs earlier.
|
||||
local strict = true
|
||||
|
||||
--------------------------------------------------------------------------------
|
||||
|
||||
local bigint = {}
|
||||
|
||||
local named_powers = require("libs.bigint.named-powers-of-ten")
|
||||
|
||||
-- Create a new bigint or convert a number or string into a big
|
||||
-- Returns an empty, positive bigint if no number or string is given
|
||||
function bigint.new(num)
|
||||
local self = {
|
||||
sign = "+",
|
||||
digits = {}
|
||||
}
|
||||
|
||||
-- Return a new bigint with the same sign and digits
|
||||
function self:clone()
|
||||
local newint = bigint.new()
|
||||
newint.sign = self.sign
|
||||
for _, digit in pairs(self.digits) do
|
||||
newint.digits[#newint.digits + 1] = digit
|
||||
end
|
||||
return newint
|
||||
end
|
||||
|
||||
setmetatable(self, {
|
||||
__add = function(lhs, rhs)
|
||||
return bigint.add(lhs, rhs)
|
||||
end,
|
||||
__unm = function()
|
||||
if (self.sign == "+") then
|
||||
self.sign = "-"
|
||||
else
|
||||
self.sign = "+"
|
||||
end
|
||||
return self
|
||||
end,
|
||||
__sub = function(lhs, rhs)
|
||||
return bigint.subtract(lhs, rhs)
|
||||
end,
|
||||
__mul = function(lhs, rhs)
|
||||
return bigint.multiply(lhs, rhs)
|
||||
end,
|
||||
__div = function(lhs, rhs)
|
||||
return bigint.divide(lhs, rhs)
|
||||
end,
|
||||
__mod = function(lhs, rhs)
|
||||
return bigint.modulus(lhs, rhs)
|
||||
end,
|
||||
__pow = function(lhs, rhs)
|
||||
return bigint.exponentiate(lhs, rhs)
|
||||
end,
|
||||
__eq = function(lhs, rhs)
|
||||
return bigint.compare(lhs, rhs, "==")
|
||||
end,
|
||||
__lt = function(lhs, rhs)
|
||||
return bigint.compare(lhs, rhs, "<")
|
||||
end,
|
||||
__le = function(lhs, rhs)
|
||||
return bigint.compare(lhs, rhs, "<=")
|
||||
end,
|
||||
__tostring = function()
|
||||
return bigint.unserialize(self, "s")
|
||||
end
|
||||
})
|
||||
|
||||
if (num) then
|
||||
local num_string = tostring(num)
|
||||
for digit in string.gmatch(num_string, "[0-9]") do
|
||||
table.insert(self.digits, tonumber(digit))
|
||||
end
|
||||
if string.sub(num_string, 1, 1) == "-" then
|
||||
self.sign = "-"
|
||||
end
|
||||
end
|
||||
|
||||
return self
|
||||
end
|
||||
|
||||
-- Check the type of a big
|
||||
-- Normally only runs when global variable "strict" == true, but checking can be
|
||||
-- forced by supplying "true" as the second argument.
|
||||
function bigint.check(big, force)
|
||||
if (strict or force) then
|
||||
assert(#big.digits > 0, "bigint is empty")
|
||||
assert(type(big.sign) == "string", "bigint is unsigned")
|
||||
for _, digit in pairs(big.digits) do
|
||||
assert(type(digit) == "number", digit .. " is not a number")
|
||||
assert(digit < 10, digit .. " is greater than or equal to 10")
|
||||
end
|
||||
end
|
||||
return true
|
||||
end
|
||||
|
||||
-- Return a new big with the same digits but with a positive sign (absolute
|
||||
-- value)
|
||||
function bigint.abs(big)
|
||||
bigint.check(big)
|
||||
local result = big:clone()
|
||||
result.sign = "+"
|
||||
return result
|
||||
end
|
||||
|
||||
-- Convert a big to a number or string
|
||||
function bigint.unserialize(big, output_type, precision)
|
||||
bigint.check(big)
|
||||
|
||||
local num = ""
|
||||
if big.sign == "-" then
|
||||
num = "-"
|
||||
end
|
||||
|
||||
|
||||
if ((output_type == nil)
|
||||
or (output_type == "number")
|
||||
or (output_type == "n")
|
||||
or (output_type == "string")
|
||||
or (output_type == "s")) then
|
||||
-- Unserialization to a string or number requires reconstructing the
|
||||
-- entire number
|
||||
|
||||
for _, digit in pairs(big.digits) do
|
||||
num = num .. math.floor(digit) -- lazy way of getting rid of .0$
|
||||
end
|
||||
|
||||
if ((output_type == nil)
|
||||
or (output_type == "number")
|
||||
or (output_type == "n")) then
|
||||
return tonumber(num)
|
||||
else
|
||||
return num
|
||||
end
|
||||
|
||||
else
|
||||
-- Unserialization to human-readable form or scientific notation only
|
||||
-- requires reading the first few digits
|
||||
if (precision == nil) then
|
||||
precision = 3
|
||||
else
|
||||
assert(precision > 0, "Precision cannot be less than 1")
|
||||
assert(math.floor(precision) == precision,
|
||||
"Precision must be a positive integer")
|
||||
end
|
||||
|
||||
-- num is the first (precision + 1) digits, the first being separated by
|
||||
-- a decimal point from the others
|
||||
num = num .. big.digits[1]
|
||||
if (precision > 1) then
|
||||
num = num .. "."
|
||||
for i = 1, (precision - 1) do
|
||||
num = num .. big.digits[i + 1]
|
||||
end
|
||||
end
|
||||
|
||||
if ((output_type == "human-readable")
|
||||
or (output_type == "human")
|
||||
or (output_type == "h")) then
|
||||
-- Human-readable output contributed by 123eee555
|
||||
|
||||
local name
|
||||
local walkback = 0 -- Used to enumerate "ten", "hundred", etc
|
||||
|
||||
-- Walk backwards in the index of named_powers starting at the
|
||||
-- number of digits of the input until the first value is found
|
||||
for i = (#big.digits - 1), (#big.digits - 4), -1 do
|
||||
name = named_powers[i]
|
||||
if (name) then
|
||||
if (walkback == 1) then
|
||||
name = "ten " .. name
|
||||
elseif (walkback == 2) then
|
||||
name = "hundred " .. name
|
||||
end
|
||||
break
|
||||
else
|
||||
walkback = walkback + 1
|
||||
end
|
||||
end
|
||||
|
||||
return num .. " " .. name
|
||||
|
||||
else
|
||||
return num .. "*10^" .. (#big.digits - 1)
|
||||
end
|
||||
|
||||
end
|
||||
end
|
||||
|
||||
-- Basic comparisons
|
||||
-- Accepts symbols (<, >=, ~=) and Unix shell-like options (lt, ge, ne)
|
||||
function bigint.compare(big1, big2, comparison)
|
||||
bigint.check(big1)
|
||||
bigint.check(big2)
|
||||
|
||||
local greater = false -- If big1.digits > big2.digits
|
||||
local equal = false
|
||||
|
||||
if (big1.sign == "-") and (big2.sign == "+") then
|
||||
greater = false
|
||||
elseif (#big1.digits > #big2.digits)
|
||||
or ((big1.sign == "+") and (big2.sign == "-")) then
|
||||
greater = true
|
||||
elseif (#big1.digits == #big2.digits) then
|
||||
-- Walk left to right, comparing digits
|
||||
for digit = 1, #big1.digits do
|
||||
if (big1.digits[digit] > big2.digits[digit]) then
|
||||
greater = true
|
||||
break
|
||||
elseif (big2.digits[digit] > big1.digits[digit]) then
|
||||
break
|
||||
elseif (digit == #big1.digits)
|
||||
and (big1.digits[digit] == big2.digits[digit]) then
|
||||
equal = true
|
||||
end
|
||||
end
|
||||
|
||||
end
|
||||
|
||||
-- If both numbers are negative, then the requirements for greater are
|
||||
-- reversed
|
||||
if (not equal) and (big1.sign == "-") and (big2.sign == "-") then
|
||||
greater = not greater
|
||||
end
|
||||
|
||||
return (((comparison == "<") or (comparison == "lt"))
|
||||
and ((not greater) and (not equal)) and true)
|
||||
or (((comparison == ">") or (comparison == "gt"))
|
||||
and ((greater) and (not equal)) and true)
|
||||
or (((comparison == "==") or (comparison == "eq"))
|
||||
and (equal) and true)
|
||||
or (((comparison == ">=") or (comparison == "ge"))
|
||||
and (equal or greater) and true)
|
||||
or (((comparison == "<=") or (comparison == "le"))
|
||||
and (equal or not greater) and true)
|
||||
or (((comparison == "~=") or (comparison == "!=") or (comparison == "ne"))
|
||||
and (not equal) and true)
|
||||
or false
|
||||
end
|
||||
|
||||
-- BACKEND: Add big1 and big2, ignoring signs
|
||||
function bigint.add_raw(big1, big2)
|
||||
bigint.check(big1)
|
||||
bigint.check(big2)
|
||||
|
||||
local result = bigint.new()
|
||||
local max_digits = 0
|
||||
local carry = 0
|
||||
|
||||
if (#big1.digits >= #big2.digits) then
|
||||
max_digits = #big1.digits
|
||||
else
|
||||
max_digits = #big2.digits
|
||||
end
|
||||
|
||||
-- Walk backwards right to left, like in long addition
|
||||
for digit = 0, max_digits - 1 do
|
||||
local sum = (big1.digits[#big1.digits - digit] or 0)
|
||||
+ (big2.digits[#big2.digits - digit] or 0)
|
||||
+ carry
|
||||
|
||||
if (sum >= 10) then
|
||||
carry = 1
|
||||
sum = sum - 10
|
||||
else
|
||||
carry = 0
|
||||
end
|
||||
|
||||
result.digits[max_digits - digit] = sum
|
||||
end
|
||||
|
||||
-- Leftover carry in cases when #big1.digits == #big2.digits and sum > 10, ex. 7 + 9
|
||||
if (carry == 1) then
|
||||
table.insert(result.digits, 1, 1)
|
||||
end
|
||||
|
||||
return result
|
||||
|
||||
end
|
||||
|
||||
-- BACKEND: Subtract big2 from big1, ignoring signs
|
||||
function bigint.subtract_raw(big1, big2)
|
||||
-- Type checking is done by bigint.compare
|
||||
assert(bigint.compare(bigint.abs(big1), bigint.abs(big2), ">="),
|
||||
"Size of " .. bigint.unserialize(big1, "string") .. " is less than "
|
||||
.. bigint.unserialize(big2, "string"))
|
||||
|
||||
local result = big1:clone()
|
||||
local max_digits = #big1.digits
|
||||
local borrow = 0
|
||||
|
||||
-- Logic mostly copied from bigint.add_raw ---------------------------------
|
||||
-- Walk backwards right to left, like in long subtraction
|
||||
for digit = 0, max_digits - 1 do
|
||||
local diff = (big1.digits[#big1.digits - digit] or 0)
|
||||
- (big2.digits[#big2.digits - digit] or 0)
|
||||
- borrow
|
||||
|
||||
if (diff < 0) then
|
||||
borrow = 1
|
||||
diff = diff + 10
|
||||
else
|
||||
borrow = 0
|
||||
end
|
||||
|
||||
result.digits[max_digits - digit] = diff
|
||||
end
|
||||
----------------------------------------------------------------------------
|
||||
|
||||
|
||||
-- Strip leading zeroes if any, but not if 0 is the only digit
|
||||
while (#result.digits > 1) and (result.digits[1] == 0) do
|
||||
table.remove(result.digits, 1)
|
||||
end
|
||||
|
||||
return result
|
||||
end
|
||||
|
||||
-- FRONTEND: Addition and subtraction operations, accounting for signs
|
||||
function bigint.add(big1, big2)
|
||||
-- Type checking is done by bigint.compare
|
||||
|
||||
local result
|
||||
|
||||
-- If adding numbers of different sign, subtract the smaller sized one from
|
||||
-- the bigger sized one and take the sign of the bigger sized one
|
||||
if (big1.sign ~= big2.sign) then
|
||||
if (bigint.compare(bigint.abs(big1), bigint.abs(big2), ">")) then
|
||||
result = bigint.subtract_raw(big1, big2)
|
||||
result.sign = big1.sign
|
||||
else
|
||||
result = bigint.subtract_raw(big2, big1)
|
||||
result.sign = big2.sign
|
||||
end
|
||||
|
||||
elseif (big1.sign == "+") and (big2.sign == "+") then
|
||||
result = bigint.add_raw(big1, big2)
|
||||
|
||||
elseif (big1.sign == "-") and (big2.sign == "-") then
|
||||
result = bigint.add_raw(big1, big2)
|
||||
result.sign = "-"
|
||||
end
|
||||
|
||||
return result
|
||||
end
|
||||
function bigint.subtract(big1, big2)
|
||||
-- Type checking is done by bigint.compare in bigint.add
|
||||
-- Subtracting is like adding a negative
|
||||
local big2_local = big2:clone()
|
||||
if (big2.sign == "+") then
|
||||
big2_local.sign = "-"
|
||||
else
|
||||
big2_local.sign = "+"
|
||||
end
|
||||
return bigint.add(big1, big2_local)
|
||||
end
|
||||
|
||||
-- BACKEND: Multiply a big by a single digit big, ignoring signs
|
||||
function bigint.multiply_single(big1, big2)
|
||||
bigint.check(big1)
|
||||
bigint.check(big2)
|
||||
assert(#big2.digits == 1, bigint.unserialize(big2, "string")
|
||||
.. " has more than one digit")
|
||||
|
||||
local result = bigint.new()
|
||||
local carry = 0
|
||||
|
||||
-- Logic mostly copied from bigint.add_raw ---------------------------------
|
||||
-- Walk backwards right to left, like in long multiplication
|
||||
for digit = 0, #big1.digits - 1 do
|
||||
local this_digit = big1.digits[#big1.digits - digit]
|
||||
* big2.digits[1]
|
||||
+ carry
|
||||
|
||||
if (this_digit >= 10) then
|
||||
carry = math.floor(this_digit / 10)
|
||||
this_digit = this_digit - (carry * 10)
|
||||
else
|
||||
carry = 0
|
||||
end
|
||||
|
||||
result.digits[#big1.digits - digit] = this_digit
|
||||
end
|
||||
|
||||
-- Leftover carry in cases when big1.digits[1] * big2.digits[1] > 0
|
||||
if (carry > 0) then
|
||||
table.insert(result.digits, 1, carry)
|
||||
end
|
||||
----------------------------------------------------------------------------
|
||||
|
||||
return result
|
||||
end
|
||||
|
||||
-- FRONTEND: Multiply two bigs, accounting for signs
|
||||
function bigint.multiply(big1, big2)
|
||||
-- Type checking done by bigint.multiply_single
|
||||
|
||||
local result = bigint.new(0)
|
||||
local larger, smaller -- Larger and smaller in terms of digits, not size
|
||||
|
||||
if (bigint.unserialize(big1) == 0) or (bigint.unserialize(big2) == 0) then
|
||||
return result
|
||||
end
|
||||
|
||||
if (#big1.digits >= #big2.digits) then
|
||||
larger = big1
|
||||
smaller = big2
|
||||
else
|
||||
larger = big2
|
||||
smaller = big1
|
||||
end
|
||||
|
||||
-- Walk backwards right to left, like in long multiplication
|
||||
for digit = 0, #smaller.digits - 1 do
|
||||
-- Sorry for going over column 80! There's lots of big names here
|
||||
local this_digit_product = bigint.multiply_single(larger,
|
||||
bigint.new(smaller.digits[#smaller.digits - digit]))
|
||||
|
||||
-- "Placeholding zeroes"
|
||||
if (digit > 0) then
|
||||
for placeholder = 1, digit do
|
||||
table.insert(this_digit_product.digits, 0)
|
||||
end
|
||||
end
|
||||
|
||||
result = bigint.add(result, this_digit_product)
|
||||
end
|
||||
|
||||
if (larger.sign == smaller.sign) then
|
||||
result.sign = "+"
|
||||
else
|
||||
result.sign = "-"
|
||||
end
|
||||
|
||||
return result
|
||||
end
|
||||
|
||||
|
||||
-- Raise a big to a positive integer or big power (TODO: negative integer power)
|
||||
function bigint.exponentiate(big, power)
|
||||
-- Type checking for big done by bigint.multiply
|
||||
assert(bigint.compare(power, bigint.new(0), ">="),
|
||||
" negative powers are not supported")
|
||||
local exp = power:clone()
|
||||
|
||||
if (bigint.compare(exp, bigint.new(0), "==")) then
|
||||
return bigint.new(1)
|
||||
elseif (bigint.compare(exp, bigint.new(1), "==")) then
|
||||
return big
|
||||
else
|
||||
local result = big:clone()
|
||||
|
||||
while (bigint.compare(exp, bigint.new(1), ">")) do
|
||||
result = bigint.multiply(result, big)
|
||||
exp = bigint.subtract(exp, bigint.new(1))
|
||||
end
|
||||
|
||||
return result
|
||||
end
|
||||
|
||||
end
|
||||
|
||||
-- BACKEND: Divide two bigs (decimals not supported), returning big result and
|
||||
-- big remainder
|
||||
-- WARNING: Only supports positive integers
|
||||
function bigint.divide_raw(big1, big2)
|
||||
-- Type checking done by bigint.compare
|
||||
if (bigint.compare(big1, big2, "==")) then
|
||||
return bigint.new(1), bigint.new(0)
|
||||
elseif (bigint.compare(big1, big2, "<")) then
|
||||
return bigint.new(0), bigint.new(0)
|
||||
else
|
||||
assert(bigint.compare(big2, bigint.new(0), "!="), "error: divide by zero")
|
||||
assert(big1.sign == "+", "error: big1 is not positive")
|
||||
assert(big2.sign == "+", "error: big2 is not positive")
|
||||
|
||||
local result = bigint.new()
|
||||
|
||||
local dividend = bigint.new() -- Dividend of a single operation, not the
|
||||
-- dividend of the overall function
|
||||
local divisor = big2:clone()
|
||||
local factor = 1
|
||||
|
||||
-- Walk left to right among digits in the dividend, like in long
|
||||
-- division
|
||||
for _, digit in pairs(big1.digits) do
|
||||
dividend.digits[#dividend.digits + 1] = digit
|
||||
|
||||
-- The dividend is smaller than the divisor, so a zero is appended
|
||||
-- to the result and the loop ends
|
||||
if (bigint.compare(dividend, divisor, "<")) then
|
||||
if (#result.digits > 0) then -- Don't add leading zeroes
|
||||
result.digits[#result.digits + 1] = 0
|
||||
end
|
||||
else
|
||||
-- Find the maximum number of divisors that fit into the
|
||||
-- dividend
|
||||
factor = 0
|
||||
while (bigint.compare(divisor, dividend, "<=")) do
|
||||
divisor = bigint.add(divisor, big2)
|
||||
factor = factor + 1
|
||||
end
|
||||
|
||||
-- Append the factor to the result
|
||||
if (factor == 10) then
|
||||
-- Fixes a weird bug that introduces a new bug if fixed by
|
||||
-- changing the comparison in the while loop to "<="
|
||||
result.digits[#result.digits] = 1
|
||||
result.digits[#result.digits + 1] = 0
|
||||
else
|
||||
result.digits[#result.digits + 1] = factor
|
||||
end
|
||||
|
||||
-- Subtract the divisor from the dividend to obtain the
|
||||
-- remainder, which is the new dividend for the next loop
|
||||
dividend = bigint.subtract(dividend,
|
||||
bigint.subtract(divisor, big2))
|
||||
|
||||
-- Reset the divisor
|
||||
divisor = big2:clone()
|
||||
end
|
||||
|
||||
end
|
||||
|
||||
-- The remainder of the final loop is returned as the function's
|
||||
-- overall remainder
|
||||
return result, dividend
|
||||
end
|
||||
end
|
||||
|
||||
-- FRONTEND: Divide two bigs (decimals not supported), returning big result and
|
||||
-- big remainder, accounting for signs
|
||||
function bigint.divide(big1, big2)
|
||||
local result, remainder = bigint.divide_raw(bigint.abs(big1),
|
||||
bigint.abs(big2))
|
||||
if (big1.sign == big2.sign) then
|
||||
result.sign = "+"
|
||||
else
|
||||
result.sign = "-"
|
||||
end
|
||||
|
||||
return result, remainder
|
||||
end
|
||||
|
||||
-- FRONTEND: Return only the remainder from bigint.divide
|
||||
function bigint.modulus(big1, big2)
|
||||
local result, remainder = bigint.divide(big1, big2)
|
||||
|
||||
-- Remainder will always have the same sign as the dividend per C standard
|
||||
-- https://en.wikipedia.org/wiki/Modulo_operation#Remainder_calculation_for_the_modulo_operation
|
||||
remainder.sign = big1.sign
|
||||
return remainder
|
||||
end
|
||||
|
||||
return bigint
|
||||
3340
libs/bigint/named-powers-of-ten.lua
Normal file
BIN
libs/discord-rpc.dll
Normal file
BIN
libs/discord-rpc.dylib
Normal file
BIN
libs/discord-rpc.so
Normal file
282
libs/discordRPC.lua
Normal file
@@ -0,0 +1,282 @@
|
||||
local ffi = require "ffi"
|
||||
|
||||
|
||||
-- Get the host os to load correct lib
|
||||
local osname = love.system.getOS()
|
||||
local discordRPClib = nil
|
||||
|
||||
-- FFI requires the libraries really be files just sitting in the filesystem. It
|
||||
-- can't load libraries from a .love archive, nor a fused executable on Windows.
|
||||
-- Merely using love.filesystem.getSource() only works when running LOVE with
|
||||
-- the game unarchived from command line, like "love .".
|
||||
--
|
||||
-- The code here setting "source" will set the directory where the game was run
|
||||
-- from, so FFI can load discordRPC. We assume that the discordRPC library's
|
||||
-- libs directory is in the same directory as the .love archive; if it's
|
||||
-- missing, it just won't load.
|
||||
local source = love.filesystem.getSource()
|
||||
if string.sub(source, -5) == ".love" or love.filesystem.isFused() then
|
||||
source = love.filesystem.getSourceBaseDirectory()
|
||||
end
|
||||
|
||||
if osname == "Linux" then
|
||||
discordRPClib = ffi.load(source.."/libs/discord-rpc.so")
|
||||
elseif osname == "OS X" then
|
||||
discordRPClib = ffi.load(source.."/libs/discord-rpc.dylib")
|
||||
elseif osname == "Windows" then
|
||||
discordRPClib = ffi.load(source.."/libs/discord-rpc.dll")
|
||||
else
|
||||
-- Else it crashes later on
|
||||
error(string.format("Discord rpc not supported on platform (%s)", osname))
|
||||
end
|
||||
|
||||
|
||||
ffi.cdef[[
|
||||
typedef struct DiscordRichPresence {
|
||||
const char* state; /* max 128 bytes */
|
||||
const char* details; /* max 128 bytes */
|
||||
int64_t startTimestamp;
|
||||
int64_t endTimestamp;
|
||||
const char* largeImageKey; /* max 32 bytes */
|
||||
const char* largeImageText; /* max 128 bytes */
|
||||
const char* smallImageKey; /* max 32 bytes */
|
||||
const char* smallImageText; /* max 128 bytes */
|
||||
const char* partyId; /* max 128 bytes */
|
||||
int partySize;
|
||||
int partyMax;
|
||||
const char* matchSecret; /* max 128 bytes */
|
||||
const char* joinSecret; /* max 128 bytes */
|
||||
const char* spectateSecret; /* max 128 bytes */
|
||||
int8_t instance;
|
||||
} DiscordRichPresence;
|
||||
|
||||
typedef struct DiscordUser {
|
||||
const char* userId;
|
||||
const char* username;
|
||||
const char* discriminator;
|
||||
const char* avatar;
|
||||
} DiscordUser;
|
||||
|
||||
typedef void (*readyPtr)(const DiscordUser* request);
|
||||
typedef void (*disconnectedPtr)(int errorCode, const char* message);
|
||||
typedef void (*erroredPtr)(int errorCode, const char* message);
|
||||
typedef void (*joinGamePtr)(const char* joinSecret);
|
||||
typedef void (*spectateGamePtr)(const char* spectateSecret);
|
||||
typedef void (*joinRequestPtr)(const DiscordUser* request);
|
||||
|
||||
typedef struct DiscordEventHandlers {
|
||||
readyPtr ready;
|
||||
disconnectedPtr disconnected;
|
||||
erroredPtr errored;
|
||||
joinGamePtr joinGame;
|
||||
spectateGamePtr spectateGame;
|
||||
joinRequestPtr joinRequest;
|
||||
} DiscordEventHandlers;
|
||||
|
||||
void Discord_Initialize(const char* applicationId,
|
||||
DiscordEventHandlers* handlers,
|
||||
int autoRegister,
|
||||
const char* optionalSteamId);
|
||||
|
||||
void Discord_Shutdown(void);
|
||||
|
||||
void Discord_RunCallbacks(void);
|
||||
|
||||
void Discord_UpdatePresence(const DiscordRichPresence* presence);
|
||||
|
||||
void Discord_ClearPresence(void);
|
||||
|
||||
void Discord_Respond(const char* userid, int reply);
|
||||
|
||||
void Discord_UpdateHandlers(DiscordEventHandlers* handlers);
|
||||
]]
|
||||
|
||||
local discordRPC = {} -- module table
|
||||
|
||||
-- proxy to detect garbage collection of the module
|
||||
discordRPC.gcDummy = newproxy(true)
|
||||
|
||||
local function unpackDiscordUser(request)
|
||||
return ffi.string(request.userId), ffi.string(request.username),
|
||||
ffi.string(request.discriminator), ffi.string(request.avatar)
|
||||
end
|
||||
|
||||
-- callback proxies
|
||||
-- note: callbacks are not JIT compiled (= SLOW), try to avoid doing performance critical tasks in them
|
||||
-- luajit.org/ext_ffi_semantics.html
|
||||
local ready_proxy = ffi.cast("readyPtr", function(request)
|
||||
if discordRPC.ready then
|
||||
discordRPC.ready(unpackDiscordUser(request))
|
||||
end
|
||||
end)
|
||||
|
||||
local disconnected_proxy = ffi.cast("disconnectedPtr", function(errorCode, message)
|
||||
if discordRPC.disconnected then
|
||||
discordRPC.disconnected(errorCode, ffi.string(message))
|
||||
end
|
||||
end)
|
||||
|
||||
local errored_proxy = ffi.cast("erroredPtr", function(errorCode, message)
|
||||
if discordRPC.errored then
|
||||
discordRPC.errored(errorCode, ffi.string(message))
|
||||
end
|
||||
end)
|
||||
|
||||
local joinGame_proxy = ffi.cast("joinGamePtr", function(joinSecret)
|
||||
if discordRPC.joinGame then
|
||||
discordRPC.joinGame(ffi.string(joinSecret))
|
||||
end
|
||||
end)
|
||||
|
||||
local spectateGame_proxy = ffi.cast("spectateGamePtr", function(spectateSecret)
|
||||
if discordRPC.spectateGame then
|
||||
discordRPC.spectateGame(ffi.string(spectateSecret))
|
||||
end
|
||||
end)
|
||||
|
||||
local joinRequest_proxy = ffi.cast("joinRequestPtr", function(request)
|
||||
if discordRPC.joinRequest then
|
||||
discordRPC.joinRequest(unpackDiscordUser(request))
|
||||
end
|
||||
end)
|
||||
|
||||
-- helpers
|
||||
local function checkArg(arg, argType, argName, func, maybeNil)
|
||||
assert(type(arg) == argType or (maybeNil and arg == nil),
|
||||
string.format("Argument \"%s\" to function \"%s\" has to be of type \"%s\"",
|
||||
argName, func, argType))
|
||||
end
|
||||
|
||||
local function checkStrArg(arg, maxLen, argName, func, maybeNil)
|
||||
if maxLen then
|
||||
assert(type(arg) == "string" and arg:len() <= maxLen or (maybeNil and arg == nil),
|
||||
string.format("Argument \"%s\" of function \"%s\" has to be of type string with maximum length %d",
|
||||
argName, func, maxLen))
|
||||
else
|
||||
checkArg(arg, "string", argName, func, true)
|
||||
end
|
||||
end
|
||||
|
||||
local function checkIntArg(arg, maxBits, argName, func, maybeNil)
|
||||
maxBits = math.min(maxBits or 32, 52) -- lua number (double) can only store integers < 2^53
|
||||
local maxVal = 2^(maxBits-1) -- assuming signed integers, which, for now, are the only ones in use
|
||||
assert(type(arg) == "number" and math.floor(arg) == arg
|
||||
and arg < maxVal and arg >= -maxVal
|
||||
or (maybeNil and arg == nil),
|
||||
string.format("Argument \"%s\" of function \"%s\" has to be a whole number <= %d",
|
||||
argName, func, maxVal))
|
||||
end
|
||||
|
||||
-- function wrappers
|
||||
function discordRPC.initialize(applicationId, autoRegister, optionalSteamId)
|
||||
local func = "discordRPC.Initialize"
|
||||
checkStrArg(applicationId, nil, "applicationId", func)
|
||||
checkArg(autoRegister, "boolean", "autoRegister", func)
|
||||
if optionalSteamId ~= nil then
|
||||
checkStrArg(optionalSteamId, nil, "optionalSteamId", func)
|
||||
end
|
||||
|
||||
local eventHandlers = ffi.new("struct DiscordEventHandlers")
|
||||
eventHandlers.ready = ready_proxy
|
||||
eventHandlers.disconnected = disconnected_proxy
|
||||
eventHandlers.errored = errored_proxy
|
||||
eventHandlers.joinGame = joinGame_proxy
|
||||
eventHandlers.spectateGame = spectateGame_proxy
|
||||
eventHandlers.joinRequest = joinRequest_proxy
|
||||
|
||||
discordRPClib.Discord_Initialize(applicationId, eventHandlers,
|
||||
autoRegister and 1 or 0, optionalSteamId)
|
||||
end
|
||||
|
||||
function discordRPC.shutdown()
|
||||
discordRPClib.Discord_Shutdown()
|
||||
end
|
||||
|
||||
function discordRPC.runCallbacks()
|
||||
discordRPClib.Discord_RunCallbacks()
|
||||
end
|
||||
-- http://luajit.org/ext_ffi_semantics.html#callback :
|
||||
-- It is not allowed, to let an FFI call into a C function (runCallbacks)
|
||||
-- get JIT-compiled, which in turn calls a callback, calling into Lua again (e.g. discordRPC.ready).
|
||||
-- Usually this attempt is caught by the interpreter first and the C function
|
||||
-- is blacklisted for compilation.
|
||||
-- solution:
|
||||
-- "Then you'll need to manually turn off JIT-compilation with jit.off() for
|
||||
-- the surrounding Lua function that invokes such a message polling function."
|
||||
jit.off(discordRPC.runCallbacks)
|
||||
|
||||
function discordRPC.updatePresence(presence)
|
||||
local func = "discordRPC.updatePresence"
|
||||
checkArg(presence, "table", "presence", func)
|
||||
|
||||
-- -1 for string length because of 0-termination
|
||||
checkStrArg(presence.state, 127, "presence.state", func, true)
|
||||
checkStrArg(presence.details, 127, "presence.details", func, true)
|
||||
|
||||
checkIntArg(presence.startTimestamp, 64, "presence.startTimestamp", func, true)
|
||||
checkIntArg(presence.endTimestamp, 64, "presence.endTimestamp", func, true)
|
||||
|
||||
checkStrArg(presence.largeImageKey, 31, "presence.largeImageKey", func, true)
|
||||
checkStrArg(presence.largeImageText, 127, "presence.largeImageText", func, true)
|
||||
checkStrArg(presence.smallImageKey, 31, "presence.smallImageKey", func, true)
|
||||
checkStrArg(presence.smallImageText, 127, "presence.smallImageText", func, true)
|
||||
checkStrArg(presence.partyId, 127, "presence.partyId", func, true)
|
||||
|
||||
checkIntArg(presence.partySize, 32, "presence.partySize", func, true)
|
||||
checkIntArg(presence.partyMax, 32, "presence.partyMax", func, true)
|
||||
|
||||
checkStrArg(presence.matchSecret, 127, "presence.matchSecret", func, true)
|
||||
checkStrArg(presence.joinSecret, 127, "presence.joinSecret", func, true)
|
||||
checkStrArg(presence.spectateSecret, 127, "presence.spectateSecret", func, true)
|
||||
|
||||
checkIntArg(presence.instance, 8, "presence.instance", func, true)
|
||||
|
||||
local cpresence = ffi.new("struct DiscordRichPresence")
|
||||
cpresence.state = presence.state
|
||||
cpresence.details = presence.details
|
||||
cpresence.startTimestamp = presence.startTimestamp or 0
|
||||
cpresence.endTimestamp = presence.endTimestamp or 0
|
||||
cpresence.largeImageKey = presence.largeImageKey
|
||||
cpresence.largeImageText = presence.largeImageText
|
||||
cpresence.smallImageKey = presence.smallImageKey
|
||||
cpresence.smallImageText = presence.smallImageText
|
||||
cpresence.partyId = presence.partyId
|
||||
cpresence.partySize = presence.partySize or 0
|
||||
cpresence.partyMax = presence.partyMax or 0
|
||||
cpresence.matchSecret = presence.matchSecret
|
||||
cpresence.joinSecret = presence.joinSecret
|
||||
cpresence.spectateSecret = presence.spectateSecret
|
||||
cpresence.instance = presence.instance or 0
|
||||
|
||||
discordRPClib.Discord_UpdatePresence(cpresence)
|
||||
end
|
||||
|
||||
function discordRPC.clearPresence()
|
||||
discordRPClib.Discord_ClearPresence()
|
||||
end
|
||||
|
||||
local replyMap = {
|
||||
no = 0,
|
||||
yes = 1,
|
||||
ignore = 2
|
||||
}
|
||||
|
||||
-- maybe let reply take ints too (0, 1, 2) and add constants to the module
|
||||
function discordRPC.respond(userId, reply)
|
||||
checkStrArg(userId, nil, "userId", "discordRPC.respond")
|
||||
assert(replyMap[reply], "Argument 'reply' to discordRPC.respond has to be one of \"yes\", \"no\" or \"ignore\"")
|
||||
discordRPClib.Discord_Respond(userId, replyMap[reply])
|
||||
end
|
||||
|
||||
-- garbage collection callback
|
||||
getmetatable(discordRPC.gcDummy).__gc = function()
|
||||
discordRPC.shutdown()
|
||||
ready_proxy:free()
|
||||
disconnected_proxy:free()
|
||||
errored_proxy:free()
|
||||
joinGame_proxy:free()
|
||||
spectateGame_proxy:free()
|
||||
joinRequest_proxy:free()
|
||||
end
|
||||
|
||||
return discordRPC
|
||||
138
libs/simple-slider.lua
Normal file
@@ -0,0 +1,138 @@
|
||||
--[[
|
||||
Copyright (c) 2016 George Prosser
|
||||
|
||||
Permission is hereby granted, free of charge, to any person
|
||||
obtaining a copy of this software and associated documentation
|
||||
files (the "Software"), to deal in the Software without
|
||||
restriction, including without limitation the rights to use,
|
||||
copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the
|
||||
Software is furnished to do so, subject to the following
|
||||
conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be
|
||||
included in all copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
||||
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES
|
||||
OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
|
||||
NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT
|
||||
HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
|
||||
WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
|
||||
FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR
|
||||
OTHER DEALINGS IN THE SOFTWARE.
|
||||
]]
|
||||
|
||||
local slider = {}
|
||||
slider.__index = slider
|
||||
|
||||
function newSlider(x, y, length, value, min, max, setter, style)
|
||||
local s = {}
|
||||
s.value = (value - min) / (max - min)
|
||||
s.min = min
|
||||
s.max = max
|
||||
s.setter = setter
|
||||
s.x = x
|
||||
s.y = y
|
||||
s.length = length
|
||||
|
||||
local p = style or {}
|
||||
s.width = p.width or length * 0.1
|
||||
s.orientation = p.orientation or 'horizontal'
|
||||
s.track = p.track or 'rectangle'
|
||||
s.knob = p.knob or 'rectangle'
|
||||
|
||||
s.grabbed = false
|
||||
s.wasDown = true
|
||||
s.ox = 0
|
||||
s.oy = 0
|
||||
|
||||
return setmetatable(s, slider)
|
||||
end
|
||||
|
||||
function slider:update(mouseX, mouseY, mouseDown)
|
||||
local x = mouseX or love.mouse.getX()
|
||||
local y = mouseY or love.mouse.getY()
|
||||
local down = love.mouse.isDown(1)
|
||||
if mouseDown ~= nil then
|
||||
down = mouseDown
|
||||
end
|
||||
|
||||
local knobX = self.x
|
||||
local knobY = self.y
|
||||
if self.orientation == 'horizontal' then
|
||||
knobX = self.x - self.length/2 + self.length * self.value
|
||||
elseif self.orientation == 'vertical' then
|
||||
knobY = self.y + self.length/2 - self.length * self.value
|
||||
end
|
||||
|
||||
local ox = x - knobX
|
||||
local oy = y - knobY
|
||||
|
||||
local dx = ox - self.ox
|
||||
local dy = oy - self.oy
|
||||
|
||||
if down then
|
||||
if self.grabbed then
|
||||
if self.orientation == 'horizontal' then
|
||||
self.value = self.value + dx / self.length
|
||||
elseif self.orientation == 'vertical' then
|
||||
self.value = self.value - dy / self.length
|
||||
end
|
||||
elseif (x > knobX - self.width/2 and x < knobX + self.width/2 and y > knobY - self.width/2 and y < knobY + self.width/2) and not self.wasDown then
|
||||
self.ox = ox
|
||||
self.oy = oy
|
||||
self.grabbed = true
|
||||
end
|
||||
else
|
||||
self.grabbed = false
|
||||
end
|
||||
|
||||
self.value = math.max(0, math.min(1, self.value))
|
||||
|
||||
if self.setter ~= nil then
|
||||
self.setter(self.min + self.value * (self.max - self.min))
|
||||
end
|
||||
|
||||
self.wasDown = down
|
||||
end
|
||||
|
||||
function slider:draw()
|
||||
if self.track == 'rectangle' then
|
||||
if self.orientation == 'horizontal' then
|
||||
love.graphics.rectangle('line', self.x - self.length/2 - self.width/2, self.y - self.width/2, self.length + self.width, self.width)
|
||||
elseif self.orientation == 'vertical' then
|
||||
love.graphics.rectangle('line', self.x - self.width/2, self.y - self.length/2 - self.width/2, self.width, self.length + self.width)
|
||||
end
|
||||
elseif self.track == 'line' then
|
||||
if self.orientation == 'horizontal' then
|
||||
love.graphics.line(self.x - self.length/2, self.y, self.x + self.length/2, self.y)
|
||||
elseif self.orientation == 'vertical' then
|
||||
love.graphics.line(self.x, self.y - self.length/2, self.x, self.y + self.length/2)
|
||||
end
|
||||
elseif self.track == 'roundrect' then
|
||||
if self.orientation == 'horizontal' then
|
||||
love.graphics.rectangle('line', self.x - self.length/2 - self.width/2, self.y - self.width/2, self.length + self.width, self.width, self.width/2, self.width)
|
||||
elseif self.orientation == 'vertical' then
|
||||
love.graphics.rectangle('line', self.x - self.width/2, self.y - self.length/2 - self.width/2, self.width, self.length + self.width, self.width, self.width/2)
|
||||
end
|
||||
end
|
||||
|
||||
local knobX = self.x
|
||||
local knobY = self.y
|
||||
if self.orientation == 'horizontal' then
|
||||
knobX = self.x - self.length/2 + self.length * self.value
|
||||
elseif self.orientation == 'vertical' then
|
||||
knobY = self.y + self.length/2 - self.length * self.value
|
||||
end
|
||||
|
||||
if self.knob == 'rectangle' then
|
||||
love.graphics.rectangle('fill', knobX - self.width/2, knobY - self.width/2, self.width, self.width)
|
||||
elseif self.knob == 'circle' then
|
||||
love.graphics.circle('fill', knobX, knobY, self.width/2)
|
||||
end
|
||||
end
|
||||
|
||||
function slider:getValue()
|
||||
return self.min + self.value * (self.max - self.min)
|
||||
end
|
||||
@@ -47,7 +47,7 @@ function fadeoutBGM(time)
|
||||
end
|
||||
|
||||
function resetBGMFadeout(time)
|
||||
current_bgm:setVolume(1)
|
||||
current_bgm:setVolume(config.bgm_volume)
|
||||
fading_bgm = false
|
||||
current_bgm:play()
|
||||
end
|
||||
@@ -59,7 +59,7 @@ function processBGMFadeout(dt)
|
||||
fadeout_time = 0
|
||||
fading_bgm = false
|
||||
end
|
||||
current_bgm:setVolume(fadeout_time / total_fadeout_time)
|
||||
current_bgm:setVolume(fadeout_time * config.bgm_volume / total_fadeout_time)
|
||||
end
|
||||
end
|
||||
|
||||
|
||||
2
load/bigint.lua
Normal file
@@ -0,0 +1,2 @@
|
||||
bigint = require "libs.bigint.bigint"
|
||||
number_names = require "libs.bigint.named-powers-of-ten"
|
||||
@@ -1,54 +1,95 @@
|
||||
backgrounds = {
|
||||
[0] = love.graphics.newImage("res/backgrounds/0-quantum-foam.png"),
|
||||
love.graphics.newImage("res/backgrounds/100-big-bang.png"),
|
||||
love.graphics.newImage("res/backgrounds/200-spiral-galaxy.png"),
|
||||
love.graphics.newImage("res/backgrounds/300-sun-and-dust.png"),
|
||||
love.graphics.newImage("res/backgrounds/400-earth-and-moon.png"),
|
||||
love.graphics.newImage("res/backgrounds/500-cambrian-explosion.png"),
|
||||
love.graphics.newImage("res/backgrounds/600-dinosaurs.png"),
|
||||
love.graphics.newImage("res/backgrounds/700-asteroid.png"),
|
||||
love.graphics.newImage("res/backgrounds/800-human-fire.png"),
|
||||
love.graphics.newImage("res/backgrounds/900-early-civilization.png"),
|
||||
love.graphics.newImage("res/backgrounds/1000-vikings.png"),
|
||||
love.graphics.newImage("res/backgrounds/1100-crusades.png"),
|
||||
love.graphics.newImage("res/backgrounds/1200-genghis-khan.png"),
|
||||
love.graphics.newImage("res/backgrounds/1300-black-death.png"),
|
||||
love.graphics.newImage("res/backgrounds/1400-columbus-discovery.png"),
|
||||
love.graphics.newImage("res/backgrounds/1500-aztecas.png"),
|
||||
love.graphics.newImage("res/backgrounds/1600-telescope.png"),
|
||||
love.graphics.newImage("res/backgrounds/1700-american-revolution.png"),
|
||||
love.graphics.newImage("res/backgrounds/1800-railways.png"),
|
||||
love.graphics.newImage("res/backgrounds/1900-world-wide-web.png"),
|
||||
title = love.graphics.newImage("res/backgrounds/title_v0.1.png"),
|
||||
[0] = love.graphics.newImage("res/backgrounds/0.png"),
|
||||
love.graphics.newImage("res/backgrounds/100.png"),
|
||||
love.graphics.newImage("res/backgrounds/200.png"),
|
||||
love.graphics.newImage("res/backgrounds/300.png"),
|
||||
love.graphics.newImage("res/backgrounds/400.png"),
|
||||
love.graphics.newImage("res/backgrounds/500.png"),
|
||||
love.graphics.newImage("res/backgrounds/600.png"),
|
||||
love.graphics.newImage("res/backgrounds/700.png"),
|
||||
love.graphics.newImage("res/backgrounds/800.png"),
|
||||
love.graphics.newImage("res/backgrounds/900.png"),
|
||||
love.graphics.newImage("res/backgrounds/1000.png"),
|
||||
love.graphics.newImage("res/backgrounds/1100.png"),
|
||||
love.graphics.newImage("res/backgrounds/1200.png"),
|
||||
love.graphics.newImage("res/backgrounds/1300.png"),
|
||||
love.graphics.newImage("res/backgrounds/1400.png"),
|
||||
love.graphics.newImage("res/backgrounds/1500.png"),
|
||||
love.graphics.newImage("res/backgrounds/1600.png"),
|
||||
love.graphics.newImage("res/backgrounds/1700.png"),
|
||||
love.graphics.newImage("res/backgrounds/1800.png"),
|
||||
love.graphics.newImage("res/backgrounds/1900.png"),
|
||||
title = love.graphics.newImage("res/backgrounds/title.png"),
|
||||
snow = love.graphics.newImage("res/backgrounds/snow.png"),
|
||||
input_config = love.graphics.newImage("res/backgrounds/options-input.png"),
|
||||
game_config = love.graphics.newImage("res/backgrounds/options-game.png"),
|
||||
}
|
||||
|
||||
blocks = {
|
||||
["2tie"] = {
|
||||
I = love.graphics.newImage("res/img/s1.png"),
|
||||
J = love.graphics.newImage("res/img/s4.png"),
|
||||
L = love.graphics.newImage("res/img/s3.png"),
|
||||
O = love.graphics.newImage("res/img/s7.png"),
|
||||
S = love.graphics.newImage("res/img/s5.png"),
|
||||
T = love.graphics.newImage("res/img/s2.png"),
|
||||
Z = love.graphics.newImage("res/img/s6.png"),
|
||||
R = love.graphics.newImage("res/img/s1.png"),
|
||||
O = love.graphics.newImage("res/img/s3.png"),
|
||||
Y = love.graphics.newImage("res/img/s7.png"),
|
||||
G = love.graphics.newImage("res/img/s6.png"),
|
||||
C = love.graphics.newImage("res/img/s2.png"),
|
||||
B = love.graphics.newImage("res/img/s4.png"),
|
||||
M = love.graphics.newImage("res/img/s5.png"),
|
||||
F = love.graphics.newImage("res/img/s9.png"),
|
||||
G = love.graphics.newImage("res/img/s9.png"),
|
||||
A = love.graphics.newImage("res/img/s8.png"),
|
||||
X = love.graphics.newImage("res/img/s9.png"),
|
||||
},
|
||||
["bone"] = {
|
||||
I = love.graphics.newImage("res/img/bone.png"),
|
||||
J = love.graphics.newImage("res/img/bone.png"),
|
||||
L = love.graphics.newImage("res/img/bone.png"),
|
||||
R = love.graphics.newImage("res/img/bone.png"),
|
||||
O = love.graphics.newImage("res/img/bone.png"),
|
||||
S = love.graphics.newImage("res/img/bone.png"),
|
||||
T = love.graphics.newImage("res/img/bone.png"),
|
||||
Z = love.graphics.newImage("res/img/bone.png"),
|
||||
F = love.graphics.newImage("res/img/bone.png"),
|
||||
Y = love.graphics.newImage("res/img/bone.png"),
|
||||
G = love.graphics.newImage("res/img/bone.png"),
|
||||
C = love.graphics.newImage("res/img/bone.png"),
|
||||
B = love.graphics.newImage("res/img/bone.png"),
|
||||
M = love.graphics.newImage("res/img/bone.png"),
|
||||
F = love.graphics.newImage("res/img/bone.png"),
|
||||
A = love.graphics.newImage("res/img/bone.png"),
|
||||
X = love.graphics.newImage("res/img/bone.png"),
|
||||
},
|
||||
["gem"] = {
|
||||
R = love.graphics.newImage("res/img/gem1.png"),
|
||||
O = love.graphics.newImage("res/img/gem3.png"),
|
||||
Y = love.graphics.newImage("res/img/gem7.png"),
|
||||
G = love.graphics.newImage("res/img/gem6.png"),
|
||||
C = love.graphics.newImage("res/img/gem2.png"),
|
||||
B = love.graphics.newImage("res/img/gem4.png"),
|
||||
M = love.graphics.newImage("res/img/gem5.png"),
|
||||
F = love.graphics.newImage("res/img/gem9.png"),
|
||||
A = love.graphics.newImage("res/img/gem9.png"),
|
||||
X = love.graphics.newImage("res/img/gem9.png"),
|
||||
},
|
||||
["square"] = {
|
||||
F = love.graphics.newImage("res/img/squares.png"),
|
||||
Y = love.graphics.newImage("res/img/squareg.png"),
|
||||
X = love.graphics.newImage("res/img/squares.png"),
|
||||
}
|
||||
}
|
||||
|
||||
ColourSchemes = {
|
||||
Arika = {
|
||||
I = "R",
|
||||
L = "O",
|
||||
J = "B",
|
||||
S = "M",
|
||||
Z = "G",
|
||||
O = "Y",
|
||||
T = "C",
|
||||
},
|
||||
TTC = {
|
||||
I = "C",
|
||||
L = "O",
|
||||
J = "B",
|
||||
S = "G",
|
||||
Z = "R",
|
||||
O = "Y",
|
||||
T = "M",
|
||||
},
|
||||
}
|
||||
|
||||
for name, blockset in pairs(blocks) do
|
||||
for shape, image in pairs(blockset) do
|
||||
image:setFilter("nearest")
|
||||
@@ -61,4 +102,5 @@ misc_graphics = {
|
||||
go = love.graphics.newImage("res/img/go.png"),
|
||||
select_mode = love.graphics.newImage("res/img/select_mode.png"),
|
||||
strike = love.graphics.newImage("res/img/strike.png"),
|
||||
}
|
||||
santa = love.graphics.newImage("res/img/santa.png")
|
||||
}
|
||||
58
load/rpc.lua
Normal file
@@ -0,0 +1,58 @@
|
||||
print("Loading discord RPC...")
|
||||
DiscordRPC = {
|
||||
loaded = false
|
||||
}
|
||||
local success, RPC = pcall(require, "libs.discordRPC")
|
||||
if success then
|
||||
DiscordRPC.loaded = true
|
||||
DiscordRPC.appId = "599778517789573120"
|
||||
|
||||
function RPC.ready(userId, username, discriminator, avatar)
|
||||
print(string.format("Discord: ready (%s, %s, %s, %s)", userId, username, discriminator, avatar))
|
||||
end
|
||||
|
||||
function RPC.disconnected(errorCode, message)
|
||||
print(string.format("Discord: disconnected (%d: %s)", errorCode, message))
|
||||
end
|
||||
|
||||
function RPC.errored(errorCode, message)
|
||||
print(string.format("Discord: error (%d: %s)", errorCode, message))
|
||||
end
|
||||
|
||||
function RPC.joinGame(joinSecret)
|
||||
print(string.format("Discord: join (%s)", joinSecret))
|
||||
end
|
||||
|
||||
function RPC.spectateGame(spectateSecret)
|
||||
print(string.format("Discord: spectate (%s)", spectateSecret))
|
||||
end
|
||||
|
||||
function RPC.joinRequest(userId, username, discriminator, avatar)
|
||||
print(string.format("Discord: join request (%s, %s, %s, %s)", userId, username, discriminator, avatar))
|
||||
RPC.respond(userId, "yes")
|
||||
end
|
||||
|
||||
RPC.initialize(DiscordRPC.appId, true)
|
||||
local now = os.time(os.date("*t"))
|
||||
|
||||
DiscordRPC.RPC = RPC
|
||||
print("DiscordRPC successfully loaded.")
|
||||
else
|
||||
print("DiscordRPC failed to load!")
|
||||
print(RPC)
|
||||
end
|
||||
|
||||
DiscordRPC.presence = {
|
||||
startTimestamp = now,
|
||||
details = "Loading game...",
|
||||
state = "",
|
||||
largeImageKey = "icon2",
|
||||
largeImageText = "Arcade Stacker",
|
||||
smallImageKey = "",
|
||||
smallImageText = ""
|
||||
}
|
||||
|
||||
function DiscordRPC:update(newstuff)
|
||||
for k, v in pairs(newstuff) do self.presence[k] = v end
|
||||
if self.loaded then self.RPC.updatePresence(self.presence) end
|
||||
end
|
||||
@@ -10,20 +10,50 @@ sounds = {
|
||||
},
|
||||
move = love.audio.newSource("res/se/move.wav", "static"),
|
||||
bottom = love.audio.newSource("res/se/bottom.wav", "static"),
|
||||
cursor = love.audio.newSource("res/se/cursor.wav", "static"),
|
||||
cursor_lr = love.audio.newSource("res/se/cursor_lr.wav", "static"),
|
||||
main_decide = love.audio.newSource("res/se/main_decide.wav", "static"),
|
||||
mode_decide = love.audio.newSource("res/se/mode_decide.wav", "static"),
|
||||
lock = love.audio.newSource("res/se/lock.wav", "static"),
|
||||
hold = love.audio.newSource("res/se/hold.wav", "static"),
|
||||
erase = love.audio.newSource("res/se/erase.wav", "static"),
|
||||
fall = love.audio.newSource("res/se/fall.wav", "static"),
|
||||
ready = love.audio.newSource("res/se/ready.wav", "static"),
|
||||
go = love.audio.newSource("res/se/go.wav", "static"),
|
||||
irs = love.audio.newSource("res/se/irs.wav", "static"),
|
||||
ihs = love.audio.newSource("res/se/ihs.wav", "static"),
|
||||
-- a secret sound!
|
||||
welcome = love.audio.newSource("res/se/welcomeToCambridge.wav", "static"),
|
||||
}
|
||||
|
||||
function playSE(sound, subsound)
|
||||
if subsound == nil then
|
||||
sounds[sound]:setVolume(0.1)
|
||||
sounds[sound]:setVolume(config.sfx_volume)
|
||||
if sounds[sound]:isPlaying() then
|
||||
sounds[sound]:stop()
|
||||
end
|
||||
sounds[sound]:play()
|
||||
else
|
||||
sounds[sound][subsound]:setVolume(0.1)
|
||||
sounds[sound][subsound]:setVolume(config.sfx_volume)
|
||||
if sounds[sound][subsound]:isPlaying() then
|
||||
sounds[sound][subsound]:stop()
|
||||
end
|
||||
sounds[sound][subsound]:play()
|
||||
end
|
||||
end
|
||||
|
||||
function playSEOnce(sound, subsound)
|
||||
if subsound == nil then
|
||||
sounds[sound]:setVolume(config.sfx_volume)
|
||||
if sounds[sound]:isPlaying() then
|
||||
return
|
||||
end
|
||||
sounds[sound]:play()
|
||||
else
|
||||
sounds[sound][subsound]:setVolume(config.sfx_volume)
|
||||
if sounds[sound][subsound]:isPlaying() then
|
||||
return
|
||||
end
|
||||
sounds[sound][subsound]:play()
|
||||
end
|
||||
end
|
||||
178
main.lua
@@ -1,27 +1,72 @@
|
||||
function love.load()
|
||||
math.randomseed(os.time())
|
||||
highscores = {}
|
||||
require "load.rpc"
|
||||
require "load.graphics"
|
||||
require "load.fonts"
|
||||
require "load.sounds"
|
||||
require "load.bgm"
|
||||
require "load.save"
|
||||
require "load.bigint"
|
||||
loadSave()
|
||||
require "scene"
|
||||
config["side_next"] = false
|
||||
config["reverse_rotate"] = true
|
||||
--config["side_next"] = false
|
||||
--config["reverse_rotate"] = true
|
||||
config["fullscreen"] = false
|
||||
|
||||
love.window.setMode(love.graphics.getWidth(), love.graphics.getHeight(), {resizable = true});
|
||||
|
||||
-- init config
|
||||
if not config.das then config.das = 10 end
|
||||
if not config.arr then config.arr = 2 end
|
||||
if not config.dcd then config.dcd = 0 end
|
||||
if not config.sfx_volume then config.sfx_volume = 0.5 end
|
||||
if not config.bgm_volume then config.bgm_volume = 0.5 end
|
||||
|
||||
if config.secret == nil then config.secret = false
|
||||
elseif config.secret == true then playSE("welcome") end
|
||||
|
||||
if not config.gamesettings then
|
||||
config.gamesettings = {}
|
||||
config["das_last_key"] = false
|
||||
else
|
||||
config["das_last_key"] = config.gamesettings.das_last_key == 2
|
||||
end
|
||||
for _, option in ipairs(GameConfigScene.options) do
|
||||
if not config.gamesettings[option[1]] then
|
||||
config.gamesettings[option[1]] = 1
|
||||
end
|
||||
end
|
||||
|
||||
if not config.input then
|
||||
config.input = {}
|
||||
scene = InputConfigScene()
|
||||
else
|
||||
if config.current_mode then current_mode = config.current_mode end
|
||||
if config.current_ruleset then current_ruleset = config.current_ruleset end
|
||||
scene = TitleScene()
|
||||
end
|
||||
|
||||
game_modes = {}
|
||||
mode_list = love.filesystem.getDirectoryItems("tetris/modes")
|
||||
for i=1,#mode_list do
|
||||
if(mode_list[i] ~= "gamemode.lua" and mode_list[i] ~= "unrefactored_modes") then
|
||||
game_modes[#game_modes+1] = require ("tetris.modes."..string.sub(mode_list[i],1,-5))
|
||||
end
|
||||
end
|
||||
rulesets = {}
|
||||
rule_list = love.filesystem.getDirectoryItems("tetris/rulesets")
|
||||
for i=1,#rule_list do
|
||||
if(rule_list[i] ~= "ruleset.lua" and rule_list[i] ~= "unrefactored_rulesets") then
|
||||
rulesets[#rulesets+1] = require ("tetris.rulesets."..string.sub(rule_list[i],1,-5))
|
||||
end
|
||||
end
|
||||
--sort mode/rule lists
|
||||
local function padnum(d) return ("%03d%s"):format(#d, d) end
|
||||
table.sort(game_modes, function(a,b)
|
||||
return tostring(a.name):gsub("%d+",padnum) < tostring(b.name):gsub("%d+",padnum) end)
|
||||
table.sort(rulesets, function(a,b)
|
||||
return tostring(a.name):gsub("%d+",padnum) < tostring(b.name):gsub("%d+",padnum) end)
|
||||
|
||||
end
|
||||
|
||||
local TARGET_FPS = 60
|
||||
@@ -64,7 +109,7 @@ end
|
||||
|
||||
function love.draw()
|
||||
love.graphics.push()
|
||||
|
||||
|
||||
-- get offset matrix
|
||||
love.graphics.setDefaultFilter("linear", "nearest")
|
||||
local width = love.graphics.getWidth()
|
||||
@@ -80,18 +125,137 @@ function love.draw()
|
||||
love.graphics.pop()
|
||||
end
|
||||
|
||||
function love.keypressed(key, scancode, isrepeat)
|
||||
function love.keypressed(key, scancode)
|
||||
-- global hotkeys
|
||||
if scancode == "f4" then
|
||||
config["fullscreen"] = not config["fullscreen"]
|
||||
love.window.setFullscreen(config["fullscreen"])
|
||||
elseif scancode == "f2" and scene.title ~= "Input Config" and scene.title ~= "Game" then
|
||||
scene = InputConfigScene()
|
||||
switchBGM(nil)
|
||||
-- secret sound playing :eyes:
|
||||
elseif scancode == "f8" and scene.title == "Title" then
|
||||
config.secret = not config.secret
|
||||
saveConfig()
|
||||
scene.restart_message = true
|
||||
if config.secret then playSE("mode_decide")
|
||||
else playSE("erase") end
|
||||
-- function keys are reserved
|
||||
elseif string.match(scancode, "^f[1-9]$") or string.match(scancode, "^f[1-9][0-9]+$") then
|
||||
return
|
||||
-- escape is reserved for menu_back
|
||||
elseif scancode == "escape" then
|
||||
scene:onInputPress({input="menu_back", type="key", key=key, scancode=scancode})
|
||||
-- pass any other key to the scene, with its configured mapping
|
||||
else
|
||||
scene:onKeyPress({key=key, scancode=scancode, isRepeat=isrepeat})
|
||||
local input_pressed = nil
|
||||
if config.input and config.input.keys then
|
||||
input_pressed = config.input.keys[scancode]
|
||||
end
|
||||
scene:onInputPress({input=input_pressed, type="key", key=key, scancode=scancode})
|
||||
end
|
||||
end
|
||||
|
||||
function love.keyreleased(key, scancode)
|
||||
-- escape is reserved for menu_back
|
||||
if scancode == "escape" then
|
||||
scene:onInputRelease({input="menu_back", type="key", key=key, scancode=scancode})
|
||||
-- function keys are reserved
|
||||
elseif string.match(scancode, "^f[1-9]$") or string.match(scancode, "^f[1-9][0-9]+$") then
|
||||
return
|
||||
-- handle all other keys; tab is reserved, but the input config scene keeps it from getting configured as a game input, so pass tab to the scene here
|
||||
else
|
||||
local input_released = nil
|
||||
if config.input and config.input.keys then
|
||||
input_released = config.input.keys[scancode]
|
||||
end
|
||||
scene:onInputRelease({input=input_released, type="key", key=key, scancode=scancode})
|
||||
end
|
||||
end
|
||||
|
||||
function love.joystickpressed(joystick, button)
|
||||
local input_pressed = nil
|
||||
if
|
||||
config.input and
|
||||
config.input.joysticks and
|
||||
config.input.joysticks[joystick:getName()] and
|
||||
config.input.joysticks[joystick:getName()].buttons
|
||||
then
|
||||
input_pressed = config.input.joysticks[joystick:getName()].buttons[button]
|
||||
end
|
||||
scene:onInputPress({input=input_pressed, type="joybutton", name=joystick:getName(), button=button})
|
||||
end
|
||||
|
||||
function love.joystickreleased(joystick, button)
|
||||
local input_released = nil
|
||||
if
|
||||
config.input and
|
||||
config.input.joysticks and
|
||||
config.input.joysticks[joystick:getName()] and
|
||||
config.input.joysticks[joystick:getName()].buttons
|
||||
then
|
||||
input_released = config.input.joysticks[joystick:getName()].buttons[button]
|
||||
end
|
||||
scene:onInputRelease({input=input_released, type="joybutton", name=joystick:getName(), button=button})
|
||||
end
|
||||
|
||||
function love.joystickaxis(joystick, axis, value)
|
||||
local input_pressed = nil
|
||||
local positive_released = nil
|
||||
local negative_released = nil
|
||||
if
|
||||
config.input and
|
||||
config.input.joysticks and
|
||||
config.input.joysticks[joystick:getName()] and
|
||||
config.input.joysticks[joystick:getName()].axes and
|
||||
config.input.joysticks[joystick:getName()].axes[axis]
|
||||
then
|
||||
if math.abs(value) >= 0.5 then
|
||||
input_pressed = config.input.joysticks[joystick:getName()].axes[axis][value >= 0.5 and "positive" or "negative"]
|
||||
end
|
||||
positive_released = config.input.joysticks[joystick:getName()].axes[axis].positive
|
||||
negative_released = config.input.joysticks[joystick:getName()].axes[axis].negative
|
||||
end
|
||||
if math.abs(value) >= 0.5 then
|
||||
scene:onInputPress({input=input_pressed, type="joyaxis", name=joystick:getName(), axis=axis, value=value})
|
||||
else
|
||||
scene:onInputRelease({input=positive_released, type="joyaxis", name=joystick:getName(), axis=axis, value=value})
|
||||
scene:onInputRelease({input=negative_released, type="joyaxis", name=joystick:getName(), axis=axis, value=value})
|
||||
end
|
||||
end
|
||||
|
||||
function love.joystickhat(joystick, hat, direction)
|
||||
local input_pressed = nil
|
||||
local has_hat = false
|
||||
if
|
||||
config.input and
|
||||
config.input.joysticks and
|
||||
config.input.joysticks[joystick:getName()] and
|
||||
config.input.joysticks[joystick:getName()].hats and
|
||||
config.input.joysticks[joystick:getName()].hats[hat]
|
||||
then
|
||||
if direction ~= "c" then
|
||||
input_pressed = config.input.joysticks[joystick:getName()].hats[hat][direction]
|
||||
end
|
||||
has_hat = true
|
||||
end
|
||||
if input_pressed then
|
||||
scene:onInputPress({input=input_pressed, type="joyhat", name=joystick:getName(), hat=hat, direction=direction})
|
||||
elseif has_hat then
|
||||
for i, direction in ipairs{"d", "l", "ld", "lu", "r", "rd", "ru", "u"} do
|
||||
scene:onInputRelease({input=config.input.joysticks[joystick:getName()].hats[hat][direction], type="joyhat", name=joystick:getName(), hat=hat, direction=direction})
|
||||
end
|
||||
elseif direction ~= "c" then
|
||||
scene:onInputPress({input=nil, type="joyhat", name=joystick:getName(), hat=hat, direction=direction})
|
||||
else
|
||||
for i, direction in ipairs{"d", "l", "ld", "lu", "r", "rd", "ru", "u"} do
|
||||
scene:onInputRelease({input=nil, type="joyhat", name=joystick:getName(), hat=hat, direction=direction})
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
function love.focus(f)
|
||||
if f then
|
||||
if f and (scene.title ~= "Game" or not scene.paused) then
|
||||
resumeBGM()
|
||||
else
|
||||
pauseBGM()
|
||||
|
||||
2
package.bat
Normal file
@@ -0,0 +1,2 @@
|
||||
tar -a -c -f cambridge.zip libs/binser.lua libs/classic.lua libs/simple-slider.lua libs/discordRPC.lua load res scene tetris conf.lua main.lua scene.lua funcs.lua
|
||||
rename cambridge.zip cambridge.love
|
||||
4
release
@@ -4,6 +4,6 @@ mkdir dist/windows
|
||||
mkdir dist/win32
|
||||
cp cambridge.love dist/
|
||||
cat dist/windows/love.exe cambridge.love > dist/windows/cambridge.exe
|
||||
zip dist/cambridge-windows.zip dist/windows/* SOURCES.md LICENSE
|
||||
zip dist/cambridge-windows.zip dist/windows/* SOURCES.md LICENSE.md
|
||||
cat dist/win32/love.exe cambridge.love > dist/win32/cambridge.exe
|
||||
zip dist/cambridge-win32.zip dist/win32/* SOURCES.md LICENSE
|
||||
zip dist/cambridge-win32.zip dist/win32/* SOURCES.md LICENSE.md
|
||||
|
||||
26
release.bat
Normal file
@@ -0,0 +1,26 @@
|
||||
call package.bat
|
||||
|
||||
mkdir dist
|
||||
mkdir dist\windows
|
||||
mkdir dist\windows\libs
|
||||
mkdir dist\win32
|
||||
mkdir dist\win32\libs
|
||||
|
||||
copy /b dist\windows\love.exe+cambridge.love dist\windows\cambridge.exe
|
||||
copy /b dist\win32\love.exe+cambridge.love dist\win32\cambridge.exe
|
||||
|
||||
copy libs\discord-rpc.dll dist\windows\libs
|
||||
copy libs\discord-rpc.dll dist\win32\libs
|
||||
|
||||
copy SOURCES.md dist\windows
|
||||
copy LICENSE.md dist\windows
|
||||
copy SOURCES.md dist\win32
|
||||
copy LICENSE.md dist\win32
|
||||
|
||||
cd dist\windows
|
||||
tar -a -c -f ..\cambridge-windows.zip cambridge.exe *.dll libs *.md
|
||||
cd ..\..
|
||||
|
||||
cd dist\win32
|
||||
tar -a -c -f ..\cambridge-win32.zip cambridge.exe *.dll libs *.md
|
||||
cd ..\..
|
||||
0
res/backgrounds/0-quantum-foam.png → res/backgrounds/0.png
Executable file → Normal file
|
Before Width: | Height: | Size: 2.4 MiB After Width: | Height: | Size: 2.4 MiB |
0
res/backgrounds/100-big-bang.png → res/backgrounds/100.png
Executable file → Normal file
|
Before Width: | Height: | Size: 1.4 MiB After Width: | Height: | Size: 1.4 MiB |
0
res/backgrounds/1000-vikings.png → res/backgrounds/1000.png
Executable file → Normal file
|
Before Width: | Height: | Size: 2.0 MiB After Width: | Height: | Size: 2.0 MiB |
0
res/backgrounds/1100-crusades.png → res/backgrounds/1100.png
Executable file → Normal file
|
Before Width: | Height: | Size: 2.9 MiB After Width: | Height: | Size: 2.9 MiB |
0
res/backgrounds/1200-genghis-khan.png → res/backgrounds/1200.png
Executable file → Normal file
|
Before Width: | Height: | Size: 2.1 MiB After Width: | Height: | Size: 2.1 MiB |
0
res/backgrounds/1300-black-death.png → res/backgrounds/1300.png
Executable file → Normal file
|
Before Width: | Height: | Size: 1.4 MiB After Width: | Height: | Size: 1.4 MiB |
0
res/backgrounds/1400-columbus-discovery.png → res/backgrounds/1400.png
Executable file → Normal file
|
Before Width: | Height: | Size: 2.6 MiB After Width: | Height: | Size: 2.6 MiB |
0
res/backgrounds/1500-aztecas.png → res/backgrounds/1500.png
Executable file → Normal file
|
Before Width: | Height: | Size: 2.1 MiB After Width: | Height: | Size: 2.1 MiB |
0
res/backgrounds/1600-telescope.png → res/backgrounds/1600.png
Executable file → Normal file
|
Before Width: | Height: | Size: 2.1 MiB After Width: | Height: | Size: 2.1 MiB |
0
res/backgrounds/1700-american-revolution.png → res/backgrounds/1700.png
Executable file → Normal file
|
Before Width: | Height: | Size: 2.1 MiB After Width: | Height: | Size: 2.1 MiB |
0
res/backgrounds/1800-railways.png → res/backgrounds/1800.png
Executable file → Normal file
|
Before Width: | Height: | Size: 2.4 MiB After Width: | Height: | Size: 2.4 MiB |
0
res/backgrounds/1900-world-wide-web.png → res/backgrounds/1900.png
Executable file → Normal file
|
Before Width: | Height: | Size: 1.9 MiB After Width: | Height: | Size: 1.9 MiB |
0
res/backgrounds/200-spiral-galaxy.png → res/backgrounds/200.png
Executable file → Normal file
|
Before Width: | Height: | Size: 1.6 MiB After Width: | Height: | Size: 1.6 MiB |
0
res/backgrounds/300-sun-and-dust.png → res/backgrounds/300.png
Executable file → Normal file
|
Before Width: | Height: | Size: 1.7 MiB After Width: | Height: | Size: 1.7 MiB |
0
res/backgrounds/400-earth-and-moon.png → res/backgrounds/400.png
Executable file → Normal file
|
Before Width: | Height: | Size: 2.0 MiB After Width: | Height: | Size: 2.0 MiB |
0
res/backgrounds/500-cambrian-explosion.png → res/backgrounds/500.png
Executable file → Normal file
|
Before Width: | Height: | Size: 2.1 MiB After Width: | Height: | Size: 2.1 MiB |
0
res/backgrounds/600-dinosaurs.png → res/backgrounds/600.png
Executable file → Normal file
|
Before Width: | Height: | Size: 3.1 MiB After Width: | Height: | Size: 3.1 MiB |
0
res/backgrounds/700-asteroid.png → res/backgrounds/700.png
Executable file → Normal file
|
Before Width: | Height: | Size: 1.2 MiB After Width: | Height: | Size: 1.2 MiB |
0
res/backgrounds/800-human-fire.png → res/backgrounds/800.png
Executable file → Normal file
|
Before Width: | Height: | Size: 2.7 MiB After Width: | Height: | Size: 2.7 MiB |
0
res/backgrounds/900-early-civilization.png → res/backgrounds/900.png
Executable file → Normal file
|
Before Width: | Height: | Size: 3.6 MiB After Width: | Height: | Size: 3.6 MiB |
BIN
res/backgrounds/options-game.png
Normal file
|
After Width: | Height: | Size: 2.9 MiB |
BIN
res/backgrounds/options-input.png
Normal file
|
After Width: | Height: | Size: 3.0 MiB |
BIN
res/backgrounds/snow.png
Normal file
|
After Width: | Height: | Size: 1.7 MiB |
BIN
res/backgrounds/title.png
Normal file
|
After Width: | Height: | Size: 1.3 MiB |
|
Before Width: | Height: | Size: 1.6 MiB |
BIN
res/img/bonew.png
Normal file
|
After Width: | Height: | Size: 229 B |
BIN
res/img/gem1.png
Normal file
|
After Width: | Height: | Size: 462 B |
BIN
res/img/gem2.png
Normal file
|
After Width: | Height: | Size: 388 B |
BIN
res/img/gem3.png
Normal file
|
After Width: | Height: | Size: 445 B |
BIN
res/img/gem4.png
Normal file
|
After Width: | Height: | Size: 426 B |
BIN
res/img/gem5.png
Normal file
|
After Width: | Height: | Size: 376 B |
BIN
res/img/gem6.png
Normal file
|
After Width: | Height: | Size: 377 B |
BIN
res/img/gem7.png
Normal file
|
After Width: | Height: | Size: 399 B |
BIN
res/img/gem9.png
Normal file
|
After Width: | Height: | Size: 354 B |
BIN
res/img/s8.png
Normal file
|
After Width: | Height: | Size: 233 B |
BIN
res/img/santa.png
Normal file
|
After Width: | Height: | Size: 75 KiB |
BIN
res/img/squareg.png
Normal file
|
After Width: | Height: | Size: 708 B |
BIN
res/img/squares.png
Normal file
|
After Width: | Height: | Size: 639 B |
BIN
res/se/cursor.wav
Normal file
BIN
res/se/cursor_lr.wav
Normal file
BIN
res/se/erase.wav
Normal file
BIN
res/se/fall.wav
Normal file
BIN
res/se/go.wav
Normal file
BIN
res/se/hold.wav
Normal file
BIN
res/se/ihs.wav
Normal file
BIN
res/se/irs.wav
Normal file
BIN
res/se/lock.wav
Normal file
BIN
res/se/main_decide.wav
Normal file
BIN
res/se/mode_decide.wav
Normal file
BIN
res/se/ready.wav
Normal file
BIN
res/se/welcomeToCambridge.wav
Normal file
@@ -5,10 +5,15 @@ Scene = Object:extend()
|
||||
function Scene:new() end
|
||||
function Scene:update() end
|
||||
function Scene:render() end
|
||||
function Scene:onKeyPress() end
|
||||
function Scene:onInputPress() end
|
||||
function Scene:onInputRelease() end
|
||||
|
||||
ExitScene = require "scene.exit"
|
||||
GameScene = require "scene.game"
|
||||
ModeSelectScene = require "scene.mode_select"
|
||||
InputConfigScene = require "scene.input_config"
|
||||
ConfigScene = require "scene.config"
|
||||
GameConfigScene = require "scene.game_config"
|
||||
TuningScene = require "scene.tuning"
|
||||
SettingsScene = require "scene.settings"
|
||||
CreditsScene = require "scene.credits"
|
||||
TitleScene = require "scene.title"
|
||||
|
||||
@@ -17,7 +17,7 @@ function ConfigScene:changeOption(rel)
|
||||
self.main_menu_state = (self.main_menu_state + len + rel - 1) % len + 1
|
||||
end
|
||||
|
||||
function ConfigScene:onKeyPress(e)
|
||||
function ConfigScene:onInputPress(e)
|
||||
end
|
||||
|
||||
return ConfigScene
|
||||
|
||||
64
scene/credits.lua
Normal file
@@ -0,0 +1,64 @@
|
||||
local CreditsScene = Scene:extend()
|
||||
|
||||
CreditsScene.title = "Credits"
|
||||
|
||||
function CreditsScene:new()
|
||||
self.frames = 0
|
||||
switchBGM("credit_roll", "gm3")
|
||||
end
|
||||
|
||||
function CreditsScene:update()
|
||||
if love.window.hasFocus() then
|
||||
self.frames = self.frames + 1
|
||||
end
|
||||
if self.frames >= 4200 then
|
||||
playSE("mode_decide")
|
||||
scene = TitleScene()
|
||||
switchBGM(nil)
|
||||
elseif self.frames == 3600 then
|
||||
fadeoutBGM(2)
|
||||
end
|
||||
end
|
||||
|
||||
function CreditsScene:render()
|
||||
love.graphics.setColor(1, 1, 1, 1)
|
||||
love.graphics.draw(
|
||||
backgrounds[19],
|
||||
0, 0, 0,
|
||||
0.5, 0.5
|
||||
)
|
||||
|
||||
love.graphics.setFont(font_3x5_4)
|
||||
love.graphics.print("Cambridge Credits", 320, 500 - self.frames / 2)
|
||||
love.graphics.print("THANK YOU\nFOR PLAYING!", 320, math.max(1500 - self.frames / 2, 240))
|
||||
|
||||
love.graphics.setFont(font_3x5_3)
|
||||
love.graphics.print("Game Developers", 320, 550 - self.frames / 2)
|
||||
love.graphics.print("Project Heads", 320, 640 - self.frames / 2)
|
||||
love.graphics.print("Other Game Developers", 320, 730 - self.frames / 2)
|
||||
love.graphics.print("Special Thanks", 320, 900 - self.frames / 2)
|
||||
love.graphics.print("- SashLilac / SpinTriple", 320, math.max(2000 - self.frames / 2, 320))
|
||||
|
||||
love.graphics.setFont(font_3x5_2)
|
||||
love.graphics.print("Oshisaure\nJoe Zeng", 320, 590 - self.frames / 2)
|
||||
love.graphics.print("Mizu\nHailey", 320, 680 - self.frames / 2)
|
||||
love.graphics.print("Axel Fox - Multimino\nMine - Tetra Online\nDr Ocelot - Tetra Legends\nFelicity / nightmareci - Shiromino\n2Tie - TGMsim\nPhoenix Flare - Master of Blocks", 320, 770 - self.frames / 2)
|
||||
love.graphics.print(
|
||||
"RocketLanterns\nCylinderKnot\nHammrTime\nKirby703\nMattMayuga\nMyPasswordIsWeak\n" ..
|
||||
"Nikki Karissa\noffwo\nsinefuse\nTetro48\nTimmSkiller\nuser74003\nAgentBasey\n" ..
|
||||
"CheeZed_Fish\neightsixfivezero\nEricICX\ngizmo4487\nM1ssing0\nMarkGamed7794\n" ..
|
||||
"pokemonfan1937\nSimon\nstratus\nZaptorZap\nThe Absolute PLUS Discord\nTetra Legends Discord\n" ..
|
||||
"Tetra Online Discord\nMultimino Discord\nCambridge Discord\nAnd to you, the player!",
|
||||
320, 940 - self.frames / 2
|
||||
)
|
||||
end
|
||||
|
||||
function CreditsScene:onInputPress(e)
|
||||
if e.input == "menu_decide" or e.scancode == "return" or
|
||||
e.input == "menu_back" or e.scancode == "delete" or e.scancode == "backspace" then
|
||||
scene = TitleScene()
|
||||
switchBGM(nil)
|
||||
end
|
||||
end
|
||||
|
||||
return CreditsScene
|
||||
23
scene/exit.lua
Normal file
@@ -0,0 +1,23 @@
|
||||
local ExitScene = Scene:extend()
|
||||
require 'load.save'
|
||||
|
||||
ExitScene.title = "Exit Game"
|
||||
|
||||
function ExitScene:new()
|
||||
end
|
||||
|
||||
function ExitScene:update()
|
||||
love.event.quit()
|
||||
end
|
||||
|
||||
function ExitScene:render()
|
||||
end
|
||||
|
||||
function ExitScene:changeOption(rel)
|
||||
end
|
||||
|
||||
function ExitScene:onInputPress(e)
|
||||
end
|
||||
|
||||
return ExitScene
|
||||
|
||||
132
scene/game.lua
@@ -1,29 +1,44 @@
|
||||
local GameScene = Scene:extend()
|
||||
|
||||
GameScene.title = "Game"
|
||||
|
||||
require 'load.save'
|
||||
|
||||
function GameScene:new(game_mode, ruleset)
|
||||
self.game = game_mode()
|
||||
function GameScene:new(game_mode, ruleset, inputs)
|
||||
self.retry_mode = game_mode
|
||||
self.retry_ruleset = ruleset
|
||||
self.secret_inputs = copy(inputs)
|
||||
self.game = game_mode(self.secret_inputs)
|
||||
self.ruleset = ruleset()
|
||||
self.game:initialize(self.ruleset)
|
||||
self.game:initialize(self.ruleset, self.secret_inputs)
|
||||
self.inputs = {
|
||||
left=false,
|
||||
right=false,
|
||||
up=false,
|
||||
down=false,
|
||||
rotate_left=false,
|
||||
rotate_left2=false,
|
||||
rotate_right=false,
|
||||
rotate_right2=false,
|
||||
rotate_180=false,
|
||||
hold=false,
|
||||
}
|
||||
self.paused = false
|
||||
DiscordRPC:update({
|
||||
details = self.game.rpc_details,
|
||||
state = self.game.name,
|
||||
})
|
||||
end
|
||||
|
||||
function GameScene:update()
|
||||
if love.window.hasFocus() then
|
||||
self.game:update({
|
||||
left = love.keyboard.isScancodeDown(config.input.left),
|
||||
right = love.keyboard.isScancodeDown(config.input.right),
|
||||
up = love.keyboard.isScancodeDown(config.input.up),
|
||||
down = love.keyboard.isScancodeDown(config.input.down),
|
||||
rotate_left = love.keyboard.isScancodeDown(config.input.rotate_left),
|
||||
rotate_left2 = love.keyboard.isScancodeDown(config.input.rotate_left2),
|
||||
rotate_right = love.keyboard.isScancodeDown(config.input.rotate_right),
|
||||
rotate_right2 = love.keyboard.isScancodeDown(config.input.rotate_right2),
|
||||
rotate_180 = love.keyboard.isScancodeDown(config.input.rotate_180),
|
||||
hold = love.keyboard.isScancodeDown(config.input.hold),
|
||||
}, self.ruleset)
|
||||
if love.window.hasFocus() and not self.paused then
|
||||
local inputs = {}
|
||||
for input, value in pairs(self.inputs) do
|
||||
inputs[input] = value
|
||||
end
|
||||
self.game:update(inputs, self.ruleset)
|
||||
self.game.grid:update()
|
||||
end
|
||||
|
||||
self.game.grid:update()
|
||||
end
|
||||
|
||||
function GameScene:render()
|
||||
@@ -35,9 +50,37 @@ function GameScene:render()
|
||||
)
|
||||
|
||||
-- game frame
|
||||
love.graphics.draw(misc_graphics["frame"], 48, 64)
|
||||
if self.game.grid.width == 10 and self.game.grid.height == 24 then
|
||||
love.graphics.draw(misc_graphics["frame"], 48, 64)
|
||||
end
|
||||
|
||||
love.graphics.setColor(0, 0, 0, 200)
|
||||
love.graphics.rectangle("fill", 64, 80, 160, 320)
|
||||
love.graphics.rectangle(
|
||||
"fill", 64, 80,
|
||||
16 * self.game.grid.width, 16 * (self.game.grid.height - 4)
|
||||
)
|
||||
|
||||
if self.game.grid.width ~= 10 or self.game.grid.height ~= 24 then
|
||||
love.graphics.setColor(174/255, 83/255, 76/255, 1)
|
||||
love.graphics.setLineWidth(8)
|
||||
love.graphics.line(
|
||||
60,76,
|
||||
68+16*self.game.grid.width,76,
|
||||
68+16*self.game.grid.width,84+16*(self.game.grid.height-4),
|
||||
60,84+16*(self.game.grid.height-4),
|
||||
60,76
|
||||
)
|
||||
love.graphics.setColor(203/255, 137/255, 111/255, 1)
|
||||
love.graphics.setLineWidth(4)
|
||||
love.graphics.line(
|
||||
60,76,
|
||||
68+16*self.game.grid.width,76,
|
||||
68+16*self.game.grid.width,84+16*(self.game.grid.height-4),
|
||||
60,84+16*(self.game.grid.height-4),
|
||||
60,76
|
||||
)
|
||||
love.graphics.setLineWidth(1)
|
||||
end
|
||||
|
||||
self.game:drawGrid()
|
||||
self.game:drawPiece()
|
||||
@@ -45,6 +88,7 @@ function GameScene:render()
|
||||
self.game:drawScoringInfo()
|
||||
|
||||
-- ready/go graphics
|
||||
|
||||
if self.game.ready_frames <= 100 and self.game.ready_frames > 52 then
|
||||
love.graphics.draw(misc_graphics["ready"], 144 - 50, 240 - 14)
|
||||
elseif self.game.ready_frames <= 50 and self.game.ready_frames > 2 then
|
||||
@@ -53,21 +97,61 @@ function GameScene:render()
|
||||
|
||||
self.game:drawCustom()
|
||||
|
||||
love.graphics.setFont(font_3x5_2)
|
||||
if config.gamesettings.display_gamemode == 1 then
|
||||
love.graphics.printf(self.game.name .. " - " .. self.ruleset.name, 0, 460, 640, "left")
|
||||
end
|
||||
|
||||
love.graphics.setFont(font_3x5_3)
|
||||
if self.paused then love.graphics.print("PAUSED!", 80, 100) end
|
||||
|
||||
if self.game.completed then
|
||||
self.game:onGameComplete()
|
||||
elseif self.game.game_over then
|
||||
self.game:onGameOver()
|
||||
end
|
||||
end
|
||||
|
||||
function GameScene:onKeyPress(e)
|
||||
if (self.game.completed) and
|
||||
e.scancode == "return" and e.isRepeat == false then
|
||||
function GameScene:onInputPress(e)
|
||||
if (self.game.game_over or self.game.completed) and (e.input == "menu_decide" or e.input == "menu_back" or e.input == "retry") then
|
||||
highscore_entry = self.game:getHighscoreData()
|
||||
highscore_hash = self.game.hash .. "-" .. self.ruleset.hash
|
||||
submitHighscore(highscore_hash, highscore_entry)
|
||||
scene = e.input == "retry" and GameScene(self.retry_mode, self.retry_ruleset, self.secret_inputs) or ModeSelectScene()
|
||||
elseif e.input == "retry" then
|
||||
switchBGM(nil)
|
||||
scene = GameScene(self.retry_mode, self.retry_ruleset, self.secret_inputs)
|
||||
elseif e.input == "pause" and not (self.game.game_over or self.game.completed) then
|
||||
self.paused = not self.paused
|
||||
if self.paused then pauseBGM()
|
||||
else resumeBGM() end
|
||||
elseif e.input == "menu_back" then
|
||||
scene = ModeSelectScene()
|
||||
elseif e.input and string.sub(e.input, 1, 5) ~= "menu_" then
|
||||
self.inputs[e.input] = true
|
||||
end
|
||||
end
|
||||
|
||||
function GameScene:onInputRelease(e)
|
||||
if e.input and string.sub(e.input, 1, 5) ~= "menu_" then
|
||||
self.inputs[e.input] = false
|
||||
end
|
||||
end
|
||||
|
||||
function submitHighscore(hash, data)
|
||||
function isHighscore(score, high)
|
||||
for k, _ in pairs(score) do
|
||||
if not high[k] or score[k] > high[k] then
|
||||
return true
|
||||
end
|
||||
end
|
||||
return false
|
||||
end
|
||||
|
||||
if not highscores[hash] then highscores[hash] = {} end
|
||||
table.insert(highscores[hash], data)
|
||||
if isHighscore(data, highscores[hash]) then
|
||||
highscores[hash] = data
|
||||
end
|
||||
saveHighscores()
|
||||
end
|
||||
|
||||
|
||||
124
scene/game_config.lua
Normal file
@@ -0,0 +1,124 @@
|
||||
local ConfigScene = Scene:extend()
|
||||
|
||||
ConfigScene.title = "Game Settings"
|
||||
|
||||
require 'load.save'
|
||||
require 'libs.simple-slider'
|
||||
|
||||
ConfigScene.options = {
|
||||
-- this serves as reference to what the options' values mean i guess?
|
||||
-- Format: {name in config, displayed name, uses slider?, options OR slider name}
|
||||
{"manlock", "Manual Locking", false, {"Per ruleset", "Per gamemode", "Harddrop", "Softdrop"}},
|
||||
{"piece_colour", "Piece Colours", false, {"Per ruleset", "Arika", "TTC"}},
|
||||
{"world_reverse", "A Button Rotation", false, {"Left", "Auto", "Right"}},
|
||||
{"spawn_positions", "Spawn Positions", false, {"In field", "Out of field"}},
|
||||
{"display_gamemode", "Display Gamemode", false, {"On", "Off"}},
|
||||
{"das_last_key", "DAS Switch", false, {"Default", "Instant"}},
|
||||
{"smooth_movement", "Smooth Piece Drop", false, {"On", "Off"}},
|
||||
{"synchroes_allowed", "Synchroes", false, {"Per ruleset", "On", "Off"}},
|
||||
{"diagonal_input", "Diagonal Input", false, {"On", "Off"}},
|
||||
{"buffer_lock", "Buffer Lock Inputs", false, {"On", "Off"}},
|
||||
{"sfx_volume", "SFX", true, "sfxSlider"},
|
||||
{"bgm_volume", "BGM", true, "bgmSlider"},
|
||||
}
|
||||
local optioncount = #ConfigScene.options
|
||||
|
||||
function ConfigScene:new()
|
||||
-- load current config
|
||||
self.config = config.input
|
||||
self.highlight = 1
|
||||
|
||||
DiscordRPC:update({
|
||||
details = "In menus",
|
||||
state = "Changing game settings",
|
||||
})
|
||||
|
||||
self.sfxSlider = newSlider(165, 400, 225, config.sfx_volume * 100, 0, 100, function(v) config.sfx_volume = v / 100 end, {width=20, knob="circle", track="roundrect"})
|
||||
self.bgmSlider = newSlider(465, 400, 225, config.bgm_volume * 100, 0, 100, function(v) config.bgm_volume = v / 100 end, {width=20, knob="circle", track="roundrect"})
|
||||
end
|
||||
|
||||
function ConfigScene:update()
|
||||
config["das_last_key"] = config.gamesettings.das_last_key == 2
|
||||
self.sfxSlider:update()
|
||||
self.bgmSlider:update()
|
||||
end
|
||||
|
||||
function ConfigScene:render()
|
||||
love.graphics.setColor(1, 1, 1, 1)
|
||||
love.graphics.draw(
|
||||
backgrounds["game_config"],
|
||||
0, 0, 0,
|
||||
0.5, 0.5
|
||||
)
|
||||
|
||||
love.graphics.setFont(font_3x5_4)
|
||||
love.graphics.print("GAME SETTINGS", 80, 40)
|
||||
|
||||
--Lazy check to see if we're on the SFX or BGM slider. Probably will need to be rewritten if more options get added.
|
||||
love.graphics.setColor(1, 1, 1, 0.5)
|
||||
if not ConfigScene.options[self.highlight][3] then
|
||||
love.graphics.rectangle("fill", 25, 98 + self.highlight * 20, 170, 22)
|
||||
else
|
||||
love.graphics.rectangle("fill", 65 + (1+self.highlight-#self.options) * 300, 342, 215, 33)
|
||||
end
|
||||
|
||||
love.graphics.setFont(font_3x5_2)
|
||||
for i, option in ipairs(ConfigScene.options) do
|
||||
if not option[3] then
|
||||
love.graphics.setColor(1, 1, 1, 1)
|
||||
love.graphics.printf(option[2], 40, 100 + i * 20, 150, "left")
|
||||
for j, setting in ipairs(option[4]) do
|
||||
love.graphics.setColor(1, 1, 1, config.gamesettings[option[1]] == j and 1 or 0.5)
|
||||
love.graphics.printf(setting, 100 + 110 * j, 100 + i * 20, 100, "center")
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
love.graphics.setColor(1, 1, 1, 1)
|
||||
love.graphics.setFont(font_3x5_3)
|
||||
love.graphics.print("SFX Volume: " .. math.floor(self.sfxSlider:getValue()) .. "%", 75, 345)
|
||||
love.graphics.print("BGM Volume: " .. math.floor(self.bgmSlider:getValue()) .. "%", 375, 345)
|
||||
|
||||
love.graphics.setColor(1, 1, 1, 0.75)
|
||||
self.sfxSlider:draw()
|
||||
self.bgmSlider:draw()
|
||||
end
|
||||
|
||||
function ConfigScene:onInputPress(e)
|
||||
if e.input == "menu_decide" or e.scancode == "return" then
|
||||
playSE("mode_decide")
|
||||
saveConfig()
|
||||
scene = SettingsScene()
|
||||
elseif e.input == "up" or e.scancode == "up" then
|
||||
playSE("cursor")
|
||||
self.highlight = Mod1(self.highlight-1, optioncount)
|
||||
elseif e.input == "down" or e.scancode == "down" then
|
||||
playSE("cursor")
|
||||
self.highlight = Mod1(self.highlight+1, optioncount)
|
||||
elseif e.input == "left" or e.scancode == "left" then
|
||||
if not self.options[self.highlight][3] then
|
||||
playSE("cursor_lr")
|
||||
local option = ConfigScene.options[self.highlight]
|
||||
config.gamesettings[option[1]] = Mod1(config.gamesettings[option[1]]-1, #option[4])
|
||||
else
|
||||
playSE("cursor")
|
||||
sld = self[self.options[self.highlight][4]]
|
||||
sld.value = math.max(sld.min, math.min(sld.max, (sld:getValue() - 3) / (sld.max - sld.min)))
|
||||
end
|
||||
elseif e.input == "right" or e.scancode == "right" then
|
||||
if not self.options[self.highlight][3] then
|
||||
playSE("cursor_lr")
|
||||
local option = ConfigScene.options[self.highlight]
|
||||
config.gamesettings[option[1]] = Mod1(config.gamesettings[option[1]]+1, #option[4])
|
||||
else
|
||||
playSE("cursor")
|
||||
sld = self[self.options[self.highlight][4]]
|
||||
sld.value = math.max(sld.min, math.min(sld.max, (sld:getValue() + 3) / (sld.max - sld.min)))--math.max(0, (math.floor(sld:getValue())+2)/(sld.max-sld.min))
|
||||
end
|
||||
elseif e.input == "menu_back" or e.scancode == "delete" or e.scancode == "backspace" then
|
||||
loadSave()
|
||||
scene = SettingsScene()
|
||||
end
|
||||
end
|
||||
|
||||
return ConfigScene
|
||||
@@ -5,57 +5,158 @@ ConfigScene.title = "Input Config"
|
||||
require 'load.save'
|
||||
|
||||
local configurable_inputs = {
|
||||
"menu_decide",
|
||||
"menu_back",
|
||||
"left",
|
||||
"right",
|
||||
"up",
|
||||
"down",
|
||||
"right",
|
||||
"up",
|
||||
"down",
|
||||
"rotate_left",
|
||||
"rotate_left2",
|
||||
"rotate_right",
|
||||
"rotate_right2",
|
||||
"rotate_180",
|
||||
"hold",
|
||||
"rotate_right",
|
||||
"rotate_right2",
|
||||
"rotate_180",
|
||||
"hold",
|
||||
"retry",
|
||||
"pause",
|
||||
}
|
||||
|
||||
local function newSetInputs()
|
||||
local set_inputs = {}
|
||||
for i, input in ipairs(configurable_inputs) do
|
||||
set_inputs[input] = false
|
||||
end
|
||||
return set_inputs
|
||||
end
|
||||
|
||||
function ConfigScene:new()
|
||||
-- load current config
|
||||
self.config = config.input
|
||||
self.input_state = 1
|
||||
self.set_inputs = newSetInputs()
|
||||
self.new_input = {}
|
||||
self.axis_timer = 0
|
||||
|
||||
DiscordRPC:update({
|
||||
details = "In menus",
|
||||
state = "Changing input config",
|
||||
})
|
||||
end
|
||||
|
||||
function ConfigScene:update()
|
||||
end
|
||||
|
||||
function ConfigScene:render()
|
||||
love.graphics.setColor(1, 1, 1, 1)
|
||||
love.graphics.draw(
|
||||
backgrounds["input_config"],
|
||||
0, 0, 0,
|
||||
0.5, 0.5
|
||||
)
|
||||
|
||||
love.graphics.setFont(font_3x5_2)
|
||||
for i, input in pairs(configurable_inputs) do
|
||||
if config.input[input] then
|
||||
love.graphics.printf(input, 40, 50 + i * 20, 200, "left")
|
||||
love.graphics.printf(
|
||||
love.keyboard.getKeyFromScancode(config.input[input]) .. " (" .. config.input[input] .. ")",
|
||||
240, 50 + i * 20, 200, "left"
|
||||
)
|
||||
for i, input in ipairs(configurable_inputs) do
|
||||
love.graphics.printf(input, 40, 50 + i * 20, 200, "left")
|
||||
if self.set_inputs[input] then
|
||||
love.graphics.printf(self.set_inputs[input], 240, 50 + i * 20, 300, "left")
|
||||
end
|
||||
end
|
||||
if self.input_state > table.getn(configurable_inputs) then
|
||||
love.graphics.print("press enter to confirm, delete to retry")
|
||||
love.graphics.print("press enter to confirm, delete/backspace to retry" .. (config.input and ", escape to cancel" or ""))
|
||||
else
|
||||
love.graphics.print("press key for " .. configurable_inputs[self.input_state])
|
||||
love.graphics.print("press key or joystick input for " .. configurable_inputs[self.input_state] .. ", tab to skip" .. (config.input and ", escape to cancel" or ""), 0, 0)
|
||||
love.graphics.print("function keys (F1, F2, etc.), escape, and tab can't be changed", 0, 20)
|
||||
end
|
||||
|
||||
self.axis_timer = self.axis_timer + 1
|
||||
end
|
||||
|
||||
local function addJoystick(input, name)
|
||||
if not input.joysticks then
|
||||
input.joysticks = {}
|
||||
end
|
||||
if not input.joysticks[name] then
|
||||
input.joysticks[name] = {}
|
||||
end
|
||||
end
|
||||
|
||||
function ConfigScene:onKeyPress(e)
|
||||
if self.input_state > table.getn(configurable_inputs) then
|
||||
if e.scancode == "return" then
|
||||
-- save, then load next scene
|
||||
saveConfig()
|
||||
scene = TitleScene()
|
||||
elseif e.scancode == "delete" or e.scancode == "backspace" then
|
||||
self.input_state = 1
|
||||
function ConfigScene:onInputPress(e)
|
||||
if e.type == "key" then
|
||||
-- function keys, escape, and tab are reserved and can't be remapped
|
||||
if e.scancode == "escape" and config.input then
|
||||
-- cancel only if there was an input config already
|
||||
scene = SettingsScene()
|
||||
elseif self.input_state > table.getn(configurable_inputs) then
|
||||
if e.scancode == "return" then
|
||||
-- save new input, then load next scene
|
||||
config.input = self.new_input
|
||||
saveConfig()
|
||||
scene = TitleScene()
|
||||
elseif e.scancode == "delete" or e.scancode == "backspace" then
|
||||
-- retry
|
||||
self.input_state = 1
|
||||
self.set_inputs = newSetInputs()
|
||||
self.new_input = {}
|
||||
end
|
||||
elseif e.scancode == "tab" then
|
||||
self.set_inputs[configurable_inputs[self.input_state]] = "skipped"
|
||||
self.input_state = self.input_state + 1
|
||||
elseif e.scancode ~= "escape" then
|
||||
-- all other keys can be configured
|
||||
if not self.new_input.keys then
|
||||
self.new_input.keys = {}
|
||||
end
|
||||
self.set_inputs[configurable_inputs[self.input_state]] = "key " .. love.keyboard.getKeyFromScancode(e.scancode) .. " (" .. e.scancode .. ")"
|
||||
self.new_input.keys[e.scancode] = configurable_inputs[self.input_state]
|
||||
self.input_state = self.input_state + 1
|
||||
end
|
||||
elseif string.sub(e.type, 1, 3) == "joy" then
|
||||
if self.input_state <= table.getn(configurable_inputs) then
|
||||
if e.type == "joybutton" then
|
||||
addJoystick(self.new_input, e.name)
|
||||
if not self.new_input.joysticks[e.name].buttons then
|
||||
self.new_input.joysticks[e.name].buttons = {}
|
||||
end
|
||||
self.set_inputs[configurable_inputs[self.input_state]] =
|
||||
"jbtn " ..
|
||||
e.button ..
|
||||
" " .. string.sub(e.name, 1, 10) .. (string.len(e.name) > 10 and "..." or "")
|
||||
self.new_input.joysticks[e.name].buttons[e.button] = configurable_inputs[self.input_state]
|
||||
self.input_state = self.input_state + 1
|
||||
elseif e.type == "joyaxis" then
|
||||
if (e.axis ~= self.last_axis or self.axis_timer > 30) and math.abs(e.value) >= 1 then
|
||||
addJoystick(self.new_input, e.name)
|
||||
if not self.new_input.joysticks[e.name].axes then
|
||||
self.new_input.joysticks[e.name].axes = {}
|
||||
end
|
||||
if not self.new_input.joysticks[e.name].axes[e.axis] then
|
||||
self.new_input.joysticks[e.name].axes[e.axis] = {}
|
||||
end
|
||||
self.set_inputs[configurable_inputs[self.input_state]] =
|
||||
"jaxis " ..
|
||||
(e.value >= 1 and "+" or "-") .. e.axis ..
|
||||
" " .. string.sub(e.name, 1, 10) .. (string.len(e.name) > 10 and "..." or "")
|
||||
self.new_input.joysticks[e.name].axes[e.axis][e.value >= 1 and "positive" or "negative"] = configurable_inputs[self.input_state]
|
||||
self.input_state = self.input_state + 1
|
||||
self.last_axis = e.axis
|
||||
self.axis_timer = 0
|
||||
end
|
||||
elseif e.type == "joyhat" then
|
||||
if e.direction ~= "c" then
|
||||
addJoystick(self.new_input, e.name)
|
||||
if not self.new_input.joysticks[e.name].hats then
|
||||
self.new_input.joysticks[e.name].hats = {}
|
||||
end
|
||||
if not self.new_input.joysticks[e.name].hats[e.hat] then
|
||||
self.new_input.joysticks[e.name].hats[e.hat] = {}
|
||||
end
|
||||
self.set_inputs[configurable_inputs[self.input_state]] =
|
||||
"jhat " ..
|
||||
e.hat .. " " .. e.direction ..
|
||||
" " .. string.sub(e.name, 1, 10) .. (string.len(e.name) > 10 and "..." or "")
|
||||
self.new_input.joysticks[e.name].hats[e.hat][e.direction] = configurable_inputs[self.input_state]
|
||||
self.input_state = self.input_state + 1
|
||||
end
|
||||
end
|
||||
end
|
||||
else
|
||||
config.input[configurable_inputs[self.input_state]] = e.scancode
|
||||
self.input_state = self.input_state + 1
|
||||
end
|
||||
end
|
||||
|
||||
|
||||
@@ -5,50 +5,28 @@ ModeSelectScene.title = "Game Start"
|
||||
current_mode = 1
|
||||
current_ruleset = 1
|
||||
|
||||
game_modes = {
|
||||
require 'tetris.modes.marathon_2020',
|
||||
require 'tetris.modes.survival_2020',
|
||||
--require 'tetris.modes.strategy',
|
||||
--require 'tetris.modes.interval_training',
|
||||
--require 'tetris.modes.pacer_test',
|
||||
--require 'tetris.modes.demon_mode',
|
||||
require 'tetris.modes.phantom_mania',
|
||||
require 'tetris.modes.phantom_mania2',
|
||||
require 'tetris.modes.phantom_mania_n',
|
||||
require 'tetris.modes.race_40',
|
||||
require 'tetris.modes.marathon_a1',
|
||||
require 'tetris.modes.marathon_a2',
|
||||
require 'tetris.modes.marathon_a3',
|
||||
require 'tetris.modes.marathon_ax4',
|
||||
require 'tetris.modes.marathon_c89',
|
||||
require 'tetris.modes.survival_a1',
|
||||
require 'tetris.modes.survival_a2',
|
||||
require 'tetris.modes.survival_a3',
|
||||
require 'tetris.modes.big_a2',
|
||||
require 'tetris.modes.konoha',
|
||||
}
|
||||
|
||||
rulesets = {
|
||||
require 'tetris.rulesets.cambridge',
|
||||
require 'tetris.rulesets.arika',
|
||||
require 'tetris.rulesets.arika_ti',
|
||||
require 'tetris.rulesets.arika_ace',
|
||||
require 'tetris.rulesets.arika_srs',
|
||||
require 'tetris.rulesets.standard_exp',
|
||||
--require 'tetris.rulesets.bonkers',
|
||||
--require 'tetris.rulesets.shirase',
|
||||
--require 'tetris.rulesets.super302',
|
||||
}
|
||||
|
||||
function ModeSelectScene:new()
|
||||
self.menu_state = {
|
||||
mode = current_mode,
|
||||
ruleset = current_ruleset,
|
||||
select = "mode",
|
||||
}
|
||||
self.secret_inputs = {
|
||||
rotate_left = false,
|
||||
rotate_left2 = false,
|
||||
rotate_right = false,
|
||||
rotate_right2 = false,
|
||||
rotate_180 = false,
|
||||
hold = false,
|
||||
}
|
||||
DiscordRPC:update({
|
||||
details = "In menus",
|
||||
state = "Choosing a mode",
|
||||
})
|
||||
end
|
||||
|
||||
function ModeSelectScene:update()
|
||||
switchBGM(nil) -- experimental
|
||||
end
|
||||
|
||||
function ModeSelectScene:render()
|
||||
@@ -57,49 +35,89 @@ function ModeSelectScene:render()
|
||||
0, 0, 0,
|
||||
0.5, 0.5
|
||||
)
|
||||
|
||||
|
||||
if self.menu_state.select == "mode" then
|
||||
love.graphics.setColor(1, 1, 1, 0.5)
|
||||
elseif self.menu_state.select == "ruleset" then
|
||||
love.graphics.setColor(1, 1, 1, 0.25)
|
||||
end
|
||||
love.graphics.rectangle("fill", 20, 78 + 20 * self.menu_state.mode, 240, 22)
|
||||
love.graphics.rectangle("fill", 20, 198, 240, 22)
|
||||
|
||||
if self.menu_state.select == "mode" then
|
||||
love.graphics.setColor(1, 1, 1, 0.25)
|
||||
elseif self.menu_state.select == "ruleset" then
|
||||
love.graphics.setColor(1, 1, 1, 0.5)
|
||||
end
|
||||
love.graphics.rectangle("fill", 340, 78 + 20 * self.menu_state.ruleset, 200, 22)
|
||||
|
||||
love.graphics.rectangle("fill", 340, 198, 200, 22)
|
||||
|
||||
love.graphics.setColor(1, 1, 1, 1)
|
||||
|
||||
love.graphics.draw(misc_graphics["select_mode"], 20, 40)
|
||||
|
||||
love.graphics.setFont(font_3x5_2)
|
||||
for idx, mode in pairs(game_modes) do
|
||||
love.graphics.printf(mode.name, 40, 80 + 20 * idx, 200, "left")
|
||||
if(idx >= self.menu_state.mode-6 and idx <= self.menu_state.mode+6) then
|
||||
love.graphics.printf(mode.name, 40, (200 - 20*(self.menu_state.mode)) + 20 * idx, 200, "left")
|
||||
end
|
||||
end
|
||||
for idx, ruleset in pairs(rulesets) do
|
||||
love.graphics.printf(ruleset.name, 360, 80 + 20 * idx, 160, "left")
|
||||
if(idx >= self.menu_state.ruleset-6 and idx <= self.menu_state.ruleset+6) then
|
||||
love.graphics.printf(ruleset.name, 360, (200 - 20*(self.menu_state.ruleset)) + 20 * idx, 160, "left")
|
||||
end
|
||||
end
|
||||
|
||||
-- mode description and highscore
|
||||
for midx, mode in pairs(game_modes) do
|
||||
for ridx, ruleset in pairs(rulesets) do
|
||||
if (midx == self.menu_state.mode) and (ridx == self.menu_state.ruleset) then
|
||||
love.graphics.printf(
|
||||
"Mode Description:\n\n" .. mode.tagline, 20, 350, 200, "left"
|
||||
)
|
||||
love.graphics.printf(
|
||||
ruleset.name .. " Highscore:", 240, 350, 200, "right"
|
||||
)
|
||||
local highscore_string = ""
|
||||
if highscores[mode.hash .. "-" .. ruleset.hash] then
|
||||
for k, v in ipairs(highscores[mode.hash .. "-" .. ruleset.hash]) do
|
||||
highscore_string = highscore_string .. k .. ": " .. v .. "\n"
|
||||
end
|
||||
else
|
||||
highscore_string = "You don't have any highscores yet!"
|
||||
end
|
||||
love.graphics.printf(highscore_string, 450, 350, 200, "left")
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
function ModeSelectScene:onKeyPress(e)
|
||||
if e.scancode == "return" and e.isRepeat == false then
|
||||
function ModeSelectScene:onInputPress(e)
|
||||
if e.input == "menu_decide" or e.scancode == "return" then
|
||||
current_mode = self.menu_state.mode
|
||||
current_ruleset = self.menu_state.ruleset
|
||||
config.current_mode = current_mode
|
||||
config.current_ruleset = current_ruleset
|
||||
playSE("mode_decide")
|
||||
saveConfig()
|
||||
scene = GameScene(game_modes[self.menu_state.mode], rulesets[self.menu_state.ruleset])
|
||||
elseif (e.scancode == config.input["up"] or e.scancode == "up") and e.isRepeat == false then
|
||||
scene = GameScene(game_modes[self.menu_state.mode], rulesets[self.menu_state.ruleset], self.secret_inputs)
|
||||
elseif e.input == "up" or e.scancode == "up" then
|
||||
self:changeOption(-1)
|
||||
elseif (e.scancode == config.input["down"] or e.scancode == "down") and e.isRepeat == false then
|
||||
playSE("cursor")
|
||||
elseif e.input == "down" or e.scancode == "down" then
|
||||
self:changeOption(1)
|
||||
elseif (e.scancode == config.input["left"] or e.scancode == "left") or
|
||||
(e.scancode == config.input["right"] or e.scancode == "right") then
|
||||
playSE("cursor")
|
||||
elseif e.input == "left" or e.input == "right" or e.scancode == "left" or e.scancode == "right" then
|
||||
self:switchSelect()
|
||||
playSE("cursor_lr")
|
||||
elseif e.input == "menu_back" or e.scancode == "delete" or e.scancode == "backspace" then
|
||||
scene = TitleScene()
|
||||
elseif e.input then
|
||||
self.secret_inputs[e.input] = true
|
||||
end
|
||||
end
|
||||
|
||||
function ModeSelectScene:onInputRelease(e)
|
||||
if e.input == "hold" or (e.input and string.sub(e.input, 1, 7) == "rotate_") then
|
||||
self.secret_inputs[e.input] = false
|
||||
end
|
||||
end
|
||||
|
||||
|
||||
65
scene/settings.lua
Normal file
@@ -0,0 +1,65 @@
|
||||
local SettingsScene = Scene:extend()
|
||||
|
||||
SettingsScene.title = "Settings"
|
||||
|
||||
local menu_screens = {
|
||||
InputConfigScene,
|
||||
GameConfigScene,
|
||||
TuningScene
|
||||
}
|
||||
|
||||
function SettingsScene:new()
|
||||
self.menu_state = 1
|
||||
DiscordRPC:update({
|
||||
details = "In menus",
|
||||
state = "Changing settings",
|
||||
})
|
||||
end
|
||||
|
||||
function SettingsScene:update() end
|
||||
|
||||
function SettingsScene:render()
|
||||
love.graphics.setColor(1, 1, 1, 1)
|
||||
love.graphics.draw(
|
||||
backgrounds["game_config"],
|
||||
0, 0, 0,
|
||||
0.5, 0.5
|
||||
)
|
||||
|
||||
love.graphics.setFont(font_3x5_4)
|
||||
love.graphics.print("SETTINGS", 80, 40)
|
||||
|
||||
love.graphics.setFont(font_3x5_2)
|
||||
love.graphics.print("Here, you can change some settings that change\nthe look and feel of the game.", 80, 90)
|
||||
|
||||
love.graphics.setColor(1, 1, 1, 0.5)
|
||||
love.graphics.rectangle("fill", 75, 118 + 50 * self.menu_state, 200, 33)
|
||||
|
||||
love.graphics.setFont(font_3x5_3)
|
||||
love.graphics.setColor(1, 1, 1, 1)
|
||||
for i, screen in pairs(menu_screens) do
|
||||
love.graphics.printf(screen.title, 80, 120 + 50 * i, 200, "left")
|
||||
end
|
||||
end
|
||||
|
||||
function SettingsScene:changeOption(rel)
|
||||
local len = table.getn(menu_screens)
|
||||
self.menu_state = (self.menu_state + len + rel - 1) % len + 1
|
||||
end
|
||||
|
||||
function SettingsScene:onInputPress(e)
|
||||
if e.input == "menu_decide" or e.scancode == "return" then
|
||||
playSE("main_decide")
|
||||
scene = menu_screens[self.menu_state]()
|
||||
elseif e.input == "up" or e.scancode == "up" then
|
||||
self:changeOption(-1)
|
||||
playSE("cursor")
|
||||
elseif e.input == "down" or e.scancode == "down" then
|
||||
self:changeOption(1)
|
||||
playSE("cursor")
|
||||
elseif e.input == "menu_back" or e.scancode == "backspace" or e.scancode == "delete" then
|
||||
scene = TitleScene()
|
||||
end
|
||||
end
|
||||
|
||||
return SettingsScene
|
||||
@@ -1,26 +1,80 @@
|
||||
local TitleScene = Scene:extend()
|
||||
|
||||
TitleScene.title = "Title"
|
||||
TitleScene.restart_message = false
|
||||
|
||||
local main_menu_screens = {
|
||||
ModeSelectScene,
|
||||
InputConfigScene,
|
||||
SettingsScene,
|
||||
CreditsScene,
|
||||
ExitScene,
|
||||
}
|
||||
|
||||
local mainmenuidle = {
|
||||
"Idle",
|
||||
"On title screen",
|
||||
"On main menu screen",
|
||||
"Twiddling their thumbs",
|
||||
"Admiring the main menu's BG",
|
||||
"Waiting for spring to come",
|
||||
"Actually not playing",
|
||||
"Contemplating collecting stars",
|
||||
"Preparing to put the block!!",
|
||||
"Having a nap",
|
||||
"In menus",
|
||||
"Bottom text",
|
||||
}
|
||||
|
||||
function TitleScene:new()
|
||||
self.main_menu_state = 1
|
||||
self.frames = 0
|
||||
self.snow_bg_opacity = 0
|
||||
self.y_offset = 0
|
||||
self.text = ""
|
||||
self.text_flag = false
|
||||
DiscordRPC:update({
|
||||
details = "In menus",
|
||||
state = mainmenuidle[math.random(#mainmenuidle)],
|
||||
})
|
||||
end
|
||||
|
||||
function TitleScene:update()
|
||||
if self.text_flag then
|
||||
self.frames = self.frames + 1
|
||||
self.snow_bg_opacity = self.snow_bg_opacity + 0.01
|
||||
end
|
||||
if self.frames < 125 then self.y_offset = self.frames
|
||||
elseif self.frames < 185 then self.y_offset = 125
|
||||
else self.y_offset = 310 - self.frames end
|
||||
end
|
||||
|
||||
function TitleScene:render()
|
||||
love.graphics.setFont(font_3x5_2)
|
||||
|
||||
love.graphics.setColor(1, 1, 1, 1 - self.snow_bg_opacity)
|
||||
love.graphics.draw(
|
||||
backgrounds["title"],
|
||||
0, 0, 0,
|
||||
0.5, 0.5
|
||||
)
|
||||
|
||||
love.graphics.setColor(1, 1, 1, self.snow_bg_opacity)
|
||||
love.graphics.draw(
|
||||
backgrounds["snow"],
|
||||
0, 0, 0,
|
||||
0.5, 0.5
|
||||
)
|
||||
|
||||
love.graphics.draw(
|
||||
misc_graphics["santa"],
|
||||
400, -205 + self.y_offset,
|
||||
0, 0.5, 0.5
|
||||
)
|
||||
love.graphics.print("Happy Holidays!", 320, -100 + self.y_offset)
|
||||
|
||||
love.graphics.setColor(1, 1, 1, 1)
|
||||
love.graphics.print(self.restart_message and "Restart Cambridge..." or "", 0, 0)
|
||||
|
||||
love.graphics.setColor(1, 1, 1, 0.5)
|
||||
love.graphics.rectangle("fill", 20, 278 + 20 * self.main_menu_state, 160, 22)
|
||||
|
||||
@@ -36,13 +90,23 @@ function TitleScene:changeOption(rel)
|
||||
self.main_menu_state = (self.main_menu_state + len + rel - 1) % len + 1
|
||||
end
|
||||
|
||||
function TitleScene:onKeyPress(e)
|
||||
if e.scancode == "return" and e.isRepeat == false then
|
||||
function TitleScene:onInputPress(e)
|
||||
if e.input == "menu_decide" or e.scancode == "return" then
|
||||
playSE("main_decide")
|
||||
scene = main_menu_screens[self.main_menu_state]()
|
||||
elseif (e.scancode == config.input["up"] or e.scancode == "up") and e.isRepeat == false then
|
||||
elseif e.input == "up" or e.scancode == "up" then
|
||||
self:changeOption(-1)
|
||||
elseif (e.scancode == config.input["down"] or e.scancode == "down") and e.isRepeat == false then
|
||||
playSE("cursor")
|
||||
elseif e.input == "down" or e.scancode == "down" then
|
||||
self:changeOption(1)
|
||||
playSE("cursor")
|
||||
elseif e.input == "menu_back" or e.scancode == "backspace" or e.scancode == "delete" then
|
||||
love.event.quit()
|
||||
else
|
||||
self.text = self.text .. (e.scancode ~= nil and e.scancode or "")
|
||||
if self.text == "ffffff" then
|
||||
self.text_flag = true
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
|
||||
90
scene/tuning.lua
Normal file
@@ -0,0 +1,90 @@
|
||||
local TuningScene = Scene:extend()
|
||||
|
||||
TuningScene.title = "Tuning Settings"
|
||||
|
||||
require 'load.save'
|
||||
require 'libs.simple-slider'
|
||||
|
||||
TuningScene.options = {
|
||||
-- Serves as a reference for the options available in the menu. Format: {name in config, name as displayed if applicable, slider name}
|
||||
{"das", "DAS", "dasSlider"},
|
||||
{"arr", "ARR", "arrSlider"},
|
||||
{"dcd", "DCD", "dcdSlider"},
|
||||
}
|
||||
|
||||
local optioncount = #TuningScene.options
|
||||
|
||||
function TuningScene:new()
|
||||
DiscordRPC:update({
|
||||
details = "In menus",
|
||||
state = "Changing tuning settings",
|
||||
})
|
||||
self.highlight = 1
|
||||
|
||||
self.dasSlider = newSlider(290, 225, 400, config.das, 0, 20, function(v) config.das = math.floor(v) end, {width=20, knob="circle", track="roundrect"})
|
||||
self.arrSlider = newSlider(290, 300, 400, config.arr, 0, 6, function(v) config.arr = math.floor(v) end, {width=20, knob="circle", track="roundrect"})
|
||||
self.dcdSlider = newSlider(290, 375, 400, config.dcd, 0, 6, function(v) config.dcd = math.floor(v) end, {width=20, knob="circle", track="roundrect"})
|
||||
end
|
||||
|
||||
function TuningScene:update()
|
||||
self.dasSlider:update()
|
||||
self.arrSlider:update()
|
||||
self.dcdSlider:update()
|
||||
end
|
||||
|
||||
function TuningScene:render()
|
||||
love.graphics.setColor(1, 1, 1, 1)
|
||||
love.graphics.draw(
|
||||
backgrounds["game_config"],
|
||||
0, 0, 0,
|
||||
0.5, 0.5
|
||||
)
|
||||
|
||||
love.graphics.setColor(1, 1, 1, 0.5)
|
||||
love.graphics.rectangle("fill", 75, 98 + self.highlight * 75, 400, 33)
|
||||
|
||||
love.graphics.setColor(1, 1, 1, 1)
|
||||
|
||||
love.graphics.setFont(font_3x5_4)
|
||||
love.graphics.print("TUNING SETTINGS", 80, 40)
|
||||
|
||||
love.graphics.setFont(font_3x5_2)
|
||||
love.graphics.print("These settings will only apply to modes\nthat do not use their own tunings.", 80, 90)
|
||||
|
||||
love.graphics.setFont(font_3x5_3)
|
||||
love.graphics.print("Delayed Auto-Shift (DAS): " .. math.floor(self.dasSlider:getValue()) .. "F", 80, 175)
|
||||
love.graphics.print("Auto-Repeat Rate (ARR): " .. math.floor(self.arrSlider:getValue()) .. "F", 80, 250)
|
||||
love.graphics.print("DAS Cut Delay (DCD): " .. math.floor(self.dcdSlider:getValue()) .. "F", 80, 325)
|
||||
|
||||
love.graphics.setColor(1, 1, 1, 0.75)
|
||||
self.dasSlider:draw()
|
||||
self.arrSlider:draw()
|
||||
self.dcdSlider:draw()
|
||||
end
|
||||
|
||||
function TuningScene:onInputPress(e)
|
||||
if e.input == "menu_decide" or e.scancode == "return" then
|
||||
playSE("mode_decide")
|
||||
saveConfig()
|
||||
scene = SettingsScene()
|
||||
elseif e.input == "up" or e.scancode == "up" then
|
||||
playSE("cursor")
|
||||
self.highlight = Mod1(self.highlight-1, optioncount)
|
||||
elseif e.input == "down" or e.scancode == "down" then
|
||||
playSE("cursor")
|
||||
self.highlight = Mod1(self.highlight+1, optioncount)
|
||||
elseif e.input == "left" or e.scancode == "left" then
|
||||
playSE("cursor")
|
||||
sld = self[self.options[self.highlight][3]]
|
||||
sld.value = math.max(sld.min, math.min(sld.max, (sld:getValue() - 1) / (sld.max - sld.min)))
|
||||
elseif e.input == "right" or e.scancode == "right" then
|
||||
playSE("cursor")
|
||||
sld = self[self.options[self.highlight][3]]
|
||||
sld.value = math.max(sld.min, math.min(sld.max, (sld:getValue() + 1) / (sld.max - sld.min)))
|
||||
elseif e.input == "menu_back" or e.scancode == "delete" or e.scancode == "backspace" then
|
||||
loadSave()
|
||||
scene = SettingsScene()
|
||||
end
|
||||
end
|
||||
|
||||
return TuningScene
|
||||
@@ -4,14 +4,17 @@ local Grid = Object:extend()
|
||||
|
||||
local empty = { skin = "", colour = "" }
|
||||
local oob = { skin = "", colour = "" }
|
||||
local block = { skin = "2tie", colour = "A" }
|
||||
|
||||
function Grid:new()
|
||||
function Grid:new(width, height)
|
||||
self.grid = {}
|
||||
self.grid_age = {}
|
||||
for y = 1, 24 do
|
||||
self.width = width
|
||||
self.height = height
|
||||
for y = 1, self.height do
|
||||
self.grid[y] = {}
|
||||
self.grid_age[y] = {}
|
||||
for x = 1, 10 do
|
||||
for x = 1, self.width do
|
||||
self.grid[y][x] = empty
|
||||
self.grid_age[y][x] = 0
|
||||
end
|
||||
@@ -19,8 +22,8 @@ function Grid:new()
|
||||
end
|
||||
|
||||
function Grid:clear()
|
||||
for y = 1, 24 do
|
||||
for x = 1, 10 do
|
||||
for y = 1, self.height do
|
||||
for x = 1, self.width do
|
||||
self.grid[y][x] = empty
|
||||
self.grid_age[y][x] = 0
|
||||
end
|
||||
@@ -28,10 +31,10 @@ function Grid:clear()
|
||||
end
|
||||
|
||||
function Grid:getCell(x, y)
|
||||
if x < 1 or x > 10 or y > 24 then return oob
|
||||
elseif y < 1 then return empty
|
||||
else return self.grid[y][x]
|
||||
end
|
||||
if x < 1 or x > self.width or y > self.height then return oob
|
||||
elseif y < 1 then return empty
|
||||
else return self.grid[y][x]
|
||||
end
|
||||
end
|
||||
|
||||
function Grid:isOccupied(x, y)
|
||||
@@ -66,11 +69,11 @@ function Grid:canPlaceBigPiece(piece)
|
||||
for index, offset in pairs(offsets) do
|
||||
local x = piece.position.x + offset.x
|
||||
local y = piece.position.y + offset.y
|
||||
if (
|
||||
self:isOccupied(x * 2 + 0, y * 2 + 0)
|
||||
or self:isOccupied(x * 2 + 1, y * 2 + 0)
|
||||
or self:isOccupied(x * 2 + 0, y * 2 + 1)
|
||||
or self:isOccupied(x * 2 + 1, y * 2 + 1)
|
||||
if (
|
||||
self:isOccupied(x * 2 + 0, y * 2 + 0)
|
||||
or self:isOccupied(x * 2 + 1, y * 2 + 0)
|
||||
or self:isOccupied(x * 2 + 0, y * 2 + 1)
|
||||
or self:isOccupied(x * 2 + 1, y * 2 + 1)
|
||||
) then
|
||||
return false
|
||||
end
|
||||
@@ -97,22 +100,25 @@ end
|
||||
|
||||
function Grid:getClearedRowCount()
|
||||
local count = 0
|
||||
for row = 1, 24 do
|
||||
local cleared_row_table = {}
|
||||
for row = 1, self.height do
|
||||
if self:isRowFull(row) then
|
||||
count = count + 1
|
||||
table.insert(cleared_row_table, row)
|
||||
end
|
||||
end
|
||||
return count
|
||||
return count, cleared_row_table
|
||||
end
|
||||
|
||||
function Grid:markClearedRows()
|
||||
for row = 1, 24 do
|
||||
for row = 1, self.height do
|
||||
if self:isRowFull(row) then
|
||||
for x = 1, 10 do
|
||||
for x = 1, self.width do
|
||||
self.grid[row][x] = {
|
||||
skin = self.grid[row][x].skin,
|
||||
colour = "X"
|
||||
}
|
||||
self.grid_age[row][x] = 0
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -120,35 +126,56 @@ function Grid:markClearedRows()
|
||||
end
|
||||
|
||||
function Grid:clearClearedRows()
|
||||
for row = 1, 24 do
|
||||
for row = 1, self.height do
|
||||
if self:isRowFull(row) then
|
||||
for above_row = row, 2, -1 do
|
||||
self.grid[above_row] = self.grid[above_row - 1]
|
||||
self.grid_age[above_row] = self.grid_age[above_row - 1]
|
||||
end
|
||||
self.grid[1] = {empty, empty, empty, empty, empty, empty, empty, empty, empty, empty}
|
||||
self.grid_age[1] = {0, 0, 0, 0, 0, 0, 0, 0, 0, 0}
|
||||
self.grid[1] = {}
|
||||
self.grid_age[1] = {}
|
||||
for i = 1, self.width do
|
||||
self.grid[1][i] = empty
|
||||
self.grid_age[1][i] = 0
|
||||
end
|
||||
end
|
||||
end
|
||||
return true
|
||||
end
|
||||
|
||||
function Grid:copyBottomRow()
|
||||
for row = 1, 23 do
|
||||
for row = 1, self.height - 1 do
|
||||
self.grid[row] = self.grid[row+1]
|
||||
self.grid_age[row] = self.grid_age[row+1]
|
||||
end
|
||||
self.grid[24] = {empty, empty, empty, empty, empty, empty, empty, empty, empty, empty}
|
||||
self.grid_age[24] = {0, 0, 0, 0, 0, 0, 0, 0, 0, 0}
|
||||
for col = 1, 10 do
|
||||
self.grid[24][col] = (self.grid[23][col] == empty) and empty or {
|
||||
skin = self.grid[23][col].skin,
|
||||
colour = "G"
|
||||
}
|
||||
self.grid[self.height] = {}
|
||||
self.grid_age[self.height] = {}
|
||||
for i = 1, self.width do
|
||||
self.grid[self.height][i] = (self.grid[self.height - 1][i] == empty) and empty or block
|
||||
self.grid_age[self.height][i] = 0
|
||||
end
|
||||
return true
|
||||
end
|
||||
|
||||
function Grid:garbageRise(row_vals)
|
||||
for row = 1, self.height - 1 do
|
||||
self.grid[row] = self.grid[row+1]
|
||||
self.grid_age[row] = self.grid_age[row+1]
|
||||
end
|
||||
self.grid[self.height] = {}
|
||||
self.grid_age[self.height] = {}
|
||||
for i = 1, self.width do
|
||||
self.grid[self.height][i] = (row_vals[i] == "e") and empty or block
|
||||
self.grid_age[self.height][i] = 0
|
||||
end
|
||||
end
|
||||
|
||||
function Grid:clearSpecificRow(row)
|
||||
for col = 1, self.width do
|
||||
self.grid[row][col] = empty
|
||||
end
|
||||
end
|
||||
|
||||
function Grid:applyPiece(piece)
|
||||
if piece.big then
|
||||
self:applyBigPiece(piece)
|
||||
@@ -158,10 +185,10 @@ function Grid:applyPiece(piece)
|
||||
for index, offset in pairs(offsets) do
|
||||
x = piece.position.x + offset.x
|
||||
y = piece.position.y + offset.y
|
||||
if y + 1 > 0 then
|
||||
if y + 1 > 0 and y < self.height then
|
||||
self.grid[y+1][x+1] = {
|
||||
skin = piece.skin,
|
||||
colour = piece.shape
|
||||
colour = piece.colour
|
||||
}
|
||||
end
|
||||
end
|
||||
@@ -174,20 +201,183 @@ function Grid:applyBigPiece(piece)
|
||||
y = piece.position.y + offset.y
|
||||
for a = 1, 2 do
|
||||
for b = 1, 2 do
|
||||
if y*2+a > 0 then
|
||||
self.grid[y*2+a][x*2+b] = {
|
||||
skin = piece.skin,
|
||||
colour = piece.shape
|
||||
}
|
||||
end
|
||||
if y*2+a > 0 and y*2 < self.height then
|
||||
self.grid[y*2+a][x*2+b] = {
|
||||
skin = piece.skin,
|
||||
colour = piece.colour
|
||||
}
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
function Grid:checkForBravo(cleared_row_count)
|
||||
for i = 0, self.height - 1 - cleared_row_count do
|
||||
for j = 0, self.width - 1 do
|
||||
if self:isOccupied(j, i) then return false end
|
||||
end
|
||||
end
|
||||
return true
|
||||
end
|
||||
|
||||
function Grid:checkStackHeight()
|
||||
for i = 0, self.height - 1 do
|
||||
for j = 0, self.width - 1 do
|
||||
if self:isOccupied(j, i) then return self.height - i end
|
||||
end
|
||||
end
|
||||
return 0
|
||||
end
|
||||
|
||||
function Grid:checkSecretGrade()
|
||||
local sgrade = 0
|
||||
for i=23,5,-1 do
|
||||
local validLine = true
|
||||
local emptyCell = 0
|
||||
if i > 13 then
|
||||
emptyCell = 23-i
|
||||
end
|
||||
if i <= 13 then
|
||||
emptyCell = i-5
|
||||
end
|
||||
for j=0,9 do
|
||||
if (not self:isOccupied(j,i) and j ~= emptyCell) or (j == emptyCell and self:isOccupied(j,i)) then
|
||||
validLine = false
|
||||
end
|
||||
end
|
||||
if not self:isOccupied(emptyCell,i-1) then
|
||||
validLine = false
|
||||
end
|
||||
if(validLine) then
|
||||
sgrade = sgrade + 1
|
||||
else
|
||||
return sgrade
|
||||
end
|
||||
end
|
||||
--[[
|
||||
if(sgrade == 0) then return ""
|
||||
elseif(sgrade < 10) then return 10-sgrade
|
||||
elseif(sgrade < 19) then return "S"..(sgrade-9) end
|
||||
return "GM"
|
||||
--]]
|
||||
return sgrade
|
||||
end
|
||||
|
||||
function Grid:hasGemBlocks()
|
||||
for y = 1, self.height do
|
||||
for x = 1, self.width do
|
||||
if self.grid[y][x].skin == "gem" then
|
||||
return true
|
||||
end
|
||||
end
|
||||
end
|
||||
return false
|
||||
end
|
||||
|
||||
function Grid:mirror()
|
||||
local new_grid = {}
|
||||
for y = 1, self.height do
|
||||
new_grid[y] = {}
|
||||
for x = 1, self.width do
|
||||
new_grid[y][x] = empty
|
||||
end
|
||||
end
|
||||
|
||||
for y = 1, self.height do
|
||||
for x = 1, self.width do
|
||||
new_grid[y][x] = self.grid[y][self.width + 1 - x]
|
||||
end
|
||||
end
|
||||
self.grid = new_grid
|
||||
end
|
||||
|
||||
function Grid:applyMap(map)
|
||||
for y, row in pairs(map) do
|
||||
for x, block in pairs(row) do
|
||||
self.grid_age[y][x] = 0
|
||||
self.grid[y][x] = block
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
-- inefficient algorithm for squares
|
||||
function Grid:markSquares()
|
||||
-- goes up by 1 for silver, 2 for gold
|
||||
local square_count = 0
|
||||
for i = 1, 2 do
|
||||
for y = 5, self.height - 3 do
|
||||
for x = 1, self.width - 3 do
|
||||
local age_table = {}
|
||||
local age_count = 0
|
||||
local colour_table = {}
|
||||
local is_square = true
|
||||
for j = 0, 3 do
|
||||
for k = 0, 3 do
|
||||
if self.grid[y+j][x+k].skin == "" or self.grid[y+j][x+k].skin == "square" then
|
||||
is_square = false
|
||||
end
|
||||
if age_table[self.grid_age[y+j][x+k]] == nil then
|
||||
age_table[self.grid_age[y+j][x+k]] = 1
|
||||
age_count = age_count + 1
|
||||
else
|
||||
age_table[self.grid_age[y+j][x+k]] = age_table[self.grid_age[y+j][x+k]] + 1
|
||||
end
|
||||
if age_count > 4 or age_table[self.grid_age[y+j][x+k]] > 4 then
|
||||
is_square = false
|
||||
end
|
||||
if not table.contains(colour_table, self.grid[y+j][x+k].colour) then
|
||||
table.insert(colour_table, self.grid[y+j][x+k].colour)
|
||||
end
|
||||
end
|
||||
end
|
||||
if is_square then
|
||||
if i == 1 and #colour_table == 1 then
|
||||
for j = 0, 3 do
|
||||
for k = 0, 3 do
|
||||
self.grid[y+j][x+k].colour = "Y"
|
||||
self.grid[y+j][x+k].skin = "square"
|
||||
end
|
||||
end
|
||||
square_count = square_count + 2
|
||||
elseif i == 2 then
|
||||
for j = 0, 3 do
|
||||
for k = 0, 3 do
|
||||
self.grid[y+j][x+k].colour = "F"
|
||||
self.grid[y+j][x+k].skin = "square"
|
||||
end
|
||||
|
||||
end
|
||||
square_count = square_count + 1
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
return square_count
|
||||
end
|
||||
|
||||
-- square scan
|
||||
function Grid:scanForSquares()
|
||||
local table = {}
|
||||
for row = 1, self.height do
|
||||
local silver = 0
|
||||
local gold = 0
|
||||
for col = 1, self.width do
|
||||
local colour = self.grid[row][col].colour
|
||||
if self.grid[row][col].skin == "square" then
|
||||
if colour == "Y" then gold = gold + 1
|
||||
else silver = silver + 1 end
|
||||
end
|
||||
end
|
||||
table[row] = gold * 2.5 + silver * 1.25
|
||||
end
|
||||
return table
|
||||
end
|
||||
|
||||
function Grid:update()
|
||||
for y = 1, 24 do
|
||||
for x = 1, 10 do
|
||||
for y = 1, self.height do
|
||||
for x = 1, self.width do
|
||||
if self.grid[y][x] ~= empty then
|
||||
self.grid_age[y][x] = self.grid_age[y][x] + 1
|
||||
end
|
||||
@@ -196,29 +386,36 @@ function Grid:update()
|
||||
end
|
||||
|
||||
function Grid:draw()
|
||||
for y = 1, 24 do
|
||||
for x = 1, 10 do
|
||||
for y = 5, self.height do
|
||||
for x = 1, self.width do
|
||||
if self.grid[y][x] ~= empty then
|
||||
if self.grid_age[y][x] < 1 then
|
||||
if self.grid_age[y][x] < 2 then
|
||||
love.graphics.setColor(1, 1, 1, 1)
|
||||
love.graphics.draw(blocks[self.grid[y][x].skin]["F"], 48+x*16, y*16)
|
||||
else
|
||||
love.graphics.setColor(0.5, 0.5, 0.5, 1)
|
||||
if self.grid[y][x].skin == "bone" then
|
||||
love.graphics.setColor(1, 1, 1, 1)
|
||||
elseif self.grid[y][x].colour == "X" then
|
||||
love.graphics.setColor(0.5, 0.5, 0.5, 1 - self.grid_age[y][x] / 15)
|
||||
else
|
||||
love.graphics.setColor(0.5, 0.5, 0.5, 1)
|
||||
end
|
||||
love.graphics.draw(blocks[self.grid[y][x].skin][self.grid[y][x].colour], 48+x*16, y*16)
|
||||
end
|
||||
if self.grid[y][x].skin ~= "bone" then
|
||||
if self.grid[y][x].skin ~= "bone" and self.grid[y][x].colour ~= "X" then
|
||||
love.graphics.setColor(0.8, 0.8, 0.8, 1)
|
||||
love.graphics.setLineWidth(1)
|
||||
if y > 1 and self.grid[y-1][x] == empty then
|
||||
if y > 1 and self.grid[y-1][x] == empty or self.grid[y-1][x].colour == "X" then
|
||||
love.graphics.line(48.0+x*16, -0.5+y*16, 64.0+x*16, -0.5+y*16)
|
||||
end
|
||||
if y < 24 and self.grid[y+1][x] == empty then
|
||||
if y < self.height and self.grid[y+1][x] == empty or
|
||||
(y + 1 > self.height or self.grid[y+1][x].colour == "X") then
|
||||
love.graphics.line(48.0+x*16, 16.5+y*16, 64.0+x*16, 16.5+y*16)
|
||||
end
|
||||
if x > 1 and self.grid[y][x-1] == empty then
|
||||
love.graphics.line(47.5+x*16, -0.0+y*16, 47.5+x*16, 16.0+y*16)
|
||||
end
|
||||
if x < 10 and self.grid[y][x+1] == empty then
|
||||
if x < self.width and self.grid[y][x+1] == empty then
|
||||
love.graphics.line(64.5+x*16, -0.0+y*16, 64.5+x*16, 16.0+y*16)
|
||||
end
|
||||
end
|
||||
@@ -227,35 +424,109 @@ function Grid:draw()
|
||||
end
|
||||
end
|
||||
|
||||
function Grid:drawInvisible(opacity_function, garbage_opacity_function)
|
||||
for y = 1, 24 do
|
||||
for x = 1, 10 do
|
||||
function Grid:drawOutline()
|
||||
for y = 5, self.height do
|
||||
for x = 1, self.width do
|
||||
if self.grid[y][x].colour == "X" then
|
||||
love.graphics.setColor(0.5, 0.5, 0.5, 1 - self.grid_age[y][x] / 15)
|
||||
love.graphics.draw(blocks[self.grid[y][x].skin][self.grid[y][x].colour], 48+x*16, y*16)
|
||||
end
|
||||
if self.grid[y][x] ~= empty and self.grid[y][x].colour ~= "X" then
|
||||
love.graphics.setColor(0.8, 0.8, 0.8, 1)
|
||||
love.graphics.setLineWidth(1)
|
||||
if y > 1 and self.grid[y-1][x] == empty or self.grid[y-1][x].colour == "X" then
|
||||
love.graphics.line(48.0+x*16, -0.5+y*16, 64.0+x*16, -0.5+y*16)
|
||||
end
|
||||
if y < self.height and self.grid[y+1][x] == empty or
|
||||
(y + 1 > self.height or self.grid[y+1][x].colour == "X") then
|
||||
love.graphics.line(48.0+x*16, 16.5+y*16, 64.0+x*16, 16.5+y*16)
|
||||
end
|
||||
if x > 1 and self.grid[y][x-1] == empty then
|
||||
love.graphics.line(47.5+x*16, -0.0+y*16, 47.5+x*16, 16.0+y*16)
|
||||
end
|
||||
if x < self.width and self.grid[y][x+1] == empty then
|
||||
love.graphics.line(64.5+x*16, -0.0+y*16, 64.5+x*16, 16.0+y*16)
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
function Grid:drawInvisible(opacity_function, garbage_opacity_function, lock_flash, brightness)
|
||||
lock_flash = lock_flash == nil and true or lock_flash
|
||||
brightness = brightness == nil and 0.5 or brightness
|
||||
for y = 5, self.height do
|
||||
for x = 1, self.width do
|
||||
if self.grid[y][x] ~= empty then
|
||||
if self.grid[y][x].colour == "X" then
|
||||
opacity = 1
|
||||
elseif garbage_opacity_function and self.grid[y][x].colour == "G" then
|
||||
opacity = 1 - self.grid_age[y][x] / 15
|
||||
elseif garbage_opacity_function and self.grid[y][x].colour == "A" then
|
||||
opacity = garbage_opacity_function(self.grid_age[y][x])
|
||||
else
|
||||
opacity = opacity_function(self.grid_age[y][x])
|
||||
end
|
||||
love.graphics.setColor(0.5, 0.5, 0.5, opacity)
|
||||
love.graphics.setColor(brightness, brightness, brightness, opacity)
|
||||
love.graphics.draw(blocks[self.grid[y][x].skin][self.grid[y][x].colour], 48+x*16, y*16)
|
||||
if opacity > 0 and self.grid[y][x].colour ~= "X" then
|
||||
love.graphics.setColor(0.64, 0.64, 0.64)
|
||||
love.graphics.setLineWidth(1)
|
||||
if y > 1 and self.grid[y-1][x] == empty then
|
||||
if lock_flash then
|
||||
if opacity > 0 and self.grid[y][x].colour ~= "X" then
|
||||
love.graphics.setColor(0.64, 0.64, 0.64)
|
||||
love.graphics.setLineWidth(1)
|
||||
if y > 1 and self.grid[y-1][x] == empty or self.grid[y-1][x].colour == "X" then
|
||||
love.graphics.line(48.0+x*16, -0.5+y*16, 64.0+x*16, -0.5+y*16)
|
||||
end
|
||||
if y < self.height and self.grid[y+1][x] == empty or
|
||||
(y + 1 > self.height or self.grid[y+1][x].colour == "X") then
|
||||
love.graphics.line(48.0+x*16, 16.5+y*16, 64.0+x*16, 16.5+y*16)
|
||||
end
|
||||
if x > 1 and self.grid[y][x-1] == empty then
|
||||
love.graphics.line(47.5+x*16, -0.0+y*16, 47.5+x*16, 16.0+y*16)
|
||||
end
|
||||
if x < self.width and self.grid[y][x+1] == empty then
|
||||
love.graphics.line(64.5+x*16, -0.0+y*16, 64.5+x*16, 16.0+y*16)
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
function Grid:drawCustom(colour_function, gamestate)
|
||||
--[[
|
||||
colour_function: (game, block, x, y, age) -> (R, G, B, A, outlineA)
|
||||
When called, calls the supplied function on every block passing the block itself as argument
|
||||
as well as coordinates and the grid_age value of the same cell.
|
||||
Should return a RGBA colour for the block, as well as the opacity of the stack outline (0 for no outline).
|
||||
|
||||
gamestate: the gamemode instance itself to pass in colour_function
|
||||
]]
|
||||
for y = 5, self.height do
|
||||
for x = 1, self.width do
|
||||
local block = self.grid[y][x]
|
||||
if block ~= empty then
|
||||
local R, G, B, A, outline = colour_function(gamestate, block, x, y, self.grid_age[y][x])
|
||||
if self.grid[y][x].colour == "X" then
|
||||
A = 1 - self.grid_age[y][x] / 15
|
||||
end
|
||||
love.graphics.setColor(R, G, B, A)
|
||||
love.graphics.draw(blocks[self.grid[y][x].skin][self.grid[y][x].colour], 48+x*16, y*16)
|
||||
if outline > 0 and self.grid[y][x].colour ~= "X" then
|
||||
love.graphics.setColor(0.64, 0.64, 0.64, outline)
|
||||
love.graphics.setLineWidth(1)
|
||||
if y > 1 and self.grid[y-1][x] == empty or self.grid[y-1][x].colour == "X" then
|
||||
love.graphics.line(48.0+x*16, -0.5+y*16, 64.0+x*16, -0.5+y*16)
|
||||
end
|
||||
if y < 24 and self.grid[y+1][x] == empty then
|
||||
if y < self.height and self.grid[y+1][x] == empty or
|
||||
(y + 1 > self.height or self.grid[y+1][x].colour == "X") then
|
||||
love.graphics.line(48.0+x*16, 16.5+y*16, 64.0+x*16, 16.5+y*16)
|
||||
end
|
||||
if x > 1 and self.grid[y][x-1] == empty then
|
||||
love.graphics.line(47.5+x*16, -0.0+y*16, 47.5+x*16, 16.0+y*16)
|
||||
end
|
||||
if x < 10 and self.grid[y][x+1] == empty then
|
||||
if x < self.width and self.grid[y][x+1] == empty then
|
||||
love.graphics.line(64.5+x*16, -0.0+y*16, 64.5+x*16, 16.0+y*16)
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
@@ -2,7 +2,7 @@ local Object = require 'libs.classic'
|
||||
|
||||
local Piece = Object:extend()
|
||||
|
||||
function Piece:new(shape, rotation, position, block_offsets, gravity, lock_delay, skin, big)
|
||||
function Piece:new(shape, rotation, position, block_offsets, gravity, lock_delay, skin, colour, big)
|
||||
self.shape = shape
|
||||
self.rotation = rotation
|
||||
self.position = position
|
||||
@@ -10,6 +10,7 @@ function Piece:new(shape, rotation, position, block_offsets, gravity, lock_delay
|
||||
self.gravity = gravity
|
||||
self.lock_delay = lock_delay
|
||||
self.skin = skin
|
||||
self.colour = colour
|
||||
self.ghost = false
|
||||
self.locked = false
|
||||
self.big = big
|
||||
@@ -21,7 +22,7 @@ function Piece:withOffset(offset)
|
||||
return Piece(
|
||||
self.shape, self.rotation,
|
||||
{x = self.position.x + offset.x, y = self.position.y + offset.y},
|
||||
self.block_offsets, self.gravity, self.lock_delay, self.skin, self.big
|
||||
self.block_offsets, self.gravity, self.lock_delay, self.skin, self.colour, self.big
|
||||
)
|
||||
end
|
||||
|
||||
@@ -31,7 +32,7 @@ function Piece:withRelativeRotation(rot)
|
||||
while new_rot >= 4 do new_rot = new_rot - 4 end
|
||||
return Piece(
|
||||
self.shape, new_rot, self.position,
|
||||
self.block_offsets, self.gravity, self.lock_delay, self.skin, self.big
|
||||
self.block_offsets, self.gravity, self.lock_delay, self.skin, self.colour, self.big
|
||||
)
|
||||
end
|
||||
|
||||
@@ -77,12 +78,15 @@ function Piece:setRelativeRotation(rot)
|
||||
return self
|
||||
end
|
||||
|
||||
function Piece:moveInGrid(step, squares, grid)
|
||||
function Piece:moveInGrid(step, squares, grid, instant)
|
||||
local moved = false
|
||||
for x = 1, squares do
|
||||
if grid:canPlacePiece(self:withOffset(step)) then
|
||||
moved = true
|
||||
self:setOffset(step)
|
||||
if instant then
|
||||
self:dropToBottom(grid)
|
||||
end
|
||||
else
|
||||
break
|
||||
end
|
||||
@@ -97,7 +101,7 @@ end
|
||||
|
||||
function Piece:dropToBottom(grid)
|
||||
local piece_y = self.position.y
|
||||
self:dropSquares(24, grid)
|
||||
self:dropSquares(math.huge, grid)
|
||||
self.gravity = 0
|
||||
if self.position.y > piece_y then
|
||||
-- if it got dropped any, also reset lock delay
|
||||
@@ -139,22 +143,23 @@ function Piece:draw(opacity, brightness, grid, partial_das)
|
||||
love.graphics.setColor(brightness, brightness, brightness, opacity)
|
||||
local offsets = self:getBlockOffsets()
|
||||
local gravity_offset = 0
|
||||
--if grid ~= nil and not self:isDropBlocked(grid) then
|
||||
-- gravity_offset = self.gravity * 16
|
||||
--end
|
||||
if config.gamesettings.smooth_movement == 1 and
|
||||
grid ~= nil and not self:isDropBlocked(grid) then
|
||||
gravity_offset = self.gravity * 16
|
||||
end
|
||||
if partial_das == nil then partial_das = 0 end
|
||||
for index, offset in pairs(offsets) do
|
||||
local x = self.position.x + offset.x
|
||||
local y = self.position.y + offset.y
|
||||
if self.big then
|
||||
love.graphics.draw(
|
||||
blocks[self.skin][self.shape],
|
||||
blocks[self.skin][self.colour],
|
||||
64+x*32+partial_das*2, 16+y*32+gravity_offset*2,
|
||||
0, 2, 2
|
||||
)
|
||||
else
|
||||
love.graphics.draw(
|
||||
blocks[self.skin][self.shape],
|
||||
blocks[self.skin][self.colour],
|
||||
64+x*16+partial_das, 16+y*16+gravity_offset
|
||||
)
|
||||
end
|
||||
|
||||
@@ -15,99 +15,95 @@ MarathonA2Game.tagline = "The points don't matter! Can you reach the invisible r
|
||||
|
||||
|
||||
function MarathonA2Game:new()
|
||||
MarathonA2Game.super:new()
|
||||
self.big_mode = true
|
||||
self.super:new()
|
||||
self.big_mode = true
|
||||
self.roll_frames = 0
|
||||
self.combo = 1
|
||||
self.randomizer = History6RollsRandomizer()
|
||||
self.combo = 1
|
||||
|
||||
self.grade = 0
|
||||
self.grade_points = 0
|
||||
self.grade_point_decay_counter = 0
|
||||
self.section_start_time = 0
|
||||
self.section_times = { [0] = 0 }
|
||||
self.section_tetrises = { [0] = 0, 0, 0, 0, 0, 0, 0, 0, 0, 0 }
|
||||
|
||||
|
||||
self.randomizer = History6RollsRandomizer()
|
||||
|
||||
self.lock_drop = false
|
||||
self.lock_drop = false
|
||||
self.lock_hard_drop = false
|
||||
self.enable_hold = false
|
||||
self.next_queue_length = 1
|
||||
end
|
||||
|
||||
function MarathonA2Game:getARE()
|
||||
if self.level < 700 then return 27
|
||||
elseif self.level < 800 then return 18
|
||||
else return 14 end
|
||||
if self.level < 700 then return 27
|
||||
elseif self.level < 800 then return 18
|
||||
else return 14 end
|
||||
end
|
||||
|
||||
function MarathonA2Game:getLineARE()
|
||||
if self.level < 600 then return 27
|
||||
elseif self.level < 700 then return 18
|
||||
elseif self.level < 800 then return 14
|
||||
else return 8 end
|
||||
if self.level < 600 then return 27
|
||||
elseif self.level < 700 then return 18
|
||||
elseif self.level < 800 then return 14
|
||||
else return 8 end
|
||||
end
|
||||
|
||||
function MarathonA2Game:getDasLimit()
|
||||
if self.level < 500 then return 15
|
||||
elseif self.level < 900 then return 9
|
||||
else return 7 end
|
||||
if self.level < 500 then return 15
|
||||
elseif self.level < 900 then return 9
|
||||
else return 7 end
|
||||
end
|
||||
|
||||
function MarathonA2Game:getLineClearDelay()
|
||||
if self.level < 500 then return 40
|
||||
elseif self.level < 600 then return 25
|
||||
elseif self.level < 700 then return 16
|
||||
elseif self.level < 800 then return 12
|
||||
else return 6 end
|
||||
if self.level < 500 then return 40
|
||||
elseif self.level < 600 then return 25
|
||||
elseif self.level < 700 then return 16
|
||||
elseif self.level < 800 then return 12
|
||||
else return 6 end
|
||||
end
|
||||
|
||||
function MarathonA2Game:getLockDelay()
|
||||
if self.level < 900 then return 30
|
||||
else return 17 end
|
||||
if self.level < 900 then return 30
|
||||
else return 17 end
|
||||
end
|
||||
|
||||
function MarathonA2Game:getGravity()
|
||||
if (self.level < 30) then return 4/256
|
||||
elseif (self.level < 35) then return 6/256
|
||||
elseif (self.level < 40) then return 8/256
|
||||
elseif (self.level < 50) then return 10/256
|
||||
elseif (self.level < 60) then return 12/256
|
||||
elseif (self.level < 70) then return 16/256
|
||||
elseif (self.level < 80) then return 32/256
|
||||
elseif (self.level < 90) then return 48/256
|
||||
elseif (self.level < 100) then return 64/256
|
||||
elseif (self.level < 120) then return 80/256
|
||||
elseif (self.level < 140) then return 96/256
|
||||
elseif (self.level < 160) then return 112/256
|
||||
elseif (self.level < 170) then return 128/256
|
||||
elseif (self.level < 200) then return 144/256
|
||||
elseif (self.level < 220) then return 4/256
|
||||
elseif (self.level < 230) then return 32/256
|
||||
elseif (self.level < 233) then return 64/256
|
||||
elseif (self.level < 236) then return 96/256
|
||||
elseif (self.level < 239) then return 128/256
|
||||
elseif (self.level < 243) then return 160/256
|
||||
elseif (self.level < 247) then return 192/256
|
||||
elseif (self.level < 251) then return 224/256
|
||||
elseif (self.level < 300) then return 1
|
||||
elseif (self.level < 330) then return 2
|
||||
elseif (self.level < 360) then return 3
|
||||
elseif (self.level < 400) then return 4
|
||||
elseif (self.level < 420) then return 5
|
||||
elseif (self.level < 450) then return 4
|
||||
elseif (self.level < 500) then return 3
|
||||
else return 20
|
||||
end
|
||||
if (self.level < 30) then return 4/256
|
||||
elseif (self.level < 35) then return 6/256
|
||||
elseif (self.level < 40) then return 8/256
|
||||
elseif (self.level < 50) then return 10/256
|
||||
elseif (self.level < 60) then return 12/256
|
||||
elseif (self.level < 70) then return 16/256
|
||||
elseif (self.level < 80) then return 32/256
|
||||
elseif (self.level < 90) then return 48/256
|
||||
elseif (self.level < 100) then return 64/256
|
||||
elseif (self.level < 120) then return 80/256
|
||||
elseif (self.level < 140) then return 96/256
|
||||
elseif (self.level < 160) then return 112/256
|
||||
elseif (self.level < 170) then return 128/256
|
||||
elseif (self.level < 200) then return 144/256
|
||||
elseif (self.level < 220) then return 4/256
|
||||
elseif (self.level < 230) then return 32/256
|
||||
elseif (self.level < 233) then return 64/256
|
||||
elseif (self.level < 236) then return 96/256
|
||||
elseif (self.level < 239) then return 128/256
|
||||
elseif (self.level < 243) then return 160/256
|
||||
elseif (self.level < 247) then return 192/256
|
||||
elseif (self.level < 251) then return 224/256
|
||||
elseif (self.level < 300) then return 1
|
||||
elseif (self.level < 330) then return 2
|
||||
elseif (self.level < 360) then return 3
|
||||
elseif (self.level < 400) then return 4
|
||||
elseif (self.level < 420) then return 5
|
||||
elseif (self.level < 450) then return 4
|
||||
elseif (self.level < 500) then return 3
|
||||
else return 20
|
||||
end
|
||||
end
|
||||
|
||||
function MarathonA2Game:advanceOneFrame()
|
||||
if self.clear then
|
||||
self.roll_frames = self.roll_frames + 1
|
||||
if self.roll_frames < 0 then return false end
|
||||
if self.roll_frames > 3694 then
|
||||
self.completed = true
|
||||
if self.grade == 32 then
|
||||
self.grade = 33
|
||||
end
|
||||
end
|
||||
elseif self.ready_frames == 0 then
|
||||
self.frames = self.frames + 1
|
||||
@@ -123,42 +119,31 @@ end
|
||||
|
||||
function MarathonA2Game:onLineClear(cleared_row_count)
|
||||
cleared_row_count = cleared_row_count / 2
|
||||
self:updateSectionTimes(self.level, self.level + cleared_row_count)
|
||||
self.level = math.min(self.level + cleared_row_count, 999)
|
||||
if self.level == 999 and not self.clear then
|
||||
self.clear = true
|
||||
if self:qualifiesForMRoll() then
|
||||
self.grade = 32
|
||||
end
|
||||
self.grid:clear()
|
||||
self.roll_frames = -150
|
||||
end
|
||||
self.level = math.min(self.level + cleared_row_count, 999)
|
||||
if self.level == 999 and not self.clear then
|
||||
self.clear = true
|
||||
self.grid:clear()
|
||||
self.roll_frames = -150
|
||||
end
|
||||
self.lock_drop = self.level >= 900
|
||||
self.lock_hard_drop = self.level >= 900
|
||||
end
|
||||
|
||||
function MarathonA2Game:updateScore(level, drop_bonus, cleared_lines)
|
||||
cleared_lines = cleared_lines / 2
|
||||
self:updateGrade(cleared_lines)
|
||||
if cleared_lines > 0 then
|
||||
self.score = self.score + (
|
||||
(math.ceil((level + cleared_lines) / 4) + drop_bonus) *
|
||||
cleared_lines * (cleared_lines * 2 - 1) * (self.combo * 2 - 1)
|
||||
)
|
||||
self.lines = self.lines + cleared_lines
|
||||
self.combo = self.combo + cleared_lines - 1
|
||||
else
|
||||
if not self.clear then
|
||||
cleared_lines = cleared_lines / 2
|
||||
self:updateGrade(cleared_lines)
|
||||
if self.grid:checkForBravo(cleared_lines) then self.bravo = 4 else self.bravo = 1 end
|
||||
if cleared_lines > 0 then
|
||||
self.combo = self.combo + (cleared_lines - 1) * 2
|
||||
self.score = self.score + (
|
||||
(math.ceil((level + cleared_lines) / 4) + drop_bonus) *
|
||||
cleared_lines * self.combo * self.bravo
|
||||
)
|
||||
else
|
||||
self.combo = 1
|
||||
end
|
||||
self.drop_bonus = 0
|
||||
self.combo = 1
|
||||
end
|
||||
end
|
||||
|
||||
function MarathonA2Game:updateSectionTimes(old_level, new_level)
|
||||
if self.clear then return end
|
||||
if math.floor(old_level / 100) < math.floor(new_level / 100) or
|
||||
new_level >= 999 then
|
||||
-- record new section
|
||||
section_time = self.frames - self.section_start_time
|
||||
self.section_times[math.floor(old_level / 100)] = section_time
|
||||
self.section_start_time = self.frames
|
||||
end
|
||||
end
|
||||
|
||||
@@ -223,7 +208,7 @@ local grade_conversion = {
|
||||
1, 2, 3, 4, 5, 5, 6, 6, 7, 7,
|
||||
7, 8, 8, 8, 9, 9, 9, 10, 11, 12,
|
||||
12, 12, 13, 13, 14, 14, 15, 15, 16, 16,
|
||||
17, 18, 19
|
||||
17
|
||||
}
|
||||
|
||||
function MarathonA2Game:updateGrade(cleared_lines)
|
||||
@@ -248,49 +233,12 @@ function MarathonA2Game:updateGrade(cleared_lines)
|
||||
end
|
||||
end
|
||||
|
||||
local tetris_requirements = { [0] = 2, 2, 2, 2, 2, 1, 1, 1, 1, 1 }
|
||||
|
||||
function MarathonA2Game:qualifiesForMRoll()
|
||||
if not self.clear then return false end
|
||||
-- tetris requirements
|
||||
for section = 0, 9 do
|
||||
if self.section_tetrises[section] < tetris_requirements[section] then
|
||||
return false
|
||||
end
|
||||
end
|
||||
-- section time requirements
|
||||
local section_average = 0
|
||||
for section = 0, 4 do
|
||||
section_average = section_average + self.section_times[section]
|
||||
if self.section_times[section] > frameTime(1,05) then
|
||||
return false
|
||||
end
|
||||
end
|
||||
-- section time average requirements
|
||||
if self.section_times[5] > section_average / 5 then
|
||||
return false
|
||||
end
|
||||
for section = 6, 9 do
|
||||
if self.section_times[section] > self.section_times[section - 1] + 120 then
|
||||
return false
|
||||
end
|
||||
end
|
||||
if self.grade < 17 or self.frames > frameTime(8,45) then
|
||||
return false
|
||||
end
|
||||
return true
|
||||
end
|
||||
|
||||
function MarathonA2Game:getLetterGrade()
|
||||
local grade = grade_conversion[self.grade]
|
||||
if grade < 9 then
|
||||
return tostring(9 - grade)
|
||||
elseif grade < 18 then
|
||||
return "S" .. tostring(grade - 8)
|
||||
elseif grade == 18 then
|
||||
return "M"
|
||||
else
|
||||
return "GM"
|
||||
end
|
||||
end
|
||||
|
||||
@@ -300,18 +248,9 @@ MarathonA2Game.rollOpacityFunction = function(age)
|
||||
else return 1 - (age - 240) / 60 end
|
||||
end
|
||||
|
||||
MarathonA2Game.mRollOpacityFunction = function(age)
|
||||
if age > 4 then return 0
|
||||
else return 1 - age / 4 end
|
||||
end
|
||||
|
||||
function MarathonA2Game:drawGrid(ruleset)
|
||||
if self.clear and not (self.completed or self.game_over) then
|
||||
if self:qualifiesForMRoll() then
|
||||
self.grid:drawInvisible(self.mRollOpacityFunction)
|
||||
else
|
||||
self.grid:drawInvisible(self.rollOpacityFunction)
|
||||
end
|
||||
self.grid:drawInvisible(self.rollOpacityFunction, nil, false)
|
||||
else
|
||||
self.grid:draw()
|
||||
if self.piece ~= nil and self.level < 100 then
|
||||
@@ -335,7 +274,10 @@ function MarathonA2Game:drawScoringInfo()
|
||||
love.graphics.printf("LEVEL", 240, 320, 40, "left")
|
||||
|
||||
love.graphics.setFont(font_3x5_3)
|
||||
if self.roll_frames > 3694 then love.graphics.setColor(1, 0.5, 0, 1)
|
||||
elseif self.clear then love.graphics.setColor(0, 1, 0, 1) end
|
||||
love.graphics.printf(self:getLetterGrade(), 240, 140, 90, "left")
|
||||
love.graphics.setColor(1, 1, 1, 1)
|
||||
love.graphics.printf(self.score, 240, 220, 90, "left")
|
||||
love.graphics.printf(self.level, 240, 340, 40, "right")
|
||||
love.graphics.printf(self:getSectionEndLevel(), 240, 370, 40, "right")
|
||||
|
||||
@@ -1,258 +0,0 @@
|
||||
require 'funcs'
|
||||
|
||||
local GameMode = require 'tetris.modes.gamemode'
|
||||
local Piece = require 'tetris.components.piece'
|
||||
|
||||
local History6RollsRandomizer = require 'tetris.randomizers.history_6rolls'
|
||||
|
||||
local DemonModeGame = GameMode:extend()
|
||||
|
||||
DemonModeGame.name = "Demon Mode"
|
||||
DemonModeGame.hash = "DemonMode"
|
||||
DemonModeGame.tagline = "Can you handle the ludicrous speed past level 20?"
|
||||
|
||||
|
||||
|
||||
|
||||
function DemonModeGame:new()
|
||||
DemonModeGame.super:new()
|
||||
self.roll_frames = 0
|
||||
self.combo = 1
|
||||
self.randomizer = History6RollsRandomizer()
|
||||
|
||||
self.grade = 0
|
||||
self.section_start_time = 0
|
||||
self.section_times = { [0] = 0 }
|
||||
self.section_tetris_count = 0
|
||||
self.section_tries = 0
|
||||
|
||||
self.enable_hold = true
|
||||
self.lock_drop = true
|
||||
self.next_queue_length = 3
|
||||
end
|
||||
|
||||
function DemonModeGame:getARE()
|
||||
if self.level < 500 then return 30
|
||||
elseif self.level < 600 then return 25
|
||||
elseif self.level < 700 then return 15
|
||||
elseif self.level < 800 then return 14
|
||||
elseif self.level < 900 then return 12
|
||||
elseif self.level < 1000 then return 11
|
||||
elseif self.level < 1100 then return 10
|
||||
elseif self.level < 1300 then return 8
|
||||
elseif self.level < 1400 then return 6
|
||||
elseif self.level < 1700 then return 4
|
||||
elseif self.level < 1800 then return 3
|
||||
elseif self.level < 1900 then return 2
|
||||
elseif self.level < 2000 then return 1
|
||||
else return 0 end
|
||||
end
|
||||
|
||||
function DemonModeGame:getLineARE()
|
||||
return self:getARE()
|
||||
end
|
||||
|
||||
function DemonModeGame:getDasLimit()
|
||||
if self.level < 500 then return 15
|
||||
elseif self.level < 1000 then return 10
|
||||
elseif self.level < 1500 then return 5
|
||||
elseif self.level < 1700 then return 4
|
||||
elseif self.level < 1900 then return 3
|
||||
elseif self.level < 2000 then return 2
|
||||
else return 1 end
|
||||
end
|
||||
|
||||
function DemonModeGame:getLineClearDelay()
|
||||
if self.level < 600 then return 15
|
||||
elseif self.level < 800 then return 10
|
||||
elseif self.level < 1000 then return 8
|
||||
elseif self.level < 1500 then return 5
|
||||
elseif self.level < 1700 then return 3
|
||||
elseif self.level < 1900 then return 2
|
||||
elseif self.level < 2000 then return 1
|
||||
else return 0 end
|
||||
end
|
||||
|
||||
function DemonModeGame:getLockDelay()
|
||||
if self.level < 100 then return 30
|
||||
elseif self.level < 200 then return 25
|
||||
elseif self.level < 300 then return 22
|
||||
elseif self.level < 400 then return 20
|
||||
elseif self.level < 1000 then return 15
|
||||
elseif self.level < 1200 then return 10
|
||||
elseif self.level < 1400 then return 9
|
||||
elseif self.level < 1500 then return 8
|
||||
elseif self.level < 1600 then return 7
|
||||
elseif self.level < 1700 then return 6
|
||||
elseif self.level < 1800 then return 5
|
||||
elseif self.level < 1900 then return 4
|
||||
elseif self.level < 2000 then return 3
|
||||
else return 2 end
|
||||
end
|
||||
|
||||
function DemonModeGame:getGravity()
|
||||
return 20
|
||||
end
|
||||
|
||||
local function getSectionForLevel(level)
|
||||
return math.floor(level / 100) + 1
|
||||
end
|
||||
|
||||
local cleared_row_levels = {1, 3, 6, 10}
|
||||
|
||||
function DemonModeGame:advanceOneFrame()
|
||||
if self.clear then
|
||||
self.roll_frames = self.roll_frames + 1
|
||||
if self.roll_frames < 0 then
|
||||
return false
|
||||
elseif self.roll_frames >= 1337 then
|
||||
self.completed = true
|
||||
end
|
||||
elseif self.ready_frames == 0 then
|
||||
self.frames = self.frames + 1
|
||||
end
|
||||
end
|
||||
|
||||
function DemonModeGame:onPieceEnter()
|
||||
if (self.level % 100 ~= 99) and self.frames ~= 0 then
|
||||
self.level = self.level + 1
|
||||
end
|
||||
end
|
||||
|
||||
function DemonModeGame:onLineClear(cleared_row_count)
|
||||
if cleared_row_count == 4 then
|
||||
self.section_tetris_count = self.section_tetris_count + 1
|
||||
end
|
||||
local advanced_levels = cleared_row_levels[cleared_row_count]
|
||||
if not self.clear then
|
||||
self:updateSectionTimes(self.level, self.level + advanced_levels)
|
||||
end
|
||||
end
|
||||
|
||||
function DemonModeGame:updateSectionTimes(old_level, new_level)
|
||||
local section = math.floor(old_level / 100) + 1
|
||||
if math.floor(old_level / 100) < math.floor(new_level / 100) then
|
||||
-- If at least one Tetris in this section hasn't been made,
|
||||
-- deny section passage.
|
||||
if old_level > 500 then
|
||||
if self.section_tetris_count == 0 then
|
||||
self.level = 100 * math.floor(old_level / 100)
|
||||
self.section_tries = self.section_tries + 1
|
||||
else
|
||||
self.level = math.min(new_level, 2500)
|
||||
-- if this is first try (no denials, add a grade)
|
||||
if self.section_tries == 0 then
|
||||
self.grade = self.grade + 1
|
||||
end
|
||||
self.section_tries = 0
|
||||
self.section_tetris_count = 0
|
||||
-- record new section
|
||||
section_time = self.frames - self.section_start_time
|
||||
table.insert(self.section_times, section_time)
|
||||
self.section_start_time = self.frames
|
||||
-- maybe clear
|
||||
if self.level == 2500 and not self.clear then
|
||||
self.clear = true
|
||||
self.grid:clear()
|
||||
self.roll_frames = -150
|
||||
end
|
||||
end
|
||||
elseif old_level < 100 then
|
||||
-- If section time is under cutoff, skip to level 500.
|
||||
if self.frames < frameTime(1,00) then
|
||||
self.level = 500
|
||||
self.grade = 5
|
||||
self.section_tries = 0
|
||||
self.section_tetris_count = 0
|
||||
else
|
||||
self.level = math.min(new_level, 2500)
|
||||
end
|
||||
-- record new section
|
||||
section_time = self.frames - self.section_start_time
|
||||
table.insert(self.section_times, section_time)
|
||||
self.section_start_time = self.frames
|
||||
end
|
||||
else
|
||||
self.level = math.min(new_level, 2500)
|
||||
end
|
||||
end
|
||||
|
||||
function DemonModeGame:updateScore(level, drop_bonus, cleared_lines)
|
||||
if cleared_lines > 0 then
|
||||
self.score = self.score + (
|
||||
(math.ceil((level + cleared_lines) / 4) + drop_bonus) *
|
||||
cleared_lines * (cleared_lines * 2 - 1) * (self.combo * 2 - 1)
|
||||
)
|
||||
self.lines = self.lines + cleared_lines
|
||||
self.combo = self.combo + cleared_lines - 1
|
||||
else
|
||||
self.drop_bonus = 0
|
||||
self.combo = 1
|
||||
end
|
||||
end
|
||||
|
||||
local letter_grades = {
|
||||
[0] = "", "D", "C", "B", "A",
|
||||
"S", "S-A", "S-B", "S-C", "S-D",
|
||||
"X", "X-A", "X-B", "X-C", "X-D",
|
||||
"W", "W-A", "W-B", "W-C", "W-D",
|
||||
"Master", "MasterS", "MasterX", "MasterW", "Grand Master",
|
||||
"Demon Master"
|
||||
}
|
||||
|
||||
function DemonModeGame:getLetterGrade()
|
||||
return letter_grades[self.grade]
|
||||
end
|
||||
|
||||
function DemonModeGame:drawGrid()
|
||||
if self.clear and not (self.completed or self.game_over) then
|
||||
self.grid:drawInvisible(self.rollOpacityFunction)
|
||||
else
|
||||
self.grid:draw()
|
||||
end
|
||||
end
|
||||
|
||||
DemonModeGame.rollOpacityFunction = function(age)
|
||||
if age > 4 then return 0
|
||||
else return 1 - age / 4 end
|
||||
end
|
||||
|
||||
function DemonModeGame:drawScoringInfo()
|
||||
DemonModeGame.super.drawScoringInfo(self)
|
||||
|
||||
love.graphics.setColor(1, 1, 1, 1)
|
||||
|
||||
love.graphics.setFont(font_3x5_2)
|
||||
love.graphics.print(
|
||||
self.das.direction .. " " ..
|
||||
self.das.frames .. " " ..
|
||||
strTrueValues(self.prev_inputs)
|
||||
)
|
||||
love.graphics.printf("NEXT", 64, 40, 40, "left")
|
||||
love.graphics.printf("GRADE", 240, 120, 40, "left")
|
||||
love.graphics.printf("SCORE", 240, 200, 40, "left")
|
||||
love.graphics.printf("LEVEL", 240, 320, 40, "left")
|
||||
|
||||
-- draw section time data
|
||||
local current_section = getSectionForLevel(self.level)
|
||||
self:drawSectionTimesWithSecondary(current_section)
|
||||
|
||||
love.graphics.setFont(font_3x5_3)
|
||||
love.graphics.printf(self.score, 240, 220, 90, "left")
|
||||
love.graphics.printf(self:getLetterGrade(), 240, 140, 90, "left")
|
||||
love.graphics.printf(string.format("%.2f", self.level / 100), 240, 340, 70, "right")
|
||||
end
|
||||
|
||||
function DemonModeGame:getHighscoreData()
|
||||
return {
|
||||
grade = self.grade,
|
||||
level = self.level,
|
||||
frames = self.frames,
|
||||
}
|
||||
end
|
||||
|
||||
function DemonModeGame:getBackground()
|
||||
return math.floor(self.level / 100)
|
||||
end
|
||||
|
||||
return DemonModeGame
|
||||
@@ -1,15 +1,19 @@
|
||||
local Object = require 'libs.classic'
|
||||
require 'funcs'
|
||||
|
||||
local playedReadySE = false
|
||||
local playedGoSE = false
|
||||
|
||||
local Grid = require 'tetris.components.grid'
|
||||
local Randomizer = require 'tetris.randomizers.randomizer'
|
||||
local Randomizer = require 'tetris.randomizers.bag7'
|
||||
local BagRandomizer = require 'tetris.randomizers.bag'
|
||||
|
||||
local GameMode = Object:extend()
|
||||
|
||||
GameMode.rollOpacityFunction = function(age) return 0 end
|
||||
|
||||
function GameMode:new()
|
||||
self.grid = Grid()
|
||||
function GameMode:new(secret_inputs)
|
||||
self.grid = Grid(10, 24)
|
||||
self.randomizer = Randomizer()
|
||||
self.piece = nil
|
||||
self.ready_frames = 100
|
||||
@@ -18,6 +22,7 @@ function GameMode:new()
|
||||
self.score = 0
|
||||
self.level = 0
|
||||
self.lines = 0
|
||||
self.squares = 0
|
||||
self.drop_bonus = 0
|
||||
self.are = 0
|
||||
self.lcd = 0
|
||||
@@ -40,9 +45,22 @@ function GameMode:new()
|
||||
self.draw_section_times = false
|
||||
self.draw_secondary_section_times = false
|
||||
self.big_mode = false
|
||||
self.irs = true
|
||||
self.ihs = true
|
||||
self.square_mode = false
|
||||
self.immobile_spin_bonus = false
|
||||
self.rpc_details = "In game"
|
||||
self.SGnames = {
|
||||
"9", "8", "7", "6", "5", "4", "3", "2", "1",
|
||||
"S1", "S2", "S3", "S4", "S5", "S6", "S7", "S8", "S9",
|
||||
"GM"
|
||||
}
|
||||
-- variables related to configurable parameters
|
||||
self.drop_locked = false
|
||||
self.hard_drop_locked = false
|
||||
self.lock_on_soft_drop = false
|
||||
self.lock_on_hard_drop = false
|
||||
self.used_randomizer = nil
|
||||
self.hold_queue = nil
|
||||
self.held = false
|
||||
self.section_start_time = 0
|
||||
@@ -57,37 +75,88 @@ function GameMode:getLineARE() return 25 end
|
||||
function GameMode:getLockDelay() return 30 end
|
||||
function GameMode:getLineClearDelay() return 40 end
|
||||
function GameMode:getDasLimit() return 15 end
|
||||
function GameMode:getDasCutDelay() return 0 end
|
||||
|
||||
function GameMode:getNextPiece(ruleset)
|
||||
return {
|
||||
skin = "2tie",
|
||||
shape = self.randomizer:nextPiece(),
|
||||
skin = self:getSkin(),
|
||||
shape = self.used_randomizer:nextPiece(),
|
||||
orientation = ruleset:getDefaultOrientation(),
|
||||
}
|
||||
end
|
||||
|
||||
function GameMode:initialize(ruleset)
|
||||
function GameMode:getSkin()
|
||||
return "2tie"
|
||||
end
|
||||
|
||||
function GameMode:initialize(ruleset, secret_inputs)
|
||||
-- generate next queue
|
||||
self:new(secret_inputs)
|
||||
self.used_randomizer = (
|
||||
ruleset.pieces == self.randomizer.possible_pieces and
|
||||
self.randomizer or
|
||||
(
|
||||
ruleset.pieces == 7 and
|
||||
Randomizer() or
|
||||
BagRandomizer(ruleset.pieces)
|
||||
)
|
||||
)
|
||||
for i = 1, self.next_queue_length do
|
||||
table.insert(self.next_queue, self:getNextPiece(ruleset))
|
||||
end
|
||||
self.lock_on_soft_drop = ({ruleset.softdrop_lock, self.instant_soft_drop, false, true })[config.gamesettings.manlock]
|
||||
self.lock_on_hard_drop = ({ruleset.harddrop_lock, self.instant_hard_drop, true, false})[config.gamesettings.manlock]
|
||||
end
|
||||
|
||||
function GameMode:update(inputs, ruleset)
|
||||
if self.game_over then
|
||||
if self.game_over or self.completed then
|
||||
self.game_over_frames = self.game_over_frames + 1
|
||||
if self.game_over_frames >= 60 then
|
||||
self.completed = true
|
||||
end
|
||||
return
|
||||
end
|
||||
if self.completed then return end
|
||||
|
||||
if config.gamesettings.diagonal_input == 2 then
|
||||
if inputs["left"] or inputs["right"] then
|
||||
inputs["up"] = false
|
||||
inputs["down"] = false
|
||||
elseif inputs["up"] or inputs["down"] then
|
||||
inputs["left"] = false
|
||||
inputs["right"] = false
|
||||
end
|
||||
end
|
||||
|
||||
-- advance one frame
|
||||
if self:advanceOneFrame(inputs) == false then return end
|
||||
if self:advanceOneFrame(inputs, ruleset) == false then return end
|
||||
|
||||
self:chargeDAS(inputs, self:getDasLimit(), self.getARR())
|
||||
self:chargeDAS(inputs, self:getDasLimit(), self:getARR())
|
||||
|
||||
-- set attempt flags
|
||||
if inputs["left"] or inputs["right"] then
|
||||
self:onAttemptPieceMove(self.piece)
|
||||
if self.immobile_spin_bonus and self.piece ~= nil then
|
||||
if not self.piece:isMoveBlocked(self.grid, { x=-1, y=0 }) and
|
||||
not self.piece:isMoveBlocked(self.grid, { x=1, y=0 }) then
|
||||
self.piece.spin = false
|
||||
end
|
||||
end
|
||||
end
|
||||
if
|
||||
inputs["rotate_left"] or inputs["rotate_right"] or
|
||||
inputs["rotate_left2"] or inputs["rotate_right2"] or
|
||||
inputs["rotate_180"]
|
||||
then
|
||||
self:onAttemptPieceRotate(self.piece)
|
||||
if self.immobile_spin_bonus and self.piece ~= nil then
|
||||
if self.piece:isDropBlocked(self.grid) and
|
||||
self.piece:isMoveBlocked(self.grid, { x=-1, y=0 }) and
|
||||
self.piece:isMoveBlocked(self.grid, { x=1, y=0 }) and
|
||||
self.piece:isMoveBlocked(self.grid, { x=0, y=-1 }) then
|
||||
self.piece.spin = true
|
||||
else
|
||||
self.piece.spin = false
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
if self.piece == nil then
|
||||
self:processDelays(inputs, ruleset)
|
||||
else
|
||||
@@ -95,7 +164,7 @@ function GameMode:update(inputs, ruleset)
|
||||
self:whilePieceActive()
|
||||
local gravity = self:getGravity()
|
||||
|
||||
if self.enable_hold and inputs["hold"] == true and self.held == false then
|
||||
if self.enable_hold and inputs["hold"] == true and self.held == false and self.prev_inputs["hold"] == false then
|
||||
self:hold(inputs, ruleset)
|
||||
self.prev_inputs = inputs
|
||||
return
|
||||
@@ -109,7 +178,9 @@ function GameMode:update(inputs, ruleset)
|
||||
self.hard_drop_locked = false
|
||||
end
|
||||
|
||||
-- diff vars to use in checks
|
||||
local piece_y = self.piece.position.y
|
||||
local piece_rot = self.piece.rotation
|
||||
|
||||
ruleset:processPiece(
|
||||
inputs, self.piece, self.grid, self:getGravity(), self.prev_inputs,
|
||||
@@ -119,12 +190,29 @@ function GameMode:update(inputs, ruleset)
|
||||
)
|
||||
|
||||
local piece_dy = self.piece.position.y - piece_y
|
||||
local piece_drot = self.piece.rotation - piece_rot
|
||||
|
||||
-- das cut
|
||||
if (
|
||||
(piece_dy ~= 0 and (inputs.up or inputs.down)) or
|
||||
(piece_drot ~= 0 and (
|
||||
inputs.rotate_left or inputs.rotate_right or
|
||||
inputs.rotate_left2 or inputs.rotate_right2 or
|
||||
inputs.rotate_180
|
||||
))
|
||||
) then
|
||||
self.das.frames = math.max(
|
||||
self.das.frames - self:getDasCutDelay(),
|
||||
-self:getDasCutDelay()
|
||||
)
|
||||
end
|
||||
|
||||
if inputs["up"] == true and
|
||||
self.piece:isDropBlocked(self.grid) and
|
||||
not self.hard_drop_locked then
|
||||
self:onHardDrop(piece_dy)
|
||||
if self.instant_hard_drop then
|
||||
if self.lock_on_hard_drop then
|
||||
self.piece_hard_dropped = true
|
||||
self.piece.locked = true
|
||||
end
|
||||
end
|
||||
@@ -133,7 +221,7 @@ function GameMode:update(inputs, ruleset)
|
||||
self:onSoftDrop(piece_dy)
|
||||
if self.piece:isDropBlocked(self.grid) and
|
||||
not self.drop_locked and
|
||||
self.instant_soft_drop
|
||||
self.lock_on_soft_drop
|
||||
then
|
||||
self.piece.locked = true
|
||||
end
|
||||
@@ -141,21 +229,26 @@ function GameMode:update(inputs, ruleset)
|
||||
|
||||
if self.piece.locked == true then
|
||||
self.grid:applyPiece(self.piece)
|
||||
self.grid:markClearedRows()
|
||||
if self.square_mode then
|
||||
self.squares = self.squares + self.grid:markSquares()
|
||||
end
|
||||
|
||||
local cleared_row_count = self.grid:getClearedRowCount()
|
||||
|
||||
self:onPieceLock(self.piece, cleared_row_count)
|
||||
self:updateScore(self.level, self.drop_bonus, cleared_row_count)
|
||||
|
||||
self.grid:markClearedRows()
|
||||
self.piece = nil
|
||||
if self.enable_hold then
|
||||
self.held = false
|
||||
end
|
||||
|
||||
if cleared_row_count > 0 then
|
||||
playSE("erase")
|
||||
self.lcd = self:getLineClearDelay()
|
||||
self.are = self:getLineARE()
|
||||
self.are = (
|
||||
ruleset.are and self:getLineARE() or 0
|
||||
)
|
||||
if self.lcd == 0 then
|
||||
self.grid:clearClearedRows()
|
||||
if self.are == 0 then
|
||||
@@ -164,7 +257,7 @@ function GameMode:update(inputs, ruleset)
|
||||
end
|
||||
self:onLineClear(cleared_row_count)
|
||||
else
|
||||
if self:getARE() == 0 then
|
||||
if self:getARE() == 0 or not ruleset.are then
|
||||
self:initializeOrHold(inputs, ruleset)
|
||||
else
|
||||
self.are = self:getARE()
|
||||
@@ -187,8 +280,14 @@ end
|
||||
|
||||
-- event functions
|
||||
function GameMode:whilePieceActive() end
|
||||
function GameMode:onPieceLock(piece, cleared_row_count) end
|
||||
function GameMode:onAttemptPieceMove(piece) end
|
||||
function GameMode:onAttemptPieceRotate(piece) end
|
||||
function GameMode:onPieceLock(piece, cleared_row_count)
|
||||
playSE("lock")
|
||||
end
|
||||
|
||||
function GameMode:onLineClear(cleared_row_count) end
|
||||
|
||||
function GameMode:onPieceEnter() end
|
||||
function GameMode:onHold() end
|
||||
|
||||
@@ -202,51 +301,138 @@ end
|
||||
|
||||
function GameMode:onGameOver()
|
||||
switchBGM(nil)
|
||||
love.graphics.setColor(0, 0, 0, 1 - 2 ^ (-self.game_over_frames / 30))
|
||||
love.graphics.rectangle(
|
||||
"fill", 64, 80,
|
||||
16 * self.grid.width, 16 * (self.grid.height - 4)
|
||||
)
|
||||
end
|
||||
|
||||
function GameMode:onGameComplete()
|
||||
self:onGameOver()
|
||||
end
|
||||
|
||||
-- DAS functions
|
||||
|
||||
function GameMode:startRightDAS()
|
||||
self.move = "right"
|
||||
self.das = { direction = "right", frames = 0 }
|
||||
if self:getDasLimit() == 0 then
|
||||
self:continueDAS()
|
||||
end
|
||||
end
|
||||
|
||||
function GameMode:startLeftDAS()
|
||||
self.move = "left"
|
||||
self.das = { direction = "left", frames = 0 }
|
||||
if self:getDasLimit() == 0 then
|
||||
self:continueDAS()
|
||||
end
|
||||
end
|
||||
|
||||
function GameMode:continueDAS()
|
||||
local das_frames = self.das.frames + 1
|
||||
if das_frames >= self:getDasLimit() then
|
||||
if self.das.direction == "left" then
|
||||
self.move = (self:getARR() == 0 and "speed" or "") .. "left"
|
||||
self.das.frames = self:getDasLimit() - self:getARR()
|
||||
elseif self.das.direction == "right" then
|
||||
self.move = (self:getARR() == 0 and "speed" or "") .. "right"
|
||||
self.das.frames = self:getDasLimit() - self:getARR()
|
||||
end
|
||||
else
|
||||
self.move = "none"
|
||||
self.das.frames = das_frames
|
||||
end
|
||||
end
|
||||
|
||||
function GameMode:stopDAS()
|
||||
self.move = "none"
|
||||
self.das = { direction = "none", frames = -1 }
|
||||
end
|
||||
|
||||
function GameMode:chargeDAS(inputs)
|
||||
if inputs[self.das.direction] == true then
|
||||
local das_frames = self.das.frames + 1
|
||||
if das_frames >= self:getDasLimit() then
|
||||
if self.das.direction == "left" then
|
||||
self.move = (self:getARR() == 0 and "speed" or "") .. "left"
|
||||
self.das.frames = self:getDasLimit() - self:getARR()
|
||||
elseif self.das.direction == "right" then
|
||||
self.move = (self:getARR() == 0 and "speed" or "") .. "right"
|
||||
self.das.frames = self:getDasLimit() - self:getARR()
|
||||
end
|
||||
if config["das_last_key"] then
|
||||
if inputs["right"] == true and self.das.direction ~= "right" and not self.prev_inputs["right"] then
|
||||
self:startRightDAS()
|
||||
elseif inputs["left"] == true and self.das.direction ~= "left" and not self.prev_inputs["left"] then
|
||||
self:startLeftDAS()
|
||||
elseif inputs[self.das.direction] == true then
|
||||
self:continueDAS()
|
||||
else
|
||||
self.move = "none"
|
||||
self.das.frames = das_frames
|
||||
self:stopDAS()
|
||||
end
|
||||
elseif inputs["right"] == true then
|
||||
self.move = "right"
|
||||
self.das = { direction = "right", frames = 0 }
|
||||
elseif inputs["left"] == true then
|
||||
self.move = "left"
|
||||
self.das = { direction = "left", frames = 0 }
|
||||
else
|
||||
self.move = "none"
|
||||
self.das = { direction = "none", frames = -1 }
|
||||
else -- default behaviour, das first key pressed
|
||||
if inputs[self.das.direction] == true then
|
||||
self:continueDAS()
|
||||
elseif inputs["right"] == true then
|
||||
self:startRightDAS()
|
||||
elseif inputs["left"] == true then
|
||||
self:startLeftDAS()
|
||||
else
|
||||
self:stopDAS()
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
function GameMode:areCancel(inputs, ruleset)
|
||||
if ruleset.are_cancel and self.piece_hard_dropped and
|
||||
not self.prev_inputs.up and
|
||||
strTrueValues(inputs) ~= "" then
|
||||
self.lcd = 0
|
||||
self.are = 0
|
||||
end
|
||||
end
|
||||
|
||||
function GameMode:processDelays(inputs, ruleset, drop_speed)
|
||||
if self.ready_frames == 100 then
|
||||
playedReadySE = false
|
||||
playedGoSE = false
|
||||
end
|
||||
if self.ready_frames > 0 then
|
||||
if not self.prev_inputs["up"] and inputs["up"] and self.enable_hard_drop then
|
||||
self.buffer_hard_drop = true
|
||||
end
|
||||
if not self.prev_inputs["down"] and inputs["down"] then
|
||||
self.buffer_soft_drop = true
|
||||
end
|
||||
if not playedReadySE then
|
||||
playedReadySE = true
|
||||
playSEOnce("ready")
|
||||
end
|
||||
self.ready_frames = self.ready_frames - 1
|
||||
if self.ready_frames == 50 and not playedGoSE then
|
||||
playedGoSE = true
|
||||
playSEOnce("go")
|
||||
end
|
||||
if self.ready_frames == 0 then
|
||||
self:initializeOrHold(inputs, ruleset)
|
||||
end
|
||||
elseif self.lcd > 0 then
|
||||
if not self.prev_inputs["up"] and inputs["up"] and self.enable_hard_drop then
|
||||
self.buffer_hard_drop = true
|
||||
end
|
||||
if not self.prev_inputs["down"] and inputs["down"] then
|
||||
self.buffer_soft_drop = true
|
||||
end
|
||||
self.lcd = self.lcd - 1
|
||||
self:areCancel(inputs, ruleset)
|
||||
if self.lcd == 0 then
|
||||
self.grid:clearClearedRows()
|
||||
playSE("fall")
|
||||
if self.are == 0 then
|
||||
self:initializeOrHold(inputs, ruleset)
|
||||
end
|
||||
end
|
||||
elseif self.are > 0 then
|
||||
if not self.prev_inputs["up"] and inputs["up"] and self.enable_hard_drop then
|
||||
self.buffer_hard_drop = true
|
||||
end
|
||||
if not self.prev_inputs["down"] and inputs["down"] then
|
||||
self.buffer_soft_drop = true
|
||||
end
|
||||
self.are = self.are - 1
|
||||
self:areCancel(inputs, ruleset)
|
||||
if self.are == 0 then
|
||||
self:initializeOrHold(inputs, ruleset)
|
||||
end
|
||||
@@ -254,19 +440,20 @@ function GameMode:processDelays(inputs, ruleset, drop_speed)
|
||||
end
|
||||
|
||||
function GameMode:initializeOrHold(inputs, ruleset)
|
||||
if self.enable_hold and inputs["hold"] == true then
|
||||
self:hold(inputs, ruleset)
|
||||
if (
|
||||
self.frames == 0 or (ruleset.are and self:getARE() ~= 0) and self.ihs or false
|
||||
) and self.enable_hold and inputs["hold"] == true then
|
||||
self:hold(inputs, ruleset, true)
|
||||
else
|
||||
self:initializeNextPiece(inputs, ruleset, self.next_queue[1])
|
||||
end
|
||||
self:onPieceEnter()
|
||||
if not self.grid:canPlacePiece(self.piece) then
|
||||
self:onGameOver()
|
||||
self.game_over = true
|
||||
end
|
||||
end
|
||||
|
||||
function GameMode:hold(inputs, ruleset)
|
||||
function GameMode:hold(inputs, ruleset, ihs)
|
||||
local data = copy(self.hold_queue)
|
||||
if self.piece == nil then
|
||||
self.hold_queue = self.next_queue[1]
|
||||
@@ -285,17 +472,40 @@ function GameMode:hold(inputs, ruleset)
|
||||
self:initializeNextPiece(inputs, ruleset, data, false)
|
||||
end
|
||||
self.held = true
|
||||
if ihs then playSE("ihs")
|
||||
else playSE("hold") end
|
||||
self:onHold()
|
||||
end
|
||||
|
||||
function GameMode:initializeNextPiece(inputs, ruleset, piece_data, generate_next_piece)
|
||||
self.piece_hard_dropped = false
|
||||
local gravity = self:getGravity()
|
||||
self.piece = ruleset:initializePiece(
|
||||
inputs, piece_data, self.grid, gravity,
|
||||
self.prev_inputs, self.move,
|
||||
self:getLockDelay(), self:getDropSpeed(),
|
||||
self.lock_drop, self.lock_hard_drop, self.big_mode
|
||||
self.lock_drop, self.lock_hard_drop, self.big_mode,
|
||||
(
|
||||
self.frames == 0 or (ruleset.are and self:getARE() ~= 0)
|
||||
) and self.irs or false,
|
||||
self.buffer_hard_drop, self.buffer_soft_drop,
|
||||
self.lock_on_hard_drop, self.lock_on_soft_drop
|
||||
)
|
||||
if self.piece:isDropBlocked(self.grid) and
|
||||
self.grid:canPlacePiece(self.piece) then
|
||||
playSE("bottom")
|
||||
end
|
||||
if self.buffer_hard_drop then
|
||||
self.buffer_hard_drop = false
|
||||
self:onHardDrop(self.piece.position.y - (
|
||||
self.big_mode and
|
||||
ruleset.big_spawn_positions[self.piece.shape].y or
|
||||
ruleset.spawn_positions[self.piece.shape].y)
|
||||
)
|
||||
end
|
||||
if self.buffer_soft_drop then
|
||||
self.buffer_soft_drop = false
|
||||
end
|
||||
if self.lock_drop then
|
||||
self.drop_locked = true
|
||||
end
|
||||
@@ -306,11 +516,11 @@ function GameMode:initializeNextPiece(inputs, ruleset, piece_data, generate_next
|
||||
table.remove(self.next_queue, 1)
|
||||
table.insert(self.next_queue, self:getNextPiece(ruleset))
|
||||
end
|
||||
self:playNextSound()
|
||||
self:playNextSound(ruleset)
|
||||
end
|
||||
|
||||
function GameMode:playNextSound()
|
||||
playSE("blocks", self.next_queue[1].shape)
|
||||
function GameMode:playNextSound(ruleset)
|
||||
playSE("blocks", ruleset.next_sounds[self.next_queue[1].shape])
|
||||
end
|
||||
|
||||
function GameMode:getHighScoreData()
|
||||
@@ -339,11 +549,12 @@ function GameMode:drawGhostPiece(ruleset)
|
||||
end
|
||||
|
||||
function GameMode:drawNextQueue(ruleset)
|
||||
local colourscheme = ({ruleset.colourscheme, ColourSchemes.Arika, ColourSchemes.TTC})[config.gamesettings.piece_colour]
|
||||
function drawPiece(piece, skin, offsets, pos_x, pos_y)
|
||||
for index, offset in pairs(offsets) do
|
||||
local x = offset.x + ruleset.spawn_positions[piece].x
|
||||
local y = offset.y + 4.7
|
||||
love.graphics.draw(blocks[skin][piece], pos_x+x*16, pos_y+y*16)
|
||||
local x = offset.x + ruleset.draw_offsets[piece].x + ruleset.spawn_positions[piece].x
|
||||
local y = offset.y + ruleset.draw_offsets[piece].y + 4.7
|
||||
love.graphics.draw(blocks[skin][colourscheme[piece]], pos_x+x*16, pos_y+y*16)
|
||||
end
|
||||
end
|
||||
for i = 1, self.next_queue_length do
|
||||
@@ -357,7 +568,7 @@ function GameMode:drawNextQueue(ruleset)
|
||||
drawPiece(next_piece, skin, ruleset.block_offsets[next_piece][rotation], -16+i*80, -32)
|
||||
end
|
||||
end
|
||||
if self.hold_queue ~= nil then
|
||||
if self.hold_queue ~= nil and self.enable_hold then
|
||||
self:setHoldOpacity()
|
||||
drawPiece(
|
||||
self.hold_queue.shape,
|
||||
@@ -369,8 +580,14 @@ function GameMode:drawNextQueue(ruleset)
|
||||
return false
|
||||
end
|
||||
|
||||
function GameMode:setNextOpacity(i) love.graphics.setColor(1, 1, 1, 1) end
|
||||
function GameMode:setHoldOpacity() love.graphics.setColor(1, 1, 1, 1) end
|
||||
function GameMode:setNextOpacity(i)
|
||||
love.graphics.setColor(1, 1, 1, 1)
|
||||
end
|
||||
|
||||
function GameMode:setHoldOpacity()
|
||||
local colour = self.held and 0.6 or 1
|
||||
love.graphics.setColor(colour, colour, colour, 1)
|
||||
end
|
||||
|
||||
function GameMode:drawScoringInfo()
|
||||
love.graphics.setColor(1, 1, 1, 1)
|
||||
@@ -405,7 +622,12 @@ function GameMode:drawSectionTimes(current_section)
|
||||
love.graphics.printf(formatTime(self.frames - self.section_start_time), section_x, 40 + 20 * current_section, 90, "left")
|
||||
end
|
||||
|
||||
function GameMode:drawSectionTimesWithSecondary(current_section)
|
||||
function GameMode:sectionColourFunction(section)
|
||||
return { 1, 1, 1, 1 }
|
||||
end
|
||||
|
||||
function GameMode:drawSectionTimesWithSecondary(current_section, section_limit)
|
||||
section_limit = section_limit or math.huge
|
||||
local section_x = 530
|
||||
local section_secondary_x = 440
|
||||
|
||||
@@ -416,9 +638,11 @@ function GameMode:drawSectionTimesWithSecondary(current_section)
|
||||
end
|
||||
|
||||
for section, time in pairs(self.secondary_section_times) do
|
||||
love.graphics.setColor(self:sectionColourFunction(section))
|
||||
if section > 0 then
|
||||
love.graphics.printf(formatTime(time), section_secondary_x, 40 + 20 * section, 90, "left")
|
||||
end
|
||||
love.graphics.setColor(1, 1, 1, 1)
|
||||
end
|
||||
|
||||
local current_x
|
||||
@@ -428,7 +652,9 @@ function GameMode:drawSectionTimesWithSecondary(current_section)
|
||||
current_x = section_secondary_x
|
||||
end
|
||||
|
||||
love.graphics.printf(formatTime(self.frames - self.section_start_time), current_x, 40 + 20 * current_section, 90, "left")
|
||||
if current_section <= section_limit then
|
||||
love.graphics.printf(formatTime(self.frames - self.section_start_time), current_x, 40 + 20 * current_section, 90, "left")
|
||||
end
|
||||
end
|
||||
|
||||
function GameMode:drawSectionTimesWithSplits(current_section)
|
||||
|
||||
@@ -1,155 +0,0 @@
|
||||
require 'funcs'
|
||||
|
||||
local GameMode = require 'tetris.modes.gamemode'
|
||||
local Piece = require 'tetris.components.piece'
|
||||
|
||||
local History6RollsRandomizer = require 'tetris.randomizers.history_6rolls'
|
||||
|
||||
local IntervalTrainingGame = GameMode:extend()
|
||||
|
||||
IntervalTrainingGame.name = "Interval Training"
|
||||
IntervalTrainingGame.hash = "IntervalTraining"
|
||||
IntervalTrainingGame.tagline = "Can you clear the time hurdles when the game goes this fast?"
|
||||
|
||||
|
||||
|
||||
|
||||
function IntervalTrainingGame:new()
|
||||
IntervalTrainingGame.super:new()
|
||||
self.roll_frames = 0
|
||||
self.combo = 1
|
||||
self.randomizer = History6RollsRandomizer()
|
||||
self.section_time_limit = 1800
|
||||
self.section_start_time = 0
|
||||
self.section_times = { [0] = 0 }
|
||||
self.lock_drop = true
|
||||
self.enable_hold = true
|
||||
self.next_queue_length = 3
|
||||
end
|
||||
|
||||
function IntervalTrainingGame:getARE()
|
||||
return 4
|
||||
end
|
||||
|
||||
function IntervalTrainingGame:getLineARE()
|
||||
return 4
|
||||
end
|
||||
|
||||
function IntervalTrainingGame:getDasLimit()
|
||||
return 6
|
||||
end
|
||||
|
||||
function IntervalTrainingGame:getLineClearDelay()
|
||||
return 6
|
||||
end
|
||||
|
||||
function IntervalTrainingGame:getLockDelay()
|
||||
return 15
|
||||
end
|
||||
|
||||
function IntervalTrainingGame:getGravity()
|
||||
return 20
|
||||
end
|
||||
|
||||
function IntervalTrainingGame:getSection()
|
||||
return math.floor(level / 100) + 1
|
||||
end
|
||||
|
||||
function IntervalTrainingGame:advanceOneFrame()
|
||||
if self.clear then
|
||||
self.roll_frames = self.roll_frames + 1
|
||||
if self.roll_frames > 2968 then
|
||||
self.completed = true
|
||||
end
|
||||
return false
|
||||
elseif self.ready_frames == 0 then
|
||||
self.frames = self.frames + 1
|
||||
if self:getSectionTime() >= self.section_time_limit then
|
||||
self.game_over = true
|
||||
end
|
||||
end
|
||||
return true
|
||||
end
|
||||
|
||||
function IntervalTrainingGame:onPieceEnter()
|
||||
if (self.level % 100 ~= 99 or self.level == 998) and not self.clear and self.frames ~= 0 then
|
||||
self.level = self.level + 1
|
||||
end
|
||||
end
|
||||
|
||||
function IntervalTrainingGame:onLineClear(cleared_row_count)
|
||||
if not self.clear then
|
||||
local new_level = self.level + cleared_row_count
|
||||
self:updateSectionTimes(self.level, new_level)
|
||||
self.level = math.min(new_level, 999)
|
||||
if self.level == 999 then
|
||||
self.clear = true
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
function IntervalTrainingGame:getSectionTime()
|
||||
return self.frames - self.section_start_time
|
||||
end
|
||||
|
||||
function IntervalTrainingGame:updateSectionTimes(old_level, new_level)
|
||||
if math.floor(old_level / 100) < math.floor(new_level / 100) then
|
||||
-- record new section
|
||||
table.insert(self.section_times, self:getSectionTime())
|
||||
self.section_start_time = self.frames
|
||||
else
|
||||
self.level = math.min(new_level, 999)
|
||||
end
|
||||
end
|
||||
|
||||
function IntervalTrainingGame:drawGrid(ruleset)
|
||||
self.grid:draw()
|
||||
end
|
||||
|
||||
function IntervalTrainingGame:getHighscoreData()
|
||||
return {
|
||||
level = self.level,
|
||||
frames = self.frames,
|
||||
}
|
||||
end
|
||||
|
||||
function IntervalTrainingGame:drawScoringInfo()
|
||||
love.graphics.setColor(1, 1, 1, 1)
|
||||
|
||||
love.graphics.setFont(font_3x5_2)
|
||||
love.graphics.print(
|
||||
self.das.direction .. " " ..
|
||||
self.das.frames .. " " ..
|
||||
strTrueValues(self.prev_inputs)
|
||||
)
|
||||
love.graphics.printf("NEXT", 64, 40, 40, "left")
|
||||
love.graphics.printf("TIME LEFT", 240, 250, 80, "left")
|
||||
love.graphics.printf("LEVEL", 240, 320, 40, "left")
|
||||
|
||||
local current_section = math.floor(self.level / 100) + 1
|
||||
self:drawSectionTimesWithSplits(current_section)
|
||||
|
||||
love.graphics.setFont(font_3x5_3)
|
||||
love.graphics.printf(self.level, 240, 340, 40, "right")
|
||||
|
||||
-- draw time left, flash red if necessary
|
||||
local time_left = self.section_time_limit - math.max(self:getSectionTime(), 0)
|
||||
if not self.game_over and not self.clear and time_left < frameTime(0,10) and time_left % 4 < 2 then
|
||||
love.graphics.setColor(1, 0.3, 0.3, 1)
|
||||
end
|
||||
love.graphics.printf(formatTime(time_left), 240, 270, 160, "left")
|
||||
|
||||
love.graphics.setColor(1, 1, 1, 1)
|
||||
love.graphics.printf(self:getSectionEndLevel(), 240, 370, 40, "right")
|
||||
end
|
||||
|
||||
function IntervalTrainingGame:getSectionEndLevel()
|
||||
if self.level >= 900 then return 999
|
||||
else return math.floor(self.level / 100 + 1) * 100 end
|
||||
end
|
||||
|
||||
function IntervalTrainingGame:getBackground()
|
||||
return math.floor(self.level / 100)
|
||||
end
|
||||
|
||||
return IntervalTrainingGame
|
||||
@@ -1,198 +0,0 @@
|
||||
require 'funcs'
|
||||
|
||||
local GameMode = require 'tetris.modes.gamemode'
|
||||
local Piece = require 'tetris.components.piece'
|
||||
|
||||
local KonohaRandomizer = require 'tetris.randomizers.bag_konoha'
|
||||
|
||||
local KonohaGame = GameMode:extend()
|
||||
|
||||
KonohaGame.name = "All Clear A4"
|
||||
KonohaGame.hash = "AllClearA4"
|
||||
KonohaGame.tagline = "Get as many bravos as you can under the time limit!"
|
||||
|
||||
function KonohaGame:new()
|
||||
KonohaGame.super:new()
|
||||
|
||||
self.randomizer = KonohaRandomizer()
|
||||
self.bravos = 0
|
||||
self.last_bonus_amount = 0
|
||||
self.last_bonus_display_time = 0
|
||||
self.time_limit = 10800
|
||||
self.big_mode = true
|
||||
|
||||
self.enable_hold = true
|
||||
self.next_queue_length = 3
|
||||
end
|
||||
|
||||
function KonohaGame:getARE()
|
||||
if self.level < 300 then return 30
|
||||
elseif self.level < 400 then return 25
|
||||
elseif self.level < 500 then return 20
|
||||
elseif self.level < 600 then return 17
|
||||
elseif self.level < 800 then return 15
|
||||
elseif self.level < 900 then return 13
|
||||
elseif self.level < 1000 then return 10
|
||||
elseif self.level < 1300 then return 8
|
||||
else return 6 end
|
||||
end
|
||||
|
||||
function KonohaGame:getLineARE()
|
||||
return self:getARE()
|
||||
end
|
||||
|
||||
function KonohaGame:getDasLimit()
|
||||
if self.level < 500 then return 10
|
||||
elseif self.level < 800 then return 9
|
||||
elseif self.level < 1000 then return 8
|
||||
else return 7 end
|
||||
end
|
||||
|
||||
function KonohaGame:getLineClearDelay()
|
||||
if self.level < 200 then return 14
|
||||
elseif self.level < 500 then return 9
|
||||
elseif self.level < 800 then return 8
|
||||
elseif self.level < 1000 then return 7
|
||||
else return 6 end
|
||||
end
|
||||
|
||||
function KonohaGame:getLockDelay()
|
||||
if self.level < 500 then return 30
|
||||
elseif self.level < 600 then return 25
|
||||
elseif self.level < 700 then return 23
|
||||
elseif self.level < 800 then return 20
|
||||
elseif self.level < 900 then return 17
|
||||
elseif self.level < 1000 then return 15
|
||||
elseif self.level < 1200 then return 13
|
||||
elseif self.level < 1300 then return 10
|
||||
else return 8 end
|
||||
end
|
||||
|
||||
function KonohaGame:getGravity()
|
||||
if (self.level < 30) then return 4/256
|
||||
elseif (self.level < 35) then return 8/256
|
||||
elseif (self.level < 40) then return 12/256
|
||||
elseif (self.level < 50) then return 16/256
|
||||
elseif (self.level < 60) then return 32/256
|
||||
elseif (self.level < 70) then return 48/256
|
||||
elseif (self.level < 80) then return 64/256
|
||||
elseif (self.level < 90) then return 128/256
|
||||
elseif (self.level < 100) then return 192/256
|
||||
elseif (self.level < 120) then return 1
|
||||
elseif (self.level < 140) then return 2
|
||||
elseif (self.level < 160) then return 3
|
||||
elseif (self.level < 170) then return 4
|
||||
elseif (self.level < 200) then return 5
|
||||
else return 20 end
|
||||
end
|
||||
|
||||
function KonohaGame:getSection()
|
||||
return math.floor(level / 100) + 1
|
||||
end
|
||||
|
||||
function KonohaGame:getSectionEndLevel()
|
||||
return math.floor(self.level / 100 + 1) * 100
|
||||
end
|
||||
|
||||
function KonohaGame:advanceOneFrame()
|
||||
if self.ready_frames == 0 then
|
||||
self.time_limit = self.time_limit - 1
|
||||
self.frames = self.frames + 1
|
||||
end
|
||||
if self.time_limit <= 0 then
|
||||
self.game_over = true
|
||||
end
|
||||
self.last_bonus_display_time = self.last_bonus_display_time - 1
|
||||
end
|
||||
|
||||
function KonohaGame:onPieceEnter()
|
||||
if (self.level % 100 ~= 99) and self.frames ~= 0 then
|
||||
self.level = self.level + 1
|
||||
end
|
||||
end
|
||||
|
||||
function KonohaGame:drawGrid(ruleset)
|
||||
self.grid:draw()
|
||||
if self.piece ~= nil and self.level < 100 then
|
||||
self:drawGhostPiece(ruleset)
|
||||
end
|
||||
end
|
||||
|
||||
local cleared_row_levels = {2, 4, 6, 12}
|
||||
local bravo_bonus = {300, 480, 660, 900}
|
||||
local non_bravo_bonus = {0, 0, 20, 40}
|
||||
local bravo_ot_bonus = {0, 60, 120, 180}
|
||||
|
||||
function KonohaGame:onLineClear(cleared_row_count)
|
||||
local oldtime = self.time_limit
|
||||
|
||||
self.level = self.level + cleared_row_levels[cleared_row_count / 2]
|
||||
self.bravo = true
|
||||
for i = 0, 23 - cleared_row_count do
|
||||
for j = 0, 9 do
|
||||
if self.grid:isOccupied(j, i) then
|
||||
self.bravo = false
|
||||
end
|
||||
end
|
||||
end
|
||||
if self.bravo then
|
||||
self.bravos = self.bravos + 1
|
||||
if self.level < 1000 then self.time_limit = self.time_limit + bravo_bonus[cleared_row_count / 2]
|
||||
else self.time_limit = self.time_limit + bravo_ot_bonus[cleared_row_count / 2]
|
||||
end
|
||||
if self.bravos == 11 then self.randomizer.allowrepeat = true end
|
||||
elseif self.level < 1000 then
|
||||
self.time_limit = self.time_limit + non_bravo_bonus[cleared_row_count / 2]
|
||||
end
|
||||
|
||||
local bonus = self.time_limit - oldtime
|
||||
if bonus > 0 then
|
||||
self.last_bonus_amount = bonus
|
||||
self.last_bonus_display_time = 120
|
||||
end
|
||||
end
|
||||
|
||||
function KonohaGame:getBackground()
|
||||
return math.min(math.floor(self.level / 100), 9)
|
||||
end
|
||||
|
||||
function KonohaGame:drawScoringInfo()
|
||||
love.graphics.setColor(1, 1, 1, 1)
|
||||
|
||||
love.graphics.setFont(font_3x5_2)
|
||||
love.graphics.print(
|
||||
self.das.direction .. " " ..
|
||||
self.das.frames .. " " ..
|
||||
strTrueValues(self.prev_inputs)
|
||||
)
|
||||
love.graphics.printf("NEXT", 64, 40, 40, "left")
|
||||
love.graphics.printf("TIME LIMIT", 240, 120, 120, "left")
|
||||
love.graphics.printf("BRAVOS", 240, 200, 50, "left")
|
||||
love.graphics.printf("LEVEL", 240, 320, 40, "left")
|
||||
|
||||
love.graphics.setFont(font_3x5_3)
|
||||
if not self.game_over and self.time_limit < frameTime(0,10) and self.time_limit % 4 < 2 then
|
||||
love.graphics.setColor(1, 0.3, 0.3, 1)
|
||||
end
|
||||
love.graphics.printf(formatTime(self.time_limit), 240, 140, 120, "right")
|
||||
if self.last_bonus_display_time > 0 then
|
||||
love.graphics.printf("+"..formatTime(self.last_bonus_amount), 240, 160, 120, "right")
|
||||
end
|
||||
love.graphics.setColor(1, 1, 1, 1)
|
||||
love.graphics.printf(self.bravos, 240, 220, 90, "left")
|
||||
love.graphics.printf(self.level, 240, 340, 50, "right")
|
||||
love.graphics.printf(self:getSectionEndLevel(), 240, 370, 50, "right")
|
||||
|
||||
love.graphics.setFont(font_8x11)
|
||||
love.graphics.printf(formatTime(self.frames), 64, 420, 160, "center")
|
||||
end
|
||||
|
||||
function KonohaGame:getHighscoreData()
|
||||
return {
|
||||
bravos = self.bravos,
|
||||
level = self.level,
|
||||
frames = self.frames,
|
||||
}
|
||||
end
|
||||
|
||||
return KonohaGame
|
||||