Compare commits
522 Commits
v0.1.6.2
...
v0.3-beta7
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
ffd808e6a0 | ||
|
|
dd96db170e | ||
|
|
7fa547c307 | ||
| b2d0838f90 | |||
|
|
42375cb2b8 | ||
|
|
fe162ed215 | ||
|
|
dda116f00f | ||
|
|
2d3aeeb47d | ||
|
|
784c768c57 | ||
|
|
c18e7ed244 | ||
|
|
9df6bb9989 | ||
|
|
f5873c97bc | ||
|
|
fabdad056e | ||
|
|
0e82a8758c | ||
|
|
e78df19112 | ||
|
|
49775b9578 | ||
|
|
6a3c6ecac0 | ||
|
|
90cf2ebef5 | ||
|
|
799a905a9c | ||
|
|
985f73c39d | ||
|
|
b5db5bbdc3 | ||
|
|
438acde2e2 | ||
|
|
0e1f40ad30 | ||
|
|
6cf6568a57 | ||
|
|
dafc113038 | ||
|
|
923f3d3696 | ||
|
|
db4132bf31 | ||
|
|
c58018dd51 | ||
|
|
c7d0034f9b | ||
|
|
ed5ea72e66 | ||
|
|
dc3ad825dc | ||
|
|
40cba83003 | ||
| a1b3f73787 | |||
|
|
4243d6b2ba | ||
| 33b3ad2889 | |||
|
|
adab1df480 | ||
|
|
711fa830a3 | ||
|
|
c434a3406b | ||
|
|
769b5043e3 | ||
|
|
713c62d807 | ||
|
|
c3f6e34518 | ||
|
|
4d0f6ab9fc | ||
|
|
594aa2620f | ||
|
|
199b535f70 | ||
|
|
9fbfbd5cda | ||
|
|
c5c4c4d95c | ||
|
|
53c51c2062 | ||
|
|
e4eb9972e6 | ||
|
|
7dbfe23059 | ||
|
|
61d5410f22 | ||
|
|
2cb0416548 | ||
| 83f3e297ce | |||
|
|
8fb01dc9a8 | ||
|
|
61de3c6dbf | ||
|
|
3c718c38e4 | ||
|
|
d18c3e298d | ||
|
|
33934bfb53 | ||
|
|
3e2d107687 | ||
|
|
312b95728d | ||
|
|
5013443302 | ||
|
|
a8ac8f5966 | ||
|
|
a5032386e6 | ||
|
|
264255290d | ||
|
|
a5839bede2 | ||
|
|
4ebf24316a | ||
|
|
f2acab4496 | ||
| 929069c1b6 | |||
|
|
3f2b38f7b3 | ||
|
|
56fb5aebea | ||
|
|
6c201596b0 | ||
|
|
50466c5902 | ||
| ae1231c47a | |||
|
|
366ac1d552 | ||
| 302353f716 | |||
|
|
7f550b629f | ||
|
|
6b2252e6d9 | ||
|
|
e1741440f2 | ||
|
|
c56f290921 | ||
|
|
86e975f929 | ||
|
|
9f8e9a9778 | ||
|
|
62f9475fa9 | ||
|
|
99d3732d00 | ||
|
|
f5121b62e5 | ||
|
|
cbdbfa6633 | ||
|
|
1b1abc9792 | ||
|
|
894e99e677 | ||
|
|
d4b619da89 | ||
|
|
3766149cb7 | ||
|
|
449ca16bc4 | ||
|
|
71d76e8a6b | ||
|
|
3bdc6e1b2d | ||
|
|
d9b6c85704 | ||
|
|
5ce0686e1a | ||
|
|
3cf496ba98 | ||
|
|
5ddc6ec561 | ||
|
|
b91ffc913b | ||
|
|
ab445ff699 | ||
|
|
7b7a255bf8 | ||
|
|
57721ed35d | ||
|
|
8383d3f445 | ||
|
|
116284f31c | ||
|
|
2189e3a7b8 | ||
|
|
b1d325b714 | ||
|
|
4ab5e3747a | ||
|
|
9761ead48f | ||
|
|
8dedc8a70e | ||
|
|
21769f21c8 | ||
|
|
5cf26b4500 | ||
|
|
4b4a968632 | ||
|
|
e0d98de50d | ||
|
|
4992ea733c | ||
|
|
30ca434027 | ||
|
|
502a50d004 | ||
|
|
36f5287a39 | ||
|
|
a9bbe4a08d | ||
|
|
ee431f5fd8 | ||
|
|
36f2672e06 | ||
|
|
6ecea7edb1 | ||
|
|
dc764b9177 | ||
|
|
5a1494cb5a | ||
|
|
684c4f5b78 | ||
|
|
b568c0fe69 | ||
|
|
2ea75cdfaf | ||
|
|
1f0b43f1b7 | ||
|
|
40bdc5ed99 | ||
|
|
33f2a96ae8 | ||
|
|
846013ce7a | ||
|
|
37c85adc75 | ||
|
|
e7bb44deb4 | ||
|
|
57518dc299 | ||
|
|
0453a3db97 | ||
|
|
b85de17e51 | ||
|
|
163b8f6cc5 | ||
|
|
7250bee619 | ||
|
|
83de216408 | ||
|
|
ba235c8a41 | ||
|
|
ca18d090c9 | ||
|
|
a3a27d2566 | ||
|
|
b15cd9802f | ||
|
|
4c4a818c5c | ||
|
|
716de2814b | ||
|
|
bf19f49323 | ||
|
|
1234e78354 | ||
|
|
9129503d54 | ||
|
|
eae58f11e9 | ||
|
|
3cf5daeb2e | ||
|
|
1dfe68ccff | ||
|
|
8a459b68ba | ||
|
|
cb2b693bcb | ||
|
|
ef6d156d38 | ||
|
|
83e498534c | ||
|
|
8f19c73e2a | ||
|
|
e36b855ff7 | ||
|
|
23b58951cb | ||
|
|
1d73916b7c | ||
|
|
3947e9f02f | ||
|
|
99b15803ee | ||
|
|
d350b25726 | ||
|
|
44e4d00172 | ||
|
|
31e2529265 | ||
|
|
ea7c75f0b3 | ||
|
|
714c6b5e99 | ||
|
|
6a5d5a9c88 | ||
|
|
03491ba151 | ||
|
|
6e22e3d15b | ||
|
|
66ab5992ad | ||
|
|
2c07c2a58c | ||
|
|
a4d3f3bffc | ||
|
|
9ac60cbb5e | ||
|
|
cdd846c3e6 | ||
|
|
33d260b753 | ||
|
|
1644fcdf8e | ||
|
|
f3c1cf6e1f | ||
|
|
06a8a2ebf7 | ||
|
|
15354ce004 | ||
|
|
af02cd3467 | ||
|
|
acb05918c1 | ||
|
|
b644c8e457 | ||
|
|
288961e12a | ||
|
|
a047e51681 | ||
|
|
77f24f5ee5 | ||
|
|
32c2274bef | ||
|
|
4920e5de1c | ||
|
|
8418fc8ab7 | ||
|
|
711a5120f1 | ||
|
|
e7c3c9446a | ||
|
|
3ac39acd7a | ||
|
|
d0505251b3 | ||
|
|
bb0fe2ac20 | ||
|
|
986ebac47f | ||
|
|
9799147f96 | ||
|
|
1dda12e4be | ||
|
|
38947e00c0 | ||
|
|
035f6dd7b4 | ||
|
|
aa3eadc93d | ||
|
|
cb6962825f | ||
|
|
b5e7ce5be6 | ||
|
|
1ccd6a09d3 | ||
|
|
5a074f77cf | ||
|
|
81677221f1 | ||
|
|
a998be6f7b | ||
|
|
9c1c8eea21 | ||
|
|
f022c6c4b7 | ||
|
|
38f3d23b95 | ||
|
|
816d27db39 | ||
|
|
ce08ffd3da | ||
|
|
f0e84a8874 | ||
|
|
5e02471fb4 | ||
|
|
fa2fe77081 | ||
|
|
682c4a485a | ||
|
|
68760105cc | ||
|
|
e19da98ea1 | ||
|
|
e8904b92ed | ||
|
|
4f574e7716 | ||
|
|
f1528e8d71 | ||
|
|
79a25c3954 | ||
|
|
0f3883e18d | ||
|
|
1acd0ec65a | ||
|
|
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 | ||
|
|
d0f1d869a8 | ||
|
|
29ee000998 | ||
|
|
995fd7fee9 | ||
|
|
67abf35a28 | ||
|
|
629beb7240 |
3
.gitignore
vendored
@@ -1,4 +1,7 @@
|
|||||||
*.sav
|
*.sav
|
||||||
*.love
|
*.love
|
||||||
|
*.zip
|
||||||
dist/*.zip
|
dist/*.zip
|
||||||
dist/**/cambridge.exe
|
dist/**/cambridge.exe
|
||||||
|
dist/**/libs
|
||||||
|
dist/**/*.md
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
Copyright (c) 2018-2019 Joe Zeng
|
Copyright (c) 2018-2021 Joe Zeng, Ishaan Bhardwaj
|
||||||
|
|
||||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||||
of this software and associated documentation files (the "Software"), to deal
|
of this software and associated documentation files (the "Software"), to deal
|
||||||
|
|||||||
89
README.md
@@ -1,56 +1,72 @@
|
|||||||
|

|
||||||
|
|
||||||
Cambridge
|
Cambridge
|
||||||
=========
|
=========
|
||||||
|
|
||||||
Welcome to Cambridge, the next open-source falling-block game engine!
|
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)!
|
The project is written and maintained exclusively by [Milla](https://github.com/MillaBasset), [joezeng](https://github.com/joezeng) and [Oshisaure](https://github.com/oshisaure)!
|
||||||
|
|
||||||
Credits
|
The Discord server has been reopened! https://discord.gg/AADZUmgsph
|
||||||
-------
|
|
||||||
|
|
||||||
- [Lilla Oshisaure](https://www.youtube.com/user/LeSpyroshisaure) for their amazing contributions to my life in general!
|
The game also has a website now with more detail than seen on this README: https://t-sp.in/cambridge
|
||||||
- [The Tetra Legends Discord](http://discord.com/invite/7hMx5r2) for supporting me and playtesting!
|
|
||||||
- [joezeng](https://github.com/joezeng) for the original project.
|
|
||||||
|
|
||||||
Installation instructions
|
Playing the game
|
||||||
-------------------------
|
----------------
|
||||||
|
|
||||||
Pre-built releases are available on the releases page.
|
|
||||||
|
|
||||||
### Windows
|
### 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
|
#### Stable release
|
||||||
|
|
||||||
For the time being, the file `cambridge.love` only works on the command line. Install `love` with [Homebrew](https://brew.sh), and run:
|
To get the stable release, simply download either `cambridge-win32.zip` (32-bit) or `cambridge-windows.zip` (64-bit) in the [latest release](https://github.com/MillaBasset/cambridge/releases/latest).
|
||||||
|
|
||||||
$ love cambridge.love
|
All assets needed are bundled with the executable.
|
||||||
|
|
||||||
### Linux
|
#### Bleeding edge
|
||||||
|
|
||||||
Same as macOS, except install `love` with your favourite package manager.
|
If you want the bleeding edge version, download [this](https://github.com/MillaBasset/cambridge/archive/master.zip).
|
||||||
|
|
||||||
|
Extract the ZIP, open a Command Prompt at the folder you extracted Cambridge to, then run this command:
|
||||||
|
|
||||||
Running from source
|
dist\windows\love.exe .
|
||||||
-------------------
|
|
||||||
|
|
||||||
If you want the bleeding-edge release, you can also clone the code straight from this repository.
|
Alternatively, if you're on a 32-bit system, run this instead:
|
||||||
|
|
||||||
|
dist\win32\love.exe .
|
||||||
|
|
||||||
|
32-bit systems do not support rich presence integration.
|
||||||
|
|
||||||
|
Then, check the mod pack section at the bottom of this page.
|
||||||
|
|
||||||
### macOS, Linux
|
### 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!**
|
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:
|
Clone the repository in git:
|
||||||
|
|
||||||
git clone https://github.com/SashLilac/cambridge
|
git clone https://github.com/MillaBasset/cambridge
|
||||||
|
|
||||||
|
Alternatively, download the source code ZIP in the latest release.
|
||||||
|
|
||||||
Then, navigate to the root directory that you just cloned, and type:
|
Then, navigate to the root directory that you just cloned, and type:
|
||||||
|
|
||||||
love .
|
love .
|
||||||
|
|
||||||
It should run automatically!
|
It should run automatically!
|
||||||
|
|
||||||
|
## Installing modpacks
|
||||||
|
|
||||||
|
For instructions on how to install modpacks, go to [this](https://github.com/MillaBasset/cambridge-modpack) mod pack to get a taste of the mod potential.
|
||||||
|
|
||||||
License
|
License
|
||||||
-------
|
-------
|
||||||
@@ -62,3 +78,34 @@ community, as well as borrowed from other places, either with licensing
|
|||||||
or as placeholders until suitable material can be found that is properly
|
or as placeholders until suitable material can be found that is properly
|
||||||
licensed. Their original sources, and copyright notices if applicable, are
|
licensed. Their original sources, and copyright notices if applicable, are
|
||||||
listed in the file SOURCES.
|
listed in the file SOURCES.
|
||||||
|
|
||||||
|
Credits
|
||||||
|
-------
|
||||||
|
|
||||||
|
- [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!
|
||||||
|
- [The Absolute Plus](https://discord.gg/6Gf2awJ) for being another source of motivation!
|
||||||
|
|
||||||
|
More special thanks can be found in-game, under the "Credits" menu.
|
||||||
|
|
||||||
|
Other Notable Games
|
||||||
|
-------------------
|
||||||
|
|
||||||
|
- [TGMsim](https://github.com/2Tie/TGMsim) by 2Tie
|
||||||
|
- [Multimino](https://gamejolt.com/games/multimino/556683) by Axel Fox
|
||||||
|
- [Tetra Legends](https://tetralegends.app) by Dr Ocelot
|
||||||
|
- [ZTrix](https://discord.gg/MGhqCBDGNH) by Electra
|
||||||
|
- [Shiromino](https://github.com/shiromino/shiromino) by Felicity/nightmareci/kdex
|
||||||
|
- [Cursed Blocks](https://github.com/Manabender/Cursed-Blocks) by Manabender
|
||||||
|
- [Picoris 2](https://www.lexaloffle.com/bbs/?tid=41733) by MarkGamed
|
||||||
|
- [Tetra Online](https://github.com/Juan-Cartes/Tetra-Offline) by Mine
|
||||||
|
- [Techmino](https://discord.gg/6Yuww44tq8) by MrZ
|
||||||
|
- [Example Block Game](https://github.com/oshisaure/example-block-game) by Oshisaure
|
||||||
|
- [TETR.IO](https://tetr.io) by osk
|
||||||
|
- [Master of Blocks](https://discord.gg/72FZ49mjWh) by Phoenix Flare
|
||||||
|
- [Spirit Drop](https://rayblastgames.com/spiritdrop.php) by RayRay26
|
||||||
|
- [Puzzle Trial](https://kagamine-rin.itch.io/puzzle-trial) by Rin
|
||||||
|
- [stackfuse](https://github.com/sinefuse/stackfuse) by sinefuse
|
||||||
|
|
||||||
|

|
||||||
|
|||||||
116
SOURCES.md
@@ -8,7 +8,7 @@ Some of the assets are used without proper licenses. We aim to have fully licens
|
|||||||
Backgrounds
|
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/
|
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
|
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.
|
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
|
Music
|
||||||
-----
|
-----
|
||||||
|
|
||||||
1. TGM3 credit roll music.
|
1. Second Reality opening scene music (1993).
|
||||||
2. The FitnessGram™ Pacer Test.
|
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.
|
All background music is (currently) only unofficially included. In later releases they may be replaced with specifically licensed music as applicable.
|
||||||
@@ -106,3 +114,107 @@ 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 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.
|
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.
|
||||||
|
|
||||||
|
discord-rpc (https://github.com/discord/discord-rpc)
|
||||||
|
--------------------
|
||||||
|
|
||||||
|
Copyright 2017 Discord, Inc.
|
||||||
|
|
||||||
|
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.
|
||||||
|
|
||||||
|
lua-discordRPC (https://github.com/pfirsich/lua-discordRPC)
|
||||||
|
--------------------
|
||||||
|
|
||||||
|
MIT License
|
||||||
|
|
||||||
|
Copyright (c) 2018 Joel Schumacher
|
||||||
|
|
||||||
|
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.
|
||||||
17
clean.bat
Normal file
@@ -0,0 +1,17 @@
|
|||||||
|
@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\other\cambridge.love
|
||||||
|
@del dist\other\SOURCES.md
|
||||||
|
@del dist\other\LICENSE.md
|
||||||
|
@rmdir /Q /S dist\other\libs
|
||||||
|
@rmdir /Q /S dist\other
|
||||||
|
@del dist\cambridge-windows.zip
|
||||||
|
@del dist\cambridge-win32.zip
|
||||||
|
@del dist\cambridge-other.zip
|
||||||
1
conf.lua
@@ -6,5 +6,6 @@ function love.conf(t)
|
|||||||
t.window.title = "Cambridge"
|
t.window.title = "Cambridge"
|
||||||
t.window.width = 640
|
t.window.width = 640
|
||||||
t.window.height = 480
|
t.window.height = 480
|
||||||
|
t.window.icon = "res/img/cambridge_icon.png"
|
||||||
t.window.vsync = false
|
t.window.vsync = false
|
||||||
end
|
end
|
||||||
|
|||||||
@@ -3,19 +3,11 @@ Game modes
|
|||||||
|
|
||||||
There are several classes of game modes. The modes that originate from other games are organized by suffix:
|
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.
|
|
||||||
* The "A" series stand for "Arika" games, or games in the Tetris the Grand Master series.
|
* 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).
|
* A1 - Tetris The Grand Master (the original from 1998).
|
||||||
* A2 - Tetris The Absolute The Grand Master 2 PLUS.
|
* A2 - Tetris The Absolute The Grand Master 2 PLUS.
|
||||||
* A3 - Tetris The Grand Master 3 Terror-Instinct.
|
* A3 - Tetris The Grand Master 3 Terror-Instinct.
|
||||||
* AX - Tetris The Grand Master ACE (X for Xbox).
|
* 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)
|
|
||||||
* N stands for Nullpomino, only used for Phantom Mania N.
|
|
||||||
|
|
||||||
MARATHON
|
MARATHON
|
||||||
--------
|
--------
|
||||||
@@ -28,8 +20,6 @@ From other games:
|
|||||||
* **MARATHON A1**: Tetris the Grand Master 1.
|
* **MARATHON A1**: Tetris the Grand Master 1.
|
||||||
* **MARATHON A2**: Tetris the Grand Master 2 (TAP Master).
|
* **MARATHON A2**: Tetris the Grand Master 2 (TAP Master).
|
||||||
* **MARATHON A3**: Tetris the Grand Master 3 (no exams).
|
* **MARATHON A3**: Tetris the Grand Master 3 (no exams).
|
||||||
* **MARATHON AX4**: Another mode from TGM Ace.
|
|
||||||
* **MARATHON C89**: Nintendo NES Tetris. Can you transition and make it to the killscreen?
|
|
||||||
|
|
||||||
|
|
||||||
SURVIVAL
|
SURVIVAL
|
||||||
@@ -43,14 +33,7 @@ From other games:
|
|||||||
* **SURVIVAL A1**: 20G mode from Tetris the Grand Master.
|
* **SURVIVAL A1**: 20G mode from Tetris the Grand Master.
|
||||||
* **SURVIVAL A2**: T.A. Death.
|
* **SURVIVAL A2**: T.A. Death.
|
||||||
* **SURVIVAL A3**: Ti Shirase.
|
* **SURVIVAL A3**: Ti Shirase.
|
||||||
|
* **SURVIVAL AX**: Another mode from TGM Ace.
|
||||||
|
|
||||||
RACE
|
|
||||||
----
|
|
||||||
|
|
||||||
Modes with no levels, just a single timed goal.
|
|
||||||
|
|
||||||
* **Race 40**: How fast can you clear 40 lines? No limits, no holds barred.
|
|
||||||
|
|
||||||
|
|
||||||
PHANTOM MANIA
|
PHANTOM MANIA
|
||||||
@@ -69,8 +52,4 @@ OTHER MODES
|
|||||||
|
|
||||||
* **Strategy**: How well can you plan ahead your movements? Can you handle only having a short time to place each piece?
|
* **Strategy**: How well can you plan ahead your movements? Can you handle only having a short time to place each piece?
|
||||||
|
|
||||||
* **TetrisGram™ Pacer Test**: is a multi-stage piece-placing ability test that progressively gets more difficult as it continues.
|
* **Big A2**: Marathon A2 but all the pieces are BIG!
|
||||||
|
|
||||||
* **Interval Training**: 30 seconds per section. 20G. 15 frames of lock delay. How long can you last?
|
|
||||||
|
|
||||||
* **Demon Mode**: An original mode from Oshisaure! Can you push through the ever faster levels and not get denied?
|
|
||||||
61
funcs.lua
@@ -1,11 +1,21 @@
|
|||||||
function copy(t)
|
function copy(t)
|
||||||
-- returns deep copy of t (as opposed to the shallow copy you get from var = t)
|
-- returns top-layer shallow copy of t
|
||||||
if type(t) ~= "table" then return t end
|
if type(t) ~= "table" then return t end
|
||||||
local meta = getmetatable(t)
|
local target = {}
|
||||||
local target = {}
|
for k, v in next, t do target[k] = v end
|
||||||
for k, v in pairs(t) do target[k] = v end
|
setmetatable(target, getmetatable(t))
|
||||||
setmetatable(target, meta)
|
return target
|
||||||
return target
|
end
|
||||||
|
|
||||||
|
function deepcopy(t)
|
||||||
|
-- returns infinite-layer deep copy of t
|
||||||
|
if type(t) ~= "table" then return t end
|
||||||
|
local target = {}
|
||||||
|
for k, v in next, t do
|
||||||
|
target[deepcopy(k)] = deepcopy(v)
|
||||||
|
end
|
||||||
|
setmetatable(target, deepcopy(getmetatable(t)))
|
||||||
|
return target
|
||||||
end
|
end
|
||||||
|
|
||||||
function strTrueValues(tbl)
|
function strTrueValues(tbl)
|
||||||
@@ -56,9 +66,40 @@ end
|
|||||||
|
|
||||||
function formatBigNum(number)
|
function formatBigNum(number)
|
||||||
-- returns a string representing a number with commas as thousands separator (e.g. 12,345,678)
|
-- returns a string representing a number with commas as thousands separator (e.g. 12,345,678)
|
||||||
local s = string.format("%d", number)
|
local s
|
||||||
local pos = string.len(s) % 3
|
if type(number) == "number" then
|
||||||
if pos == 0 then pos = 3 end
|
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)
|
return string.sub(s, 1, pos)
|
||||||
.. string.gsub(string.sub(s, pos+1), "(...)", ",%1")
|
.. 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
|
||||||
|
|
||||||
|
function clamp(x, min, max)
|
||||||
|
if max < min then
|
||||||
|
min, max = max, min
|
||||||
|
end
|
||||||
|
return x < min and min or (x > max and max or x)
|
||||||
|
end
|
||||||
|
|||||||
566
libs/bigint/bigint.lua
Normal file
@@ -0,0 +1,566 @@
|
|||||||
|
#!/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 = {}
|
||||||
|
setmetatable(bigint, {__call = function(_, arg) return bigint.new(arg) end})
|
||||||
|
|
||||||
|
local mt = {
|
||||||
|
__add = function(lhs, rhs)
|
||||||
|
return bigint.add(lhs, rhs)
|
||||||
|
end,
|
||||||
|
__unm = function(arg)
|
||||||
|
return bigint.negate(arg)
|
||||||
|
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,
|
||||||
|
__tostring = function(arg)
|
||||||
|
return bigint.unserialize(arg, "s")
|
||||||
|
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
|
||||||
|
}
|
||||||
|
|
||||||
|
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, mt)
|
||||||
|
|
||||||
|
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 bigint.strip(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(getmetatable(big) == mt, "at least one arg is not a bigint")
|
||||||
|
assert(#big.digits > 0, "bigint is empty")
|
||||||
|
assert(big.sign == "+" or big.sign == "-", "bigint is unsigned")
|
||||||
|
for _, digit in pairs(big.digits) do
|
||||||
|
assert(type(digit) == "number", "at least one digit is invalid")
|
||||||
|
assert(digit <= 9 and digit >= 0, digit .. " is not between 0 and 9")
|
||||||
|
assert(math.floor(digit) == digit, digit .. " is not an integer")
|
||||||
|
end
|
||||||
|
end
|
||||||
|
return true
|
||||||
|
end
|
||||||
|
|
||||||
|
-- Strip leading zeroes from a big, but don't remove the last zero
|
||||||
|
function bigint.strip(big)
|
||||||
|
while (#big.digits > 1) and (big.digits[1] == 0) do
|
||||||
|
table.remove(big.digits, 1)
|
||||||
|
end
|
||||||
|
return big
|
||||||
|
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
|
||||||
|
|
||||||
|
-- Return a new big with the same digits but the opposite sign (negation)
|
||||||
|
function bigint.negate(big)
|
||||||
|
bigint.check(big)
|
||||||
|
local result = big:clone()
|
||||||
|
if (result.sign == "+") then
|
||||||
|
result.sign = "-"
|
||||||
|
else
|
||||||
|
result.sign = "+"
|
||||||
|
end
|
||||||
|
return result
|
||||||
|
end
|
||||||
|
|
||||||
|
-- Return the number of digits in the big
|
||||||
|
function bigint.digits(big)
|
||||||
|
bigint.check(big)
|
||||||
|
return #big.digits
|
||||||
|
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 = math.min(#big.digits, 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 .. math.floor(big.digits[1])
|
||||||
|
if (precision > 1) then
|
||||||
|
num = num .. "."
|
||||||
|
for i = 1, (precision - 1) do
|
||||||
|
num = num .. math.floor(big.digits[i + 1])
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
if ((output_type == "human-readable")
|
||||||
|
or (output_type == "human")
|
||||||
|
or (output_type == "h"))
|
||||||
|
and (#big.digits >= 3 and #big.digits <= 10002) 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
|
||||||
|
----------------------------------------------------------------------------
|
||||||
|
|
||||||
|
|
||||||
|
return bigint.strip(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:clone()
|
||||||
|
else
|
||||||
|
local result = bigint.new(1)
|
||||||
|
local base = big:clone()
|
||||||
|
|
||||||
|
while (true) do
|
||||||
|
if (bigint.compare(
|
||||||
|
bigint.modulus(exp, bigint.new(2)), bigint.new(1), "=="
|
||||||
|
)) then
|
||||||
|
result = bigint.multiply(result, base)
|
||||||
|
end
|
||||||
|
if (bigint.compare(exp, bigint.new(1), "==")) then
|
||||||
|
break
|
||||||
|
else
|
||||||
|
exp = bigint.divide(exp, bigint.new(2))
|
||||||
|
base = bigint.multiply(base, base)
|
||||||
|
end
|
||||||
|
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), big1:clone()
|
||||||
|
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
|
||||||
|
|
||||||
|
local neg_zero = bigint.new(0)
|
||||||
|
neg_zero.sign = "-"
|
||||||
|
|
||||||
|
for i = 1, #big1.digits do
|
||||||
|
-- Fixes a negative zero bug
|
||||||
|
if (#dividend.digits ~= 0) and (bigint.compare(dividend, neg_zero, "==")) then
|
||||||
|
dividend = bigint.new()
|
||||||
|
end
|
||||||
|
|
||||||
|
table.insert(dividend.digits, big1.digits[i])
|
||||||
|
|
||||||
|
local factor = bigint.new(0)
|
||||||
|
while bigint.compare(dividend, big2, ">=") do
|
||||||
|
dividend = bigint.subtract(dividend, big2)
|
||||||
|
factor = bigint.add(factor, bigint.new(1))
|
||||||
|
end
|
||||||
|
|
||||||
|
for i = 0, #factor.digits - 1 do
|
||||||
|
result.digits[#result.digits + 1 - i] = factor.digits[i + 1]
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
return bigint.strip(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
335
libs/discordRPC.lua
Normal file
@@ -0,0 +1,335 @@
|
|||||||
|
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
|
||||||
|
-- I would strongly advise never touching this. It was not trivial to get correct. -nightmareci
|
||||||
|
|
||||||
|
ffi.cdef[[
|
||||||
|
typedef uint32_t DWORD;
|
||||||
|
typedef char CHAR;
|
||||||
|
typedef CHAR *LPSTR;
|
||||||
|
typedef const CHAR *LPCSTR;
|
||||||
|
typedef wchar_t WCHAR;
|
||||||
|
typedef WCHAR *LPWSTR;
|
||||||
|
typedef LPWSTR PWSTR;
|
||||||
|
typedef const WCHAR *LPCWSTR;
|
||||||
|
|
||||||
|
static const DWORD CP_UTF8 = 65001;
|
||||||
|
int32_t MultiByteToWideChar(
|
||||||
|
DWORD CodePage,
|
||||||
|
DWORD dwFlags,
|
||||||
|
LPCSTR lpMultiByteStr,
|
||||||
|
int32_t cbMultiByte,
|
||||||
|
LPWSTR lpWideCharStr,
|
||||||
|
int32_t cchWideChar
|
||||||
|
);
|
||||||
|
|
||||||
|
int32_t WideCharToMultiByte(
|
||||||
|
DWORD CodePage,
|
||||||
|
DWORD dwFlags,
|
||||||
|
LPCWSTR lpWideCharStr,
|
||||||
|
int32_t cchWideChar,
|
||||||
|
LPSTR lpMultiByteStr,
|
||||||
|
int32_t cbMultiByte,
|
||||||
|
void* lpDefaultChar,
|
||||||
|
void* lpUsedDefaultChar
|
||||||
|
);
|
||||||
|
|
||||||
|
DWORD GetShortPathNameW(
|
||||||
|
LPCWSTR lpszLongPath,
|
||||||
|
LPWSTR lpszShortPath,
|
||||||
|
DWORD cchBuffer
|
||||||
|
);
|
||||||
|
]]
|
||||||
|
|
||||||
|
local originalWideSize = ffi.C.MultiByteToWideChar(ffi.C.CP_UTF8, 0, source, -1, nil, 0)
|
||||||
|
local originalWide = ffi.new('WCHAR[?]', originalWideSize)
|
||||||
|
ffi.C.MultiByteToWideChar(ffi.C.CP_UTF8, 0, source, -1, originalWide, originalWideSize)
|
||||||
|
|
||||||
|
local sourceSize = ffi.C.GetShortPathNameW(originalWide, nil, 0)
|
||||||
|
local sourceWide = ffi.new('WCHAR[?]', sourceSize)
|
||||||
|
ffi.C.GetShortPathNameW(originalWide, sourceWide, sourceSize)
|
||||||
|
|
||||||
|
local sourceChar = ffi.new('char[?]', sourceSize)
|
||||||
|
ffi.C.WideCharToMultiByte(ffi.C.CP_UTF8, 0, sourceWide, sourceSize, sourceChar, sourceSize, nil, nil)
|
||||||
|
|
||||||
|
source = ffi.string(sourceChar)
|
||||||
|
|
||||||
|
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
|
||||||
43
load/bgm.lua
@@ -7,33 +7,41 @@ bgm = {
|
|||||||
|
|
||||||
local current_bgm = nil
|
local current_bgm = nil
|
||||||
local bgm_locked = false
|
local bgm_locked = false
|
||||||
|
local unfocused = false
|
||||||
|
|
||||||
function switchBGM(sound, subsound)
|
function switchBGM(sound, subsound)
|
||||||
if bgm_locked then return end
|
|
||||||
if current_bgm ~= nil then
|
if current_bgm ~= nil then
|
||||||
current_bgm:stop()
|
current_bgm:stop()
|
||||||
end
|
end
|
||||||
if subsound ~= nil then
|
if bgm_locked or config.bgm_volume <= 0 then
|
||||||
current_bgm = bgm[sound][subsound]
|
current_bgm = nil
|
||||||
resetBGMFadeout()
|
|
||||||
elseif sound ~= nil then
|
elseif sound ~= nil then
|
||||||
current_bgm = bgm[sound]
|
if subsound ~= nil then
|
||||||
resetBGMFadeout()
|
current_bgm = bgm[sound][subsound]
|
||||||
|
else
|
||||||
|
current_bgm = bgm[sound]
|
||||||
|
end
|
||||||
else
|
else
|
||||||
current_bgm = nil
|
current_bgm = nil
|
||||||
end
|
end
|
||||||
|
if current_bgm ~= nil then
|
||||||
|
resetBGMFadeout()
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
function switchBGMLoop(sound, subsound)
|
function switchBGMLoop(sound, subsound)
|
||||||
if bgm_locked then return end
|
|
||||||
switchBGM(sound, subsound)
|
switchBGM(sound, subsound)
|
||||||
current_bgm:setLooping(true)
|
if current_bgm then current_bgm:setLooping(true) end
|
||||||
end
|
end
|
||||||
|
|
||||||
function lockBGM()
|
function lockBGM()
|
||||||
bgm_locked = true
|
bgm_locked = true
|
||||||
end
|
end
|
||||||
|
|
||||||
|
function unlockBGM()
|
||||||
|
bgm_locked = false
|
||||||
|
end
|
||||||
|
|
||||||
local fading_bgm = false
|
local fading_bgm = false
|
||||||
local fadeout_time = 0
|
local fadeout_time = 0
|
||||||
local total_fadeout_time = 0
|
local total_fadeout_time = 0
|
||||||
@@ -47,29 +55,36 @@ function fadeoutBGM(time)
|
|||||||
end
|
end
|
||||||
|
|
||||||
function resetBGMFadeout(time)
|
function resetBGMFadeout(time)
|
||||||
current_bgm:setVolume(1)
|
current_bgm:setVolume(config.bgm_volume)
|
||||||
fading_bgm = false
|
fading_bgm = false
|
||||||
current_bgm:play()
|
resumeBGM()
|
||||||
end
|
end
|
||||||
|
|
||||||
function processBGMFadeout(dt)
|
function processBGMFadeout(dt)
|
||||||
if fading_bgm then
|
if current_bgm and fading_bgm then
|
||||||
fadeout_time = fadeout_time - dt
|
fadeout_time = fadeout_time - dt
|
||||||
if fadeout_time < 0 then
|
if fadeout_time < 0 then
|
||||||
fadeout_time = 0
|
fadeout_time = 0
|
||||||
fading_bgm = false
|
fading_bgm = false
|
||||||
end
|
end
|
||||||
current_bgm:setVolume(fadeout_time / total_fadeout_time)
|
current_bgm:setVolume(fadeout_time * config.bgm_volume / total_fadeout_time)
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
function pauseBGM()
|
function pauseBGM(f)
|
||||||
|
if f then
|
||||||
|
unfocused = true
|
||||||
|
end
|
||||||
if current_bgm ~= nil then
|
if current_bgm ~= nil then
|
||||||
current_bgm:pause()
|
current_bgm:pause()
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
function resumeBGM()
|
function resumeBGM(f)
|
||||||
|
if f and scene.paused and unfocused then
|
||||||
|
unfocused = false
|
||||||
|
return
|
||||||
|
end
|
||||||
if current_bgm ~= nil then
|
if current_bgm ~= nil then
|
||||||
current_bgm:play()
|
current_bgm:play()
|
||||||
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,52 +1,108 @@
|
|||||||
backgrounds = {
|
backgrounds = {
|
||||||
[0] = love.graphics.newImage("res/backgrounds/0-quantum-foam.png"),
|
[0] = love.graphics.newImage("res/backgrounds/0.png"),
|
||||||
love.graphics.newImage("res/backgrounds/100-big-bang.png"),
|
love.graphics.newImage("res/backgrounds/100.png"),
|
||||||
love.graphics.newImage("res/backgrounds/200-spiral-galaxy.png"),
|
love.graphics.newImage("res/backgrounds/200.png"),
|
||||||
love.graphics.newImage("res/backgrounds/300-sun-and-dust.png"),
|
love.graphics.newImage("res/backgrounds/300.png"),
|
||||||
love.graphics.newImage("res/backgrounds/400-earth-and-moon.png"),
|
love.graphics.newImage("res/backgrounds/400.png"),
|
||||||
love.graphics.newImage("res/backgrounds/500-cambrian-explosion.png"),
|
love.graphics.newImage("res/backgrounds/500.png"),
|
||||||
love.graphics.newImage("res/backgrounds/600-dinosaurs.png"),
|
love.graphics.newImage("res/backgrounds/600.png"),
|
||||||
love.graphics.newImage("res/backgrounds/700-asteroid.png"),
|
love.graphics.newImage("res/backgrounds/700.png"),
|
||||||
love.graphics.newImage("res/backgrounds/800-human-fire.png"),
|
love.graphics.newImage("res/backgrounds/800.png"),
|
||||||
love.graphics.newImage("res/backgrounds/900-early-civilization.png"),
|
love.graphics.newImage("res/backgrounds/900.png"),
|
||||||
love.graphics.newImage("res/backgrounds/1000-vikings.png"),
|
love.graphics.newImage("res/backgrounds/1000.png"),
|
||||||
love.graphics.newImage("res/backgrounds/1100-crusades.png"),
|
love.graphics.newImage("res/backgrounds/1100.png"),
|
||||||
love.graphics.newImage("res/backgrounds/1200-genghis-khan.png"),
|
love.graphics.newImage("res/backgrounds/1200.png"),
|
||||||
love.graphics.newImage("res/backgrounds/1300-black-death.png"),
|
love.graphics.newImage("res/backgrounds/1300.png"),
|
||||||
love.graphics.newImage("res/backgrounds/1400-columbus-discovery.png"),
|
love.graphics.newImage("res/backgrounds/1400.png"),
|
||||||
love.graphics.newImage("res/backgrounds/1500-aztecas.png"),
|
love.graphics.newImage("res/backgrounds/1500.png"),
|
||||||
love.graphics.newImage("res/backgrounds/1600-telescope.png"),
|
love.graphics.newImage("res/backgrounds/1600.png"),
|
||||||
love.graphics.newImage("res/backgrounds/1700-american-revolution.png"),
|
love.graphics.newImage("res/backgrounds/1700.png"),
|
||||||
love.graphics.newImage("res/backgrounds/1800-railways.png"),
|
love.graphics.newImage("res/backgrounds/1800.png"),
|
||||||
love.graphics.newImage("res/backgrounds/1900-world-wide-web.png"),
|
love.graphics.newImage("res/backgrounds/1900.png"),
|
||||||
title = love.graphics.newImage("res/backgrounds/title_v0.1.png"),
|
title = love.graphics.newImage("res/backgrounds/title.png"),
|
||||||
input_config = love.graphics.newImage("res/backgrounds/options-gears.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"),
|
||||||
}
|
}
|
||||||
|
|
||||||
|
-- in order, the colors are:
|
||||||
|
-- red, orange, yellow, green, cyan, blue
|
||||||
|
-- magenta (or purple), white, black
|
||||||
|
-- the next three don't have colors tied to them
|
||||||
|
-- F is used for lock flash
|
||||||
|
-- A is a garbage block
|
||||||
|
-- X is an invisible "block"
|
||||||
|
-- don't use these for piece colors when making a ruleset
|
||||||
|
-- all the others are fine to use
|
||||||
blocks = {
|
blocks = {
|
||||||
["2tie"] = {
|
["2tie"] = {
|
||||||
I = love.graphics.newImage("res/img/s1.png"),
|
R = love.graphics.newImage("res/img/s1.png"),
|
||||||
J = love.graphics.newImage("res/img/s4.png"),
|
O = love.graphics.newImage("res/img/s3.png"),
|
||||||
L = love.graphics.newImage("res/img/s3.png"),
|
Y = love.graphics.newImage("res/img/s7.png"),
|
||||||
O = love.graphics.newImage("res/img/s7.png"),
|
G = love.graphics.newImage("res/img/s6.png"),
|
||||||
S = love.graphics.newImage("res/img/s5.png"),
|
C = love.graphics.newImage("res/img/s2.png"),
|
||||||
T = love.graphics.newImage("res/img/s2.png"),
|
B = love.graphics.newImage("res/img/s4.png"),
|
||||||
Z = love.graphics.newImage("res/img/s6.png"),
|
M = love.graphics.newImage("res/img/s5.png"),
|
||||||
|
W = love.graphics.newImage("res/img/s9.png"),
|
||||||
|
D = love.graphics.newImage("res/img/s8.png"),
|
||||||
F = love.graphics.newImage("res/img/s9.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"),
|
X = love.graphics.newImage("res/img/s9.png"),
|
||||||
},
|
},
|
||||||
["bone"] = {
|
["bone"] = {
|
||||||
I = love.graphics.newImage("res/img/bone.png"),
|
R = love.graphics.newImage("res/img/bone.png"),
|
||||||
J = love.graphics.newImage("res/img/bone.png"),
|
|
||||||
L = love.graphics.newImage("res/img/bone.png"),
|
|
||||||
O = love.graphics.newImage("res/img/bone.png"),
|
O = love.graphics.newImage("res/img/bone.png"),
|
||||||
S = love.graphics.newImage("res/img/bone.png"),
|
Y = 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"),
|
|
||||||
G = 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"),
|
||||||
|
W = love.graphics.newImage("res/img/bone.png"),
|
||||||
|
D = 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"),
|
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"),
|
||||||
|
W = love.graphics.newImage("res/img/gem9.png"),
|
||||||
|
D = love.graphics.newImage("res/img/gem9.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"] = {
|
||||||
|
W = love.graphics.newImage("res/img/squares.png"),
|
||||||
|
Y = love.graphics.newImage("res/img/squareg.png"),
|
||||||
|
F = love.graphics.newImage("res/img/squares.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",
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -62,4 +118,5 @@ misc_graphics = {
|
|||||||
go = love.graphics.newImage("res/img/go.png"),
|
go = love.graphics.newImage("res/img/go.png"),
|
||||||
select_mode = love.graphics.newImage("res/img/select_mode.png"),
|
select_mode = love.graphics.newImage("res/img/select_mode.png"),
|
||||||
strike = love.graphics.newImage("res/img/strike.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
|
||||||
@@ -6,19 +6,51 @@ function loadSave()
|
|||||||
end
|
end
|
||||||
|
|
||||||
function loadFromFile(filename)
|
function loadFromFile(filename)
|
||||||
local save_data, len = binser.readFile(filename)
|
local file_data = love.filesystem.read(filename)
|
||||||
|
if file_data == nil then
|
||||||
|
return {} -- new object
|
||||||
|
end
|
||||||
|
local save_data = binser.deserialize(file_data)
|
||||||
if save_data == nil then
|
if save_data == nil then
|
||||||
return {} -- new object
|
return {} -- new object
|
||||||
end
|
end
|
||||||
return save_data[1]
|
return save_data[1]
|
||||||
end
|
end
|
||||||
|
|
||||||
|
function initConfig()
|
||||||
|
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.fullscreen == nil then config.fullscreen = false end
|
||||||
|
if config.secret == nil then config.secret = false end
|
||||||
|
|
||||||
|
if not config.gamesettings then config.gamesettings = {} 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
|
||||||
|
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
|
||||||
|
end
|
||||||
|
|
||||||
function saveConfig()
|
function saveConfig()
|
||||||
binser.writeFile('config.sav', config)
|
love.filesystem.write(
|
||||||
|
'config.sav', binser.serialize(config)
|
||||||
|
)
|
||||||
end
|
end
|
||||||
|
|
||||||
function saveHighscores()
|
function saveHighscores()
|
||||||
binser.writeFile('highscores.sav', highscores)
|
love.filesystem.write(
|
||||||
|
'highscores.sav', binser.serialize(highscores)
|
||||||
|
)
|
||||||
end
|
end
|
||||||
|
|||||||
@@ -10,20 +10,54 @@ sounds = {
|
|||||||
},
|
},
|
||||||
move = love.audio.newSource("res/se/move.wav", "static"),
|
move = love.audio.newSource("res/se/move.wav", "static"),
|
||||||
bottom = love.audio.newSource("res/se/bottom.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)
|
function playSE(sound, subsound)
|
||||||
if subsound == nil then
|
if sound ~= nil then
|
||||||
sounds[sound]:setVolume(0.1)
|
if subsound ~= nil then
|
||||||
if sounds[sound]:isPlaying() then
|
sounds[sound][subsound]:setVolume(config.sfx_volume)
|
||||||
sounds[sound]:stop()
|
if sounds[sound][subsound]:isPlaying() then
|
||||||
|
sounds[sound][subsound]:stop()
|
||||||
|
end
|
||||||
|
sounds[sound][subsound]:play()
|
||||||
|
else
|
||||||
|
sounds[sound]:setVolume(config.sfx_volume)
|
||||||
|
if sounds[sound]:isPlaying() then
|
||||||
|
sounds[sound]:stop()
|
||||||
|
end
|
||||||
|
sounds[sound]:play()
|
||||||
end
|
end
|
||||||
sounds[sound]:play()
|
|
||||||
else
|
|
||||||
sounds[sound][subsound]:setVolume(0.1)
|
|
||||||
if sounds[sound][subsound]:isPlaying() then
|
|
||||||
sounds[sound][subsound]:stop()
|
|
||||||
end
|
|
||||||
sounds[sound][subsound]:play()
|
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
function playSEOnce(sound, subsound)
|
||||||
|
if sound ~= nil then
|
||||||
|
if subsound ~= nil then
|
||||||
|
sounds[sound][subsound]:setVolume(config.sfx_volume)
|
||||||
|
if sounds[sound][subsound]:isPlaying() then
|
||||||
|
return
|
||||||
|
end
|
||||||
|
sounds[sound][subsound]:play()
|
||||||
|
else
|
||||||
|
sounds[sound]:setVolume(config.sfx_volume)
|
||||||
|
if sounds[sound]:isPlaying() then
|
||||||
|
return
|
||||||
|
end
|
||||||
|
sounds[sound]:play()
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
1
load/version.lua
Normal file
@@ -0,0 +1 @@
|
|||||||
|
version = "v0.3-beta6"
|
||||||
335
main.lua
@@ -1,70 +1,67 @@
|
|||||||
function love.load()
|
function love.load()
|
||||||
math.randomseed(os.time())
|
math.randomseed(os.time())
|
||||||
highscores = {}
|
highscores = {}
|
||||||
|
require "load.rpc"
|
||||||
require "load.graphics"
|
require "load.graphics"
|
||||||
require "load.fonts"
|
require "load.fonts"
|
||||||
require "load.sounds"
|
require "load.sounds"
|
||||||
require "load.bgm"
|
require "load.bgm"
|
||||||
require "load.save"
|
require "load.save"
|
||||||
|
require "load.bigint"
|
||||||
|
require "load.version"
|
||||||
loadSave()
|
loadSave()
|
||||||
|
require "funcs"
|
||||||
require "scene"
|
require "scene"
|
||||||
config["side_next"] = false
|
|
||||||
config["reverse_rotate"] = true
|
--config["side_next"] = false
|
||||||
config["fullscreen"] = false
|
--config["reverse_rotate"] = true
|
||||||
|
--config["das_last_key"] = false
|
||||||
|
--config["fullscreen"] = false
|
||||||
|
|
||||||
love.window.setMode(love.graphics.getWidth(), love.graphics.getHeight(), {resizable = true});
|
love.window.setMode(love.graphics.getWidth(), love.graphics.getHeight(), {resizable = true});
|
||||||
|
|
||||||
|
-- used for screenshots
|
||||||
|
GLOBAL_CANVAS = love.graphics.newCanvas()
|
||||||
|
|
||||||
if not config.input then
|
-- init config
|
||||||
config.input = {}
|
initConfig()
|
||||||
scene = InputConfigScene()
|
|
||||||
else
|
love.window.setFullscreen(config["fullscreen"])
|
||||||
if config.current_mode then current_mode = config.current_mode end
|
if config.secret then playSE("welcome") end
|
||||||
if config.current_ruleset then current_ruleset = config.current_ruleset end
|
|
||||||
scene = TitleScene()
|
-- import custom modules
|
||||||
end
|
initModules()
|
||||||
end
|
end
|
||||||
|
|
||||||
local TARGET_FPS = 60
|
function initModules()
|
||||||
local SAMPLE_SIZE = 60
|
game_modes = {}
|
||||||
|
mode_list = love.filesystem.getDirectoryItems("tetris/modes")
|
||||||
local rolling_samples = {}
|
for i=1,#mode_list do
|
||||||
local rolling_total = 0
|
if(mode_list[i] ~= "gamemode.lua" and string.sub(mode_list[i], -4) == ".lua") then
|
||||||
local average_n = 0
|
game_modes[#game_modes+1] = require ("tetris.modes."..string.sub(mode_list[i],1,-5))
|
||||||
local frame = 0
|
end
|
||||||
|
|
||||||
function getSmoothedDt(dt)
|
|
||||||
rolling_total = rolling_total + dt
|
|
||||||
frame = frame + 1
|
|
||||||
if frame > SAMPLE_SIZE then frame = frame - SAMPLE_SIZE end
|
|
||||||
if average_n == SAMPLE_SIZE then
|
|
||||||
rolling_total = rolling_total - rolling_samples[frame]
|
|
||||||
else
|
|
||||||
average_n = average_n + 1
|
|
||||||
end
|
end
|
||||||
rolling_samples[frame] = dt
|
rulesets = {}
|
||||||
return rolling_total / average_n
|
rule_list = love.filesystem.getDirectoryItems("tetris/rulesets")
|
||||||
end
|
for i=1,#rule_list do
|
||||||
|
if(rule_list[i] ~= "ruleset.lua" and string.sub(rule_list[i], -4) == ".lua") then
|
||||||
local update_time = 0.52
|
rulesets[#rulesets+1] = require ("tetris.rulesets."..string.sub(rule_list[i],1,-5))
|
||||||
|
end
|
||||||
function love.update(dt)
|
|
||||||
processBGMFadeout(dt)
|
|
||||||
local old_update_time = update_time
|
|
||||||
update_time = update_time + getSmoothedDt(dt) * TARGET_FPS
|
|
||||||
updates = 0
|
|
||||||
while (update_time >= 1.02) do
|
|
||||||
scene:update()
|
|
||||||
updates = updates + 1
|
|
||||||
update_time = update_time - 1
|
|
||||||
end
|
|
||||||
if math.abs(update_time - old_update_time) < 0.02 then
|
|
||||||
update_time = old_update_time
|
|
||||||
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
|
end
|
||||||
|
|
||||||
function love.draw()
|
function love.draw()
|
||||||
|
love.graphics.setCanvas(GLOBAL_CANVAS)
|
||||||
|
love.graphics.clear()
|
||||||
|
|
||||||
love.graphics.push()
|
love.graphics.push()
|
||||||
|
|
||||||
-- get offset matrix
|
-- get offset matrix
|
||||||
love.graphics.setDefaultFilter("linear", "nearest")
|
love.graphics.setDefaultFilter("linear", "nearest")
|
||||||
local width = love.graphics.getWidth()
|
local width = love.graphics.getWidth()
|
||||||
@@ -75,25 +72,259 @@ function love.draw()
|
|||||||
(height - scale_factor * 480) / 2
|
(height - scale_factor * 480) / 2
|
||||||
)
|
)
|
||||||
love.graphics.scale(scale_factor)
|
love.graphics.scale(scale_factor)
|
||||||
|
|
||||||
scene:render()
|
scene:render()
|
||||||
love.graphics.pop()
|
love.graphics.pop()
|
||||||
|
|
||||||
|
love.graphics.setCanvas()
|
||||||
|
love.graphics.setColor(1,1,1,1)
|
||||||
|
love.graphics.draw(GLOBAL_CANVAS)
|
||||||
end
|
end
|
||||||
|
|
||||||
function love.keypressed(key, scancode, isrepeat)
|
function love.keypressed(key, scancode)
|
||||||
-- global hotkeys
|
-- global hotkeys
|
||||||
if scancode == "f4" then
|
if scancode == "f11" then
|
||||||
config["fullscreen"] = not config["fullscreen"]
|
config["fullscreen"] = not config["fullscreen"]
|
||||||
|
saveConfig()
|
||||||
love.window.setFullscreen(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
|
||||||
|
-- f12 is reserved for saving screenshots
|
||||||
|
elseif scancode == "f12" then
|
||||||
|
local ss_name = os.date("ss/%Y-%m-%d_%H-%M-%S.png")
|
||||||
|
local info = love.filesystem.getInfo("ss", "directory")
|
||||||
|
if not info then
|
||||||
|
love.filesystem.remove("ss")
|
||||||
|
love.filesystem.createDirectory("ss")
|
||||||
|
end
|
||||||
|
print("Saving screenshot as "..ss_name)
|
||||||
|
GLOBAL_CANVAS:newImageData():encode("png", ss_name)
|
||||||
|
-- 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
|
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
|
||||||
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) >= 1 then
|
||||||
|
input_pressed = config.input.joysticks[joystick:getName()].axes[axis][value >= 1 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) >= 1 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
|
||||||
|
|
||||||
|
local last_hat_direction = ""
|
||||||
|
local directions = {
|
||||||
|
["u"] = "up",
|
||||||
|
["d"] = "down",
|
||||||
|
["l"] = "left",
|
||||||
|
["r"] = "right",
|
||||||
|
}
|
||||||
|
|
||||||
|
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
|
||||||
|
for i = 1, #direction do
|
||||||
|
local char = direction:sub(i, i)
|
||||||
|
local _, count = last_hat_direction:gsub(char, char)
|
||||||
|
if count == 0 then
|
||||||
|
scene:onInputPress({input=config.input.joysticks[joystick:getName()].hats[hat][char], type="joyhat", name=joystick:getName(), hat=hat, direction=char})
|
||||||
|
end
|
||||||
|
end
|
||||||
|
for i = 1, #last_hat_direction do
|
||||||
|
local char = last_hat_direction:sub(i, i)
|
||||||
|
local _, count = direction:gsub(char, char)
|
||||||
|
if count == 0 then
|
||||||
|
scene:onInputRelease({input=config.input.joysticks[joystick:getName()].hats[hat][char], type="joyhat", name=joystick:getName(), hat=hat, direction=char})
|
||||||
|
end
|
||||||
|
end
|
||||||
|
last_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
|
||||||
|
last_hat_direction = ""
|
||||||
|
elseif direction ~= "c" then
|
||||||
|
for i = 1, #direction do
|
||||||
|
local char = direction:sub(i, i)
|
||||||
|
local _, count = last_hat_direction:gsub(char, char)
|
||||||
|
if count == 0 then
|
||||||
|
scene:onInputPress({input=directions[char], type="joyhat", name=joystick:getName(), hat=hat, direction=char})
|
||||||
|
end
|
||||||
|
end
|
||||||
|
for i = 1, #last_hat_direction do
|
||||||
|
local char = last_hat_direction:sub(i, i)
|
||||||
|
local _, count = direction:gsub(char, char)
|
||||||
|
if count == 0 then
|
||||||
|
scene:onInputRelease({input=directions[char], type="joyhat", name=joystick:getName(), hat=hat, direction=char})
|
||||||
|
end
|
||||||
|
end
|
||||||
|
last_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
|
||||||
|
last_hat_direction = ""
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
function love.wheelmoved(x, y)
|
||||||
|
scene:onInputPress({input=nil, type="wheel", x=x, y=y})
|
||||||
|
end
|
||||||
|
|
||||||
function love.focus(f)
|
function love.focus(f)
|
||||||
if f then
|
if f then
|
||||||
resumeBGM()
|
resumeBGM(true)
|
||||||
else
|
else
|
||||||
pauseBGM()
|
pauseBGM(true)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
function love.resize(w, h)
|
||||||
|
GLOBAL_CANVAS:release()
|
||||||
|
GLOBAL_CANVAS = love.graphics.newCanvas(w, h)
|
||||||
|
end
|
||||||
|
|
||||||
|
local TARGET_FPS = 60
|
||||||
|
|
||||||
|
function love.run()
|
||||||
|
if love.load then love.load(love.arg.parseGameArguments(arg), arg) end
|
||||||
|
|
||||||
|
if love.timer then love.timer.step() end
|
||||||
|
|
||||||
|
local dt = 0
|
||||||
|
|
||||||
|
local last_time = love.timer.getTime()
|
||||||
|
local time_accumulator = 0
|
||||||
|
return function()
|
||||||
|
if love.event then
|
||||||
|
love.event.pump()
|
||||||
|
for name, a,b,c,d,e,f in love.event.poll() do
|
||||||
|
if name == "quit" then
|
||||||
|
if not love.quit or not love.quit() then
|
||||||
|
return a or 0
|
||||||
|
end
|
||||||
|
end
|
||||||
|
love.handlers[name](a,b,c,d,e,f)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
if love.timer then
|
||||||
|
processBGMFadeout(love.timer.step())
|
||||||
|
end
|
||||||
|
|
||||||
|
if scene and scene.update and love.timer then
|
||||||
|
scene:update()
|
||||||
|
|
||||||
|
local frame_duration = 1.0 / TARGET_FPS
|
||||||
|
if time_accumulator < frame_duration then
|
||||||
|
if love.graphics and love.graphics.isActive() and love.draw then
|
||||||
|
love.graphics.origin()
|
||||||
|
love.graphics.clear(love.graphics.getBackgroundColor())
|
||||||
|
love.draw()
|
||||||
|
love.graphics.present()
|
||||||
|
end
|
||||||
|
local end_time = last_time + frame_duration
|
||||||
|
local time = love.timer.getTime()
|
||||||
|
while time < end_time do
|
||||||
|
love.timer.sleep(0.001)
|
||||||
|
time = love.timer.getTime()
|
||||||
|
end
|
||||||
|
time_accumulator = time_accumulator + time - last_time
|
||||||
|
end
|
||||||
|
time_accumulator = time_accumulator - frame_duration
|
||||||
|
end
|
||||||
|
last_time = love.timer.getTime()
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|||||||
2
package.bat
Normal file
@@ -0,0 +1,2 @@
|
|||||||
|
tar -a -c -f cambridge.zip libs load res scene tetris conf.lua main.lua scene.lua funcs.lua
|
||||||
|
rename cambridge.zip cambridge.love
|
||||||
8
release
@@ -2,8 +2,10 @@
|
|||||||
mkdir dist
|
mkdir dist
|
||||||
mkdir dist/windows
|
mkdir dist/windows
|
||||||
mkdir dist/win32
|
mkdir dist/win32
|
||||||
cp cambridge.love dist/
|
mkdir dist/other
|
||||||
cat dist/windows/love.exe cambridge.love > dist/windows/cambridge.exe
|
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
|
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
|
||||||
|
cp cambridge.love dist/other/
|
||||||
|
zip dist/cambridge-other.zip cambridge.love libs/discord-rpc.* SOURCES.md LICENSE.md
|
||||||
36
release.bat
Normal file
@@ -0,0 +1,36 @@
|
|||||||
|
call package.bat
|
||||||
|
|
||||||
|
mkdir dist
|
||||||
|
mkdir dist\windows
|
||||||
|
mkdir dist\windows\libs
|
||||||
|
mkdir dist\win32
|
||||||
|
mkdir dist\win32\libs
|
||||||
|
mkdir dist\other
|
||||||
|
mkdir dist\other\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 /b cambridge.love dist\other\cambridge.love
|
||||||
|
|
||||||
|
copy libs\discord-rpc.dll dist\windows\libs
|
||||||
|
copy libs\discord-rpc.dll dist\win32\libs
|
||||||
|
copy libs\discord-rpc.* dist\other\libs
|
||||||
|
|
||||||
|
copy SOURCES.md dist\windows
|
||||||
|
copy LICENSE.md dist\windows
|
||||||
|
copy SOURCES.md dist\win32
|
||||||
|
copy LICENSE.md dist\win32
|
||||||
|
copy SOURCES.md dist\other
|
||||||
|
copy LICENSE.md dist\other
|
||||||
|
|
||||||
|
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 ..\..
|
||||||
|
|
||||||
|
cd dist\other
|
||||||
|
tar -a -c -f ..\cambridge-other.zip cambridge.love 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 |
|
Before Width: | Height: | Size: 2.9 MiB 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 |
|
Before Width: | Height: | Size: 1.3 KiB After Width: | Height: | Size: 1.8 KiB |
|
Before Width: | Height: | Size: 1.7 KiB After Width: | Height: | Size: 2.1 KiB |
|
Before Width: | Height: | Size: 2.2 KiB After Width: | Height: | Size: 2.7 KiB |
|
Before Width: | Height: | Size: 1.9 KiB After Width: | Height: | Size: 2.4 KiB |
BIN
res/img/bone.png
|
Before Width: | Height: | Size: 188 B After Width: | Height: | Size: 151 B |
BIN
res/img/bonew.png
Normal file
|
After Width: | Height: | Size: 151 B |
BIN
res/img/cambridge_icon.png
Normal file
|
After Width: | Height: | Size: 2.5 KiB |
|
Before Width: | Height: | Size: 926 B After Width: | Height: | Size: 1.1 KiB |
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
10
scene.lua
@@ -5,11 +5,17 @@ Scene = Object:extend()
|
|||||||
function Scene:new() end
|
function Scene:new() end
|
||||||
function Scene:update() end
|
function Scene:update() end
|
||||||
function Scene:render() end
|
function Scene:render() end
|
||||||
function Scene:onKeyPress() end
|
function Scene:onInputPress() end
|
||||||
|
function Scene:onInputRelease() end
|
||||||
|
|
||||||
ExitScene = require "scene.exit"
|
ExitScene = require "scene.exit"
|
||||||
GameScene = require "scene.game"
|
GameScene = require "scene.game"
|
||||||
ModeSelectScene = require "scene.mode_select"
|
ModeSelectScene = require "scene.mode_select"
|
||||||
|
KeyConfigScene = require "scene.key_config"
|
||||||
|
StickConfigScene = require "scene.stick_config"
|
||||||
InputConfigScene = require "scene.input_config"
|
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"
|
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
|
self.main_menu_state = (self.main_menu_state + len + rel - 1) % len + 1
|
||||||
end
|
end
|
||||||
|
|
||||||
function ConfigScene:onKeyPress(e)
|
function ConfigScene:onInputPress(e)
|
||||||
end
|
end
|
||||||
|
|
||||||
return ConfigScene
|
return ConfigScene
|
||||||
|
|||||||
84
scene/credits.lua
Normal file
@@ -0,0 +1,84 @@
|
|||||||
|
local CreditsScene = Scene:extend()
|
||||||
|
|
||||||
|
CreditsScene.title = "Credits"
|
||||||
|
|
||||||
|
function CreditsScene:new()
|
||||||
|
self.frames = 0
|
||||||
|
-- higher = slower
|
||||||
|
self.scroll_speed = 1.85
|
||||||
|
switchBGM("credit_roll", "gm3")
|
||||||
|
end
|
||||||
|
|
||||||
|
function CreditsScene:update()
|
||||||
|
if love.window.hasFocus() then
|
||||||
|
self.frames = self.frames + 1
|
||||||
|
end
|
||||||
|
if self.frames >= 2100 * self.scroll_speed then
|
||||||
|
playSE("mode_decide")
|
||||||
|
scene = TitleScene()
|
||||||
|
switchBGM(nil)
|
||||||
|
elseif self.frames == math.floor(1950 * self.scroll_speed) then
|
||||||
|
fadeoutBGM(2)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
function CreditsScene:render()
|
||||||
|
local offset = self.frames / self.scroll_speed
|
||||||
|
|
||||||
|
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 - offset)
|
||||||
|
love.graphics.print("THANK YOU\nFOR PLAYING!", 320, math.max(2030 - offset, 240))
|
||||||
|
|
||||||
|
love.graphics.setFont(font_3x5_3)
|
||||||
|
love.graphics.print("Game Developers", 320, 550 - offset)
|
||||||
|
love.graphics.print("Project Heads", 320, 640 - offset)
|
||||||
|
love.graphics.print("Notable Game Developers", 320, 730 - offset)
|
||||||
|
love.graphics.print("Special Thanks", 320, 1000 - offset)
|
||||||
|
love.graphics.print("- Milla", 320, math.max(2110 - offset, 320))
|
||||||
|
|
||||||
|
love.graphics.setFont(font_3x5_2)
|
||||||
|
love.graphics.print("Oshisaure\nJoe Zeng", 320, 590 - offset)
|
||||||
|
love.graphics.print("Mizu\nMarkGamed", 320, 680 - offset)
|
||||||
|
love.graphics.print(
|
||||||
|
"2Tie - TGMsim\nAxel Fox - Multimino\nDr Ocelot - Tetra Legends\n" ..
|
||||||
|
"Electra - ZTrix\nFelicity/nightmareci/kdex - Shiromino\n" ..
|
||||||
|
"Mine - Tetra Online\nMrZ - Techmino\nosk - TETR.IO\n" ..
|
||||||
|
"Phoenix Flare - Master of Blocks\nRayRay26 - Spirit Drop\n" ..
|
||||||
|
"Rin - Puzzle Trial\nsinefuse - stackfuse",
|
||||||
|
320, 770 - offset
|
||||||
|
)
|
||||||
|
love.graphics.print(
|
||||||
|
"321MrHaatz\nAdventium\nAgentBasey\nArchina\nAurora\n" ..
|
||||||
|
"Caithness\nCheez\ncolour_thief\nCommando\nCublex\n" ..
|
||||||
|
"CylinderKnot\neightsixfivezero\nEricICX\nGesomaru\n" ..
|
||||||
|
"gizmo4487\nJBroms\nKirby703\nKitaru\n" ..
|
||||||
|
"M1ssing0\nMattMayuga\nMyPasswordIsWeak\n" ..
|
||||||
|
"Nikki Karissa\noffwo\nOliver\nPineapple\npokemonfan1937\n" ..
|
||||||
|
"Pyra Neoxi\nRDST64\nRocketLanterns\nRustyFoxxo\n" ..
|
||||||
|
"saphie\nShelleloch\nSimon\nstratus\nSuper302\n" ..
|
||||||
|
"switchpalacecorner\nterpyderp\nTetrian22\nTetro48\nThatCookie\n" ..
|
||||||
|
"TimmSkiller\nTrixciel\nuser74003\nZaptorZap\nZircean\n" ..
|
||||||
|
"All other contributors and friends!\nThe Absolute PLUS Discord\n" ..
|
||||||
|
"Tetra Legends Discord\nTetra Online Discord\nMultimino Discord\n" ..
|
||||||
|
"Hard Drop Discord\nRusty's Systemspace\nCambridge Discord\n" ..
|
||||||
|
"And to you, the player!",
|
||||||
|
320, 1040 - offset
|
||||||
|
)
|
||||||
|
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
|
||||||
@@ -7,7 +7,7 @@ function ExitScene:new()
|
|||||||
end
|
end
|
||||||
|
|
||||||
function ExitScene:update()
|
function ExitScene:update()
|
||||||
love.event.quit()
|
love.event.quit()
|
||||||
end
|
end
|
||||||
|
|
||||||
function ExitScene:render()
|
function ExitScene:render()
|
||||||
@@ -16,7 +16,7 @@ end
|
|||||||
function ExitScene:changeOption(rel)
|
function ExitScene:changeOption(rel)
|
||||||
end
|
end
|
||||||
|
|
||||||
function ExitScene:onKeyPress(e)
|
function ExitScene:onInputPress(e)
|
||||||
end
|
end
|
||||||
|
|
||||||
return ExitScene
|
return ExitScene
|
||||||
|
|||||||
119
scene/game.lua
@@ -1,77 +1,82 @@
|
|||||||
local GameScene = Scene:extend()
|
local GameScene = Scene:extend()
|
||||||
|
|
||||||
|
GameScene.title = "Game"
|
||||||
|
|
||||||
require 'load.save'
|
require 'load.save'
|
||||||
|
|
||||||
function GameScene:new(game_mode, ruleset)
|
function GameScene:new(game_mode, ruleset, inputs)
|
||||||
self.game = game_mode()
|
self.retry_mode = game_mode
|
||||||
self.ruleset = ruleset()
|
self.retry_ruleset = ruleset
|
||||||
|
self.secret_inputs = inputs
|
||||||
|
self.game = game_mode(self.secret_inputs)
|
||||||
|
self.ruleset = ruleset(self.game)
|
||||||
self.game:initialize(self.ruleset)
|
self.game:initialize(self.ruleset)
|
||||||
|
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
|
end
|
||||||
|
|
||||||
function GameScene:update()
|
function GameScene:update()
|
||||||
if love.window.hasFocus() then
|
if love.window.hasFocus() and not self.paused then
|
||||||
self.game:update({
|
local inputs = {}
|
||||||
left = love.keyboard.isScancodeDown(config.input.left),
|
for input, value in pairs(self.inputs) do
|
||||||
right = love.keyboard.isScancodeDown(config.input.right),
|
inputs[input] = value
|
||||||
up = love.keyboard.isScancodeDown(config.input.up),
|
end
|
||||||
down = love.keyboard.isScancodeDown(config.input.down),
|
self.game:update(inputs, self.ruleset)
|
||||||
rotate_left = love.keyboard.isScancodeDown(config.input.rotate_left),
|
self.game.grid:update()
|
||||||
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)
|
|
||||||
end
|
end
|
||||||
|
|
||||||
self.game.grid:update()
|
|
||||||
end
|
end
|
||||||
|
|
||||||
function GameScene:render()
|
function GameScene:render()
|
||||||
love.graphics.setColor(1, 1, 1, 1)
|
self.game:draw(self.paused)
|
||||||
love.graphics.draw(
|
|
||||||
backgrounds[self.game:getBackground()],
|
|
||||||
0, 0, 0,
|
|
||||||
0.5, 0.5
|
|
||||||
)
|
|
||||||
|
|
||||||
-- game frame
|
|
||||||
love.graphics.draw(misc_graphics["frame"], 48, 64)
|
|
||||||
love.graphics.setColor(0, 0, 0, 200)
|
|
||||||
love.graphics.rectangle("fill", 64, 80, 160, 320)
|
|
||||||
|
|
||||||
self.game:drawGrid()
|
|
||||||
self.game:drawPiece()
|
|
||||||
self.game:drawNextQueue(self.ruleset)
|
|
||||||
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
|
|
||||||
love.graphics.draw(misc_graphics["go"], 144 - 27, 240 - 14)
|
|
||||||
end
|
|
||||||
|
|
||||||
self.game:drawCustom()
|
|
||||||
|
|
||||||
end
|
end
|
||||||
|
|
||||||
function GameScene:onKeyPress(e)
|
function GameScene:onInputPress(e)
|
||||||
if (self.game.completed) and
|
if (
|
||||||
(e.scancode == "return" or e.scancode == "escape") and e.isRepeat == false then
|
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_entry = self.game:getHighscoreData()
|
||||||
highscore_hash = self.game.hash .. "-" .. self.ruleset.hash
|
highscore_hash = self.game.hash .. "-" .. self.ruleset.hash
|
||||||
submitHighscore(highscore_hash, highscore_entry)
|
submitHighscore(highscore_hash, highscore_entry)
|
||||||
|
self.game:onExit()
|
||||||
|
scene = e.input == "retry" and GameScene(self.retry_mode, self.retry_ruleset, self.secret_inputs) or ModeSelectScene()
|
||||||
|
elseif e.input == "retry" then
|
||||||
|
switchBGM(nil)
|
||||||
|
self.game:onExit()
|
||||||
|
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
|
||||||
|
self.game:onExit()
|
||||||
scene = ModeSelectScene()
|
scene = ModeSelectScene()
|
||||||
elseif (e.scancode == config.input.retry) then
|
elseif e.input and string.sub(e.input, 1, 5) ~= "menu_" then
|
||||||
-- fuck this, this is hacky but the way this codebase is setup prevents anything else
|
self.inputs[e.input] = true
|
||||||
-- it seems like all the values that get touched in the child gamemode class
|
end
|
||||||
-- stop being linked to the values of the GameMode superclass because of how `mt.__index` works
|
end
|
||||||
-- not even sure this is the actual problem, but I don't want to have to rebuild everything about
|
|
||||||
-- the core organisation of everything. this hacky way will have to do until someone figures out something.
|
function GameScene:onInputRelease(e)
|
||||||
love.keypressed("escape", "escape", false)
|
if e.input and string.sub(e.input, 1, 5) ~= "menu_" then
|
||||||
love.keypressed("return", "return", false)
|
self.inputs[e.input] = false
|
||||||
elseif e.scancode == "escape" then
|
|
||||||
scene = ModeSelectScene()
|
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
|||||||