mirror of
https://github.com/ninjamuffin99/Funkin.git
synced 2025-08-31 02:45:13 +00:00
Compare commits
1203 commits
7392371a39
...
211ae319d0
Author | SHA1 | Date | |
---|---|---|---|
|
211ae319d0 | ||
|
6178f4b62b | ||
|
9c1e22309b | ||
|
4ecef6db74 | ||
|
b128741075 | ||
|
86e69cfbd1 | ||
|
2071e5e913 | ||
|
18c2063355 | ||
|
aa8690fce3 | ||
|
e281bdddca | ||
|
19fdda7cd9 | ||
|
d72b989bf5 | ||
|
cd99905bdf | ||
|
3f82f3792b | ||
|
1d8ed5198c | ||
|
8cd3785e3c | ||
|
7e01e7332b | ||
|
c3a837b054 | ||
|
a92567886c | ||
|
4cbfaefc1a | ||
|
705e6b3308 | ||
|
56878befef | ||
|
44cdbb08c4 | ||
|
d6fe54ee79 | ||
|
32c775ef3f | ||
|
1605514424 | ||
|
743e8b13c2 | ||
|
661ddb15f7 | ||
|
3170346b83 | ||
|
e6d737125e | ||
|
3d88fbb11f | ||
|
0d6bc2bedd | ||
|
da5f691865 | ||
|
f63cbf073a | ||
|
30721c3a30 | ||
|
2b1f346097 | ||
|
12117557c7 | ||
|
2dcc528847 | ||
|
5cb33e895c | ||
|
1b91f3b57c | ||
|
e5e3270fef | ||
|
e003a38cab | ||
|
890d1dafcb | ||
|
9d2cfa9686 | ||
|
e6fd5d9b12 | ||
|
967397311b | ||
|
542f59ef72 | ||
|
065ee74aea | ||
|
312b5c8812 | ||
|
e9aa6872a3 | ||
|
4f9b63f22c | ||
|
962519e7f0 | ||
|
34bae4a431 | ||
|
79478080f6 | ||
|
65a3ccc8fe | ||
|
4ac851c387 | ||
|
70d5730b70 | ||
|
c527fed70c | ||
|
054a5cd00b | ||
|
19c5de8fe2 | ||
|
dbc26fd621 | ||
|
eb1986b246 | ||
|
d661d93184 | ||
|
4a07d0a347 | ||
|
832f01345f | ||
|
e8d41c938c | ||
|
a94bd7f36c | ||
|
c185f10e65 | ||
|
52f3c63071 | ||
|
e71062b914 | ||
|
3aa6a36b5e | ||
|
a89fa9c121 | ||
|
cb2015f51a | ||
|
139ce3847c | ||
|
87b40c1203 | ||
|
b9852c316b | ||
|
444697620c | ||
|
6abeefb15d | ||
|
530bd1f11d | ||
|
76c0dea92b | ||
|
2d7a2202f5 | ||
|
1f18a3d1f4 | ||
|
0d57f61ffa | ||
|
9fa70775b9 | ||
|
463afb43c0 | ||
|
2833827fbf | ||
|
b4e52df156 | ||
|
cf70bf2c6c | ||
|
fb6d1b36d3 | ||
|
0c0c63bc03 | ||
|
01620fda9b | ||
|
3bcd6813c1 | ||
|
d44966efd2 | ||
|
79c3e2de11 | ||
|
8372d47d11 | ||
|
d89ebdf58f | ||
|
1e8c472084 | ||
|
17cef6f897 | ||
|
2cc43ee1f6 | ||
|
2277acccfa | ||
|
ed86b326ab | ||
|
596c5813d7 | ||
|
ae68925911 | ||
|
d7c3799562 | ||
|
47ab14a855 | ||
|
940e165612 | ||
|
5181023887 | ||
|
32e774b005 | ||
|
c353cc54a1 | ||
|
48d0798e5f | ||
|
0463b4e26b | ||
|
0c1842038f | ||
|
aa0b8cbbbe | ||
|
52cd4f6c36 | ||
|
6be7e89222 | ||
|
abea46939f | ||
|
3a5f1e14cf | ||
|
849e413bb7 | ||
|
b4a7bd7462 | ||
|
beb64528fb | ||
|
eac940bccb | ||
|
aa1c71cafd | ||
|
f4bedc359a | ||
|
01428dd5cf | ||
|
c6e19c8512 | ||
|
e30a4ebb7b | ||
|
949f409cff | ||
|
4c5aa9743e | ||
|
3fa2b3574b | ||
|
1e41ac3052 | ||
|
936812ae1e | ||
|
ad3bf2d2d2 | ||
|
1b1c2a124b | ||
|
2aff2fa201 | ||
|
7ec602f6ec | ||
|
4fbeffb57c | ||
|
f029a27a3f | ||
|
8a99fa4184 | ||
|
f21b015292 | ||
|
bc8e35409b | ||
|
006f1dce0a | ||
|
78186188d7 | ||
|
762486639e | ||
|
b5bb9721da | ||
|
4029e92181 | ||
|
9c7f87c225 | ||
|
43420092f3 | ||
|
d8cd580535 | ||
|
13c08b285d | ||
|
6238b72d8c | ||
|
bf92bf3f60 | ||
|
6467cd75a8 | ||
|
32dc25bbf6 | ||
|
11cb409bd2 | ||
|
f245365ed3 | ||
|
e0d0230b88 | ||
|
f8634e2b81 | ||
|
4f68f63e2d | ||
|
e9e0567f8b | ||
|
8db768fd2a | ||
|
cf814ee4e2 | ||
|
43b7be4e71 | ||
|
7d555b4155 | ||
|
88ad99c3ef | ||
|
d92b7ff3f5 | ||
|
66c6a78319 | ||
|
e6a2a09801 | ||
|
8944e07fe1 | ||
|
188260ea81 | ||
|
22c6ce42ba | ||
|
d8445fe54a | ||
|
c3592ee6f5 | ||
|
e402328969 | ||
|
150a1d5b0a | ||
|
d468477663 | ||
|
ecaf51ef41 | ||
|
05627a50d5 | ||
|
efe333da92 | ||
|
e946b54249 | ||
|
1d4a94041b | ||
|
11da19ce1e | ||
|
126ed3a67a | ||
|
468197e74e | ||
|
7f17ad8bb8 | ||
|
e9ef340c63 | ||
|
61bda6c2c2 | ||
|
07f649ca37 | ||
|
11ecd07606 | ||
|
822c5ba405 | ||
|
d61f89b0e4 | ||
|
9dfae07225 | ||
|
01fad012f2 | ||
|
9843f9549c | ||
|
044106e8a9 | ||
|
d338be9270 | ||
|
e4d3e18e4f | ||
|
dd104365e7 | ||
|
b0581f728a | ||
|
5e50432b30 | ||
|
314d8af9c8 | ||
|
f91261f748 | ||
|
0567849665 | ||
|
a701e6a4a9 | ||
|
abfc4ca79c | ||
|
797a8bfcba | ||
|
d70838cca9 | ||
|
0d845154ef | ||
|
9e04db2c5c | ||
|
c0d3b9daa2 | ||
|
0f336ae0e6 | ||
|
8c8eedf17a | ||
|
793b533b88 | ||
|
852a74c33d | ||
|
bfdafd7f37 | ||
|
37d0d1b14e | ||
|
2649e247fa | ||
|
fafe157a70 | ||
|
39a4292dc5 | ||
|
aad0a0afb6 | ||
|
f8516cfb4d | ||
|
3987445bd6 | ||
|
eea829ca7d | ||
|
d98628ca0f | ||
|
a6b3eb0b81 | ||
|
49e64a517e | ||
|
ef9fd48ab2 | ||
|
efdc869206 | ||
|
077e0c920f | ||
|
bb714f210b | ||
|
f0a112f185 | ||
|
1025fed57f | ||
|
9e182f70d2 | ||
|
87fea7674b | ||
|
e309bac626 | ||
|
8c6324641e | ||
|
6b7b5d0bf7 | ||
|
a74dcbc6f9 | ||
|
d6d7a0886f | ||
|
7e76cf6634 | ||
|
48013168ef | ||
|
1201a781c9 | ||
|
9ad5126806 | ||
|
c2755a745c | ||
|
a73b879fee | ||
|
e7c4b1ba38 | ||
|
62d24fcf4c | ||
|
3b897417fb | ||
|
7bb2336972 | ||
|
f13eb5be53 | ||
|
7aa77a11cf | ||
|
f54e140b65 | ||
|
0ea42e18e9 | ||
|
a8a99c327d | ||
|
e2722434e0 | ||
|
a08ada7e69 | ||
|
21e5501835 | ||
|
125aa0a438 | ||
|
6591fd8f2a | ||
|
b205a0b931 | ||
|
7a6326b178 | ||
|
df1dbeaef0 | ||
|
91a16f6dc3 | ||
|
acd10ffbe5 | ||
|
86241d53da | ||
|
99769cb50b | ||
|
9036f94970 | ||
|
f00f631db8 | ||
|
d0b64fe4da | ||
|
f378c0adb6 | ||
|
ae6562f42b | ||
|
313cf4b253 | ||
|
dbd52a5bf1 | ||
|
810162de27 | ||
|
711656bcf4 | ||
|
95262b316a | ||
|
82bbb9d30d | ||
|
b06a28e79c | ||
|
811cc4b4b0 | ||
|
00a4f47e42 | ||
|
d703609a0a | ||
|
2c035d77cc | ||
|
96d1324af1 | ||
|
543d6454f6 | ||
|
c414de48f2 | ||
|
70fbd200bd | ||
|
ec10f273b6 | ||
|
2f223f84f5 | ||
|
32f32ba0bd | ||
|
25bf04d581 | ||
|
f036593037 | ||
|
9ec7a5068a | ||
|
55e1967423 | ||
|
d3207de231 | ||
|
afc08e285c | ||
|
28fc5109d4 | ||
|
2b5ba8dfa9 | ||
|
0ef1df33c9 | ||
|
b3aa6a0b8d | ||
|
126da44a95 | ||
|
f6d9acb63a | ||
|
f78431da3c | ||
|
21bbef5542 | ||
|
46c40f67cc | ||
|
cdfce1af12 | ||
|
8aff75b308 | ||
|
58f101bf01 | ||
|
c49666c4b4 | ||
|
73ec37a473 | ||
|
adf2b49a72 | ||
|
a11bde6f76 | ||
|
8b6b91cc23 | ||
|
94eae116c7 | ||
|
faadf406c3 | ||
|
d4c7339373 | ||
|
55d34c3c9d | ||
|
7ed374f1e5 | ||
|
4e87c7d40c | ||
|
377697a358 | ||
|
32701d9ab0 | ||
|
c897d89b35 | ||
|
1f65a9c5a1 | ||
|
d061ba4e43 | ||
|
d4d52a1394 | ||
|
cedf17fb99 | ||
|
97620e6038 | ||
|
1d3b5eb246 | ||
|
40c292127d | ||
|
79cc20ffe8 | ||
|
4eb3c764ab | ||
|
e2efd2bc81 | ||
|
894d8cb463 | ||
|
3fd80504e3 | ||
|
42ec7f7796 | ||
|
bc4c944d4d | ||
|
715d2ba65f | ||
|
067b56c5fa | ||
|
d73eb4e392 | ||
|
5a19397481 | ||
|
eb80a52841 | ||
|
57d0871b5c | ||
|
066ed53a6d | ||
|
6f35209d63 | ||
|
5be5abf4c9 | ||
|
ab89cd33f9 | ||
|
7febe5b5ad | ||
|
4cc922c373 | ||
|
4557a681d1 | ||
|
2e99737485 | ||
|
7db71574d1 | ||
|
b1376f7401 | ||
|
06a440f21c | ||
|
6dcec592f4 | ||
|
a0d3f8ec55 | ||
|
9f9af722ca | ||
|
3962989f83 | ||
|
b3d1adbd7f | ||
|
cf0da45dd8 | ||
|
68695329cf | ||
|
1b68c3a8d6 | ||
|
647ce880e1 | ||
|
e9915e52fa | ||
|
18fe868233 | ||
|
0869362fa1 | ||
|
87a09cae21 | ||
|
4766a92946 | ||
|
74b30c0be4 | ||
|
d0ff60ad0a | ||
|
1dd6fb8dd4 | ||
|
2042f3db8a | ||
|
b67cbc132d | ||
|
720d279f89 | ||
|
97754e5ed3 | ||
|
e2e8c383cd | ||
|
19d1a8c593 | ||
|
8dd007a7c2 | ||
|
3747b94246 | ||
|
7cc9464573 | ||
|
7b9bce3b4e | ||
|
91550181f5 | ||
|
c5308ccbb9 | ||
|
dcac362003 | ||
|
ae51c2ea5a | ||
|
905181c9af | ||
|
11d9998e5c | ||
|
37dc66bc18 | ||
|
74627795d2 | ||
|
903ca851bc | ||
|
24c469dc31 | ||
|
eb6becc03f | ||
|
fbd80ea41f | ||
|
ccd0148e9b | ||
|
3a7379e851 | ||
|
a7055efb27 | ||
|
98cd45453b | ||
|
c13167ece8 | ||
|
7d9a9f452d | ||
|
c30b519bc4 | ||
|
9b06bd6b11 | ||
|
f3db69cca8 | ||
|
9174b83c93 | ||
|
74a8b8862e | ||
|
88ed66affa | ||
|
3ac2a02291 | ||
|
c1d4bba813 | ||
|
80f3fb6150 | ||
|
94742eb9ba | ||
|
5c06d263b2 | ||
|
be60e52725 | ||
|
9e07f5dd5a | ||
|
f1bef04165 | ||
|
9687cff9d4 | ||
|
f9c1f7a5f7 | ||
|
3d3e2bd378 | ||
|
9804111583 | ||
|
7cbb896a57 | ||
|
0001017c00 | ||
|
7d6a4cb763 | ||
|
fee5ab49af | ||
|
5201160973 | ||
|
4772dfdc13 | ||
|
9e3efc0de7 | ||
|
c4d24e6d3e | ||
|
fc82b79c9a | ||
|
a98f6f9778 | ||
|
4ca42c3975 | ||
|
51127dfd87 | ||
|
cae7c8a785 | ||
|
204501779d | ||
|
d7c83a5b70 | ||
|
4e31003a0f | ||
|
cce8c18822 | ||
|
ea6cb75b77 | ||
|
9bd2c0b13b | ||
|
af716f9273 | ||
|
88d0e8c3b0 | ||
|
6114dfcedb | ||
|
52d891c8ff | ||
|
d040e1f72f | ||
|
8393c789e2 | ||
|
39104fe77f | ||
|
0e9bb20592 | ||
|
56a18e1cf6 | ||
|
948030ef1d | ||
|
8dce8afdfc | ||
|
e7c6798f44 | ||
|
5482dee750 | ||
|
5549fddec3 | ||
|
65461d839b | ||
|
2a63c1a7ba | ||
|
fee29f69ec | ||
|
539720f84d | ||
|
576f8e54ff | ||
|
50ca508fdd | ||
|
8824a382b9 | ||
|
d6ec32f334 | ||
|
d356cfaee6 | ||
|
49eff01afd | ||
|
c4aff95886 | ||
|
9f41e22428 | ||
|
123c9396ba | ||
|
136a5dfad4 | ||
|
f23eddcf2c | ||
|
13d8127e75 | ||
|
7d0772170f | ||
|
43bf102d68 | ||
|
24101d654d | ||
|
e82b578eb8 | ||
|
3eace5a7c8 | ||
|
153f2be177 | ||
|
380f9e70d5 | ||
|
ff2dff0635 | ||
|
24d1116deb | ||
|
8e085171d5 | ||
|
4819a74c29 | ||
|
f95258358c | ||
|
962d0b0dc2 | ||
|
6fa4280e31 | ||
|
0d2a737a8a | ||
|
779919d11b | ||
|
4a9e8490fc | ||
|
80db44be2b | ||
|
0f85b4a85d | ||
|
c0e0523651 | ||
|
41a246c9a5 | ||
|
cfed20983b | ||
|
2273a1b346 | ||
|
a69e08e986 | ||
|
1c7abb6391 | ||
|
839ff43123 | ||
|
84e0104984 | ||
|
092732a535 | ||
|
ff6aea9efd | ||
|
f1c15c8a84 | ||
|
beab53a489 | ||
|
8bbdc8c0f4 | ||
|
80948ba4ad | ||
|
5c2cc82340 | ||
|
d8a6abc48d | ||
|
5cbea2851c | ||
|
2cf29a67d9 | ||
|
e8cd87c1f1 | ||
|
075f03235d | ||
|
fcb5fe85ff | ||
|
bbd598a560 | ||
|
5ca21d1bb3 | ||
|
a9ba2a7a1c | ||
|
a2c0625137 | ||
|
d0d1d3c376 | ||
|
d0fe228e21 | ||
|
1b7dc5f1ac | ||
|
ea0e4ee56d | ||
|
1b1d0a978d | ||
|
bf007e2a2c | ||
|
aa24e08fdb | ||
|
d72b2bb2da | ||
|
49693f06e9 | ||
|
99ad45997e | ||
|
849553e0f0 | ||
|
48ca0ecef3 | ||
|
403c6250f8 | ||
|
ffbc8c80c8 | ||
|
aacb200638 | ||
|
603e370908 | ||
|
43d8b522cb | ||
|
b75cfcb833 | ||
|
dc800dc48e | ||
|
9706a55eca | ||
|
6b11f8b852 | ||
|
6ad9ffc7f9 | ||
|
895eeed852 | ||
|
4541f91d84 | ||
|
068a995cdd | ||
|
1a07e082c9 | ||
|
4fa12a3486 | ||
|
0a9ac0e93c | ||
|
974a78919d | ||
|
fb9bf59a7e | ||
|
e0b4c30719 | ||
|
80ffe6faa7 | ||
|
15083a4776 | ||
|
0dbffa6875 | ||
|
a776ce1a81 | ||
|
738bd8b7c7 | ||
|
d79f99475e | ||
|
221c3db6e6 | ||
|
e92c6ec8f7 | ||
|
de0a56ec95 | ||
|
aefa8d149f | ||
|
29ee386dfe | ||
|
0d965ba91c | ||
|
a29e20098b | ||
|
a2c5758112 | ||
|
01fd37e08b | ||
|
5eecef17a1 | ||
|
9df5395ff8 | ||
|
50d9584a38 | ||
|
0e24ae911f | ||
|
c87a39b6c2 | ||
|
760190d135 | ||
|
9f8e42bd3c | ||
|
1f75a641e0 | ||
|
47f0fc7236 | ||
|
79185596c1 | ||
|
8cae34eed7 | ||
|
a0ae4de5e8 | ||
|
e7df920e74 | ||
|
07958590a4 | ||
|
bc01d5cffa | ||
|
295e6f652e | ||
|
ea01052b18 | ||
|
cacb95bdd3 | ||
|
44fa64542a | ||
|
43c7f4e883 | ||
|
157a0a36dc | ||
|
034a861c69 | ||
|
333e5326a2 | ||
|
8e7de54aa0 | ||
|
ea81e061d2 | ||
|
cd3b7d7b95 | ||
|
5d4e1d276b | ||
|
55221646b8 | ||
|
f599e53448 | ||
|
7629a1a92d | ||
|
1c8f320a54 | ||
|
e7ddf9fbc8 | ||
|
9d6450454e | ||
|
b80a8d60af | ||
|
d968fb271c | ||
|
70f0a54191 | ||
|
29b94ea736 | ||
|
8ca4d933aa | ||
|
20e8a4566d | ||
|
69c5c05039 | ||
|
299a720575 | ||
|
8d7500e8c0 | ||
|
cadfa3b7ce | ||
|
5342601f8b | ||
|
f6b1ff13bc | ||
|
d43e9506b3 | ||
|
753f51dcf6 | ||
|
9303e4c272 | ||
|
b0f4a075b5 | ||
|
031f888a25 | ||
|
bc94a5a6ac | ||
|
2450961b28 | ||
|
8b8737006f | ||
|
5c4fa108c0 | ||
|
f4245ee0da | ||
|
aa8f5087b5 | ||
|
edc668dad5 | ||
|
2771a13d5e | ||
|
b63e02d9e7 | ||
|
9d3595c855 | ||
|
250b5cb54f | ||
|
e457b3e03b | ||
|
38e0cb6ab1 | ||
|
4746e8d7a5 | ||
|
db2644c0ff | ||
|
f9aff5b11b | ||
|
cc9797c452 | ||
|
a97acfe18e | ||
|
65ed58350b | ||
|
9e6c17507f | ||
|
3360493444 | ||
|
e657bc900b | ||
|
5a5f1f83f3 | ||
|
277416b6a3 | ||
|
37a40c24a1 | ||
|
e90b74f788 | ||
|
ad11fb26b9 | ||
|
287603ad30 | ||
|
1c25713464 | ||
|
7cbc5b0dc7 | ||
|
7ccf75cd86 | ||
|
eec304f42f | ||
|
7e4508f991 | ||
|
f74d1655d5 | ||
|
5057f3d1de | ||
|
a103b9641f | ||
|
9e58ef70fb | ||
|
f5a453b2e7 | ||
|
d432e086cc | ||
|
f2280b1c0f | ||
|
7f619640f2 | ||
|
9d5613cc95 | ||
|
96c788a420 | ||
|
07f792e358 | ||
|
24db9235e1 | ||
|
370e00c9b9 | ||
|
6d5767aef2 | ||
|
b3ebfd0910 | ||
|
a9a4dad511 | ||
|
dc04ff23d7 | ||
|
72f7317db2 | ||
|
57ebeb25e4 | ||
|
7fd1d312d7 | ||
|
c2643c3873 | ||
|
a1c1187b43 | ||
|
a7e46ed55b | ||
|
22c24f53f6 | ||
|
262c4fb9a1 | ||
|
75aea15539 | ||
|
6e8494e615 | ||
|
4392f5e642 | ||
|
21616e9399 | ||
|
814c563c55 | ||
|
537c7cb5a1 | ||
|
1801c542a6 | ||
|
f854a6e07a | ||
|
e193f7392a | ||
|
eb8d878aeb | ||
|
f19ae72b27 | ||
|
63eca96c98 | ||
|
b008f88e0d | ||
|
a50a014de3 | ||
|
f78ab4da1d | ||
|
1d1c1403c3 | ||
|
743462d96e | ||
|
72a6b92daf | ||
|
285a607d9a | ||
|
c6526527f1 | ||
|
7b3c9f1131 | ||
|
402f2707b3 | ||
|
e89f9f50dc | ||
|
4eaa3842c6 | ||
|
1d9e34359c | ||
|
5361df2544 | ||
|
60a707b963 | ||
|
6222c389e3 | ||
|
6265d751f8 | ||
|
aff83bab4d | ||
|
d3d8aaae7b | ||
|
c263b4bc83 | ||
|
7a2be205b9 | ||
|
cd0e8a301d | ||
|
8b80478229 | ||
|
0c62428fc8 | ||
|
2904e660e9 | ||
|
e2b9cc9ed5 | ||
|
5ac9cee76c | ||
|
62fa3ef934 | ||
|
d81e409d93 | ||
|
aac09f86fe | ||
|
428c5443f6 | ||
|
1e5ec7850f | ||
|
2e0dfab9fb | ||
|
d78117e377 | ||
|
7aa0db844b | ||
|
d72d93083f | ||
|
9769a626bc | ||
|
afcf9a26cc | ||
|
28363652ef | ||
|
42ff442ef8 | ||
|
183cec62dc | ||
|
da0964a7b7 | ||
|
fabc16d265 | ||
|
57bb462a38 | ||
|
2f647127b1 | ||
|
bbddb34853 | ||
|
1d88ec473e | ||
|
bb98883807 | ||
|
451d4d1399 | ||
|
b66a955d65 | ||
|
34d5ed1169 | ||
|
ea6e2aef7b | ||
|
099d1b3eb1 | ||
|
ab817bb1ea | ||
|
6077ba67f7 | ||
|
3bbb4b06c8 | ||
|
f92fe3ff15 | ||
|
067a04e069 | ||
|
e9697e6f40 | ||
|
0345107e71 | ||
|
5177e1275e | ||
|
be57d2fa3c | ||
|
ff7b7b5d3e | ||
|
e7c42dae0c | ||
|
07ad53a09b | ||
|
113326089a | ||
|
ed0abb8215 | ||
|
405fe21c53 | ||
|
e5b280c646 | ||
|
36cbfe91bb | ||
|
4081f329d2 | ||
|
402d5b730b | ||
|
5e7e2f45f3 | ||
|
80393bbc6d | ||
|
2c44785c39 | ||
|
329be321eb | ||
|
e79d4e69c2 | ||
|
fda944a9f7 | ||
|
ab00a126fa | ||
|
7689c7d2e4 | ||
|
91d1fb74f7 | ||
|
a5921ab96c | ||
|
7fca9d4c7e | ||
|
0e26f9d936 | ||
|
af71c3acd7 | ||
|
9e8c4f1fa1 | ||
|
268166b7e0 | ||
|
9bd65a4782 | ||
|
8a3e623d28 | ||
|
9d671888ef | ||
|
1fc71a3852 | ||
|
af546a14a2 | ||
|
7d5904b539 | ||
|
fb2d587a15 | ||
|
2b4a42ac0a | ||
|
0f36a05311 | ||
|
12fc467312 | ||
|
f3dbd763d2 | ||
|
9ed4d7e8fc | ||
|
43abebcafd | ||
|
a5ae068801 | ||
|
3f8e96965b | ||
|
367c56c5e7 | ||
|
b2aa111b71 | ||
|
c6c48bd9a8 | ||
|
835b79e3de | ||
|
ba12d505d6 | ||
|
193f83e000 | ||
|
b69ef72d51 | ||
|
44eb13ef9b | ||
|
3fca9cee63 | ||
|
e7a460042b | ||
|
ad1021ee33 | ||
|
e9c411f770 | ||
|
8711db49c7 | ||
|
7485bc990e | ||
|
c333bbaa23 | ||
|
28d3999797 | ||
|
8f216ada02 | ||
|
52dfee61fa | ||
|
a62d2b94a9 | ||
|
2e486d987b | ||
|
83e1e1f68f | ||
|
3d6882c282 | ||
|
11383e9385 | ||
|
7d567de24f | ||
|
dc56ccada5 | ||
|
d3490f8c99 | ||
|
1959a5250f | ||
|
ae2c2db28f | ||
|
82af988dce | ||
|
e3fca16793 | ||
|
2221594883 | ||
|
be7313453f | ||
|
c9c284787e | ||
|
93e4f799f4 | ||
|
c7abb19698 | ||
|
7c7dc11f18 | ||
|
b883ad3d50 | ||
|
426a9c0c10 | ||
|
0b7a94b1cc | ||
|
711d3896cb | ||
|
98236a541a | ||
|
3179d33a17 | ||
|
3f23b79087 | ||
|
f16b8630ff | ||
|
1c747657d2 | ||
|
924f72f96c | ||
|
26dc895a27 | ||
|
58257f6ac1 | ||
|
b52c73f2b0 | ||
|
564d679f96 | ||
|
5695bc20e7 | ||
|
3dc7699aac | ||
|
dc60c12963 | ||
|
6b63884e38 | ||
|
4eff23a6f2 | ||
|
d8db5f7d88 | ||
|
23d31ff73b | ||
|
a89351f083 | ||
|
fb3277ea35 | ||
|
71cadd31ed | ||
|
1bd281830e | ||
|
751f569e69 | ||
|
c363a53f9d | ||
|
fc83685b0f | ||
|
25e04cf061 | ||
|
8965bb059e | ||
|
d61cc1d5be | ||
|
45bcb93c21 | ||
|
2e8338a5dc | ||
|
cb43dd34b4 | ||
|
83216e171c | ||
|
3b55c5b15f | ||
|
d4ef73fb93 | ||
|
7f924cd563 | ||
|
bbfbce6404 | ||
|
91e9d5d55c | ||
|
6ec380e699 | ||
|
d311a16459 | ||
|
a5b4a7d2cc | ||
|
59a2b4ce6f | ||
|
5788d04890 | ||
|
30e9b2fb67 | ||
|
a60560e36c | ||
|
511fd88ee9 | ||
|
a73ba04706 | ||
|
17f614ea06 | ||
|
29143505cc | ||
|
c462a5f8d0 | ||
|
0d5fd4a0c5 | ||
|
54547e7dd1 | ||
|
4b58cd0812 | ||
|
dd809e0455 | ||
|
22d2b95e7f | ||
|
310c074d78 | ||
|
94f1720870 | ||
|
4f3f43d6e7 | ||
|
6689ae9e9d | ||
|
8e7ddc0f53 | ||
|
fda719ae7b | ||
|
666b57a1cb | ||
|
a413318782 | ||
|
6289dfdd02 | ||
|
c77e164513 | ||
|
d2acb5d167 | ||
|
adfd73d6ab | ||
|
728558b279 | ||
|
d104e3381b | ||
|
14e8e1f42f | ||
|
24e535c90a | ||
|
cbff9a3719 | ||
|
9a3e58a7d4 | ||
|
c8a86d3df8 | ||
|
44c32cb0fc | ||
|
35f7113091 | ||
|
83d198493c | ||
|
cd71102f8d | ||
|
283d7c0971 | ||
|
e9b1d5fc04 | ||
|
13a2b192b9 | ||
|
27b1cd32b7 | ||
|
fffea09270 | ||
|
d3705042cf | ||
|
f9b0acb998 | ||
|
377626c279 | ||
|
b536619773 | ||
|
b189518383 | ||
|
f6569907c8 | ||
|
a34cbaf87f | ||
|
ec5a736be6 | ||
|
00b54150a4 | ||
|
b0c3c020e0 | ||
|
045b240949 | ||
|
b84b531a2c | ||
|
fc78eeee4f | ||
|
910500ea26 | ||
|
3dc693f8b6 | ||
|
a7b4f3d420 | ||
|
c992180f7c | ||
|
b0468fa056 | ||
|
c026c104f5 | ||
|
353f1780d1 | ||
|
c648a5dbf2 | ||
|
2777ec03a4 | ||
|
86ead7096e | ||
|
bdb8857d5c | ||
|
2325317902 | ||
|
adb74b4539 | ||
|
f737ce6ed3 | ||
|
a2e647ac8b | ||
|
c823bba3d1 | ||
|
e917da0a7b | ||
|
8de74c361c | ||
|
6c842fea20 | ||
|
41ce874595 | ||
|
9216fff9b0 | ||
|
0253df7269 | ||
|
40f44ece5b | ||
|
49114c5d37 | ||
|
9b8330ced1 | ||
|
1baf7fea80 | ||
|
588eea019d | ||
|
9307f514fe | ||
|
e6df6d5116 | ||
|
d82d231539 | ||
|
0d7c78305d | ||
|
e71bd1f5a4 | ||
|
7e62782fa1 | ||
|
7fe621a839 | ||
|
7dc4bb940c | ||
|
5279aefec9 | ||
|
b541c5d974 | ||
|
4d77d21b15 | ||
|
7f19faabc4 | ||
|
2ba225265f | ||
|
10d58c26b0 | ||
|
e0e660f357 | ||
|
647b69bb0c | ||
|
94cd22e243 | ||
|
324ef37505 | ||
|
3cc189869b | ||
|
cc7def5178 | ||
|
842d1a72a4 | ||
|
3d52aab7a3 | ||
|
f3b20dbbd3 | ||
|
a54bf9c7fd | ||
|
2ae503cfec | ||
|
b8db4947d6 | ||
|
53794ab4cc | ||
|
804984b8c1 | ||
|
63ec721aa1 | ||
|
0444d47458 | ||
|
2ac0627063 | ||
|
b755ed8d98 | ||
|
64126a3df5 | ||
|
e78d3ecf8f | ||
|
c246de1ee1 | ||
|
a7ef5580a4 | ||
|
f961b1cae2 | ||
|
827066b589 | ||
|
1dfe8866ab | ||
|
14cf382192 | ||
|
1f22313b8a | ||
|
e359dad1e8 | ||
|
4d88df2120 | ||
|
1c11a7003f | ||
|
2932c24418 | ||
|
bb46b699eb | ||
|
dc000b33eb | ||
|
75d8101030 | ||
|
90fb8a1883 | ||
|
50baf0c869 | ||
|
9e133b0a8a | ||
|
da3f5a48c9 | ||
|
36d361aa22 | ||
|
b6e31e581a | ||
|
5dd26101f2 | ||
|
81c92fc11f | ||
|
44e166d411 | ||
|
5b8b943b99 | ||
|
c9138e4d1f | ||
|
2bfc24f22b | ||
|
ebb9d373c3 | ||
|
fc2e28ab18 | ||
|
b4f30c341e | ||
|
995f6c5adb | ||
|
7dee8e6e54 | ||
|
bcdbe2993c | ||
|
8bf14b7d0c | ||
|
261a237934 | ||
|
ede705987c | ||
|
edfcfc8c55 | ||
|
828de1c512 | ||
|
85e77486f7 | ||
|
50f467c366 | ||
|
97f8de0de4 | ||
|
dfa8e635f1 | ||
|
ed5b7fbb31 | ||
|
418f7d71f9 | ||
|
7111492578 | ||
|
baaaaddead | ||
|
c2956cd253 | ||
|
999557b501 | ||
|
73914c6858 | ||
|
4976a099af | ||
|
ec5f8344ed | ||
|
219c46f56b | ||
|
4f5b0f2453 | ||
|
0d2c48c374 | ||
|
359e40bf4d | ||
|
bd39e2023c | ||
|
809f4107ab | ||
|
e838fef8e6 | ||
|
2c5c66286a | ||
|
a64f7aa5d3 | ||
|
5ca22a6910 | ||
|
bed90dc230 | ||
|
ff144cf30b | ||
|
58468abba7 | ||
|
5d26f8b433 | ||
|
b68ba97ca6 | ||
|
ba449cabd6 | ||
|
61f1bdd89a | ||
|
6d6441de6f | ||
|
f7f502dd92 | ||
|
6133087c2c | ||
|
4226ba8978 | ||
|
b012096151 | ||
|
39313be86b | ||
|
b1986fb9c5 | ||
|
4d74ccbc4e | ||
|
bef3815901 | ||
|
37fa11fe08 | ||
|
e8d3fa8a35 | ||
|
0a0789faea | ||
|
5d6e44d5f1 | ||
|
7119d6a476 | ||
|
1093dd3400 | ||
|
c7906edc1e | ||
|
3fcc2cc5bc | ||
|
34f97e3e3e | ||
|
e0fcd655e7 | ||
|
b1089958c7 | ||
|
ea2b63b1bb | ||
|
7fb5d8c65c | ||
|
86f8cb76f2 | ||
|
b03eef01ca | ||
|
5d7517b839 | ||
|
1790c13fac | ||
|
180ec17ce3 | ||
|
5a39439fcc | ||
|
2f6e6ef7cd | ||
|
75e0992509 | ||
|
67f0425e89 | ||
|
3a084accc9 | ||
|
31c96bc6a1 | ||
|
f439624404 | ||
|
0b2bb3391c | ||
|
ffee837bcd | ||
|
e4733f9a5c | ||
|
4dcaae5786 | ||
|
8fa6809723 | ||
|
6b3fb46ae5 | ||
|
d05a3519a3 | ||
|
1a75c094b3 | ||
|
def86a44eb | ||
|
3b3f4543b4 | ||
|
d92d3130c8 | ||
|
01cc9f1ae4 | ||
|
9775034d7a | ||
|
824767af66 | ||
|
07818b84fd | ||
|
ae4384453c | ||
|
a80e3df799 | ||
|
52929de50a | ||
|
b81ff3c136 | ||
|
f11d7e8147 | ||
|
8f123e4ab9 | ||
|
25d27e62ce | ||
|
b9230b6eb2 | ||
|
2236865f0f | ||
|
e197b7325a | ||
|
6080cd5bf3 | ||
|
ef35ba759e | ||
|
f246fa2e6c | ||
|
2973b65c69 | ||
|
f6ee1c8f92 | ||
|
3389eaef96 | ||
|
86d050a11f | ||
|
5c0de03a74 | ||
|
afe0087591 | ||
|
616004d7f6 | ||
|
854f62ba54 | ||
|
7374986c05 | ||
|
6a064dc7f7 | ||
|
d9bc989b91 | ||
|
7bd6e09dcf | ||
|
45986f34c1 | ||
|
3386d62951 | ||
|
85330f73a1 | ||
|
318e73a030 | ||
|
62acfd8365 | ||
|
cbc3b6c0b5 | ||
|
3e71f12871 | ||
|
1a28e48434 | ||
|
d57409ca20 | ||
|
195e3082df | ||
|
27adb17a38 | ||
|
85f3f4b047 | ||
|
16f8678d0f | ||
|
64877bc999 | ||
|
6b2572c176 | ||
|
7a747993e7 | ||
|
5d2bd38f72 | ||
|
1e40568ca9 | ||
|
51b82a0a7c | ||
|
6c2ff099da | ||
|
8119dcca8e | ||
|
a289fb7f54 | ||
|
6f9b8f0284 | ||
|
46d10ede43 | ||
|
16884ba28e | ||
|
c636024f75 | ||
|
a4207c1020 | ||
|
a2c2b8d9dd | ||
|
c466e1aa12 | ||
|
3bdd7b20b0 | ||
|
5b4ed6e726 | ||
|
96ce6086e1 | ||
|
0e256021b1 | ||
|
18dcb8f446 | ||
|
ca391c6e32 | ||
|
5ae73361b0 | ||
|
4cee6a9d56 | ||
|
c3ed2019b5 | ||
|
6720f7f9ca | ||
|
6b090542f1 | ||
|
eb3022a0fc | ||
|
ea9938a318 | ||
|
f17b9d234c | ||
|
5b9bd1af00 | ||
|
9d48a04564 | ||
|
f970ea6acf | ||
|
8b1b81a925 | ||
|
2d8a493897 | ||
|
3bcb63cb84 | ||
|
0d35b66903 | ||
|
db84186627 | ||
|
e3e98dc057 | ||
|
5d70cdbeb2 | ||
|
3e75701c1c | ||
|
0fc5b84237 | ||
|
f23a6a8ac8 | ||
|
4b797d3526 | ||
|
82a9dabc7f | ||
|
3f7ae02d54 | ||
|
e9b1800ec9 | ||
|
bee846ec8c | ||
|
e5f3223e76 | ||
|
d80afc35b4 | ||
|
5c9d76791b | ||
|
2a6720da2e | ||
|
068747b01a | ||
|
4962d55b22 | ||
|
66e6ea9d41 | ||
|
d43dfac3e4 | ||
|
9cf134fc2a | ||
|
7b1abe437f | ||
|
193d293dea | ||
|
530ca0d929 | ||
|
c99778266b | ||
|
2680b064fe | ||
|
9dc48978f8 | ||
|
f6a261e5bd | ||
|
a707472270 | ||
|
7babcfaf6b | ||
|
1b2f75395f | ||
|
8704666edb | ||
|
66683c11a0 | ||
|
5d624fe1b9 | ||
|
c4bb161865 | ||
|
b1b30ed09f | ||
|
f5ffdbe82d | ||
|
ad4b1ce51b | ||
|
9657b7eb72 | ||
|
381bc6521c | ||
|
8a60bc26b5 | ||
|
a732d27e93 | ||
|
944141292d |
2
.github/ISSUE_TEMPLATE/bug.yml
vendored
2
.github/ISSUE_TEMPLATE/bug.yml
vendored
|
@ -25,6 +25,8 @@ body:
|
|||
- Itch.io (Downloadable Build) - Windows
|
||||
- Itch.io (Downloadable Build) - MacOS
|
||||
- Itch.io (Downloadable Build) - Linux
|
||||
- Google Playstore - Android
|
||||
- App Store - iOS
|
||||
- Compiled from GitHub Source Code
|
||||
validations:
|
||||
required: true
|
||||
|
|
21
.github/workflows/build-game.yml
vendored
21
.github/workflows/build-game.yml
vendored
|
@ -1,3 +1,6 @@
|
|||
# Builds the game on all platforms, to ensure it compiles on all target platforms.
|
||||
# This helps to ensure workers focus on the master branch.
|
||||
|
||||
name: Build and Upload nightly game builds
|
||||
|
||||
on:
|
||||
|
@ -21,7 +24,7 @@ jobs:
|
|||
trigger-build: ${{ steps.should-trigger.outputs.result }}
|
||||
steps:
|
||||
- name: Checkout repo
|
||||
uses: funkincrew/ci-checkout@v6
|
||||
uses: funkincrew/ci-checkout@v7.3.3
|
||||
with:
|
||||
submodules: false
|
||||
- uses: dorny/paths-filter@v3
|
||||
|
@ -93,7 +96,7 @@ jobs:
|
|||
packages: write
|
||||
steps:
|
||||
- name: Checkout repo
|
||||
uses: funkincrew/ci-checkout@v6
|
||||
uses: funkincrew/ci-checkout@v7.3.3
|
||||
with:
|
||||
submodules: false
|
||||
- name: Log into GitHub Container Registry
|
||||
|
@ -125,6 +128,9 @@ jobs:
|
|||
runs-on: windows
|
||||
- target: macos
|
||||
runs-on: macos
|
||||
# TODO: Install XCode to build iOS
|
||||
# - target: ios
|
||||
# runs-on: macos
|
||||
runs-on:
|
||||
- ${{ matrix.runs-on }}
|
||||
defaults:
|
||||
|
@ -144,11 +150,14 @@ jobs:
|
|||
private-key: ${{ secrets.APP_PEM }}
|
||||
owner: ${{ github.repository_owner }}
|
||||
- name: Checkout repo
|
||||
uses: funkincrew/ci-checkout@v6
|
||||
uses: funkincrew/ci-checkout@v7.3.3
|
||||
with:
|
||||
submodules: 'recursive'
|
||||
token: ${{ steps.app_token.outputs.token }}
|
||||
persist-credentials: false
|
||||
submodule-aliases: |
|
||||
https://github.com/FunkinCrew/Funkin.assets > https://github.com/FunkinCrew/Funkin-assets-secret
|
||||
https://github.com/FunkinCrew/Funkin.art > https://github.com/FunkinCrew/Funkin-art-secret
|
||||
- name: Setup build environment
|
||||
uses: ./.github/actions/setup-haxe
|
||||
with:
|
||||
|
@ -190,6 +199,7 @@ jobs:
|
|||
include:
|
||||
- target: linux
|
||||
- target: html5
|
||||
# - target: android
|
||||
defaults:
|
||||
run:
|
||||
shell: bash
|
||||
|
@ -204,11 +214,14 @@ jobs:
|
|||
private-key: ${{ secrets.APP_PEM }}
|
||||
owner: ${{ github.repository_owner }}
|
||||
- name: Checkout repo
|
||||
uses: funkincrew/ci-checkout@v6
|
||||
uses: funkincrew/ci-checkout@v7.3.3
|
||||
with:
|
||||
submodules: 'recursive'
|
||||
token: ${{ steps.app_token.outputs.token }}
|
||||
persist-credentials: false
|
||||
submodule-aliases: |
|
||||
https://github.com/FunkinCrew/Funkin.assets > https://github.com/FunkinCrew/Funkin-assets-secret
|
||||
https://github.com/FunkinCrew/Funkin.art > https://github.com/FunkinCrew/Funkin-art-secret
|
||||
- name: Config haxelib
|
||||
run: |
|
||||
haxelib --never newrepo
|
||||
|
|
5
.github/workflows/cancel-merged-branches.yml
vendored
5
.github/workflows/cancel-merged-branches.yml
vendored
|
@ -1,3 +1,6 @@
|
|||
# When a pull request is merged, cancel all queued workflows for that branch
|
||||
# This helps to ensure workers focus on the master branch.
|
||||
|
||||
name: Cancel queued workflows on PR merge
|
||||
|
||||
on:
|
||||
|
@ -23,7 +26,7 @@ jobs:
|
|||
let branch_workflows = await github.rest.actions.listWorkflowRuns({
|
||||
owner: context.repo.owner,
|
||||
repo: context.repo.repo,
|
||||
workflow_id: "build-shit.yml",
|
||||
workflow_id: "build-game.yml",
|
||||
status: "queued",
|
||||
branch: "${{ github.event.pull_request.head.ref }}"
|
||||
});
|
||||
|
|
1
.github/workflows/label-actions.yml
vendored
1
.github/workflows/label-actions.yml
vendored
|
@ -1,5 +1,6 @@
|
|||
# Perform actions when labels are applied to issues, discussions, or pull requests
|
||||
# See .github/label-actions.yml
|
||||
|
||||
name: 'Label Actions'
|
||||
|
||||
on:
|
||||
|
|
|
@ -1,9 +1,13 @@
|
|||
# Applies the following labels to pull requests when created:
|
||||
# - status: pending triage
|
||||
|
||||
name: "Pull Request Labeler 2 (Runs on PR creation)"
|
||||
on:
|
||||
pull_request_target:
|
||||
types:
|
||||
- opened
|
||||
|
||||
|
||||
jobs:
|
||||
# Apply `status: pending triage` to newly created pull requests
|
||||
apply-pending-triage:
|
||||
|
|
12
.github/workflows/label-pull-request.yml
vendored
12
.github/workflows/label-pull-request.yml
vendored
|
@ -1,7 +1,19 @@
|
|||
# Applies the following labels to pull requests whenver they are created or modified:
|
||||
# - pr: documentation
|
||||
# - pr: haxe
|
||||
# - pr: github
|
||||
# - size: tiny
|
||||
# - size: small
|
||||
# - size: medium
|
||||
# - size: large
|
||||
# - size: huge
|
||||
# see .github/labeler.yml and .github/changed-lines-count-labeler.yml
|
||||
|
||||
name: "Pull Request Labeler"
|
||||
on:
|
||||
- pull_request_target
|
||||
|
||||
|
||||
jobs:
|
||||
# Apply labels to pull requests based on which files were edited
|
||||
labeler:
|
||||
|
|
9
.gitignore
vendored
9
.gitignore
vendored
|
@ -3,14 +3,21 @@
|
|||
.vs/
|
||||
APIStuff.hx
|
||||
dump/
|
||||
temp/
|
||||
docs/temp/
|
||||
export/
|
||||
project/
|
||||
astc-textures/
|
||||
RECOVER_*.fla
|
||||
shitAudio/
|
||||
.build_time
|
||||
.swp
|
||||
NewgroundsCredentials.hx
|
||||
|
||||
# Mobile signing stuff
|
||||
.apple/
|
||||
.env
|
||||
key.keystore
|
||||
|
||||
# Exclude JS stuff
|
||||
node_modules/
|
||||
package.json
|
||||
|
|
66
.vscode/schema/hmm.json
vendored
Normal file
66
.vscode/schema/hmm.json
vendored
Normal file
|
@ -0,0 +1,66 @@
|
|||
{
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"dependencies": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"name": {
|
||||
"title": "Name",
|
||||
"type": "string",
|
||||
"description": "The name of the Haxe library"
|
||||
},
|
||||
"type": {
|
||||
"title": "Type",
|
||||
"type": "string",
|
||||
"description": "one of haxelib, git, hg, or dev",
|
||||
"enum": ["haxelib", "git", "hg", "dev"]
|
||||
},
|
||||
"ref": {
|
||||
"type": "string",
|
||||
"description": "the git/hg ref (branch, commit, tag, etc.)"
|
||||
},
|
||||
"url": {
|
||||
"title": "URL",
|
||||
"type": "string",
|
||||
"description": "the git/hg URL"
|
||||
},
|
||||
"version": {
|
||||
"title": "Version",
|
||||
"type": "string",
|
||||
"description": "the haxelib library version. Must be in SemVer format (MAJOR.MINOR.PATCH)",
|
||||
"pattern": "^(0|[1-9]\\d*)\\.(0|[1-9]\\d*)\\.(0|[1-9]\\d*)(?:-((?:0|[1-9]\\d*|\\d*[a-zA-Z-][0-9a-zA-Z-]*)(?:\\.(?:0|[1-9]\\d*|\\d*[a-zA-Z-][0-9a-zA-Z-]*))*))?(?:\\+([0-9a-zA-Z-]+(?:\\.[0-9a-zA-Z-]+)*))?$"
|
||||
}
|
||||
},
|
||||
"required": ["name", "type"],
|
||||
"dependentSchemas": {
|
||||
"type": {
|
||||
"if": {
|
||||
"properties": {
|
||||
"type": {
|
||||
"const": "git"
|
||||
}
|
||||
}
|
||||
},
|
||||
"then": {
|
||||
"required": ["url"]
|
||||
},
|
||||
"else": {
|
||||
"if": {
|
||||
"properties": {
|
||||
"type": {
|
||||
"const": "haxelib"
|
||||
}
|
||||
}
|
||||
},
|
||||
"then": {
|
||||
"required": ["version"]
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
15
.vscode/settings.json
vendored
15
.vscode/settings.json
vendored
|
@ -90,6 +90,8 @@
|
|||
"haxecheckstyle.configurationFile": "checkstyle.json",
|
||||
"haxecheckstyle.codeSimilarityBufferSize": 100,
|
||||
|
||||
"lime.projectFile": "project.hxp",
|
||||
|
||||
"lime.targetConfigurations": [
|
||||
{
|
||||
"label": "Windows / Debug (Discord)",
|
||||
|
@ -168,7 +170,7 @@
|
|||
{
|
||||
"label": "Windows / Debug (Debug hxvlc)",
|
||||
"target": "windows",
|
||||
"args": ["-debug", "-DHXC_LIBVLC_LOGGING", "-DFEATURE_DEBUG_FUNCTIONS"]
|
||||
"args": ["-debug", "-DHXVLC_LOGGING", "-DFEATURE_DEBUG_FUNCTIONS"]
|
||||
},
|
||||
{
|
||||
"label": "HashLink / Debug (Straight to Animation Editor)",
|
||||
|
@ -211,6 +213,10 @@
|
|||
"label": "Debug",
|
||||
"args": ["-debug", "-DFEATURE_DEBUG_FUNCTIONS"]
|
||||
},
|
||||
{
|
||||
"label": "Debug (Unlock Everything)",
|
||||
"args": ["-debug", "-DUNLOCK_EVERYTHING"]
|
||||
},
|
||||
{
|
||||
"label": "Debug (Tracy)",
|
||||
"args": ["-debug", "-DFEATURE_DEBUG_TRACY", "-DFEATURE_DEBUG_FUNCTIONS"]
|
||||
|
@ -235,5 +241,12 @@
|
|||
"coverage.xml",
|
||||
"jacoco.xml",
|
||||
"coverage.cobertura.xml"
|
||||
],
|
||||
"vscord.app.privacyMode.enable": true,
|
||||
"json.schemas": [
|
||||
{
|
||||
"fileMatch": ["/hmm.json"],
|
||||
"url": "./.vscode/schema/hmm.json"
|
||||
}
|
||||
]
|
||||
}
|
||||
|
|
299
CHANGELOG.md
299
CHANGELOG.md
|
@ -4,6 +4,303 @@ All notable changes will be documented in this file.
|
|||
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
|
||||
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
||||
|
||||
## [0.7.3] - 2025-07-21
|
||||
|
||||
### Fixed
|
||||
|
||||
- Fixed stuttering throughout the game caused by the Polymod upgrade. (Thanks NotHyper-474!)
|
||||
- [MOBILE] Fixed buttons in the Main Menu not working.
|
||||
- [iOS] The Upgrade button no longer appears if you have already purchased it (actually this time).
|
||||
- Fixed the countdown overlapping itself when restarting the song. (Thanks NotHyper-474!)
|
||||
- Optimized the Week 6 Erect stage.
|
||||
- Fixed an oversight when clearing the cache. (Thanks cherrythecool!)
|
||||
- The Input Offset Test menu text now displays in the correct position.
|
||||
- Fixed script errors appearing in the Week 3 Erect stage.
|
||||
- Fixed adding variations in the Chart Editor erasing difficulties. (Thanks NotHyper-474!)
|
||||
|
||||
## New Contributors for 0.7.3
|
||||
|
||||
* @cherrythecool made their first contribution in [#5458](https://github.com/FunkinCrew/Funkin/pull/5458)
|
||||
|
||||
|
||||
|
||||
## [0.7.2] - 2025-07-18
|
||||
|
||||
### Added
|
||||
|
||||
- [ANDROID] Added a button in the Options menu to access the mods folder.
|
||||
- [MOBILE] Added a preference to adjust the intensity of haptic feedback, ranging from 0.1 to 5.
|
||||
- [MOBILE] Added an easter egg when tapping the player's healthbar icon.
|
||||
|
||||
### Changed
|
||||
|
||||
- Changed default OpenAL configuration settings to improve audio quality. (Thanks Smokey555, cyn0x8, and CCobaltDev!)
|
||||
- The difference may be more or less noticeable on different devices and hardware.
|
||||
- Applies to Desktop and Android, but not iOS yet.
|
||||
- Made several improvements to Polymod and HScript. These changes might break some mods, so please update them accordingly!
|
||||
- Fixed an issue where scripted classes can define two or more fields with the same name.
|
||||
- Fixed an issue causing some syntax errors (such as missing commas) to be ignored by the parser.
|
||||
- Scripted classes can now create static fields and functions.
|
||||
- Scripted classes can now create variables with the `final` keyword.
|
||||
- Scripted classes can now access variables from another scripted class with `class.someVariable`, instead of `class.scriptGet("someVariable")`
|
||||
- This applies to functions too: `class.someFunction()`
|
||||
- Scripted classes that don't extend another class can now be created!
|
||||
- This only works if you access the class in a static context. Creating an instance of said class doesn't work just yet!
|
||||
- Added support for properties (`get_` and `set_` functions) (Thanks KoloInDaCrib!)
|
||||
- Added support for abstracts in a static context. (Thanks lemz1!)
|
||||
- You can now use classes like `FlxColor` properly!
|
||||
- Added support for creating and using enums. (Thanks lemz1!)
|
||||
- You can import them in another script as usual.
|
||||
- Added support for renaming imported classes using the `as` keyword. (Thanks KoloInDaCrib!)
|
||||
- Fixed `try`/`catch` blocks not working properly. (Thanks NotHyper-474!)
|
||||
- Fixed null-safe field access not working properly for functions (ex. `class?.someFunction()). (Thanks KoloInDaCrib!)
|
||||
|
||||
### Fixed
|
||||
|
||||
- [MOBILE] Weekend 1 Story Mode no longer crashes before loading into Blazin'.
|
||||
- [MOBILE] Beating 2hot from Freeplay no longer crashes in the Results screen.
|
||||
- [MOBILE] Retrying and pressing the Back button at the same time no longer crashes the game.
|
||||
- [MOBILE] Pressing the Options and Back buttons at the same time no longer softlocks the game.
|
||||
- [HTML5] Pausing while the train passes by on the Week 3 Erect stage no longer crashes.
|
||||
- [DESKTOP] Getting a Bad/Shit rating on Blazin' no longer breaks animations.
|
||||
- The scroll sound no longer plays once after entering Freeplay.
|
||||
- The Freeplay song preview and album cover now update properly when switching variations.
|
||||
- The Freeplay clear percent counter now consistently displays the correct value on unranked songs.
|
||||
- The Freeplay difficulty star flames no longer become offset from the stars.
|
||||
- The Freeplay difficulty star flames no longer appear during a new rank animation.
|
||||
- The Freeplay menu now correctly assigns the `currentCharacterId`. (Thanks TechnikTil!)
|
||||
- Boyfriend's Perfect (Gold) Results animation now loops properly.
|
||||
- [DESKTOP] The Input Offsets menu no longer activates the debug cursor.
|
||||
- The Input Offsets Test menu no longer generates stacked notes.
|
||||
- The Input Offsets Test menu drums no longer desync from the rest of the track.
|
||||
- The Input Offsets Test menu no longer breaks when a keyboard or controller is connected.
|
||||
- [MOBILE] Sustain trails now display properly with upscroll enabled.
|
||||
- [MOBILE] Added a Back button to the keyboard/gamepad Controls menu.
|
||||
- [iOS] Fixed app name spacing on the Home Screen.
|
||||
- [iOS] Adjusted the preloader to accommodate for different screen sizes.
|
||||
- [iOS] The Upgrade button no longer appears if you have already purchased it.
|
||||
- [ANDROID] Fixed some issues with scrolling.
|
||||
- [ANDROID] Toasts with blank messages no longer appear.
|
||||
- Fixed a critical security vulnerability that could be exploited in mods.
|
||||
- A few more bugfixes and optimizations here and there.
|
||||
|
||||
## New Contributors for 0.7.2
|
||||
|
||||
* @Smokey555 made their first contribution in [#3318](https://github.com/FunkinCrew/Funkin/pull/3318)
|
||||
* @CCobaltDev made their first contribution in [#3318](https://github.com/FunkinCrew/Funkin/pull/3318)
|
||||
|
||||
|
||||
|
||||
## [0.7.1] - 2025-07-15
|
||||
|
||||
### Fixed
|
||||
- Properly implemented ad playback on iOS devices.
|
||||
|
||||
|
||||
|
||||
## [0.7.0] - 2025-07-15
|
||||
|
||||
### Added
|
||||
|
||||
- Friday Night Funkin' now has OFFICIAL mobile versions for Android and iOS, available on the Google Play Store and Apple App Store!
|
||||
- This version contains 100% of the songs from the desktop version of the game.
|
||||
- [MOBILE] New touch input compatibility for all menus.
|
||||
- [MOBILE] Added banner and interstitial advertisements to the game. You can upgrade to the full version through an in-app purchase to permanently disable advertisements.
|
||||
- [MOBILE] Graphics are compressed using the ASTC algorithm, decreasing memory usage in exchange for a slightly larger file size.
|
||||
- [MOBILE] Added haptic feedback to several areas of the game.
|
||||
- Added a visual indicator that shows available difficulties for the currently selected song in Freeplay.
|
||||
- Overhauled the input offsets system, including:
|
||||
- One unified "offset" value.
|
||||
- An "Offset Calibration" screen where the game determines your ideal offset.
|
||||
- A "Test" screen where you can play a short note pattern to try out your offset.
|
||||
- A brand new offset testing theme: Syncobation by Kawai Sprite!
|
||||
- The ability to change your offsets in the Pause Menu, mid-song!
|
||||
- The Input Offsets menu isn't yet available on HTML5, but offsets are still configurable through the Pause Menu.
|
||||
- Added null-safety to a bunch of classes in the source code.
|
||||
- Added the Changelog back to the game files, written by Hundrec and AbnormalPoof!
|
||||
- Added a few sandboxed classes to give mods limited access to the Discord and Newgrounds APIs. ([50d9584](https://github.com/FunkinCrew/Funkin/commit/50d9584a388bd891aa2f8b68a5cde894a6e1ede6)) - by @KoloInDaCrib in [#5040](https://github.com/FunkinCrew/Funkin/pull/5040)
|
||||
- Added script support for Freeplay Backing Cards. ([0001017](https://github.com/FunkinCrew/Funkin/commit/0001017c003be653236c6cc56487c7d0ee33633e)) - by @KoloInDaCrib in [#5233](https://github.com/FunkinCrew/Funkin/pull/5233)
|
||||
- Sparrow results screen animations can now be scriptable. ([7bb2336](https://github.com/FunkinCrew/Funkin/commit/7bb23369727ca4955aa1fbe25e5798809e8169bd)) - by @KoloInDaCrib in [#5168](https://github.com/FunkinCrew/Funkin/pull/5168)
|
||||
- Added a blank `Object` class for scripts to extend, and made `FlxObject` and `FlxBasic` scriptable. ([eb6becc](https://github.com/FunkinCrew/Funkin/commit/eb6becc03fff76117ee3fcbeb32fe254236ca232)) - by @cyn0x8 in [#3119](https://github.com/FunkinCrew/Funkin/pull/3119)
|
||||
- Added default gamepad controls for two recently added Freeplay controls. ([a0d3f8e](https://github.com/FunkinCrew/Funkin/commit/a0d3f8ec553e06b625b463c7989658edbebbbdf5)) - by @MrMadera in [#4559](https://github.com/FunkinCrew/Funkin/pull/4559)
|
||||
- Added the ability to press the Chart Editor keybind in Freeplay with a song capsule selected. ([2221594](https://github.com/FunkinCrew/Funkin/commit/2221594883afa7cd0e518fca7ea975d05626692a)) - by @Lasercar in [#4114](https://github.com/FunkinCrew/Funkin/pull/4114)
|
||||
- The Chart Editor now highlights and deletes stacked notes using a customizable threshold. ([8cae34e](https://github.com/FunkinCrew/Funkin/commit/8cae34eed711bff70e5348ffc6178a0fd69b5846)) - by @NotHyper-474 in [#3574](https://github.com/FunkinCrew/Funkin/pull/3574)
|
||||
- Added a variation indicator next to the Chart Editor playbar difficulty. ([ccd0148](https://github.com/FunkinCrew/Funkin/commit/ccd0148e9b46d512a22b4958d3f289cfc7854965)) - by @KoloInDaCrib in [#5236](https://github.com/FunkinCrew/Funkin/pull/5236)
|
||||
- Added more tween types to certain Chart Editor events. ([5177e12](https://github.com/FunkinCrew/Funkin/commit/5177e1275eb2fb2b016224c139e84debb421b895)) - by @Lasercar in [#4249](https://github.com/FunkinCrew/Funkin/pull/4249)
|
||||
- Pressing Ctrl + N now creates a new stage in the Stage Editor. ([576f8e5](https://github.com/FunkinCrew/Funkin/commit/576f8e54ff8ca8e205241fafa33d0256b62d11d5)) - by @Lasercar in [#5175](https://github.com/FunkinCrew/Funkin/pull/5175)
|
||||
- Added "Flip character horizontally" to the list of shortcuts in the Animation Editor. ([c464cae](https://github.com/FunkinCrew/funkin.assets/commit/c464caec921dcefef7b0b74b2abf95e76ce64491)) - by @AbnormalPoof in [funkin.assets#60](https://github.com/FunkinCrew/funkin.assets/pull/60)
|
||||
- Added Perfect (Gold) to the list of available ranks in Results Debug menu. ([c5308cc](https://github.com/FunkinCrew/Funkin/commit/c5308ccbb9d2b98c62fa4974b8ad7ac1e1ec7d19)) - by @AbnormalPoof in [#4642](https://github.com/FunkinCrew/Funkin/pull/4642)
|
||||
- [MOBILE] Implemented Kevin and Michael.
|
||||
|
||||
### Changed
|
||||
|
||||
- The mod API version now supports v0.7.0, along with v0.6.3. Be sure to check that your mods still work!
|
||||
- Updated the app icon for Desktop platforms.
|
||||
- [MOBILE] Modified several parts of the game to look better on phone screens with wider aspect ratios, up to 20:9.
|
||||
- [DESKTOP] The game now tries to match the window's aspect ratio when changing states, extending as wide as 20:9.
|
||||
- [DESKTOP] Included Mobile stage expansions on Desktop as well. Now you'll have more room for camera events!
|
||||
- Playable Pico and Weekend 1 songs are now always unlocked in Freeplay, even on new saves.
|
||||
- The Freeplay difficulty graphic now scrolls smoothly when changing difficulties.
|
||||
- The "Pause on Unfocus" preference now opens the Pause Menu when unfocusing during a song.
|
||||
- Scripts can now make hold note trails semi-transparent.
|
||||
- Completely reformatted every script file within the game's assets for better readability.
|
||||
- Completely reformatted and optimized every single chart file in the game.
|
||||
- Recharted pico-speaker's chart in Stress
|
||||
- Tweaked charts for the following songs:
|
||||
- Bopeebo [all difficulties] - Removed an extra hey animation event
|
||||
- Bopeebo (Pico Mix) [Hard] - Added a missing note in Section 24
|
||||
- Fresh Erect [Nightmare] - Added a missing grace note for BF in Section 24
|
||||
- South Erect [Nightmare] - Added missing grace notes for BF in Sections 13, 17, and 53
|
||||
- Philly Nice [Hard] - Added missing grace notes for Pico in Sections 30 and 62
|
||||
- Philly Nice [all difficulties] - Added hey animations throughout the song
|
||||
- Philly Nice Erect [Erect] - Added a grace note for BF in Section 33, removed a stacked note for opponent in Section 12
|
||||
- Philly Nice (Pico Mix) [Normal] - Adjusted a left note by 1/96 in Section 60
|
||||
- Blammed (Pico Mix) [Hard] - Added a missing jack in Section 46
|
||||
- Satin Panties [Hard] - Added grace notes in Sections 7-10
|
||||
- Satin Panties [Normal/Hard] - Made Mom sing a sustain rather than two notes in Section 30
|
||||
- High Erect [Erect/Nightmare] - Added a missing note in Section 16
|
||||
- Cocoa [Easy] - Added some notes to reduce sparseness, fixed Mom singing Dad's notes
|
||||
- Cocoa Erect [Erect/Nightmare] - Reimplemented BF's censored notes for Nightmare, adjusted one note by 1/48 in Section 63
|
||||
- Eggnog Erect [Erect/Nightmare] Added two grace notes in Sections 10 and 14 and a missing note for Dad in Section 44
|
||||
- Eggnog (Pico Mix) [Hard] - Added a missing grace note for Pico that was present on Normal in Section 20
|
||||
- Roses [Normal/Hard] - Made Senpai sing a sustain rather than two notes (sneaky)
|
||||
- Roses Erect [Erect/Nightmare] - Mirrored the changes from normal Roses
|
||||
- Guns [all difficulties] - Added a missing note in Sections 28 and 32 and adjusted a hold note's length in Section 73
|
||||
- Stress [Hard] - Split whole notes in halves in Sections 57-60
|
||||
- Darnell [Hard] - Added one missing note for Pico in Section 35
|
||||
- Darnell [all difficulties] - Adjusted camera event timings for consistency
|
||||
- Darnell (BF Mix) [all difficulties] - Removed 3 extra notes and fixed Darnell's pattern being offset
|
||||
- Lit Up [all difficulties] - Added 4 sustains for Darnell throughout the song
|
||||
- Lit Up (BF Mix) [all difficulties] - Added 4 sustains for Darnell throughout the song
|
||||
- 2hot [Easy/Hard] - Fixed remaining offset rhythms (for real this time)
|
||||
- Notes now scroll more smoothly by rendering based on delta timing. ([6ad9ffc](https://github.com/FunkinCrew/Funkin/commit/6ad9ffc7f9d66bbaf6ba343663de4c7268f4be3b)) - by @KutikiPlayz in [#3544](https://github.com/FunkinCrew/Funkin/pull/3544)
|
||||
- The Freeplay character select hint now always displays if you have more than one character unlocked. ([7ccf75c](https://github.com/FunkinCrew/Funkin/commit/7ccf75cd869ba4b6f18a5adc01e65e52ae7bb809)) - by @Hundrec in [#5023](https://github.com/FunkinCrew/Funkin/pull/5023)
|
||||
- Favorite songs in Freeplay are now sorted by Week order instead of alphabetically. ([da0964a](https://github.com/FunkinCrew/Funkin/commit/da0964a7b7bbe4ece1bdbd19233eb6dba0de3ac5)) - by @Hundrec in [#3609](https://github.com/FunkinCrew/Funkin/pull/3609)
|
||||
- Shifted Mommy Mearest's pixel icon to the left in Freeplay. ([d861eba](https://github.com/FunkinCrew/funkin.assets/commit/d861ebac027dd07d0254c79d7c89b59ee04b38f1)) - by @KoloInDaCrib in [funkin.assets#197](https://github.com/FunkinCrew/funkin.assets/pull/197)
|
||||
- The Character Select screen now opens on the currently selected character. ([4819a74](https://github.com/FunkinCrew/Funkin/commit/4819a74c2959cc9b32dfe2cb76c3a4c00e7c7f9a)) - by @Lasercar in [#4072](https://github.com/FunkinCrew/Funkin/pull/4072)
|
||||
- Visualizers now zero out when the game audio is muted. ([6dcec59](https://github.com/FunkinCrew/Funkin/commit/6dcec592f467a0daeb8ff1e0ce122916e36ca869)) - by @Lasercar in [#5266](https://github.com/FunkinCrew/Funkin/pull/5266)
|
||||
- The Options Menu can now scroll to display more menu items. ([70f0a54](https://github.com/FunkinCrew/Funkin/commit/70f0a54191597bd72a6d30d4d12ef5ece6ba078c)) - by @AbnormalPoof in [#4706](https://github.com/FunkinCrew/Funkin/pull/4706)
|
||||
- Raised the FPS cap preference from 300 to 500. ([be73134](https://github.com/FunkinCrew/Funkin/commit/be7313453f70983fff55e69a8b52d741f0cc53b4)) - by @Hundrec in [#5044](https://github.com/FunkinCrew/Funkin/pull/5044)
|
||||
- The Credits menu now uses less memory, especially with many entries. ([1b68c3a](https://github.com/FunkinCrew/Funkin/commit/1b68c3a8d6f66905a9a508a1cb692fe3beb7b4a2)) - by @lemz1 in [#2655](https://github.com/FunkinCrew/Funkin/pull/2655)
|
||||
- Added a timer sequence class to queue up multiple timers in scripts with ease. ([9e182f7](https://github.com/FunkinCrew/Funkin/commit/9e182f70d2bcc92eb68d730d74af143c45f7dcf8)) - by @cyn0x8 in [#2391](https://github.com/FunkinCrew/Funkin/pull/2391)
|
||||
- Replaced smoothLerp and coolLerp with smoothLerpPrecision to fix a few lerp-related bugs. ([94eae11](https://github.com/FunkinCrew/Funkin/commit/94eae116c7a5e6039683d6391208b169378b5ff1)) - by @cyn0x8 in [#3617](https://github.com/FunkinCrew/Funkin/pull/3617)
|
||||
- Fixed empty text strings softlocking the dialogue box. ([88d0e8c](https://github.com/FunkinCrew/Funkin/commit/88d0e8c3b0529654fb7eee8aebb099f7fb346f66)) - by @xenkap in [#4671](https://github.com/FunkinCrew/Funkin/pull/4671)
|
||||
- Adjusted the size of the Beat/Step display in the Chart Editor. ([905181c](https://github.com/FunkinCrew/Funkin/commit/905181c9af29bb11280bda33ef9343069678a762)) - by @NotHyper-474 in [#4994](https://github.com/FunkinCrew/Funkin/pull/4994)
|
||||
- The Chart Editor will now only fall back to the first available difficulty if the selected difficulty cannot be found. ([1c25713](https://github.com/FunkinCrew/Funkin/commit/1c257134648ebd89acf6c9d07f5a0c088fb915c6)) - by @Lasercar in [#4949](https://github.com/FunkinCrew/Funkin/pull/4949)
|
||||
- The undo/redo history is now cleared when loading another song in the Chart Editor. ([426a9c0](https://github.com/FunkinCrew/Funkin/commit/426a9c0c108ac65a042295194679d46444ec1ea5)) - by @Lasercar in [#4308](https://github.com/FunkinCrew/Funkin/pull/4308)
|
||||
- Blacklisted more classes for security reasons. ([cadfa3b](https://github.com/FunkinCrew/Funkin/commit/cadfa3b7ceae2ecabe2d544ddc4c9f453b0dfd56)) - by @NotHyper-474 in [#5185](https://github.com/FunkinCrew/Funkin/pull/5185)
|
||||
|
||||
### Fixed
|
||||
|
||||
- Fixed a ton of performance issues to help the game run better on mobile devices.
|
||||
- Exiting the Freeplay Menu no longer freezes the game for a really long time (thanks NotHyper-474!)
|
||||
- Notestyle graphics are now preloaded before the song starts, fixing the stutter at the beginning of the song.
|
||||
- Hitting many hold note trails in one song no longer leads to a lag spike.
|
||||
- The first lightning strike in Week 2 Erect no longer creates a lag spike.
|
||||
- [DESKTOP] The conductor and music no longer gradually drift out of sync to eventually trigger a resync.
|
||||
- Pixel notestyle strumlines are now properly positioned when Downscroll is enabled.
|
||||
- Added the missing graffiti to the wall in the Weekend 1 Blazin' stage.
|
||||
- Accept keybinds now properly scroll faster through the Credits.
|
||||
- Typing in most text fields in debug editors no longer triggers keyboard shortcuts.
|
||||
- The Chart Editor playback speed feature now works properly.
|
||||
- The Chart Editor metronome and hitsounds now play at exactly the right time.
|
||||
- The Chart Editor notification box no longer covers playbar info.
|
||||
- Selecting a Recent File too quickly in the Stage Editor no longer crashes the game.
|
||||
- Blacklisted a few classes for security.
|
||||
- Opening the logs or backups folder before it's created no longer crashes the game. ([d3490f8](https://github.com/FunkinCrew/Funkin/commit/d3490f8c9929eefb9879ad65ce43038c193642d6)) - by @NotHyper-474 in [#4940](https://github.com/FunkinCrew/Funkin/pull/4940)
|
||||
- Fixed a crash when mashing D or I during startup. ([b52c73f](https://github.com/FunkinCrew/Funkin/commit/b52c73f2b0fa32fbc349804621cf947dff2d364e)) - by @CrusherNotDrip in [#5160](https://github.com/FunkinCrew/Funkin/pull/5160)
|
||||
- Hot-reloading with F5 during gameplay no longer crashes the game. ([d2acb5d](https://github.com/FunkinCrew/Funkin/commit/d2acb5d167afd42299d7200ab8c67972044a09c6)) - by @AbnormalPoof in [#5065](https://github.com/FunkinCrew/Funkin/pull/5065)
|
||||
- Hot-reloading with F5 in the Input Offsets menu no longer crashes the game. ([58257f6](https://github.com/FunkinCrew/Funkin/commit/58257f6ac187925f3b23d3f1eef2c812ae569a6b)) - by @NotHyper-474 in [#5085](https://github.com/FunkinCrew/Funkin/pull/5085)
|
||||
- Songs no longer skip forward at the beginning with high offsets. ([1f75a64](https://github.com/FunkinCrew/Funkin/commit/1f75a641e0c80d15f1af10bae7ee71a6ffecf219)) - by @xenkap in [#3732](https://github.com/FunkinCrew/Funkin/pull/3732)
|
||||
- The song countdown no longer stacks when restarting or continues behind the Pause Menu.
|
||||
([63eca96](https://github.com/FunkinCrew/Funkin/commit/63eca96c98a87e7155df8b2a1735f269ea83e1b5)) - by @KoloInDaCrib and @NotHyper-474 in [#4875](https://github.com/FunkinCrew/Funkin/pull/4875)
|
||||
- Fixed incorrect highlighting and squashed text on Freeplay song capsules. ([0c62428](https://github.com/FunkinCrew/Funkin/commit/0c62428fc883c0fa6d09cd403efb287ff3af8c53)) - by @VioletSnowLeopard in [#5036](https://github.com/FunkinCrew/Funkin/pull/5036)
|
||||
- The Freeplay song preview no longer plays twice after returning from Character
|
||||
Select. ([3d3e2bd](https://github.com/FunkinCrew/Funkin/commit/3d3e2bd3786b858143d214caf55be2ee3e9483fc)) - by @Lasercar in [#5248](https://github.com/FunkinCrew/Funkin/pull/5248)
|
||||
- Freeplay song ranks no longer disappear after changing variations. ([7cc9464](https://github.com/FunkinCrew/Funkin/commit/7cc9464573d07996e4bd0d557f82847809d3a786)) - by @VioletSnowLeopard in [#4583](https://github.com/FunkinCrew/Funkin/pull/4583)
|
||||
- Freeplay song capsules now cycle through long names consistently. ([e193f73](https://github.com/FunkinCrew/Funkin/commit/e193f7392a83a04e4aac85fba4f441a78f7b6668)) - by @VioletSnowLeopard in [#4677](https://github.com/FunkinCrew/Funkin/pull/4677)
|
||||
- Fixed a few visual issues with Freeplay's rank slam animation. ([ab817bb](https://github.com/FunkinCrew/Funkin/commit/ab817bb1eab74ae71c1b1fd74d7512a11e6d4339)) - by @Lasercar in [#4986](https://github.com/FunkinCrew/Funkin/pull/4986)
|
||||
- Fixed visual errors in Freeplay after exiting Character Select. ([56a18e1](https://github.com/FunkinCrew/Funkin/commit/56a18e1cf6a15971feebcc828be8818037948cef)) - by @KoloInDaCrib in [#5245](https://github.com/FunkinCrew/Funkin/pull/5245)
|
||||
- Freeplay styles are now reloaded when hot-reloading with F5. ([f54e140](https://github.com/FunkinCrew/Funkin/commit/f54e140b65e36fcf810c52ce464799dbc0c73c6d)) - by @Keoiki in [#5286](https://github.com/FunkinCrew/Funkin/pull/5286)
|
||||
- Adjusted offsets for Freeplay DJ Pico's fistPump animation. ([382e286](https://github.com/FunkinCrew/funkin.assets/commit/382e286a2478939d3d6aca1c7c90719f33815014)) - by @AbnormalPoof in [funkin.assets#91](https://github.com/FunkinCrew/funkin.assets/pull/91)
|
||||
- The "New Highscore" text no longer appears more than once in the Results screen. ([4e31003](https://github.com/FunkinCrew/Funkin/commit/4e31003a0f60cd394edc2bd4586f9f385b3d07bc)) - by @Lasercar in [#4319](https://github.com/FunkinCrew/Funkin/pull/4319)
|
||||
- The Results Debug menu now shows the correct rank after recent scoring changes. ([11d9998](https://github.com/FunkinCrew/Funkin/commit/11d9998e5c73a3839ea39e06b7f217cbaab69c6d)) - by @NotHyper-474 in [#4905](https://github.com/FunkinCrew/Funkin/pull/4905)
|
||||
- The Input Offsets menu now exits to the Options menu instead of the Main menu. ([5361df2](https://github.com/FunkinCrew/Funkin/commit/5361df254470e68d7571b4534cf456f08d5ffd60)) - by @JackXson-Real in [#5076](https://github.com/FunkinCrew/Funkin/pull/5076)
|
||||
- The Debug Menu can no longer be opened after selecting an item in the Main Menu.
|
||||
([5695bc2](https://github.com/FunkinCrew/Funkin/commit/5695bc20e721f0afd5b97a36167f13e550c12b16)) - by @Lasercar in [#4211](https://github.com/FunkinCrew/Funkin/pull/4211)
|
||||
- The debug cursor is now always hidden when the game starts. ([6222c38](https://github.com/FunkinCrew/Funkin/commit/6222c389e301fa2bb4939697376c4e51e29a9977)) - by @Hundrec in [#4520](https://github.com/FunkinCrew/Funkin/pull/4520)
|
||||
- The Story Mode Weekend 1 level title no longer clips into other level titles below it. ([19d1a8c](https://github.com/FunkinCrew/Funkin/commit/19d1a8c59380009f0d0814c94fd0b4eccb0c80cd)) - by @KoloInDaCrib in [#4348](https://github.com/FunkinCrew/Funkin/pull/4348)
|
||||
- Hold note covers now display properly if a hold note was previously dropped. ([96d1324](https://github.com/FunkinCrew/Funkin/commit/96d1324af140858cb93edd07dc36a969c8ae84c0)) - by @T5mpler in [#5275](https://github.com/FunkinCrew/Funkin/pull/5275)
|
||||
- Girlfriend's and Nene's combo drop animations now play consistently. ([34d5ed1](https://github.com/FunkinCrew/Funkin/commit/34d5ed11695cef7348c13505f13fb1da38b7988c)) - by @VioletSnowLeopard in [#4968](https://github.com/FunkinCrew/Funkin/pull/4968)
|
||||
- Girlfriend (Tankman Stickup) now plays her combo drop animation. ([e329601](https://github.com/FunkinCrew/funkin.assets/commit/e329601834f270910ce80ea5539e4487b9895a8a)) - by @qt2k4 in [funkin.assets#149](https://github.com/FunkinCrew/funkin.assets/pull/149)
|
||||
- Darnell's idle animation now loops consistently. ([df64586](https://github.com/FunkinCrew/funkin.assets/commit/df64586771ea41c544913e049fd0f32bdc655417)) - by @qt2k4 in [funkin.assets#159](https://github.com/FunkinCrew/funkin.assets/pull/159)
|
||||
- Darnell's kneeCan animation now plays properly in 2hot. ([a2e9931](https://github.com/FunkinCrew/funkin.assets/commit/a2e993167aaa2ff6bff8806940f912e444608645)) - by @biomseed in [funkin.assets#78](https://github.com/FunkinCrew/funkin.assets/pull/78)
|
||||
- Otis and Pico (Speaker) no longer spaz out when playtesting Stress. ([3f6d75f](https://github.com/FunkinCrew/funkin.assets/commit/3f6d75f3b6f6c8deb660323e3fb9bf1974c06520)) - by @Lasercar in [funkin.assets#124](https://github.com/FunkinCrew/funkin.assets/pull/124)
|
||||
- The gasp sound now only plays once in the Week 3 Pico Mix doppelganger cutscene. ([ab4598b](https://github.com/FunkinCrew/funkin.assets/commit/ab4598baf3c6790d30cfb727d04c8b57fa18dd0d)) - by @KoloInDaCrib in [funkin.assets#126](https://github.com/FunkinCrew/funkin.assets/pull/126)
|
||||
- Fixed a few issues with the train in Week 3. ([8db2426](https://github.com/FunkinCrew/funkin.assets/commit/8db2426991caebde44d19c81b198e8e2ad86f700)) - by @ShadzXD in [funkin.assets#180](https://github.com/FunkinCrew/funkin.assets/pull/180)
|
||||
- The cars in Week 4 and Weekend 1 no longer get stuck when the song is restarted. ([9c511e3](https://github.com/FunkinCrew/funkin.assets/commit/9c511e371fd08a43ddf834766a6d35667bdef4f7)) - by @MetaBreeze in [funkin.assets#186](https://github.com/FunkinCrew/funkin.assets/pull/186)
|
||||
- A-Bot's visualizer no longer jumps to a random volume when the song ends. ([51cc118](https://github.com/FunkinCrew/funkin.assets/commit/51cc1186bc77a2ee45a47fdbde2d75d9ec69de3a)) - by @VioletSnowLeopard in [funkin.assets#183](https://github.com/FunkinCrew/funkin.assets/pull/183)
|
||||
- Pico's burpShit animation now re-enables volume for player vocals. ([cefda0e](https://github.com/FunkinCrew/funkin.assets/commit/cefda0e52fabbbe04afe60b7aed560267e2cb01e)) - by @Hundrec in [funkin.assets#71](https://github.com/FunkinCrew/funkin.assets/pull/71)
|
||||
- Removed vocals from Monster's instrumental on web builds. ([1c9473f](https://github.com/FunkinCrew/funkin.assets/commit/1c9473f3dfdfb97d97f6c8457001055322abf5ab)) - by @JVNpixels in [funkin.assets#182](https://github.com/FunkinCrew/funkin.assets/pull/182)
|
||||
- Fixed the retry sound not playing after a Tankman death quote finishes. ([e7c4b1b](https://github.com/FunkinCrew/Funkin/commit/e7c4b1ba38ba0739cfe347f6c4763f9811fb95b0)) - by @VioletSnowLeopard in [#4726](https://github.com/FunkinCrew/Funkin/pull/4726)
|
||||
- Darnell (BF Mix)'s alternate instrumental is now properly accessible. ([5abdabf](https://github.com/FunkinCrew/funkin.assets/commit/5abdabf69b39ca4eebd36ae6bbd77fab736d0b86)) - by @Hundrec in [funkin.assets#168](https://github.com/FunkinCrew/funkin.assets/pull/168)
|
||||
- Fixed Newgrounds score submissions for Lit Up and Lit Up (BF Mix). ([183cec6](https://github.com/FunkinCrew/Funkin/commit/183cec62dc1fd3c7f3f634dd3e2400e6ee77b476)) - by @Raltyro in [#4577](https://github.com/FunkinCrew/Funkin/pull/4577)
|
||||
- Inputs are now disabled before Senpai's dialogue appears. ([c43d906](https://github.com/FunkinCrew/funkin.assets/commit/c43d906d19d91d71b9096e65d5e0d3543af8cd31)) - by @anysad in [funkin.assets#165](https://github.com/FunkinCrew/funkin.assets/pull/165)
|
||||
- An easter egg now restarts the song using the correct instrumental. ([e657bc9](https://github.com/FunkinCrew/Funkin/commit/e657bc900bc62cc220276dc171dd47f0a176ac66)) - by @KoloInDaCrib in [#4956](https://github.com/FunkinCrew/Funkin/pull/4956)
|
||||
- Encountering an easter egg during a Chart Editor playtest no longer crashes the game. ([b53b5bd](https://github.com/FunkinCrew/funkin.assets/commit/b53b5bdaecf975555538725a4cdfe71d38565b08)) - by @NotHyper-474 in [funkin.assets#133](https://github.com/FunkinCrew/funkin.assets/pull/133)
|
||||
- Nonexistent characters no longer crash the Chart Editor. ([3bbb4b0](https://github.com/FunkinCrew/Funkin/commit/3bbb4b06c8c1a1885a18d4354fcfa4363a0c6c75)) - by @Lasercar in [#5008](https://github.com/FunkinCrew/Funkin/pull/5008)
|
||||
- Holding Ctrl and clicking on a hold note trail no longer crashes the Chart Editor. ([dc56cca](https://github.com/FunkinCrew/Funkin/commit/dc56ccada50e671996caf0557de1977b7ff8d236)) - by @Lasercar in [#4203](https://github.com/FunkinCrew/Funkin/pull/4203)
|
||||
- Tweens and timers are now canceled when returning to the Chart Editor. ([7e76cf6](https://github.com/FunkinCrew/Funkin/commit/7e76cf66340c00ef6ec84358e6304d62815173b6)) - by @KoloInDaCrib in [#5278](https://github.com/FunkinCrew/Funkin/pull/5278)
|
||||
- Reduced the severity of a memory leak in the Chart Editor. ([cce8c18](https://github.com/FunkinCrew/Funkin/commit/cce8c18822e083910200597f5db4d87b6e3b521f)) - by @NotHyper-474 in [#5247](https://github.com/FunkinCrew/Funkin/pull/5247)
|
||||
- Pressing the Chart Editor keybind during a song now opens to the variation and difficulty you were playing. ([e3fca16](https://github.com/FunkinCrew/Funkin/commit/e3fca167938642bea85398fe57347c10439c1892)) - by @Lasercar in [#4116](https://github.com/FunkinCrew/Funkin/pull/4116)
|
||||
- The Chart Editor now properly saves audio levels when exiting. ([f78ab4d](https://github.com/FunkinCrew/Funkin/commit/f78ab4da1db4f9527a2e1715d5cfb37670e11a74)) - by @Lasercar in [#4149](https://github.com/FunkinCrew/Funkin/pull/4149)
|
||||
- The Chart Editor "Load Metadata File" and "Load Chart File" buttons now function properly. ([9df5395](https://github.com/FunkinCrew/Funkin/commit/9df5395ff888cb6740e21204c8e19116e0472db4)) - by @Lasercar in [#4278](https://github.com/FunkinCrew/Funkin/pull/4278)
|
||||
- FNF Legacy files can now be opened in the Chart Editor on MacOS. ([d98628c](https://github.com/FunkinCrew/Funkin/commit/d98628ca0f9f60357715bd7f95fc686a83201209)) - by @AbnormalPoof in [#4580](https://github.com/FunkinCrew/Funkin/pull/4580)
|
||||
- The Chart Editor now consistently displays the correct waveform for vocal tracks. ([c0e0523](https://github.com/FunkinCrew/Funkin/commit/c0e0523651e8aaaae2a0eed6d5fef6c5ef1b7315)) - by @NotHyper-474 in [#5231](https://github.com/FunkinCrew/Funkin/pull/5231)
|
||||
- Fixed selection boxes duplicating in the Chart Editor. ([65ed583](https://github.com/FunkinCrew/Funkin/commit/65ed58350b798bca0044603510540cfe81b48611)) - by @NotHyper-474 in [#5073](https://github.com/FunkinCrew/Funkin/pull/5073)
|
||||
- Fixed the Chart Editor timer occasionally displaying incorrect millisecond values. ([26dc895](https://github.com/FunkinCrew/Funkin/commit/26dc895a27e0d7e49469251cd68b83be66384e15)) - by @Hundrec in [#4257](https://github.com/FunkinCrew/Funkin/pull/4257)
|
||||
- The Chart Editor playhead can no longer be scrolled to before the beginning of the song. ([7c7dc11](https://github.com/FunkinCrew/Funkin/commit/7c7dc11f18644882444df97ac927e11adaa4ce50)) - by @Hundrec in [#5024](https://github.com/FunkinCrew/Funkin/pull/5024)
|
||||
- The Chart Editor playbar no longer extends past the right of the grid. ([c7abb19](https://github.com/FunkinCrew/Funkin/commit/c7abb196989476cfa4db6354cc6fac5bacb2e56a)) - by @anysad in [#5090](https://github.com/FunkinCrew/Funkin/pull/5090)
|
||||
- Dragging a hold note in the Chart Editor now drags its trail along with its head. ([d3d8aaa](https://github.com/FunkinCrew/Funkin/commit/d3d8aaae7bfbfa8975a9573807b9a3ca68a1ff55)) - by @KoloInDaCrib in [#4127](https://github.com/FunkinCrew/Funkin/pull/4127)
|
||||
- Hold note trails will no longer disappear when dragged too far in the Chart Editor. ([37dc66b](https://github.com/FunkinCrew/Funkin/commit/37dc66bc189bc1941cf60587e1c068770aeec872)) - by @NotHyper-474 in
|
||||
[#5261](https://github.com/FunkinCrew/Funkin/pull/5261)
|
||||
- Undoing and redoing hold note length changes now visually updates the trail in the Chart Editor. ([06a440f](https://github.com/FunkinCrew/Funkin/commit/06a440f21c285666ce2d2bdf17a91ee82ab01061)) - by @NotHyper-474 in [#5265](https://github.com/FunkinCrew/Funkin/pull/5265)
|
||||
- The Chart Editor hold note context menu now displays the correct options. ([4801316](https://github.com/FunkinCrew/Funkin/commit/48013168ef09ddc09549268a2e5309520d3fcc18)) - by @Lasercar in [#4231](https://github.com/FunkinCrew/Funkin/pull/4231)
|
||||
- The buttons in the Chart Editor context menu for selections now do the right thing. ([62d24fc](https://github.com/FunkinCrew/Funkin/commit/62d24fcf4cbe7be328a995bad04f3eeb265262c5)) - by @Lasercar in [#4233](https://github.com/FunkinCrew/Funkin/pull/4233)
|
||||
- The charter field in the song metadata now properly displays the charter in the Chart Editor. ([894d8cb](https://github.com/FunkinCrew/Funkin/commit/894d8cb4637fd0a64359a5edc805a23403f3045c)) - by @Lasercar in [#4879](https://github.com/FunkinCrew/Funkin/pull/4879)
|
||||
- Chart Editor difficulties are now sorted in a consistent order. ([7aa77a1](https://github.com/FunkinCrew/Funkin/commit/7aa77a11cf7508fc05758045a13b2220aca96dd5)) - by @Lasercar in [#4528](https://github.com/FunkinCrew/Funkin/pull/4528)
|
||||
- Chart Editor and Stage Editor windows now consistently show a close button. ([b23b7b8](https://github.com/FunkinCrew/funkin.assets/commit/b23b7b81d843c8fa2334fbdbe1ef979633956bb9)) - by @Lasercar in [funkin.assets#121](https://github.com/FunkinCrew/funkin.assets/pull/121)
|
||||
- The Chart Editor playbar's font size no longer becomes too small. ([f9c1f7a](https://github.com/FunkinCrew/Funkin/commit/f9c1f7a5f7d3906155c2004ad2ec1d02556b9730)) - by @KoloInDaCrib in [#5253](https://github.com/FunkinCrew/Funkin/pull/5253)
|
||||
- The Chart Editor copy notification no longer chases the mouse cursor. ([0ea42e1](https://github.com/FunkinCrew/Funkin/commit/0ea42e18e93b0b57f1ae5499d70c4681f191dacb)) - by @KoloInDaCrib in [#4029](https://github.com/FunkinCrew/Funkin/pull/4029)
|
||||
- Changed "Tankman Battlefield (Erect)" to "Tankman Battlefield [Erect]" in the Chart Editor for consistency. ([52852a0](https://github.com/FunkinCrew/funkin.assets/commit/52852a02f60df81c281bfd0e49fd8fa09e118409)) - by @JVNpixels in [funkin.assets#155](https://github.com/FunkinCrew/funkin.assets/pull/155)
|
||||
- Fixed Pico (Pixel) having the incorrect name in the Chart Editor. ([3ff0e9c](https://github.com/FunkinCrew/funkin.assets/commit/3ff0e9ca4f8cdc5734c335f2d7d72fa310686d46)) - by @ExtraCode75 in [funkin.assets#158](https://github.com/FunkinCrew/funkin.assets/pull/158)
|
||||
- Fixed various issues and added missing functionalities to the Stage Editor. ([a776ce1](https://github.com/FunkinCrew/Funkin/commit/a776ce1a81a539f75f8bd2220a9768c03d347058)) - by @KoloInDaCrib in [#3974](https://github.com/FunkinCrew/Funkin/pull/3974)
|
||||
- Stage Editor windows are now able to be closed. ([65461d8](https://github.com/FunkinCrew/Funkin/commit/65461d839b3764659aab33f1a35edf64ee514952)) - by @Lasercar in [#5238](https://github.com/FunkinCrew/Funkin/pull/5238)
|
||||
- Fixed duplicate exit prompts appearing in the Stage Editor. ([136a5df](https://github.com/FunkinCrew/Funkin/commit/136a5dfad430b461a909be3a7e89f46c8be1d3b3)) - by @Lasercar in [#5239](https://github.com/FunkinCrew/Funkin/pull/5239)
|
||||
- The help guide in the Stage Editor can no longer be opened multiple times. ([564d679](https://github.com/FunkinCrew/Funkin/commit/564d679f969c500834426ce3cf50507091d69697)) - by @Lasercar in [#4128](https://github.com/FunkinCrew/Funkin/pull/4128)
|
||||
- Removed a spammy console trace from Spooky Kids (Dark). ([5935a61](https://github.com/FunkinCrew/funkin.assets/commit/5935a61dd71f51e6264dcd039f1164cd1ff295ef)) - by @NotHyper-474 in [funkin.assets#187](https://github.com/FunkinCrew/funkin.assets/pull/187)
|
||||
- Removed spammy console traces from `DiscordClient`. ([e89f9f5](https://github.com/FunkinCrew/Funkin/commit/e89f9f50dc6085e3550736459ce9e4fd02c1fc5b)) - by @AbnormalPoof in [#4207](https://github.com/FunkinCrew/Funkin/pull/4207)
|
||||
- Removed a spammy console trace from some Chart Editor events. ([b883ad3](https://github.com/FunkinCrew/Funkin/commit/b883ad3d50b990e605d7b79d042292c7371db5f0)) - by @anysad in [#5097](https://github.com/FunkinCrew/Funkin/pull/5097)
|
||||
- ANSI colors now display in the console on more computers. ([3747b94](https://github.com/FunkinCrew/Funkin/commit/3747b942461c3644fe31f25d2ce847fd74d1b1e0)) - by @AbnormalPoof in [#4676](https://github.com/FunkinCrew/Funkin/pull/4676)
|
||||
- Properly blacklist a certain class from scripts. ([3dc7699](https://github.com/FunkinCrew/Funkin/commit/3dc7699aac737998b637ff9a9f16986a434424be)) - by @charlesisfeline in [#4773](https://github.com/FunkinCrew/Funkin/pull/4773)
|
||||
|
||||
### Removed
|
||||
|
||||
- Removed the VSync preference from web builds, where it's non-functional. ([0b7a94b](https://github.com/FunkinCrew/Funkin/commit/0b7a94b1cc5c5d0dbac5a4c3d595349c4e6eb6e4)) - by @NotHyper-474 in [#5062](https://github.com/FunkinCrew/Funkin/pull/5062)
|
||||
- Removed a few non-functional screenshot preferences. ([93e4f79](https://github.com/FunkinCrew/Funkin/commit/93e4f799f4f0ea435b3d77ca8ef2ab6beeb0a955)) - by @Lasercar in [#4895](https://github.com/FunkinCrew/Funkin/pull/4895)
|
||||
|
||||
## New Contributors for 0.7.0
|
||||
|
||||
* @KutikiPlayz made their first contribution in [#3544](https://github.com/FunkinCrew/Funkin/pull/3544)
|
||||
* @xenkap made their first contribution in [#3732](https://github.com/FunkinCrew/Funkin/pull/3732)
|
||||
* @Raltyro made their first contribution in [#4577](https://github.com/FunkinCrew/Funkin/pull/4577)
|
||||
* @charlesisfeline made their first contribution in [#4773](https://github.com/FunkinCrew/Funkin/pull/4773)
|
||||
* @T5mpler made their first contribution in [#5275](https://github.com/FunkinCrew/Funkin/pull/5275)
|
||||
* @biomseed made their first contribution in [funkin.assets#78](https://github.com/FunkinCrew/funkin.assets/pull/78)
|
||||
* @qt2k4 made their first contribution in [funkin.assets#149](https://github.com/FunkinCrew/funkin.assets/pull/149)
|
||||
* @ExtraCode75 made their first contribution in [funkin.assets#158](https://github.com/FunkinCrew/funkin.assets/pull/158)
|
||||
* @MetaBreeze made their first contribution in [funkin.assets#186](https://github.com/FunkinCrew/funkin.assets/pull/186)
|
||||
|
||||
|
||||
|
||||
## [0.6.4] - 2025-05-02
|
||||
|
@ -745,7 +1042,7 @@ The Weekend 1 update!
|
|||
- Improvements to video cutscenes and dialogue, allowing them to be easily skipped or restarted.
|
||||
- Updated Polymod by several major versions, allowing for fully dynamic asset replacement and support for scripted classes.
|
||||
- Completely refactored almost every part of the game's code for performance, stability, and extensibility.
|
||||
- This is not the Ludum Dare game held together with sticks and glue you played three years ago.
|
||||
- This is not the Ludem Dare game held together with sticks and glue you played three years ago.
|
||||
- Characters, stages, songs, story levels, and dialogue are now built from JSON data registries rather than being hardcoded.
|
||||
- All of these also support attaching scripts for custom behavior, more documentation on this soon.
|
||||
- You can forcibly reload the game's JSON data and scripts by pressing F5.
|
||||
|
|
28
LaunchScreen.storyboard
Normal file
28
LaunchScreen.storyboard
Normal file
|
@ -0,0 +1,28 @@
|
|||
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
||||
<document type="com.apple.InterfaceBuilder3.CocoaTouch.Storyboard.XIB" version="3.0" toolsVersion="11134" systemVersion="15F34" targetRuntime="iOS.CocoaTouch" propertyAccessControl="none" useAutolayout="YES" launchScreen="YES" useTraitCollections="YES" colorMatched="YES" initialViewController="01J-lp-oVM">
|
||||
<dependencies>
|
||||
<plugIn identifier="com.apple.InterfaceBuilder.IBCocoaTouchPlugin" version="11106"/>
|
||||
<capability name="documents saved in the Xcode 8 format" minToolsVersion="8.0"/>
|
||||
</dependencies>
|
||||
<scenes>
|
||||
<!--View Controller-->
|
||||
<scene sceneID="EHf-IW-A2E">
|
||||
<objects>
|
||||
<viewController id="01J-lp-oVM" sceneMemberID="viewController">
|
||||
<simulatedScreenMetrics key="simulatedMetrics" type="freeform" size="375x667"/>
|
||||
<layoutGuides>
|
||||
<viewControllerLayoutGuide type="top" id="Llm-lL-Icb"/>
|
||||
<viewControllerLayoutGuide type="bottom" id="xb3-aO-Qok"/>
|
||||
</layoutGuides>
|
||||
<view key="view" contentMode="scaleToFill" id="Ze5-6b-2t3">
|
||||
<rect key="frame" x="0.0" y="0.0" width="375" height="667"/>
|
||||
<autoresizingMask key="autoresizingMask" widthSizable="YES" heightSizable="YES"/>
|
||||
<color key="backgroundColor" red="0" green="0" blue="0" alpha="1" colorSpace="custom" customColorSpace="sRGB"/>
|
||||
</view>
|
||||
</viewController>
|
||||
<placeholder placeholderIdentifier="IBFirstResponder" id="iYj-Kq-Ea1" userLabel="First Responder" sceneMemberID="firstResponder"/>
|
||||
</objects>
|
||||
<point key="canvasLocation" x="53" y="375"/>
|
||||
</scene>
|
||||
</scenes>
|
||||
</document>
|
103
README.md
103
README.md
|
@ -1,48 +1,55 @@
|
|||
# Friday Night Funkin'
|
||||
|
||||
Friday Night Funkin' is a rhythm game. Built using HaxeFlixel for Ludum Dare 47.
|
||||
|
||||
This game was made with love to Newgrounds and its community. Extra love to Tom Fulp.
|
||||
|
||||
- [Playable web demo on Newgrounds!](https://www.newgrounds.com/portal/view/770371)
|
||||
- [Demo download builds for Windows, Mac, and Linux from Itch.io!](https://ninja-muffin24.itch.io/funkin)
|
||||
|
||||
# Getting Started
|
||||
|
||||
**PLEASE USE THE LINKS ABOVE IF YOU JUST WANT TO PLAY THE GAME**
|
||||
|
||||
To learn how to install the necessary dependencies and compile the game from source, please follow our [Compiling Guide](/docs/COMPILING.md).
|
||||
|
||||
# Contributing
|
||||
|
||||
Check out our [Contributing Guide](/docs/CONTRIBUTING.md) to learn how you can actively contribute to the development of Friday Night Funkin'!
|
||||
|
||||
# Modding
|
||||
|
||||
Feel free to start learning to mod the game by reading our [documentation](https://funkincrew.github.io/funkin-modding-docs/) and guide to modding.
|
||||
|
||||
# Credits and Special Thanks
|
||||
|
||||
Full credits can be found in-game, or in the `credits.json` file which is located [here](https://github.com/FunkinCrew/funkin.assets/blob/main/exclude/data/credits.json).
|
||||
|
||||
## Programming
|
||||
- [ninjamuffin99](https://twitter.com/ninja_muffin99) - Lead Programmer
|
||||
- [EliteMasterEric](https://twitter.com/EliteMasterEric) - Programmer
|
||||
- [MtH](https://twitter.com/emmnyaa) - Charting and Additional Programming
|
||||
- [GeoKureli](https://twitter.com/Geokureli/) - Additional Programming
|
||||
- Our contributors on GitHub
|
||||
|
||||
## Art / Animation / UI
|
||||
- [PhantomArcade3K](https://twitter.com/phantomarcade3k) - Artist and Animator
|
||||
- [Evilsk8r](https://twitter.com/evilsk8r) - Art
|
||||
- [Moawling](https://twitter.com/moawko) - Week 6 Pixel Art
|
||||
- [IvanAlmighty](https://twitter.com/IvanA1mighty) - Misc UI Design
|
||||
|
||||
## Music
|
||||
- [Kawaisprite](https://twitter.com/kawaisprite) - Musician
|
||||
- [BassetFilms](https://twitter.com/Bassetfilms) - Music for "Monster", Additional Character Design
|
||||
|
||||
## Special Thanks
|
||||
- [Tom Fulp](https://twitter.com/tomfulp) - For being a great guy and for Newgrounds
|
||||
- [JohnnyUtah](https://twitter.com/JohnnyUtahNG/) - Voice of Tankman
|
||||
- [L0Litsmonica](https://twitter.com/L0Litsmonica) - Voice of Mommy Mearest
|
||||
# Friday Night Funkin'
|
||||
|
||||
Friday Night Funkin' is a rhythm game. Built using HaxeFlixel for Ludum Dare 47.
|
||||
|
||||
This game was made with love to Newgrounds and its community. Extra love to Tom Fulp.
|
||||
|
||||
- [Playable web demo on Newgrounds!](https://www.newgrounds.com/portal/view/770371)
|
||||
- [Demo download builds for Windows, Mac, and Linux from Itch.io!](https://ninja-muffin24.itch.io/funkin)
|
||||
- [Download Android builds from Google Play!](https://play.google.com/store/apps/details?id=me.funkin.fnf)
|
||||
- [Download iOS builds from the App Store!](https://apps.apple.com/app/id6740428530)
|
||||
|
||||
# Getting Started
|
||||
|
||||
**PLEASE USE THE LINKS ABOVE IF YOU JUST WANT TO PLAY THE GAME**
|
||||
|
||||
To learn how to install the necessary dependencies and compile the game from source, please follow our [Compiling Guide](/docs/COMPILING.md).
|
||||
|
||||
# Contributing
|
||||
|
||||
Check out our [Contributing Guide](/docs/CONTRIBUTING.md) to learn how you can actively contribute to the development of Friday Night Funkin'!
|
||||
|
||||
# Modding
|
||||
|
||||
Feel free to start learning to mod the game by reading our [documentation](https://funkincrew.github.io/funkin-modding-docs/) and guide to modding.
|
||||
|
||||
# Credits and Special Thanks
|
||||
|
||||
Full credits can be found in-game, or in the `credits.json` file which is located [here](https://github.com/FunkinCrew/funkin.assets/blob/main/exclude/data/credits.json).
|
||||
|
||||
## Programming
|
||||
- [ninjamuffin99](https://twitter.com/ninja_muffin99) - Lead Programmer
|
||||
- [EliteMasterEric](https://twitter.com/EliteMasterEric) - Programmer
|
||||
- [MtH](https://twitter.com/emmnyaa) - Charting and Additional Programming
|
||||
- [GeoKureli](https://twitter.com/Geokureli/) - Additional Programming
|
||||
- [ZackDroid](https://x.com/ZackDroidCoder) - Lead Mobile Programmer
|
||||
- [MAJigsaw77](https://github.com/MAJigsaw77) - Mobile Programmer
|
||||
- [Karim-Akra](https://x.com/KarimAkra_0) - Mobile Programmer
|
||||
- [Sector_5](https://github.com/sector-a) - Mobile Programmer
|
||||
- [Luckydog7](https://github.com/luckydog7) - Mobile Programmer
|
||||
- Our contributors on GitHub
|
||||
|
||||
## Art / Animation / UI
|
||||
- [PhantomArcade3K](https://twitter.com/phantomarcade3k) - Artist and Animator
|
||||
- [Evilsk8r](https://twitter.com/evilsk8r) - Art
|
||||
- [Moawling](https://twitter.com/moawko) - Week 6 Pixel Art
|
||||
- [IvanAlmighty](https://twitter.com/IvanA1mighty) - Misc UI Design
|
||||
|
||||
## Music
|
||||
- [Kawaisprite](https://twitter.com/kawaisprite) - Musician
|
||||
- [BassetFilms](https://twitter.com/Bassetfilms) - Music for "Monster", Additional Character Design
|
||||
|
||||
## Special Thanks
|
||||
- [Tom Fulp](https://twitter.com/tomfulp) - For being a great guy and for Newgrounds
|
||||
- [JohnnyUtah](https://twitter.com/JohnnyUtahNG/) - Voice of Tankman
|
||||
- [L0Litsmonica](https://twitter.com/L0Litsmonica) - Voice of Mommy Mearest
|
||||
|
|
14
alsoft.txt
14
alsoft.txt
|
@ -1,14 +0,0 @@
|
|||
[general]
|
||||
sample-type=float32
|
||||
period_size=441
|
||||
stereo-mode=speakers
|
||||
stereo-encoding=panpot
|
||||
hrtf=false
|
||||
cf_level=0
|
||||
resampler=fast_bsinc24
|
||||
front-stablizer=false
|
||||
output-limiter=false
|
||||
[decoder]
|
||||
hq-mode=false
|
||||
distance-comp=false
|
||||
nfc=false
|
2
art
2
art
|
@ -1 +1 @@
|
|||
Subproject commit 78dc310219370144719b4eeef9b3b511c5a44532
|
||||
Subproject commit 490e97f4c6e673a52ee4f9af98325b1aa2d0c3fe
|
2
assets
2
assets
|
@ -1 +1 @@
|
|||
Subproject commit 88778a44853255b082ada57e9cb77d8fb4956494
|
||||
Subproject commit a8d15febf5e37a4fc7ffeb0d3eff9c4d36457a37
|
|
@ -70,9 +70,10 @@
|
|||
},
|
||||
{
|
||||
"props": {
|
||||
"severity": "IGNORE",
|
||||
|
||||
"policy": "aligned",
|
||||
"allowSingleline": true,
|
||||
"severity": "INFO"
|
||||
"allowSingleline": true
|
||||
},
|
||||
"type": "ConditionalCompilation"
|
||||
},
|
||||
|
|
|
@ -87,4 +87,3 @@ filter_commits = false
|
|||
topo_order = false
|
||||
# sort the commits inside sections by oldest/newest order
|
||||
sort_commits = "newest"
|
||||
|
||||
|
|
21
compression-excludes.txt
Normal file
21
compression-excludes.txt
Normal file
|
@ -0,0 +1,21 @@
|
|||
assets/preload/images/cursor/*
|
||||
assets/preload/images/freeplay/*
|
||||
assets/preload/images/icons/*
|
||||
assets/preload/images/titleEnter.png
|
||||
assets/preload/images/titleEnter_mobile.png
|
||||
assets/preload/images/soundtray/*
|
||||
assets/preload/images/stageBuild/*
|
||||
assets/preload/images/ui/popup/pixel/
|
||||
assets/shared/images/characters/abotPixel/
|
||||
assets/shared/images/characters/bfPixel.png
|
||||
assets/shared/images/characters/bfPixelsDEAD.png
|
||||
assets/shared/images/characters/gfPixel.png
|
||||
assets/shared/images/characters/nenePixel/
|
||||
assets/shared/images/characters/picoPixel/
|
||||
assets/shared/images/characters/senpai.png
|
||||
assets/shared/images/characters/spirit.png
|
||||
assets/shared/images/resultScreen/*
|
||||
assets/shared/images/ui/chart-editor/*
|
||||
assets/shared/images/ui/countdown/pixel/
|
||||
assets/week1/
|
||||
assets/week6/*
|
|
@ -40,8 +40,8 @@ There are several useful build flags you can add to a build to affect how it wor
|
|||
- This feature causes the game to load exported assets from the project's assets folder rather than the exported one. Great for fast iteration, but the game will break if you try to zip it up and send it to someone, so it's disabled for release builds.
|
||||
- `-DFEATURE_DISCORD_RPC` or `-DNO_FEATURE_DISCORD_RPC` to forcibly enable or disable support for Discord Rich Presence.
|
||||
- `-DFEATURE_VIDEO_PLAYBACK` or `-DNO_FEATURE_VIDEO_PLAYBACK` to forcibly enable or disable video cutscene support.
|
||||
- `-DFEATURE_CHART_EDITOR` or `-DNO_FEATURE_CHART_EDITOR` to forcibly enable or disable the chart editor in the Debug menu.
|
||||
- `-DFEATURE_SCREENSHOTS` or `-DNO_FEATURE_SCREENSHOTS` to forcibly enable or disable the screenshots feature.
|
||||
- `-DFEATURE_CHART_EDITOR` or `-DNO_FEATURE_CHART_EDITOR` to forcibly enable or disable the chart editor in the Debug menu.
|
||||
- `-DFEATURE_STAGE_EDITOR` to forcibly enable the experimental stage editor.
|
||||
- `-DFEATURE_GHOST_TAPPING` to forcibly enable an experimental gameplay change to the anti-mash system.
|
||||
|
||||
|
|
98
docs/COMPILING_MOBILE.md
Normal file
98
docs/COMPILING_MOBILE.md
Normal file
|
@ -0,0 +1,98 @@
|
|||
# Compiling Friday Night Funkin' for Mobile Devices
|
||||
|
||||
Before starting, **make sure your game builds on desktop.**
|
||||
Check [COMPILING.md](./COMPILING.md) if you haven’t done that yet.
|
||||
|
||||
## Android
|
||||
|
||||
0. **Create a new folder** – this will store Android tools (remember where you put it!).
|
||||
1. **Open a terminal as Administrator.**
|
||||
2. Run this in the terminal (replace the path with your actual folder):
|
||||
```bash
|
||||
setx ANDROID_HOME "C:\path\to\your\folder" /M
|
||||
```
|
||||
3. Download [Android Studio Command-line Tools](https://developer.android.com/studio#command-line-tools-only).
|
||||
4. Extract the ZIP into your folder from step 1.
|
||||
5. (Optional) Close and reopen the terminal if needed.
|
||||
6. Run:
|
||||
```bash
|
||||
sdkmanager --install "build-tools;35.0.0" "ndk;29.0.13113456" "platforms;android-29" "platforms;android-35"
|
||||
```
|
||||
- The latest NDK is not compatible with Lime you have to use the old one.
|
||||
7. Download and install [JDK 17 (MSI)](https://adoptium.net/temurin/releases/?version=17&os=windows).
|
||||
8. Run:
|
||||
```bash
|
||||
lime setup android
|
||||
```
|
||||
Use these when asked:
|
||||
- **Android SDK:** `C:\path\to\your\folder`
|
||||
- **Android NDK:** `C:\path\to\your\folder\ndk\29.0.13113456`
|
||||
- **JDK:** `C:\Program Files\Java\jdk-17`
|
||||
|
||||
9. Now build your game:
|
||||
```bash
|
||||
lime test android
|
||||
```
|
||||
|
||||
|
||||
### macOS
|
||||
|
||||
0. **Create a new folder** – this will store Android tools (remember where you put it!).
|
||||
1. Open **Terminal** (Command ⌘ + Space → type “terminal” → Enter).
|
||||
2. In Terminal:
|
||||
```bash
|
||||
cd /path/to/your/folder
|
||||
export ANDROID_HOME=/path/to/your/folder
|
||||
export PATH=$PATH:$ANDROID_HOME/cmdline-tools:$ANDROID_HOME/cmdline-tools/bin:$ANDROID_HOME/platform-tools
|
||||
```
|
||||
3. Download [Android Studio Command-line Tools](https://developer.android.com/studio#command-line-tools-only).
|
||||
4. Extract the ZIP into your folder from step 1.
|
||||
5. (Optional) Restart Terminal if needed.
|
||||
6. Run:
|
||||
```bash
|
||||
sdkmanager --install "build-tools;35.0.0" "ndk;29.0.13113456" "platforms;android-29" "platforms;android-35"
|
||||
```
|
||||
7. Download and install [JDK 17 for macOS](https://adoptium.net/temurin/releases/?os=mac&version=17).
|
||||
8. Run:
|
||||
```bash
|
||||
lime setup android
|
||||
```
|
||||
Use these when asked:
|
||||
- **Android SDK:** `/path/to/your/folder`
|
||||
- **Android NDK:** `/path/to/your/folder/ndk/28.0.13004108`
|
||||
- **JDK:** `/Library/Java/JavaVirtualMachines/temurin-17.jdk/Contents/Home`
|
||||
_(If not asked for JDK, don’t worry — just skip it.)_
|
||||
|
||||
9. Build your game:
|
||||
```bash
|
||||
lime test android
|
||||
```
|
||||
|
||||
## iOS
|
||||
Note that you can only build the game for iOS on a computer running MacOS.
|
||||
|
||||
0. Build the game for desktop to make sure everything works. Check [COMPILING.md](./COMPILING.md).
|
||||
1. Get Xcode from the app store on your MacOS Machine.
|
||||
2. Download the iPhone SDK (First thing that pops up in Xcode)
|
||||
3. Open up a terminal tab and run `lime test ios -xcode`
|
||||
4. You will need to sign your own copy in order to run the game with a real iOS device! That requires an Apple Developer account, sorry!
|
||||
- To run with an iOS simulator instead of `-xcode` use `-simulator`
|
||||
|
||||
### iOS Troubleshooting
|
||||
|
||||
- **A required plugin failed to load. Please ensure system content is up-to-date — try running 'xcodebuild -runFirstLaunch'.**
|
||||
Make sure you have the iOS SDK isntalled, see Step 2.
|
||||
|
||||
- **error: No Accounts: Add a new account in Accounts settings. (in target 'Funkin' from project 'Funkin')**
|
||||
|
||||
Open XCode, press CMD+, to open Settings, select Accounts, add an Apple ID.
|
||||
|
||||
- error: No Account for Team "Z7G7AVNGSH". Add a new account in Accounts settings or verify that your accounts have valid credentials.
|
||||
|
||||
Open `project.hxp` and change `IOS_TEAM_ID` to your personal team's ID.
|
||||
|
||||
- error: Failed Registering Bundle Identifier: The app identifier "me.funkin.fnf" cannot be registered to your development team because it is not available.
|
||||
|
||||
The Funkin' Crew are the only ones that can build an iOS app with the identifier `me.funkin.fnf`. Open `project.hxp` and change `PACKAGE_NAME` to a unique value.
|
||||
|
||||
- error: No profiles for 'me.funkin.fnf' were found: Xcode couldn't find any iOS App Development provisioning profiles matching 'me.funkin.fnf'
|
32
docs/INSTALLING_MODS.md
Normal file
32
docs/INSTALLING_MODS.md
Normal file
|
@ -0,0 +1,32 @@
|
|||
# HTML5/Web
|
||||
1. Nope
|
||||
|
||||
# Windows
|
||||
1. Start the game at least once. This will create a `mods` folder if it doesn't already exist, alongside the executable.
|
||||
2. Extract the mod you downloaded from its ZIP file, and place the mod folder into the game's `mods` folder.
|
||||
3. Restart the game. The game should detect the mod and start with it.
|
||||
|
||||
# Linux
|
||||
1. Start the game at least once. This will create a `mods` folder if it doesn't already exist, alongside the executable.
|
||||
2. Extract the mod you downloaded from its ZIP file, and place the mod folder into the game's `mods` folder.
|
||||
3. Restart the game. The game should detect the mod and start with it.
|
||||
|
||||
# MacOS
|
||||
1. Start the game at least once. This will create a `mods` folder if it doesn't already exist in the game's system files.
|
||||
2. Right click `Funkin.app` and select `Show Package Contents`.
|
||||
3. Navigate to `Contents/Resources/mods`.
|
||||
4. Extract the mod you downloaded from its ZIP file, and place the mod folder into the game's `mods` folder.
|
||||
5. Restart the game. The game should detect the mod and start with it.
|
||||
|
||||
# Android
|
||||
1. Start the game at least once. This will create a `mods` folder deep in your system files.
|
||||
2. Get an Android file browser that lets you view the app data files, or use Android Studio and open up the Device Explorer.
|
||||
3. Locate the `/sdcard/Android/obb/me.funkin.fnf/mods` folder.
|
||||
4. Extract the mod you downloaded from its ZIP file, and place the mod folder into the game's `mods` folder.
|
||||
5. Restart the game (you may have to [force close](https://support.google.com/android/answer/9079646?hl=en) the app first). The game should detect the mod and start with it.
|
||||
|
||||
# iOS
|
||||
1. Start the game at least once. This will create a `mods` folder in your system files.
|
||||
2. Open the Files app, and navigate to `On My iPhone` -> `Friday Night Funkin` -> `mods`.
|
||||
3. Extract the mod you downloaded from its ZIP file, and place the mod folder into the game's `mods` folder.
|
||||
4. Restart the game (you may have to [force close](https://support.apple.com/en-us/109359) the app first). The game should detect the mod and start with it.
|
66
docs/setup-android-win64.bat
Normal file
66
docs/setup-android-win64.bat
Normal file
|
@ -0,0 +1,66 @@
|
|||
@echo off
|
||||
|
||||
set ZIP_FILE="./temp/_temp_jdk.zip"
|
||||
set OUTPUT_DIR="./temp/"
|
||||
set SIX_LINK="https://drive.usercontent.google.com/download?id=1GqFpIk_bkxFb0tNN3x9LxnN-Zh_oDUX5&export=download&authuser=0&confirm=t&uuid=43108c0a-bd53-4465-86f3-80aaceaa7a38&at=APZUnTVNS_BV9cNyC_iicDInosmz%3A1718921284514"
|
||||
set EIGHT_LINK="https://drive.usercontent.google.com/download?id=1X8jjtYYos8aDfZKwehGS9B3zFQa-sCb-&export=download&authuser=0&confirm=t&uuid=07b24a6c-5352-4ba5-9fb8-cff151a6d91e&at=APZUnTUfw26NBAl0nCMn6HBKgHwK%3A1718922303598"
|
||||
|
||||
echo MAKING TEMP
|
||||
mkdir %OUTPUT_DIR%
|
||||
echo MADE TEMP
|
||||
|
||||
|
||||
|
||||
echo INSTALLING ANDROID BUILD TOOLS
|
||||
call .\asclt\bin\sdkmanager "build-tools;32.0.0" --sdk_root="%LOCALAPPDATA%/Android/Sdk/"
|
||||
call .\asclt\bin\sdkmanager "build-tools;32.1.0-rc1" --sdk_root="%LOCALAPPDATA%/Android/Sdk/"
|
||||
echo INSTALLED ANDROID BUILD TOOLS
|
||||
|
||||
|
||||
|
||||
echo INSTALLING ANDROID SDK
|
||||
REM First install the sdks
|
||||
call .\asclt\bin\sdkmanager "platforms;android-29" --sdk_root="%LOCALAPPDATA%/Android/Sdk/"
|
||||
echo ANDROID SDK INSTALLED
|
||||
|
||||
|
||||
|
||||
echo INSTALLING ANDROID NDK
|
||||
REM then the ndks
|
||||
call ./asclt/bin/sdkmanager "ndk;21.4.7075529" --sdk_root="%LOCALAPPDATA%/Android/Sdk/"
|
||||
echo ANDROID NDK INSTALLED
|
||||
|
||||
|
||||
|
||||
echo DOWNLOADING JDK
|
||||
call curl -o %ZIP_FILE% %SIX_LINK%
|
||||
echo DOWNLOADED JDK
|
||||
|
||||
|
||||
|
||||
echo UNZIPPING JDK
|
||||
call powershell -Command "Expand-Archive -Path '%ZIP_FILE%' -DestinationPath '%OUTPUT_DIR%' -Force"
|
||||
echo UNZIPPED JDK
|
||||
|
||||
|
||||
|
||||
echo MAKING JDK PATH
|
||||
mkdir "%LOCALAPPDATA%/Android/jdk"
|
||||
echo MADE JDK PATH
|
||||
|
||||
|
||||
|
||||
echo MOVING JDK TO PROPER PATH
|
||||
call move "%OUTPUT_DIR%/jdk-17.0.11+9" "%LOCALAPPDATA%/Android/jdk/"
|
||||
echo MOVED JDK
|
||||
|
||||
|
||||
|
||||
echo LIME SETTING UP
|
||||
haxelib run lime config ANDROID_SDK %LOCALAPPDATA%\Android\Sdk
|
||||
haxelib run lime config ANDROID_NDK_ROOT %LOCALAPPDATA%\Android\Sdk\ndk\21.4.7075529
|
||||
haxelib run lime config JAVA_HOME %LOCALAPPDATA%\Android\Sdk\jdk\jdk-17.0.11+9
|
||||
haxelib run lime config ANDROID_SETUP true
|
||||
echo DONE
|
||||
|
||||
pause
|
66
docs/setup-android-win86.bat
Normal file
66
docs/setup-android-win86.bat
Normal file
|
@ -0,0 +1,66 @@
|
|||
@echo off
|
||||
|
||||
set ZIP_FILE="./temp/_temp_jdk.zip"
|
||||
set OUTPUT_DIR="./temp/"
|
||||
set SIX_LINK="https://drive.usercontent.google.com/download?id=1GqFpIk_bkxFb0tNN3x9LxnN-Zh_oDUX5&export=download&authuser=0&confirm=t&uuid=43108c0a-bd53-4465-86f3-80aaceaa7a38&at=APZUnTVNS_BV9cNyC_iicDInosmz%3A1718921284514"
|
||||
set EIGHT_LINK="https://drive.usercontent.google.com/download?id=1X8jjtYYos8aDfZKwehGS9B3zFQa-sCb-&export=download&authuser=0&confirm=t&uuid=07b24a6c-5352-4ba5-9fb8-cff151a6d91e&at=APZUnTUfw26NBAl0nCMn6HBKgHwK%3A1718922303598"
|
||||
|
||||
echo MAKING TEMP
|
||||
mkdir %OUTPUT_DIR%
|
||||
echo MADE TEMP
|
||||
|
||||
|
||||
|
||||
echo INSTALLING ANDROID BUILD TOOLS
|
||||
call .\asclt\bin\sdkmanager "build-tools;32.0.0" --sdk_root="%LOCALAPPDATA%/Android/Sdk/"
|
||||
call .\asclt\bin\sdkmanager "build-tools;32.1.0-rc1" --sdk_root="%LOCALAPPDATA%/Android/Sdk/"
|
||||
echo INSTALLED ANDROID BUILD TOOLS
|
||||
|
||||
|
||||
|
||||
echo INSTALLING ANDROID SDK
|
||||
REM First install the sdks
|
||||
call .\asclt\bin\sdkmanager "platforms;android-29" --sdk_root="%LOCALAPPDATA%/Android/Sdk/"
|
||||
echo ANDROID SDK INSTALLED
|
||||
|
||||
|
||||
|
||||
echo INSTALLING ANDROID NDK
|
||||
REM then the ndks
|
||||
call ./asclt/bin/sdkmanager "ndk;21.4.7075529" --sdk_root="%LOCALAPPDATA%/Android/Sdk/"
|
||||
echo ANDROID NDK INSTALLED
|
||||
|
||||
|
||||
|
||||
echo DOWNLOADING JDK
|
||||
call curl -o %ZIP_FILE% %EIGHT_LINK%
|
||||
echo DOWNLOADED JDK
|
||||
|
||||
|
||||
|
||||
echo UNZIPPING JDK
|
||||
call powershell -Command "Expand-Archive -Path '%ZIP_FILE%' -DestinationPath '%OUTPUT_DIR%' -Force"
|
||||
echo UNZIPPED JDK
|
||||
|
||||
|
||||
|
||||
echo MAKING JDK PATH
|
||||
mkdir "%LOCALAPPDATA%/Android/jdk"
|
||||
echo MADE JDK PATH
|
||||
|
||||
|
||||
|
||||
echo MOVING JDK TO PROPER PATH
|
||||
call move "%OUTPUT_DIR%/jdk-17.0.11+9" "%LOCALAPPDATA%/Android/jdk/"
|
||||
echo MOVED JDK
|
||||
|
||||
|
||||
|
||||
echo LIME SETTING UP
|
||||
haxelib run lime config ANDROID_SDK %LOCALAPPDATA%\Android\Sdk
|
||||
haxelib run lime config ANDROID_NDK_ROOT %LOCALAPPDATA%\Android\Sdk\ndk\21.4.7075529
|
||||
haxelib run lime config JAVA_HOME %LOCALAPPDATA%\Android\Sdk\jdk\jdk-17.0.11+9
|
||||
haxelib run lime config ANDROID_SETUP true
|
||||
echo DONE
|
||||
|
||||
pause
|
|
@ -6,7 +6,7 @@
|
|||
"name": "EliteMasterEric"
|
||||
}
|
||||
],
|
||||
"api_version": "0.5.0",
|
||||
"api_version": "0.7.0",
|
||||
"mod_version": "1.0.0",
|
||||
"license": "Apache-2.0"
|
||||
}
|
||||
|
|
|
@ -6,7 +6,7 @@
|
|||
"name": "EliteMasterEric"
|
||||
}
|
||||
],
|
||||
"api_version": "0.5.0",
|
||||
"api_version": "0.7.0",
|
||||
"mod_version": "1.0.0",
|
||||
"license": "Apache-2.0"
|
||||
}
|
||||
|
|
69
hmm.json
69
hmm.json
|
@ -4,14 +4,48 @@
|
|||
"name": "FlxPartialSound",
|
||||
"type": "git",
|
||||
"dir": null,
|
||||
"ref": "41f35ddb1eb9d10bc742e6f8b5bcc62f9ef8ad84",
|
||||
"url": "https://github.com/FunkinCrew/FlxPartialSound.git"
|
||||
"ref": "3c9f63e3501c20c0b60442089dc05306f5a87968",
|
||||
"url": "https://github.com/FunkinDroidTeam/FlxPartialSound.git"
|
||||
},
|
||||
{
|
||||
"name": "astc-compressor",
|
||||
"type": "git",
|
||||
"dir": null,
|
||||
"ref": "8c9f56927c523df7b849352c6951f04112fe15cc",
|
||||
"url": "https://github.com/KarimAkra/astc-compressor"
|
||||
},
|
||||
{
|
||||
"name": "extension-admob",
|
||||
"type": "git",
|
||||
"dir": null,
|
||||
"ref": "02334589ff9603a5f483077a44395009644f6274",
|
||||
"url": "https://github.com/FunkinCrew/extension-admob"
|
||||
},
|
||||
{
|
||||
"name": "extension-androidtools",
|
||||
"type": "haxelib",
|
||||
"version": "2.2.2"
|
||||
},
|
||||
{
|
||||
"name": "extension-haptics",
|
||||
"type": "haxelib",
|
||||
"version": "1.0.4"
|
||||
},
|
||||
{
|
||||
"name": "extension-iapcore",
|
||||
"type": "haxelib",
|
||||
"version": "1.0.4"
|
||||
},
|
||||
{
|
||||
"name": "extension-iarcore",
|
||||
"type": "haxelib",
|
||||
"version": "1.0.3"
|
||||
},
|
||||
{
|
||||
"name": "flixel",
|
||||
"type": "git",
|
||||
"dir": null,
|
||||
"ref": "fffb1a74cf08f63dacc2ab09976340563f5b6e6d",
|
||||
"ref": "08fc955ca87f192a971719a675f1d3b21709725d",
|
||||
"url": "https://github.com/FunkinCrew/flixel"
|
||||
},
|
||||
{
|
||||
|
@ -21,18 +55,11 @@
|
|||
"ref": "b9118f47f43a66bc0e5fbfcfd9903f0425e918ee",
|
||||
"url": "https://github.com/FunkinCrew/flixel-addons"
|
||||
},
|
||||
{
|
||||
"name": "flixel-text-input",
|
||||
"type": "git",
|
||||
"dir": null,
|
||||
"ref": "951a0103a17bfa55eed86703ce50b4fb0d7590bc",
|
||||
"url": "https://github.com/FunkinCrew/flixel-text-input"
|
||||
},
|
||||
{
|
||||
"name": "flxanimate",
|
||||
"type": "git",
|
||||
"dir": null,
|
||||
"ref": "f8842cea9883d6112a2c854cf93fa72856171428",
|
||||
"ref": "b1faf19885dad06c899cb71ffe07b4e40b8c6d0c",
|
||||
"url": "https://github.com/FunkinCrew/flxanimate"
|
||||
},
|
||||
{
|
||||
|
@ -51,8 +78,8 @@
|
|||
"name": "grig.audio",
|
||||
"type": "git",
|
||||
"dir": "src",
|
||||
"ref": "57f5d47f2533fd0c3dcd025a86cb86c0dfa0b6d2",
|
||||
"url": "https://gitlab.com/haxe-grig/grig.audio.git"
|
||||
"ref": "8567c4dad34cfeaf2ff23fe12c3796f5db80685e",
|
||||
"url": "https://github.com/FunkinCrew/grig.audio"
|
||||
},
|
||||
{
|
||||
"name": "hamcrest",
|
||||
|
@ -63,14 +90,14 @@
|
|||
"name": "haxeui-core",
|
||||
"type": "git",
|
||||
"dir": null,
|
||||
"ref": "07fc7aa8098deaea633b0726d01f83eb4ef8a832",
|
||||
"ref": "47ee9f5fa02d422e186e844f87e68220cabfcc5b",
|
||||
"url": "https://github.com/haxeui/haxeui-core"
|
||||
},
|
||||
{
|
||||
"name": "haxeui-flixel",
|
||||
"type": "git",
|
||||
"dir": null,
|
||||
"ref": "b899a4c7d7318c5ff2b1bb645fbc73728fad1ac9",
|
||||
"ref": "100f2c96beab619cfe72c567a058c41c71e3e998",
|
||||
"url": "https://github.com/haxeui/haxeui-flixel"
|
||||
},
|
||||
{
|
||||
|
@ -84,8 +111,8 @@
|
|||
"name": "hxcpp",
|
||||
"type": "git",
|
||||
"dir": null,
|
||||
"ref": "v4.3.75",
|
||||
"url": "https://github.com/HaxeFoundation/hxcpp"
|
||||
"ref": "5a0dc3f644dc676a4a092b7e6c8edc8be941f024",
|
||||
"url": "https://github.com/FunkinCrew/hxcpp"
|
||||
},
|
||||
{
|
||||
"name": "hxcpp-debug-server",
|
||||
|
@ -116,7 +143,7 @@
|
|||
{
|
||||
"name": "hxvlc",
|
||||
"type": "haxelib",
|
||||
"version": "2.1.2"
|
||||
"version": "2.2.2"
|
||||
},
|
||||
{
|
||||
"name": "json2object",
|
||||
|
@ -143,7 +170,7 @@
|
|||
"name": "lime",
|
||||
"type": "git",
|
||||
"dir": null,
|
||||
"ref": "be81ad7e4e1a92c3482bcc009648a4ac892cfa35",
|
||||
"ref": "e5f8c27124598505917a001588b560244731adfb",
|
||||
"url": "https://github.com/FunkinCrew/lime"
|
||||
},
|
||||
{
|
||||
|
@ -185,14 +212,14 @@
|
|||
"name": "openfl",
|
||||
"type": "git",
|
||||
"dir": null,
|
||||
"ref": "d061c936b462f040304ec2bd42d9f59d2e59e285",
|
||||
"ref": "a0df7c3afe360c9af59a76e45007dbf4e53b5131",
|
||||
"url": "https://github.com/FunkinCrew/openfl"
|
||||
},
|
||||
{
|
||||
"name": "polymod",
|
||||
"type": "git",
|
||||
"dir": null,
|
||||
"ref": "3e030c81de99ca84acde681431f806d8103bcf6e",
|
||||
"ref": "d4142dd15a3b57ed4eb149f9f6a2c3ad9935bf7b",
|
||||
"url": "https://github.com/larsiusprime/polymod"
|
||||
},
|
||||
{
|
||||
|
|
973
project.hxp
973
project.hxp
File diff suppressed because it is too large
Load diff
File diff suppressed because it is too large
Load diff
|
@ -1,12 +1,17 @@
|
|||
package;
|
||||
|
||||
import flixel.FlxG;
|
||||
import flixel.FlxGame;
|
||||
import flixel.FlxState;
|
||||
import funkin.ui.FullScreenScaleMode;
|
||||
import funkin.Preferences;
|
||||
import funkin.util.logging.CrashHandler;
|
||||
import funkin.ui.debug.MemoryCounter;
|
||||
import funkin.save.Save;
|
||||
import haxe.ui.Toolkit;
|
||||
#if hxvlc
|
||||
import hxvlc.util.Handle;
|
||||
#end
|
||||
import openfl.display.FPS;
|
||||
import openfl.display.Sprite;
|
||||
import openfl.events.Event;
|
||||
|
@ -31,6 +36,15 @@ class Main extends Sprite
|
|||
|
||||
public static function main():Void
|
||||
{
|
||||
// Set the current working directory for Android and iOS devices
|
||||
#if android
|
||||
// On Android use External Files Dir.
|
||||
Sys.setCwd(haxe.io.Path.addTrailingSlash(extension.androidtools.content.Context.getExternalFilesDir()));
|
||||
#elseif ios
|
||||
// On iOS use Documents Dir.
|
||||
Sys.setCwd(haxe.io.Path.addTrailingSlash(lime.system.System.documentsDirectory));
|
||||
#end
|
||||
|
||||
// We need to make the crash handler LITERALLY FIRST so nothing EVER gets past it.
|
||||
CrashHandler.initialize();
|
||||
CrashHandler.queryStatus();
|
||||
|
@ -97,9 +111,19 @@ class Main extends Sprite
|
|||
memoryCounter = new MemoryCounter(10, 13, 0xFFFFFF);
|
||||
#end
|
||||
|
||||
#if mobile
|
||||
// Add this signal so we can reposition and resize the memory and fps counter.
|
||||
FlxG.signals.preUpdate.add(repositionCounters.bind(true));
|
||||
#end
|
||||
|
||||
// George recommends binding the save before FlxGame is created.
|
||||
Save.load();
|
||||
|
||||
|
||||
#if hxvlc
|
||||
// Initialize hxvlc's Handle here so the videos are loading faster.
|
||||
Handle.init();
|
||||
#end
|
||||
|
||||
// Don't call anything from the preferences until the save is loaded!
|
||||
#if web
|
||||
// set this variable (which is a function) from the lime version at lime/_internal/backend/html5/HTML5Application.hx
|
||||
|
@ -109,7 +133,6 @@ class Main extends Sprite
|
|||
|
||||
WindowUtil.setVSyncMode(funkin.Preferences.vsyncMode);
|
||||
|
||||
|
||||
var game:FlxGame = new FlxGame(gameWidth, gameHeight, initialState, Preferences.framerate, Preferences.framerate, skipSplash, startFullscreen);
|
||||
|
||||
// FlxG.game._customSoundTray wants just the class, it calls new from
|
||||
|
@ -124,6 +147,15 @@ class Main extends Sprite
|
|||
game.debugger.interaction.addTool(new funkin.util.TrackerToolButtonUtil());
|
||||
#end
|
||||
|
||||
#if !html5
|
||||
FlxG.scaleMode = new FullScreenScaleMode();
|
||||
#end
|
||||
|
||||
#if mobile
|
||||
// Reposition and resize the memory and fps counter without lerping.
|
||||
repositionCounters(false);
|
||||
#end
|
||||
|
||||
#if hxcpp_debug_server
|
||||
trace('hxcpp_debug_server is enabled! You can now connect to the game with a debugger.');
|
||||
#else
|
||||
|
@ -145,4 +177,56 @@ class Main extends Sprite
|
|||
funkin.input.Cursor.registerHaxeUICursors();
|
||||
haxe.ui.tooltips.ToolTipManager.defaultDelay = 200;
|
||||
}
|
||||
|
||||
#if mobile
|
||||
function repositionCounters(lerp:Bool):Void
|
||||
{
|
||||
// Calling this so it gets scaled based on the resolution of the game and device's resolution.
|
||||
var scale:Float = Math.min(FlxG.stage.stageWidth / FlxG.width, FlxG.stage.stageHeight / FlxG.height);
|
||||
|
||||
#if android
|
||||
scale = Math.max(scale, 1);
|
||||
#else
|
||||
scale = Math.min(scale, 1);
|
||||
#end
|
||||
final thypos:Float = Math.max(FullScreenScaleMode.notchSize.x, 10);
|
||||
if (fpsCounter != null)
|
||||
{
|
||||
fpsCounter.scaleX = fpsCounter.scaleY = scale;
|
||||
|
||||
if (FlxG.game != null)
|
||||
{
|
||||
if (lerp)
|
||||
{
|
||||
fpsCounter.x = flixel.math.FlxMath.lerp(fpsCounter.x, FlxG.game.x + thypos, FlxG.elapsed * 3);
|
||||
}
|
||||
else
|
||||
{
|
||||
fpsCounter.x = FlxG.game.x + FullScreenScaleMode.notchSize.x + 10;
|
||||
}
|
||||
|
||||
fpsCounter.y = FlxG.game.y + (3 * scale);
|
||||
}
|
||||
}
|
||||
|
||||
if (memoryCounter != null)
|
||||
{
|
||||
memoryCounter.scaleX = memoryCounter.scaleY = scale;
|
||||
|
||||
if (FlxG.game != null)
|
||||
{
|
||||
if (lerp)
|
||||
{
|
||||
memoryCounter.x = flixel.math.FlxMath.lerp(memoryCounter.x, FlxG.game.x + thypos, FlxG.elapsed * 3);
|
||||
}
|
||||
else
|
||||
{
|
||||
memoryCounter.x = FlxG.game.x + FullScreenScaleMode.notchSize.x + 10;
|
||||
}
|
||||
|
||||
memoryCounter.y = FlxG.game.y + (13 * scale);
|
||||
}
|
||||
}
|
||||
}
|
||||
#end
|
||||
}
|
||||
|
|
|
@ -29,7 +29,7 @@ class Postbuild
|
|||
|
||||
var buildTime:Float = roundToTwoDecimals(end - start);
|
||||
|
||||
trace('Build took: ${buildTime} seconds');
|
||||
Sys.println('[INFO] Build took: ${buildTime} seconds');
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -9,48 +9,24 @@ class Prebuild
|
|||
{
|
||||
static inline final BUILD_TIME_FILE:String = '.build_time';
|
||||
|
||||
static final NG_CREDS_PATH:String = './source/funkin/api/newgrounds/NewgroundsCredentials.hx';
|
||||
|
||||
static final NG_CREDS_TEMPLATE:String = "package funkin.api.newgrounds;
|
||||
|
||||
class NewgroundsCredentials
|
||||
{
|
||||
public static final APP_ID:String = #if API_NG_APP_ID haxe.macro.Compiler.getDefine(\"API_NG_APP_ID\") #else 'INSERT APP ID HERE' #end;
|
||||
public static final ENCRYPTION_KEY:String = #if API_NG_ENC_KEY haxe.macro.Compiler.getDefine(\"API_NG_ENC_KEY\") #else 'INSERT ENCRYPTION KEY HERE' #end;
|
||||
}";
|
||||
|
||||
static function main():Void
|
||||
{
|
||||
trace('Building...');
|
||||
var start:Float = Sys.time();
|
||||
Sys.println('[INFO] Performing pre-build tasks...');
|
||||
|
||||
saveBuildTime();
|
||||
|
||||
buildCredsFile();
|
||||
var end:Float = Sys.time();
|
||||
var duration:Float = end - start;
|
||||
Sys.println('[INFO] Finished pre-build tasks in $duration seconds.');
|
||||
}
|
||||
|
||||
static function saveBuildTime():Void
|
||||
{
|
||||
// PostBuild.hx reads this file and computes the total build duration.
|
||||
var fo:sys.io.FileOutput = File.write(BUILD_TIME_FILE);
|
||||
var now:Float = Sys.time();
|
||||
fo.writeDouble(now);
|
||||
fo.close();
|
||||
}
|
||||
|
||||
static function buildCredsFile():Void
|
||||
{
|
||||
#if sys
|
||||
if (sys.FileSystem.exists(NG_CREDS_PATH))
|
||||
{
|
||||
trace('NewgroundsCredentials.hx already exists, skipping.');
|
||||
}
|
||||
else
|
||||
{
|
||||
trace('Creating NewgroundsCredentials.hx...');
|
||||
|
||||
var fileContents:String = NG_CREDS_TEMPLATE;
|
||||
|
||||
sys.io.File.saveContent(NG_CREDS_PATH, fileContents);
|
||||
}
|
||||
#end
|
||||
}
|
||||
}
|
||||
|
|
|
@ -24,6 +24,27 @@ extern class Native_TracyProfiler
|
|||
@:native('::__hxcpp_tracy_framemark')
|
||||
public static function frameMark():Void;
|
||||
|
||||
/**
|
||||
Mark a named frame. Allows creating multiple frame sets for different timing categories.
|
||||
Each unique name creates a separate frame set in the Tracy timeline.
|
||||
**/
|
||||
@:native('::__hxcpp_tracy_framemark_named')
|
||||
public static function frameMarkNamed(_name:String):Void;
|
||||
|
||||
/**
|
||||
Mark the start of a discontinuous frame. Use for periodic work with gaps.
|
||||
Must be paired with frameMarkEnd() using the same name.
|
||||
**/
|
||||
@:native('::__hxcpp_tracy_framemark_start')
|
||||
public static function frameMarkStart(_name:String):Void;
|
||||
|
||||
/**
|
||||
Mark the end of a discontinuous frame. Use for periodic work with gaps.
|
||||
Must be paired with frameMarkStart() using the same name.
|
||||
**/
|
||||
@:native('::__hxcpp_tracy_framemark_end')
|
||||
public static function frameMarkEnd(_name:String):Void;
|
||||
|
||||
/**
|
||||
Print a message into Tracy's log.
|
||||
**/
|
||||
|
@ -71,6 +92,18 @@ class Cppia_TracyProfiler
|
|||
public static function frameMark()
|
||||
Native_TracyProfiler.frameMark();
|
||||
|
||||
@:inheritDoc(cpp.vm.tracy.Native_TracyProfiler.frameMarkNamed)
|
||||
public static function frameMarkNamed(_name:String)
|
||||
Native_TracyProfiler.frameMarkNamed(_name);
|
||||
|
||||
@:inheritDoc(cpp.vm.tracy.Native_TracyProfiler.frameMarkStart)
|
||||
public static function frameMarkStart(_name:String)
|
||||
Native_TracyProfiler.frameMarkStart(_name);
|
||||
|
||||
@:inheritDoc(cpp.vm.tracy.Native_TracyProfiler.frameMarkEnd)
|
||||
public static function frameMarkEnd(_name:String)
|
||||
Native_TracyProfiler.frameMarkEnd(_name);
|
||||
|
||||
@:inheritDoc(cpp.vm.tracy.Native_TracyProfiler.message)
|
||||
public static function message(_msg:String, ?_color:Int = 0x000000)
|
||||
Native_TracyProfiler.message(_msg, _color);
|
||||
|
|
|
@ -9,6 +9,11 @@ import openfl.utils.Future;
|
|||
@:nullSafety
|
||||
class Assets
|
||||
{
|
||||
/**
|
||||
* The assets cache.
|
||||
*/
|
||||
public static var cache:openfl.utils.IAssetCache = openfl.utils.Assets.cache;
|
||||
|
||||
/**
|
||||
* Get the file system path for an asset
|
||||
* @param path The asset path to load from, relative to the assets folder
|
||||
|
|
|
@ -249,25 +249,18 @@ class Conductor
|
|||
* No matter if you're using a local conductor or not, this always loads
|
||||
* to/from the save file
|
||||
*/
|
||||
public var inputOffset(get, set):Int;
|
||||
public var globalOffset(get, never):Int;
|
||||
|
||||
/**
|
||||
* An offset set by the user to compensate for audio/visual lag
|
||||
* No matter if you're using a local conductor or not, this always loads
|
||||
* to/from the save file
|
||||
*/
|
||||
public var audioVisualOffset(get, set):Int;
|
||||
public var audioVisualOffset(get, never):Int;
|
||||
|
||||
function get_inputOffset():Int
|
||||
function get_globalOffset():Int
|
||||
{
|
||||
return Save?.instance?.options?.inputOffset ?? 0;
|
||||
}
|
||||
|
||||
function set_inputOffset(value:Int):Int
|
||||
{
|
||||
Save.instance.options.inputOffset = value;
|
||||
Save.instance.flush();
|
||||
return Save.instance.options.inputOffset;
|
||||
return Preferences.globalOffset;
|
||||
}
|
||||
|
||||
function get_audioVisualOffset():Int
|
||||
|
@ -275,18 +268,11 @@ class Conductor
|
|||
return Save?.instance?.options?.audioVisualOffset ?? 0;
|
||||
}
|
||||
|
||||
function set_audioVisualOffset(value:Int):Int
|
||||
{
|
||||
Save.instance.options.audioVisualOffset = value;
|
||||
Save.instance.flush();
|
||||
return Save.instance.options.audioVisualOffset;
|
||||
}
|
||||
|
||||
public var combinedOffset(get, never):Float;
|
||||
|
||||
function get_combinedOffset():Float
|
||||
{
|
||||
return instrumentalOffset + audioVisualOffset + inputOffset;
|
||||
return instrumentalOffset + formatOffset + globalOffset;
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -408,8 +394,10 @@ class Conductor
|
|||
* @param songPosition The current position in the song in milliseconds.
|
||||
* Leave blank to use the FlxG.sound.music position.
|
||||
* @param applyOffsets If it should apply the instrumentalOffset + formatOffset + audioVisualOffset
|
||||
* @param forceDispatch If it should force the dispatch of onStepHit, onBeatHit, and onMeasureHit
|
||||
* even if the current step, beat, or measure hasn't changed.
|
||||
*/
|
||||
public function update(?songPos:Float, applyOffsets:Bool = true, forceDispatch:Bool = false)
|
||||
public function update(?songPos:Float, applyOffsets:Bool = true, forceDispatch:Bool = false):Void
|
||||
{
|
||||
var currentTime:Float = (FlxG.sound.music != null) ? FlxG.sound.music.time : 0.0;
|
||||
var currentLength:Float = (FlxG.sound.music != null) ? FlxG.sound.music.length : 0.0;
|
||||
|
|
428
source/funkin/FunkinMemory.hx
Normal file
428
source/funkin/FunkinMemory.hx
Normal file
|
@ -0,0 +1,428 @@
|
|||
package funkin;
|
||||
|
||||
import flixel.graphics.FlxGraphic;
|
||||
import flixel.FlxG;
|
||||
import funkin.play.notes.notestyle.NoteStyle;
|
||||
import openfl.utils.AssetType;
|
||||
import openfl.Assets;
|
||||
import openfl.system.System;
|
||||
import openfl.media.Sound;
|
||||
import lime.app.Future;
|
||||
import lime.app.Promise;
|
||||
|
||||
/**
|
||||
* Handles caching of textures and sounds for the game.
|
||||
* TODO: Remove this once Eric finishes the memory system.
|
||||
*/
|
||||
@:nullSafety
|
||||
class FunkinMemory
|
||||
{
|
||||
static var permanentCachedTextures:Map<String, FlxGraphic> = [];
|
||||
static var currentCachedTextures:Map<String, FlxGraphic> = [];
|
||||
static var previousCachedTextures:Map<String, FlxGraphic> = [];
|
||||
|
||||
// waow
|
||||
static var permanentCachedSounds:Map<String, Sound> = [];
|
||||
static var currentCachedSounds:Map<String, Sound> = [];
|
||||
static var previousCachedSounds:Map<String, Sound> = [];
|
||||
|
||||
static var purgeFilter:Array<String> = ["/week", "/characters", "/charSelect", "/results"];
|
||||
|
||||
/**
|
||||
* Caches textures that are always required.
|
||||
*/
|
||||
public static inline function initialCache():Void
|
||||
{
|
||||
var allImages:Array<String> = Assets.list();
|
||||
|
||||
for (file in allImages)
|
||||
{
|
||||
if (!(file.endsWith(".png") #if FEATURE_COMPRESSED_TEXTURES || file.endsWith(".astc") #end)
|
||||
|| file.contains("chart-editor")
|
||||
|| !file.contains("ui/"))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
file = file.replace(" ", ""); // Handle stray spaces.
|
||||
|
||||
if (file.contains("shared") || Assets.exists('shared:$file', AssetType.IMAGE))
|
||||
{
|
||||
file = 'shared:$file';
|
||||
}
|
||||
permanentCacheTexture(file);
|
||||
}
|
||||
|
||||
permanentCacheTexture(Paths.image("healthBar"));
|
||||
permanentCacheTexture(Paths.image("menuDesat"));
|
||||
permanentCacheTexture(Paths.image("notes", "shared"));
|
||||
permanentCacheTexture(Paths.image("noteSplashes", "shared"));
|
||||
permanentCacheTexture(Paths.image("noteStrumline", "shared"));
|
||||
permanentCacheTexture(Paths.image("NOTE_hold_assets"));
|
||||
// dude
|
||||
permanentCacheTexture(Paths.image("fonts/bold", null));
|
||||
permanentCacheTexture(Paths.image("fonts/default", null));
|
||||
permanentCacheTexture(Paths.image("fonts/freeplay-clear", null));
|
||||
|
||||
var allSounds:Array<String> = Assets.list(AssetType.SOUND);
|
||||
|
||||
for (file in allSounds)
|
||||
{
|
||||
if (!file.endsWith(".ogg") || !file.contains("countdown/")) continue;
|
||||
|
||||
file = file.replace(" ", "");
|
||||
|
||||
if (file.contains("shared") || Assets.exists('shared:$file', AssetType.SOUND))
|
||||
{
|
||||
file = 'shared:$file';
|
||||
}
|
||||
|
||||
permanentCacheSound(file);
|
||||
}
|
||||
|
||||
permanentCacheSound(Paths.sound("cancelMenu"));
|
||||
permanentCacheSound(Paths.sound("confirmMenu"));
|
||||
permanentCacheSound(Paths.sound("screenshot"));
|
||||
permanentCacheSound(Paths.sound("scrollMenu"));
|
||||
permanentCacheSound(Paths.sound("soundtray/Voldown"));
|
||||
permanentCacheSound(Paths.sound("soundtray/VolMAX"));
|
||||
permanentCacheSound(Paths.sound("soundtray/Volup"));
|
||||
permanentCacheSound(Paths.music("freakyMenu/freakyMenu"));
|
||||
permanentCacheSound(Paths.music("offsetsLoop/offsetsLoop"));
|
||||
permanentCacheSound(Paths.music("offsetsLoop/drumsLoop"));
|
||||
permanentCacheSound(Paths.sound("missnote1", "shared"));
|
||||
permanentCacheSound(Paths.sound("missnote2", "shared"));
|
||||
permanentCacheSound(Paths.sound("missnote3", "shared"));
|
||||
}
|
||||
|
||||
/**
|
||||
* Clears the current texture and sound caches.
|
||||
*/
|
||||
public static inline function purgeCache(callGarbageCollector:Bool = false):Void
|
||||
{
|
||||
preparePurgeTextureCache();
|
||||
purgeTextureCache();
|
||||
preparePurgeSoundCache();
|
||||
purgeSoundCache();
|
||||
#if (cpp || neko || hl)
|
||||
if (callGarbageCollector) funkin.util.MemoryUtil.collect(true);
|
||||
#end
|
||||
}
|
||||
|
||||
///// TEXTURES /////
|
||||
|
||||
/**
|
||||
* Ensures a texture with the given key is cached.
|
||||
* @param key The key of the texture to cache.
|
||||
*/
|
||||
public static function cacheTexture(key:String):Void
|
||||
{
|
||||
if (currentCachedTextures.exists(key))
|
||||
{
|
||||
return; // Already cached.
|
||||
}
|
||||
|
||||
if (previousCachedTextures.exists(key))
|
||||
{
|
||||
// Move the texture from the previous cache to the current cache.
|
||||
var graphic:Null<FlxGraphic> = previousCachedTextures.get(key);
|
||||
previousCachedTextures.remove(key);
|
||||
if (graphic != null) currentCachedTextures.set(key, graphic);
|
||||
return;
|
||||
}
|
||||
|
||||
var graphic:Null<FlxGraphic> = FlxGraphic.fromAssetKey(key, false, null, true);
|
||||
if (graphic == null)
|
||||
{
|
||||
FlxG.log.warn('Failed to cache graphic: $key');
|
||||
}
|
||||
else
|
||||
{
|
||||
trace('Successfully cached graphic: $key');
|
||||
graphic.persist = true;
|
||||
currentCachedTextures.set(key, graphic);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Permanently caches a texture with the given key.
|
||||
* @param key The key of the texture to cache.
|
||||
*/
|
||||
static function permanentCacheTexture(key:String):Void
|
||||
{
|
||||
if (permanentCachedTextures.exists(key))
|
||||
{
|
||||
return; // Already cached.
|
||||
}
|
||||
|
||||
var graphic:Null<FlxGraphic> = FlxGraphic.fromAssetKey(key, false, null, true);
|
||||
if (graphic == null)
|
||||
{
|
||||
FlxG.log.warn('Failed to cache graphic: $key');
|
||||
}
|
||||
else
|
||||
{
|
||||
trace('Successfully cached graphic: $key');
|
||||
graphic.persist = true;
|
||||
permanentCachedTextures.set(key, graphic);
|
||||
}
|
||||
|
||||
currentCachedTextures = permanentCachedTextures;
|
||||
}
|
||||
|
||||
/**
|
||||
* Prepares the cache for purging unused textures.
|
||||
*/
|
||||
public inline static function preparePurgeTextureCache():Void
|
||||
{
|
||||
previousCachedTextures = currentCachedTextures;
|
||||
|
||||
for (graphicKey in previousCachedTextures.keys())
|
||||
{
|
||||
if (permanentCachedTextures.exists(graphicKey))
|
||||
{
|
||||
previousCachedTextures.remove(graphicKey);
|
||||
}
|
||||
}
|
||||
|
||||
currentCachedTextures = permanentCachedTextures;
|
||||
}
|
||||
|
||||
/**
|
||||
* Purges unused textures from the cache.
|
||||
*/
|
||||
public static function purgeTextureCache():Void
|
||||
{
|
||||
for (graphicKey in previousCachedTextures.keys())
|
||||
{
|
||||
if (permanentCachedTextures.exists(graphicKey))
|
||||
{
|
||||
previousCachedTextures.remove(graphicKey);
|
||||
continue;
|
||||
}
|
||||
|
||||
if (graphicKey.contains("fonts")) continue;
|
||||
|
||||
var graphic:Null<FlxGraphic> = previousCachedTextures.get(graphicKey);
|
||||
if (graphic != null)
|
||||
{
|
||||
FlxG.bitmap.remove(graphic);
|
||||
graphic.destroy();
|
||||
previousCachedTextures.remove(graphicKey);
|
||||
Assets.cache.clear(graphicKey);
|
||||
}
|
||||
}
|
||||
@:privateAccess
|
||||
if (FlxG.bitmap._cache == null)
|
||||
{
|
||||
@:privateAccess
|
||||
FlxG.bitmap._cache = new Map();
|
||||
}
|
||||
|
||||
@:privateAccess
|
||||
for (key in FlxG.bitmap._cache.keys())
|
||||
{
|
||||
var obj:Null<FlxGraphic> = FlxG.bitmap.get(key);
|
||||
|
||||
if (obj == null || obj.persist || permanentCachedTextures.exists(key) || key.contains("fonts"))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
if (obj.useCount > 0)
|
||||
{
|
||||
for (purgeEntry in purgeFilter)
|
||||
{
|
||||
if (key.contains(purgeEntry))
|
||||
{
|
||||
FlxG.bitmap.removeKey(key);
|
||||
obj.destroy();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
///// NOTE STYLE //////
|
||||
|
||||
public static function cacheNoteStyle(style:NoteStyle):Void
|
||||
{
|
||||
// TODO: Texture paths should fall back to the default values.
|
||||
cacheTexture(Paths.image(style.getNoteAssetPath() ?? "note"));
|
||||
cacheTexture(style.getHoldNoteAssetPath() ?? "noteHold");
|
||||
cacheTexture(Paths.image(style.getStrumlineAssetPath() ?? "strumline"));
|
||||
cacheTexture(Paths.image(style.getSplashAssetPath() ?? "noteSplash"));
|
||||
|
||||
cacheTexture(Paths.image(style.getHoldCoverDirectionAssetPath(LEFT) ?? "LEFT"));
|
||||
cacheTexture(Paths.image(style.getHoldCoverDirectionAssetPath(RIGHT) ?? "RIGHT"));
|
||||
cacheTexture(Paths.image(style.getHoldCoverDirectionAssetPath(UP) ?? "UP"));
|
||||
cacheTexture(Paths.image(style.getHoldCoverDirectionAssetPath(DOWN) ?? "DOWN"));
|
||||
|
||||
// cacheTexture(Paths.image(style.buildCountdownSpritePath(THREE) ?? "THREE"));
|
||||
cacheTexture(Paths.image(style.buildCountdownSpritePath(TWO) ?? "TWO"));
|
||||
cacheTexture(Paths.image(style.buildCountdownSpritePath(ONE) ?? "ONE"));
|
||||
cacheTexture(Paths.image(style.buildCountdownSpritePath(GO) ?? "GO"));
|
||||
|
||||
cacheSound(style.getCountdownSoundPath(THREE) ?? "THREE");
|
||||
cacheSound(style.getCountdownSoundPath(TWO) ?? "TWO");
|
||||
cacheSound(style.getCountdownSoundPath(ONE) ?? "ONE");
|
||||
cacheSound(style.getCountdownSoundPath(GO) ?? "GO");
|
||||
|
||||
cacheTexture(Paths.image(style.buildJudgementSpritePath("sick") ?? 'sick'));
|
||||
cacheTexture(Paths.image(style.buildJudgementSpritePath("good") ?? 'good'));
|
||||
cacheTexture(Paths.image(style.buildJudgementSpritePath("bad") ?? 'bad'));
|
||||
cacheTexture(Paths.image(style.buildJudgementSpritePath("shit") ?? 'shit'));
|
||||
|
||||
cacheTexture(Paths.image(style.buildComboNumSpritePath(0) ?? '0'));
|
||||
cacheTexture(Paths.image(style.buildComboNumSpritePath(1) ?? '1'));
|
||||
cacheTexture(Paths.image(style.buildComboNumSpritePath(2) ?? '2'));
|
||||
cacheTexture(Paths.image(style.buildComboNumSpritePath(3) ?? '3'));
|
||||
cacheTexture(Paths.image(style.buildComboNumSpritePath(4) ?? '4'));
|
||||
cacheTexture(Paths.image(style.buildComboNumSpritePath(5) ?? '5'));
|
||||
cacheTexture(Paths.image(style.buildComboNumSpritePath(6) ?? '6'));
|
||||
cacheTexture(Paths.image(style.buildComboNumSpritePath(7) ?? '7'));
|
||||
cacheTexture(Paths.image(style.buildComboNumSpritePath(8) ?? '8'));
|
||||
cacheTexture(Paths.image(style.buildComboNumSpritePath(9) ?? '9'));
|
||||
}
|
||||
|
||||
///// SOUND //////
|
||||
|
||||
public static function cacheSound(key:String):Void
|
||||
{
|
||||
if (currentCachedSounds.exists(key)) return;
|
||||
|
||||
if (previousCachedSounds.exists(key))
|
||||
{
|
||||
// Move the texture from the previous cache to the current cache.
|
||||
var sound:Null<Sound> = previousCachedSounds.get(key);
|
||||
previousCachedSounds.remove(key);
|
||||
if (sound != null) currentCachedSounds.set(key, sound);
|
||||
return;
|
||||
}
|
||||
|
||||
var sound:Null<Sound> = Assets.getSound(key, true);
|
||||
if (sound == null) return;
|
||||
else
|
||||
currentCachedSounds.set(key, sound);
|
||||
}
|
||||
|
||||
public static function permanentCacheSound(key:String):Void
|
||||
{
|
||||
if (permanentCachedSounds.exists(key)) return;
|
||||
|
||||
var sound:Null<Sound> = Assets.getSound(key, true);
|
||||
if (sound == null) return;
|
||||
else
|
||||
permanentCachedSounds.set(key, sound);
|
||||
|
||||
if (sound != null) currentCachedSounds.set(key, sound);
|
||||
}
|
||||
|
||||
public static function preparePurgeSoundCache():Void
|
||||
{
|
||||
previousCachedSounds = currentCachedSounds;
|
||||
|
||||
for (key in previousCachedSounds.keys())
|
||||
{
|
||||
if (permanentCachedSounds.exists(key))
|
||||
{
|
||||
previousCachedSounds.remove(key);
|
||||
}
|
||||
}
|
||||
|
||||
currentCachedSounds = permanentCachedSounds;
|
||||
}
|
||||
|
||||
/**
|
||||
* Purges unused sounds from the cache.
|
||||
*/
|
||||
public static inline function purgeSoundCache():Void
|
||||
{
|
||||
for (key in previousCachedSounds.keys())
|
||||
{
|
||||
if (permanentCachedSounds.exists(key))
|
||||
{
|
||||
previousCachedSounds.remove(key);
|
||||
continue;
|
||||
}
|
||||
|
||||
var sound:Null<Sound> = previousCachedSounds.get(key);
|
||||
if (sound != null)
|
||||
{
|
||||
Assets.cache.removeSound(key);
|
||||
previousCachedSounds.remove(key);
|
||||
}
|
||||
}
|
||||
Assets.cache.clear("songs");
|
||||
Assets.cache.clear("music");
|
||||
// Felt lazy.
|
||||
var key = Paths.music("freakyMenu/freakyMenu");
|
||||
var sound:Null<Sound> = Assets.getSound(key, true);
|
||||
if (sound != null)
|
||||
{
|
||||
permanentCachedSounds.set(key, sound);
|
||||
currentCachedSounds.set(key, sound);
|
||||
}
|
||||
}
|
||||
|
||||
///// MISC /////
|
||||
|
||||
public static inline function clearFreeplay():Void
|
||||
{
|
||||
var keysToRemove:Array<String> = [];
|
||||
|
||||
@:privateAccess
|
||||
for (key in FlxG.bitmap._cache.keys())
|
||||
{
|
||||
if (!key.contains("freeplay")) continue;
|
||||
if (permanentCachedTextures.exists(key) || key.contains("fonts")) continue;
|
||||
|
||||
keysToRemove.push(key);
|
||||
}
|
||||
|
||||
@:privateAccess
|
||||
for (key in keysToRemove)
|
||||
{
|
||||
trace('Cleaning up $key');
|
||||
var obj:Null<FlxGraphic> = FlxG.bitmap.get(key);
|
||||
if (obj != null)
|
||||
{
|
||||
obj.destroy();
|
||||
}
|
||||
FlxG.bitmap.removeKey(key);
|
||||
if (currentCachedTextures.exists(key)) currentCachedTextures.remove(key);
|
||||
Assets.cache.clear(key);
|
||||
}
|
||||
|
||||
preparePurgeSoundCache();
|
||||
purgeSoundCache();
|
||||
}
|
||||
|
||||
public static inline function clearStickers():Void
|
||||
{
|
||||
var keysToRemove:Array<String> = [];
|
||||
|
||||
@:privateAccess
|
||||
for (key in FlxG.bitmap._cache.keys())
|
||||
{
|
||||
if (!key.contains("stickers")) continue;
|
||||
if (permanentCachedTextures.exists(key) || key.contains("fonts")) continue;
|
||||
|
||||
keysToRemove.push(key);
|
||||
}
|
||||
|
||||
@:privateAccess
|
||||
for (key in keysToRemove)
|
||||
{
|
||||
trace('Cleaning up $key');
|
||||
var obj:Null<FlxGraphic> = FlxG.bitmap.get(key);
|
||||
if (obj != null)
|
||||
{
|
||||
obj.destroy();
|
||||
}
|
||||
FlxG.bitmap.removeKey(key);
|
||||
if (currentCachedTextures.exists(key)) currentCachedTextures.remove(key);
|
||||
Assets.cache.clear(key);
|
||||
}
|
||||
}
|
||||
}
|
|
@ -10,6 +10,7 @@ import flixel.math.FlxPoint;
|
|||
import flixel.math.FlxRect;
|
||||
import flixel.system.debug.log.LogStyle;
|
||||
import flixel.util.FlxColor;
|
||||
import funkin.graphics.FunkinSprite;
|
||||
import funkin.data.dialogue.conversation.ConversationRegistry;
|
||||
import funkin.data.dialogue.dialoguebox.DialogueBoxRegistry;
|
||||
import funkin.data.dialogue.speaker.SpeakerRegistry;
|
||||
|
@ -33,7 +34,6 @@ import funkin.ui.transition.LoadingState;
|
|||
import funkin.util.CLIUtil;
|
||||
import funkin.util.CLIUtil.CLIParams;
|
||||
import funkin.util.macro.MacroUtil;
|
||||
import funkin.util.TimerUtil;
|
||||
import funkin.util.TrackerUtil;
|
||||
import funkin.util.WindowUtil;
|
||||
import openfl.display.BitmapData;
|
||||
|
@ -92,6 +92,31 @@ class InitState extends FlxState
|
|||
funkin.util.WindowUtil.initTracy();
|
||||
#end
|
||||
|
||||
#if FEATURE_HAPTICS
|
||||
// Setup Haptic feedback
|
||||
extension.haptics.Haptic.initialize();
|
||||
#end
|
||||
|
||||
#if FEATURE_MOBILE_ADVERTISEMENTS
|
||||
// Setup Admob
|
||||
funkin.mobile.util.AdMobUtil.init();
|
||||
#end
|
||||
|
||||
#if FEATURE_MOBILE_IAP
|
||||
// Setup In-App purchases
|
||||
funkin.mobile.util.InAppPurchasesUtil.init();
|
||||
#end
|
||||
|
||||
#if FEATURE_MOBILE_IAR
|
||||
// Setup In-App purchases
|
||||
funkin.mobile.util.InAppReviewUtil.init();
|
||||
#end
|
||||
|
||||
#if ios
|
||||
// Setup Audio session
|
||||
funkin.external.ios.AudioSession.initialize();
|
||||
#end
|
||||
|
||||
// This ain't a pixel art game! (most of the time)
|
||||
FlxSprite.defaultAntialiasing = true;
|
||||
|
||||
|
@ -104,13 +129,15 @@ class InitState extends FlxState
|
|||
// but that makes our soundtray not show up on init if we have the game muted.
|
||||
// We set it to active so it at least calls it's update function once (see FlxGame.onEnterFrame(), it's called there)
|
||||
// and also see FunkinSoundTray.update() to see what we do and how we check if we are muted or not
|
||||
#if !mobile
|
||||
FlxG.game.soundTray.active = true;
|
||||
#end
|
||||
|
||||
// Set the game to a lower frame rate while it is in the background.
|
||||
FlxG.game.focusLostFramerate = 30;
|
||||
|
||||
// Makes Flixel use frame times instead of locked movements per frame for things like tweens
|
||||
FlxG.fixedTimestep = false;
|
||||
FlxG.fixedTimestep = false;
|
||||
|
||||
setupFlixelDebug();
|
||||
|
||||
|
@ -133,6 +160,25 @@ class InitState extends FlxState
|
|||
// Don't play transition in when entering the title state.
|
||||
FlxTransitionableState.skipNextTransIn = true;
|
||||
|
||||
FlxG.signals.gameResized.add(function(width:Int, height:Int) {
|
||||
FlxTransitionableState.defaultTransIn = new TransitionData(FADE, FlxColor.BLACK, 1, new FlxPoint(0, -1), tileData,
|
||||
new FlxRect(-200, -200, FlxG.width * 1.4, FlxG.height * 1.4));
|
||||
FlxTransitionableState.defaultTransOut = new TransitionData(FADE, FlxColor.BLACK, 0.7, new FlxPoint(0, 1), tileData,
|
||||
new FlxRect(-200, -200, FlxG.width * 1.4, FlxG.height * 1.4));
|
||||
});
|
||||
|
||||
// SDL for some reason enables VSync on focus lost/gained in Android
|
||||
// Since we don't really need VSync on Android we're gonna forcefully disable it on these signals for now
|
||||
// This is fixed on SDL3 from what I've heared but that doodoo isn't working poperly for Android
|
||||
#if android
|
||||
FlxG.signals.focusLost.add(function() {
|
||||
WindowUtil.setVSyncMode(lime.ui.WindowVSyncMode.OFF);
|
||||
});
|
||||
FlxG.signals.focusGained.add(function() {
|
||||
WindowUtil.setVSyncMode(lime.ui.WindowVSyncMode.OFF);
|
||||
});
|
||||
#end
|
||||
|
||||
//
|
||||
// NEWGROUNDS API SETUP
|
||||
//
|
||||
|
@ -156,6 +202,7 @@ class InitState extends FlxState
|
|||
//
|
||||
#if android
|
||||
FlxG.android.preventDefaultKeys = [flixel.input.android.FlxAndroidKey.BACK];
|
||||
funkin.external.android.CallbackUtil.init();
|
||||
#end
|
||||
|
||||
//
|
||||
|
@ -170,12 +217,20 @@ class InitState extends FlxState
|
|||
#if FEATURE_SCREENSHOTS
|
||||
funkin.util.plugins.ScreenshotPlugin.initialize();
|
||||
#end
|
||||
#if FEATURE_NEWGROUNDS
|
||||
funkin.util.plugins.NewgroundsMedalPlugin.initialize();
|
||||
#end
|
||||
funkin.util.plugins.EvacuateDebugPlugin.initialize();
|
||||
funkin.util.plugins.ForceCrashPlugin.initialize();
|
||||
funkin.util.plugins.ReloadAssetsDebugPlugin.initialize();
|
||||
#if !mobile
|
||||
funkin.util.plugins.VolumePlugin.initialize();
|
||||
#end
|
||||
funkin.util.plugins.WatchPlugin.initialize();
|
||||
#if mobile
|
||||
funkin.util.plugins.TouchPointerPlugin.initialize();
|
||||
funkin.mobile.input.ControlsHandler.initInputTrackers();
|
||||
#end
|
||||
|
||||
//
|
||||
// GAME DATA PARSING
|
||||
|
@ -184,7 +239,6 @@ class InitState extends FlxState
|
|||
// NOTE: Registries must be imported and not referenced with fully qualified names,
|
||||
// to ensure build macros work properly.
|
||||
trace('Parsing game data...');
|
||||
var perfStart:Float = TimerUtil.start();
|
||||
SongEventRegistry.loadEventCache(); // SongEventRegistry is structured differently so it's not a BaseRegistry.
|
||||
SongRegistry.instance.loadEntries();
|
||||
LevelRegistry.instance.loadEntries();
|
||||
|
@ -210,7 +264,10 @@ class InitState extends FlxState
|
|||
|
||||
funkin.input.Cursor.hide();
|
||||
|
||||
trace('Parsing game data took: ${TimerUtil.ms(perfStart)}');
|
||||
#if !html5
|
||||
// This fucking breaks on HTML5 builds because the "shared" library isn't loaded yet.
|
||||
funkin.FunkinMemory.initialCache();
|
||||
#end
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -317,7 +374,7 @@ class InitState extends FlxState
|
|||
}
|
||||
else
|
||||
{
|
||||
FlxG.sound.cache(Paths.music('freakyMenu/freakyMenu'));
|
||||
// FlxG.sound.cache(Paths.music('freakyMenu/freakyMenu'));
|
||||
FlxG.switchState(() -> new TitleState());
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,7 +1,12 @@
|
|||
package funkin;
|
||||
|
||||
#if mobile
|
||||
import funkin.mobile.ui.FunkinHitbox;
|
||||
import funkin.mobile.util.InAppPurchasesUtil;
|
||||
#end
|
||||
import funkin.save.Save;
|
||||
import funkin.util.WindowUtil;
|
||||
import funkin.util.HapticUtil.HapticsMode;
|
||||
|
||||
/**
|
||||
* A core class which provides a store of user-configurable, globally relevant values.
|
||||
|
@ -11,6 +16,7 @@ class Preferences
|
|||
{
|
||||
/**
|
||||
* FPS
|
||||
* Always the refresh rate of the display on mobile, or 60 on web.
|
||||
* @default `60`
|
||||
*/
|
||||
public static var framerate(get, set):Int;
|
||||
|
@ -19,6 +25,12 @@ class Preferences
|
|||
{
|
||||
#if web
|
||||
return 60;
|
||||
#elseif mobile
|
||||
var refreshRate:Int = FlxG.stage.window.displayMode.refreshRate;
|
||||
|
||||
if (refreshRate < 60) refreshRate = 60;
|
||||
|
||||
return refreshRate;
|
||||
#else
|
||||
return Save?.instance?.options?.framerate ?? 60;
|
||||
#end
|
||||
|
@ -46,11 +58,19 @@ class Preferences
|
|||
|
||||
static function get_naughtyness():Bool
|
||||
{
|
||||
#if NO_FEATURE_NAUGHTYNESS
|
||||
return false;
|
||||
#else
|
||||
return Save?.instance?.options?.naughtyness ?? true;
|
||||
#end
|
||||
}
|
||||
|
||||
static function set_naughtyness(value:Bool):Bool
|
||||
{
|
||||
#if NO_FEATURE_NAUGHTYNESS
|
||||
value = false;
|
||||
#end
|
||||
|
||||
var save:Save = Save.instance;
|
||||
save.options.naughtyness = value;
|
||||
save.flush();
|
||||
|
@ -65,7 +85,7 @@ class Preferences
|
|||
|
||||
static function get_downscroll():Bool
|
||||
{
|
||||
return Save?.instance?.options?.downscroll ?? false;
|
||||
return Save?.instance?.options?.downscroll #if mobile ?? true #else ?? false #end;
|
||||
}
|
||||
|
||||
static function set_downscroll(value:Bool):Bool
|
||||
|
@ -116,12 +136,16 @@ class Preferences
|
|||
|
||||
/**
|
||||
* If enabled, an FPS and memory counter will be displayed even if this is not a debug build.
|
||||
* Always disabled on mobile.
|
||||
* @default `false`
|
||||
*/
|
||||
public static var debugDisplay(get, set):Bool;
|
||||
|
||||
static function get_debugDisplay():Bool
|
||||
{
|
||||
#if mobile
|
||||
return false;
|
||||
#end
|
||||
return Save?.instance?.options?.debugDisplay ?? false;
|
||||
}
|
||||
|
||||
|
@ -138,14 +162,78 @@ class Preferences
|
|||
return value;
|
||||
}
|
||||
|
||||
/**
|
||||
* If enabled, haptic feedback will be enabled.
|
||||
* @default `All`
|
||||
*/
|
||||
public static var hapticsMode(get, set):HapticsMode;
|
||||
|
||||
static function get_hapticsMode():HapticsMode
|
||||
{
|
||||
var value = Save?.instance?.options?.hapticsMode ?? "All";
|
||||
|
||||
return switch (value)
|
||||
{
|
||||
case "None":
|
||||
HapticsMode.NONE;
|
||||
case "Notes Only":
|
||||
HapticsMode.NOTES_ONLY;
|
||||
default:
|
||||
HapticsMode.ALL;
|
||||
};
|
||||
}
|
||||
|
||||
static function set_hapticsMode(value:HapticsMode):HapticsMode
|
||||
{
|
||||
var string;
|
||||
|
||||
switch (value)
|
||||
{
|
||||
case HapticsMode.NONE:
|
||||
string = "None";
|
||||
case HapticsMode.NOTES_ONLY:
|
||||
string = "Notes Only";
|
||||
default:
|
||||
string = "All";
|
||||
};
|
||||
|
||||
var save:Save = Save.instance;
|
||||
save.options.hapticsMode = string;
|
||||
save.flush();
|
||||
return value;
|
||||
}
|
||||
|
||||
/**
|
||||
* Multiplier of intensity for all the haptic feedback effects.
|
||||
* @default `2.5`
|
||||
*/
|
||||
public static var hapticsIntensityMultiplier(get, set):Float;
|
||||
|
||||
static function get_hapticsIntensityMultiplier():Float
|
||||
{
|
||||
return Save?.instance?.options?.hapticsIntensityMultiplier ?? 1;
|
||||
}
|
||||
|
||||
static function set_hapticsIntensityMultiplier(value:Float):Float
|
||||
{
|
||||
var save:Save = Save.instance;
|
||||
save.options.hapticsIntensityMultiplier = value;
|
||||
save.flush();
|
||||
return value;
|
||||
}
|
||||
|
||||
/**
|
||||
* If enabled, the game will automatically pause when tabbing out.
|
||||
* Always enabled on mobile.
|
||||
* @default `true`
|
||||
*/
|
||||
public static var autoPause(get, set):Bool;
|
||||
|
||||
static function get_autoPause():Bool
|
||||
{
|
||||
#if mobile
|
||||
return true;
|
||||
#end
|
||||
return Save?.instance?.options?.autoPause ?? true;
|
||||
}
|
||||
|
||||
|
@ -178,6 +266,26 @@ class Preferences
|
|||
return value;
|
||||
}
|
||||
|
||||
/**
|
||||
* A global audio offset in milliseconds.
|
||||
* This is used to sync the audio.
|
||||
* @default `0`
|
||||
*/
|
||||
public static var globalOffset(get, set):Int;
|
||||
|
||||
static function get_globalOffset():Int
|
||||
{
|
||||
return Save?.instance?.options?.globalOffset ?? 0;
|
||||
}
|
||||
|
||||
static function set_globalOffset(value:Int):Int
|
||||
{
|
||||
var save:Save = Save.instance;
|
||||
save.options.globalOffset = value;
|
||||
save.flush();
|
||||
return value;
|
||||
}
|
||||
|
||||
/**
|
||||
* If enabled, the game will utilize VSync (or adaptive VSync) on startup.
|
||||
* @default `OFF`
|
||||
|
@ -357,8 +465,16 @@ class Preferences
|
|||
#if web
|
||||
toggleFramerateCap(Preferences.unlockedFramerate);
|
||||
#end
|
||||
|
||||
#if desktop
|
||||
// Apply the autoFullscreen setting (launches the game in fullscreen automatically)
|
||||
FlxG.fullscreen = Preferences.autoFullscreen;
|
||||
#end
|
||||
|
||||
#if mobile
|
||||
// Apply the allowScreenTimeout setting.
|
||||
lime.system.System.allowScreenTimeout = Preferences.screenTimeout;
|
||||
#end
|
||||
}
|
||||
|
||||
static function toggleFramerateCap(unlocked:Bool):Void
|
||||
|
@ -374,18 +490,88 @@ class Preferences
|
|||
if (show)
|
||||
{
|
||||
// Enable the debug display.
|
||||
FlxG.stage.addChild(Main.fpsCounter);
|
||||
FlxG.game.parent.addChild(Main.fpsCounter);
|
||||
|
||||
#if !html5
|
||||
FlxG.stage.addChild(Main.memoryCounter);
|
||||
FlxG.game.parent.addChild(Main.memoryCounter);
|
||||
#end
|
||||
}
|
||||
else
|
||||
{
|
||||
// Disable the debug display.
|
||||
FlxG.stage.removeChild(Main.fpsCounter);
|
||||
FlxG.game.parent.removeChild(Main.fpsCounter);
|
||||
|
||||
#if !html5
|
||||
FlxG.stage.removeChild(Main.memoryCounter);
|
||||
FlxG.game.parent.removeChild(Main.memoryCounter);
|
||||
#end
|
||||
}
|
||||
}
|
||||
|
||||
#if mobile
|
||||
/**
|
||||
* If enabled, device will be able to sleep on its own.
|
||||
* @default `false`
|
||||
*/
|
||||
public static var screenTimeout(get, set):Bool;
|
||||
|
||||
static function get_screenTimeout():Bool
|
||||
{
|
||||
return Save?.instance?.mobileOptions?.screenTimeout ?? false;
|
||||
}
|
||||
|
||||
static function set_screenTimeout(value:Bool):Bool
|
||||
{
|
||||
if (value != Save.instance.mobileOptions.screenTimeout) lime.system.System.allowScreenTimeout = value;
|
||||
|
||||
var save:Save = Save.instance;
|
||||
save.mobileOptions.screenTimeout = value;
|
||||
save.flush();
|
||||
return value;
|
||||
}
|
||||
|
||||
/**
|
||||
* Controls Scheme for the hitbox.
|
||||
* @default `4 Lanes`
|
||||
*/
|
||||
public static var controlsScheme(get, set):String;
|
||||
|
||||
static function get_controlsScheme():String
|
||||
{
|
||||
return Save?.instance?.mobileOptions?.controlsScheme ?? FunkinHitboxControlSchemes.Arrows;
|
||||
}
|
||||
|
||||
static function set_controlsScheme(value:String):String
|
||||
{
|
||||
var save:Save = Save.instance;
|
||||
save.mobileOptions.controlsScheme = value;
|
||||
save.flush();
|
||||
return value;
|
||||
}
|
||||
|
||||
#if FEATURE_MOBILE_IAP
|
||||
/**
|
||||
* If bought, the game will not show any ads.
|
||||
* @default `false`
|
||||
*/
|
||||
@:unreflective
|
||||
public static var noAds(get, set):Bool;
|
||||
|
||||
@:unreflective
|
||||
static function get_noAds():Bool
|
||||
{
|
||||
if (InAppPurchasesUtil.hasInitialized) noAds = InAppPurchasesUtil.isPurchased(InAppPurchasesUtil.UPGRADE_PRODUCT_ID);
|
||||
var returnedValue = Save?.instance?.mobileOptions?.noAds ?? false;
|
||||
return returnedValue;
|
||||
}
|
||||
|
||||
@:unreflective
|
||||
static function set_noAds(value:Bool):Bool
|
||||
{
|
||||
var save:Save = Save.instance;
|
||||
save.mobileOptions.noAds = value;
|
||||
save.flush();
|
||||
return value;
|
||||
}
|
||||
#end
|
||||
#end
|
||||
}
|
||||
|
|
|
@ -1,5 +1,6 @@
|
|||
package funkin.api.discord;
|
||||
|
||||
import funkin.util.macro.EnvironmentConfigMacro;
|
||||
#if FEATURE_DISCORD_RPC
|
||||
import hxdiscord_rpc.Discord;
|
||||
import hxdiscord_rpc.Types.DiscordButton;
|
||||
|
@ -11,7 +12,7 @@ import sys.thread.Thread;
|
|||
@:nullSafety
|
||||
class DiscordClient
|
||||
{
|
||||
static final CLIENT_ID:String = "816168432860790794";
|
||||
static final CLIENT_ID:Null<String> = EnvironmentConfigMacro.environmentConfig?.get("DESKTOP_DISCORD_CLIENT_ID");
|
||||
|
||||
public static var instance(get, never):DiscordClient;
|
||||
static var _instance:Null<DiscordClient> = null;
|
||||
|
@ -40,12 +41,28 @@ class DiscordClient
|
|||
{
|
||||
trace('[DISCORD] Initializing connection...');
|
||||
|
||||
// Discord.initialize(CLIENT_ID, handlers, true, null);
|
||||
Discord.Initialize(CLIENT_ID, cpp.RawPointer.addressOf(handlers), 1, "");
|
||||
if (!hasValidCredentials())
|
||||
{
|
||||
FlxG.log.warn("Tried to initialize Discord connection, but credentials are invalid!");
|
||||
return;
|
||||
}
|
||||
|
||||
@:nullSafety(Off)
|
||||
{
|
||||
Discord.Initialize(CLIENT_ID, cpp.RawPointer.addressOf(handlers), 1, "");
|
||||
}
|
||||
|
||||
createDaemon();
|
||||
}
|
||||
|
||||
/**
|
||||
* @returns `false` if the client ID is invalid.
|
||||
*/
|
||||
static function hasValidCredentials():Bool
|
||||
{
|
||||
return !(CLIENT_ID == null || CLIENT_ID == "" || (CLIENT_ID != null && CLIENT_ID.contains(" ")));
|
||||
}
|
||||
|
||||
var daemon:Null<Thread> = null;
|
||||
|
||||
function createDaemon():Void
|
||||
|
@ -202,14 +219,27 @@ typedef DiscordClientPresenceParams =
|
|||
|
||||
class DiscordClientSandboxed
|
||||
{
|
||||
public static function setPresence(params:DiscordClientPresenceParams)
|
||||
public static function setPresence(params:DiscordClientPresenceParams):Void
|
||||
{
|
||||
return DiscordClient.instance.setPresence(params);
|
||||
DiscordClient.instance.setPresence(params);
|
||||
}
|
||||
|
||||
public static function shutdown()
|
||||
public static function shutdown():Void
|
||||
{
|
||||
DiscordClient.instance.shutdown();
|
||||
}
|
||||
}
|
||||
#else
|
||||
class DiscordClientSandboxed
|
||||
{
|
||||
public static function setPresence(params:Dynamic):Void
|
||||
{
|
||||
// Do nothing.
|
||||
}
|
||||
|
||||
public static function shutdown():Void
|
||||
{
|
||||
// Do nothing.
|
||||
}
|
||||
}
|
||||
#end
|
||||
|
|
|
@ -1,9 +1,11 @@
|
|||
package funkin.api.newgrounds;
|
||||
|
||||
#if FEATURE_NEWGROUNDS_EVENTS
|
||||
import io.newgrounds.Call.CallOutcome;
|
||||
import io.newgrounds.NG;
|
||||
import io.newgrounds.objects.events.Outcome;
|
||||
import io.newgrounds.objects.events.Result;
|
||||
#end
|
||||
|
||||
/**
|
||||
* Use Newgrounds to perform basic telemetry. Ignore if not logged in to Newgrounds.
|
||||
|
@ -31,6 +33,7 @@ class Events
|
|||
#end
|
||||
}
|
||||
|
||||
#if FEATURE_NEWGROUNDS_EVENTS
|
||||
static function onEventLogged(eventName:String, outcome:CallOutcome<LogEventData>)
|
||||
{
|
||||
switch (outcome)
|
||||
|
@ -55,6 +58,7 @@ class Events
|
|||
}
|
||||
}
|
||||
}
|
||||
#end
|
||||
|
||||
public static inline function logStartGame():Void
|
||||
{
|
||||
|
|
156
source/funkin/api/newgrounds/NGSaveSlot.hx
Normal file
156
source/funkin/api/newgrounds/NGSaveSlot.hx
Normal file
|
@ -0,0 +1,156 @@
|
|||
package funkin.api.newgrounds;
|
||||
|
||||
#if FEATURE_NEWGROUNDS
|
||||
import io.newgrounds.utils.SaveSlotList;
|
||||
import io.newgrounds.objects.SaveSlot;
|
||||
import io.newgrounds.Call.CallError;
|
||||
import io.newgrounds.objects.events.Outcome;
|
||||
import funkin.save.Save;
|
||||
|
||||
@:nullSafety
|
||||
@:access(funkin.save.Save)
|
||||
class NGSaveSlot
|
||||
{
|
||||
public static var instance(get, never):NGSaveSlot;
|
||||
static var _instance:Null<NGSaveSlot> = null;
|
||||
|
||||
static function get_instance():NGSaveSlot
|
||||
{
|
||||
if (_instance == null)
|
||||
{
|
||||
return loadInstance();
|
||||
}
|
||||
return _instance;
|
||||
}
|
||||
|
||||
public static function loadInstance():NGSaveSlot
|
||||
{
|
||||
var loadedSave:NGSaveSlot = loadSlot(Save.BASE_SAVE_SLOT);
|
||||
if (_instance == null) _instance = loadedSave;
|
||||
|
||||
return loadedSave;
|
||||
}
|
||||
|
||||
static function loadSlot(slot:Int):NGSaveSlot
|
||||
{
|
||||
trace('[NEWGROUNDS] Getting save slot from ID $slot');
|
||||
|
||||
var saveSlot:Null<SaveSlot> = NewgroundsClient.instance.saveSlots?.getById(slot);
|
||||
|
||||
var saveSlotObj:NGSaveSlot = new NGSaveSlot(saveSlot);
|
||||
return saveSlotObj;
|
||||
}
|
||||
|
||||
public var ngSaveSlot:Null<SaveSlot> = null;
|
||||
|
||||
public function new(?ngSaveSlot:Null<SaveSlot>)
|
||||
{
|
||||
this.ngSaveSlot = ngSaveSlot;
|
||||
|
||||
#if FLX_DEBUG
|
||||
FlxG.console.registerClass(NGSaveSlot);
|
||||
FlxG.console.registerClass(Save);
|
||||
#end
|
||||
}
|
||||
|
||||
/**
|
||||
* Saves `data` to the newgrounds save slot.
|
||||
* @param data The raw save data.
|
||||
*/
|
||||
public function save(data:RawSaveData):Void
|
||||
{
|
||||
var encodedData:String = haxe.Serializer.run(data);
|
||||
|
||||
try
|
||||
{
|
||||
ngSaveSlot?.save(encodedData, function(outcome:Outcome<CallError>) {
|
||||
switch (outcome)
|
||||
{
|
||||
case SUCCESS:
|
||||
trace('[NEWGROUNDS] Successfully saved save data to save slot!');
|
||||
case FAIL(error):
|
||||
trace('[NEWGROUNDS] Failed to save data to save slot!');
|
||||
trace(error);
|
||||
}
|
||||
});
|
||||
}
|
||||
catch (error:String)
|
||||
{
|
||||
trace('[NEWGROUNDS] Failed to save data to save slot!');
|
||||
trace(error);
|
||||
}
|
||||
}
|
||||
|
||||
public function load(?onComplete:Null<Dynamic->Void>, ?onError:Null<CallError->Void>):Void
|
||||
{
|
||||
try
|
||||
{
|
||||
ngSaveSlot?.load(function(outcome:SaveSlotOutcome):Void {
|
||||
switch (outcome)
|
||||
{
|
||||
case SUCCESS(value):
|
||||
trace('[NEWGROUNDS] Loaded save slot with the ID of ${ngSaveSlot?.id}!');
|
||||
#if FEATURE_DEBUG_FUNCTIONS
|
||||
trace('Save Slot Data:');
|
||||
trace(value);
|
||||
#end
|
||||
|
||||
if (onComplete != null && value != null)
|
||||
{
|
||||
var decodedData:Dynamic = haxe.Unserializer.run(value);
|
||||
onComplete(decodedData);
|
||||
}
|
||||
case FAIL(error):
|
||||
trace('[NEWGROUNDS] Failed to load save slot with the ID of ${ngSaveSlot?.id}!');
|
||||
trace(error);
|
||||
|
||||
if (onError != null)
|
||||
{
|
||||
onError(error);
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
catch (error:String)
|
||||
{
|
||||
trace('[NEWGROUNDS] Failed to load save slot with the ID of ${ngSaveSlot?.id}!');
|
||||
trace(error);
|
||||
|
||||
if (onError != null)
|
||||
{
|
||||
onError(RESPONSE({message: error, code: 500}));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public function clear():Void
|
||||
{
|
||||
try
|
||||
{
|
||||
ngSaveSlot?.clear(function(outcome:Outcome<CallError>) {
|
||||
switch (outcome)
|
||||
{
|
||||
case SUCCESS:
|
||||
trace('[NEWGROUNDS] Successfully cleared save slot!');
|
||||
case FAIL(error):
|
||||
trace('[NEWGROUNDS] Failed to clear save slot!');
|
||||
trace(error);
|
||||
}
|
||||
});
|
||||
}
|
||||
catch (error:String)
|
||||
{
|
||||
trace('[NEWGROUNDS] Failed to clear save slot!');
|
||||
trace(error);
|
||||
}
|
||||
}
|
||||
|
||||
public function checkSlot():Void
|
||||
{
|
||||
trace('[NEWGROUNDS] Checking save slot with the ID of ${ngSaveSlot?.id}...');
|
||||
|
||||
trace(' Is null? ${ngSaveSlot == null}');
|
||||
trace(' Is empty? ${ngSaveSlot?.isEmpty() ?? false}');
|
||||
}
|
||||
}
|
||||
#end
|
|
@ -1,5 +1,6 @@
|
|||
package funkin.api.newgrounds;
|
||||
|
||||
import funkin.util.macro.EnvironmentConfigMacro;
|
||||
import funkin.save.Save;
|
||||
import funkin.api.newgrounds.Medals.Medal;
|
||||
#if FEATURE_NEWGROUNDS
|
||||
|
@ -10,13 +11,18 @@ import io.newgrounds.NGLite.LoginOutcome;
|
|||
import io.newgrounds.NGLite.LoginFail;
|
||||
import io.newgrounds.objects.events.Outcome;
|
||||
import io.newgrounds.utils.MedalList;
|
||||
import io.newgrounds.utils.SaveSlotList;
|
||||
import io.newgrounds.utils.ScoreBoardList;
|
||||
import io.newgrounds.objects.User;
|
||||
|
||||
@:nullSafety
|
||||
class NewgroundsClient
|
||||
{
|
||||
static final APP_ID:Null<String> = EnvironmentConfigMacro.environmentConfig?.get("API_NG_APP_ID");
|
||||
static final ENCRYPTION_KEY:Null<String> = EnvironmentConfigMacro.environmentConfig?.get("API_NG_ENC_KEY");
|
||||
|
||||
public static var instance(get, never):NewgroundsClient;
|
||||
|
||||
static var _instance:Null<NewgroundsClient> = null;
|
||||
|
||||
static function get_instance():NewgroundsClient
|
||||
|
@ -29,14 +35,15 @@ class NewgroundsClient
|
|||
public var user(get, never):Null<User>;
|
||||
public var medals(get, never):Null<MedalList>;
|
||||
public var leaderboards(get, never):Null<ScoreBoardList>;
|
||||
public var saveSlots(get, never):Null<SaveSlotList>;
|
||||
|
||||
private function new()
|
||||
{
|
||||
trace('[NEWGROUNDS] Initializing client...');
|
||||
|
||||
#if FEATURE_NEWGROUNDS_DEBUG
|
||||
trace('[NEWGROUNDS] App ID: ${NewgroundsCredentials.APP_ID}');
|
||||
trace('[NEWGROUNDS] Encryption Key: ${NewgroundsCredentials.ENCRYPTION_KEY}');
|
||||
trace('[NEWGROUNDS] App ID: ${APP_ID}');
|
||||
trace('[NEWGROUNDS] Encryption Key: ${ENCRYPTION_KEY}');
|
||||
#end
|
||||
|
||||
if (!hasValidCredentials())
|
||||
|
@ -45,9 +52,12 @@ class NewgroundsClient
|
|||
return;
|
||||
}
|
||||
|
||||
var debug = #if FEATURE_NEWGROUNDS_DEBUG true #else false #end;
|
||||
NG.create(NewgroundsCredentials.APP_ID, getSessionId(), debug, onLoginResolved);
|
||||
NG.core.setupEncryption(NewgroundsCredentials.ENCRYPTION_KEY);
|
||||
@:nullSafety(Off)
|
||||
{
|
||||
NG.create(APP_ID, getSessionId(), #if FEATURE_NEWGROUNDS_DEBUG true #else false #end, onLoginResolved);
|
||||
|
||||
NG.core.setupEncryption(ENCRYPTION_KEY);
|
||||
}
|
||||
}
|
||||
|
||||
public function init()
|
||||
|
@ -166,12 +176,12 @@ class NewgroundsClient
|
|||
*/
|
||||
static function hasValidCredentials():Bool
|
||||
{
|
||||
return !(NewgroundsCredentials.APP_ID == null
|
||||
|| NewgroundsCredentials.APP_ID == ""
|
||||
|| NewgroundsCredentials.APP_ID.contains(" ")
|
||||
|| NewgroundsCredentials.ENCRYPTION_KEY == null
|
||||
|| NewgroundsCredentials.ENCRYPTION_KEY == ""
|
||||
|| NewgroundsCredentials.ENCRYPTION_KEY.contains(" "));
|
||||
return !(APP_ID == null
|
||||
|| APP_ID == ""
|
||||
|| (APP_ID != null && APP_ID.contains(" "))
|
||||
|| ENCRYPTION_KEY == null
|
||||
|| ENCRYPTION_KEY == ""
|
||||
|| (ENCRYPTION_KEY != null && ENCRYPTION_KEY.contains(" ")));
|
||||
}
|
||||
|
||||
function onLoginResolved(outcome:LoginOutcome):Void
|
||||
|
@ -236,6 +246,8 @@ class NewgroundsClient
|
|||
|
||||
trace('[NEWGROUNDS] Submitting leaderboard request...');
|
||||
NG.core.scoreBoards.loadList(onFetchedLeaderboards);
|
||||
trace('[NEWGROUNDS] Submitting save slot request...');
|
||||
NG.core.saveSlots.loadList(onFetchedSaveSlots);
|
||||
}
|
||||
|
||||
function onLoginFailed(result:LoginFail):Void
|
||||
|
@ -301,6 +313,13 @@ class NewgroundsClient
|
|||
// trace(funkin.api.newgrounds.Leaderboards.listLeaderboardData());
|
||||
}
|
||||
|
||||
function onFetchedSaveSlots(outcome:Outcome<CallError>):Void
|
||||
{
|
||||
trace('[NEWGROUNDS] Fetched save slots!');
|
||||
|
||||
NGSaveSlot.instance.checkSlot();
|
||||
}
|
||||
|
||||
function get_user():Null<User>
|
||||
{
|
||||
if (NG.core == null || !this.isLoggedIn()) return null;
|
||||
|
@ -319,6 +338,12 @@ class NewgroundsClient
|
|||
return NG.core.scoreBoards;
|
||||
}
|
||||
|
||||
function get_saveSlots():Null<SaveSlotList>
|
||||
{
|
||||
if (NG.core == null || !this.isLoggedIn()) return null;
|
||||
return NG.core.saveSlots;
|
||||
}
|
||||
|
||||
static function getSessionId():Null<String>
|
||||
{
|
||||
#if js
|
||||
|
|
|
@ -1,32 +0,0 @@
|
|||
package funkin.audio;
|
||||
|
||||
#if desktop
|
||||
import haxe.io.Path;
|
||||
|
||||
import sys.FileSystem;
|
||||
|
||||
/*
|
||||
* A class that simply points the audio backend OpenALSoft to use a custom
|
||||
* configuration when the game starts up.
|
||||
*
|
||||
* The config file overrides a few global OpenALSoft settings to improve audio
|
||||
* quality on desktop targets.
|
||||
*/
|
||||
@:nullSafety
|
||||
class ALSoftConfig
|
||||
{
|
||||
private static function __init__():Void
|
||||
{
|
||||
var configPath:String = Path.directory(Path.withoutExtension(#if hl Sys.getCwd() #else Sys.programPath() #end));
|
||||
#if windows
|
||||
configPath += "/plugins/alsoft.ini";
|
||||
#elseif mac
|
||||
configPath = '${Path.directory(configPath)}/Resources/plugins/alsoft.conf';
|
||||
#else
|
||||
configPath += "/plugins/alsoft.conf";
|
||||
#end
|
||||
|
||||
Sys.putEnv("ALSOFT_CONF", FileSystem.fullPath(configPath));
|
||||
}
|
||||
}
|
||||
#end
|
|
@ -20,9 +20,6 @@ import openfl.media.Sound;
|
|||
import openfl.media.SoundChannel;
|
||||
import openfl.media.SoundMixer;
|
||||
|
||||
#if (openfl >= "8.0.0")
|
||||
#end
|
||||
|
||||
/**
|
||||
* A FlxSound which adds additional functionality:
|
||||
* - Delayed playback via negative song position.
|
||||
|
@ -45,9 +42,9 @@ class FunkinSound extends FlxSound implements ICloneable<FunkinSound>
|
|||
if (_onVolumeChanged == null)
|
||||
{
|
||||
_onVolumeChanged = new FlxTypedSignal<Float->Void>();
|
||||
FlxG.sound.volumeHandler = function(volume:Float) {
|
||||
FlxG.sound.onVolumeChange.add(function(volume:Float) {
|
||||
_onVolumeChanged.dispatch(volume);
|
||||
}
|
||||
});
|
||||
}
|
||||
return _onVolumeChanged;
|
||||
}
|
||||
|
@ -478,7 +475,7 @@ class FunkinSound extends FlxSound implements ICloneable<FunkinSound>
|
|||
|
||||
if (autoPlay) sound.play();
|
||||
sound.volume = volume;
|
||||
sound.group = FlxG.sound.defaultSoundGroup;
|
||||
FlxG.sound.defaultSoundGroup.add(sound);
|
||||
sound.persist = persist;
|
||||
sound.important = important;
|
||||
|
||||
|
|
|
@ -72,8 +72,8 @@ class ABotVis extends FlxTypedSpriteGroup<FlxSprite>
|
|||
// we use a very low minFreq since some songs use low low subbass like a boss
|
||||
analyzer.minFreq = 10;
|
||||
|
||||
#if desktop
|
||||
// On desktop it uses FFT stuff that isn't as optimized as the direct browser stuff we use on HTML5
|
||||
#if sys
|
||||
// On native it uses FFT stuff that isn't as optimized as the direct browser stuff we use on HTML5
|
||||
// So we want to manually change it!
|
||||
analyzer.fftN = 256;
|
||||
#end
|
||||
|
|
|
@ -243,9 +243,10 @@ class WaveformDataChannel
|
|||
}
|
||||
|
||||
/**
|
||||
* Retrieve a given minimum point at an index.
|
||||
* @param i Index
|
||||
* @return minimum point at an index.
|
||||
*/
|
||||
public function minSample(i:Int)
|
||||
public function minSample(i:Int):Int
|
||||
{
|
||||
var offset = (i * parent.channels + this.channelId) * 2;
|
||||
return inline parent.get(offset);
|
||||
|
|
|
@ -1,7 +1,5 @@
|
|||
package funkin.audio.waveform;
|
||||
|
||||
import funkin.util.TimerUtil;
|
||||
|
||||
@:nullSafety
|
||||
class WaveformDataParser
|
||||
{
|
||||
|
@ -45,73 +43,58 @@ class WaveformDataParser
|
|||
|
||||
public static function interpretAudioBuffer(soundBuffer:lime.media.AudioBuffer):Null<WaveformData>
|
||||
{
|
||||
var sampleRate = soundBuffer.sampleRate;
|
||||
var channels = soundBuffer.channels;
|
||||
var bitsPerSample = soundBuffer.bitsPerSample;
|
||||
var samplesPerPoint:Int = 256; // I don't think we need to configure this.
|
||||
var pointsPerSecond:Float = sampleRate / samplesPerPoint; // 172 samples per second for most songs is plenty precise while still being performant..
|
||||
|
||||
// TODO: Make this work better on HTML5.
|
||||
var soundData:lime.utils.Int16Array = cast soundBuffer.data;
|
||||
|
||||
var soundDataRawLength:Int = soundData.length;
|
||||
var soundDataSampleCount:Int = Std.int(Math.ceil(soundDataRawLength / channels / (bitsPerSample == 16 ? 2 : 1)));
|
||||
var soundDataSampleCount:Int = Std.int(Math.ceil(soundData.length / channels / (bitsPerSample == 16 ? 2 : 1)));
|
||||
var outputPointCount:Int = Std.int(Math.ceil(soundDataSampleCount / samplesPerPoint));
|
||||
|
||||
// trace('Interpreting audio buffer:');
|
||||
// trace(' sampleRate: ${sampleRate}');
|
||||
// trace(' channels: ${channels}');
|
||||
// trace(' bitsPerSample: ${bitsPerSample}');
|
||||
// trace(' samplesPerPoint: ${samplesPerPoint}');
|
||||
// trace(' pointsPerSecond: ${pointsPerSecond}');
|
||||
// trace(' soundDataRawLength: ${soundDataRawLength}');
|
||||
// trace(' soundDataSampleCount: ${soundDataSampleCount}');
|
||||
// trace(' soundDataRawLength/4: ${soundDataRawLength / 4}');
|
||||
// trace(' outputPointCount: ${outputPointCount}');
|
||||
// Pre-allocate Vector with exact final size for better performance and memory efficiency
|
||||
var outputDataLength:Int = outputPointCount * channels * 2;
|
||||
var outputData = new haxe.ds.Vector<Int>(outputDataLength);
|
||||
|
||||
var minSampleValue:Int = bitsPerSample == 16 ? INT16_MIN : INT8_MIN;
|
||||
var maxSampleValue:Int = bitsPerSample == 16 ? INT16_MAX : INT8_MAX;
|
||||
|
||||
var outputData:Array<Int> = [];
|
||||
|
||||
var perfStart:Float = TimerUtil.start();
|
||||
// Reusable min/max tracking arrays to avoid allocation in the loop
|
||||
var minValues = new haxe.ds.Vector<Int>(channels);
|
||||
var maxValues = new haxe.ds.Vector<Int>(channels);
|
||||
|
||||
for (pointIndex in 0...outputPointCount)
|
||||
{
|
||||
// minChannel1, maxChannel1, minChannel2, maxChannel2, ...
|
||||
var values:Array<Int> = [];
|
||||
var rangeStart:Int = pointIndex * samplesPerPoint;
|
||||
var rangeEnd:Int = Std.int(Math.min(rangeStart + samplesPerPoint, soundDataSampleCount));
|
||||
|
||||
// Reset min/max values for this range
|
||||
for (i in 0...channels)
|
||||
{
|
||||
values.push(bitsPerSample == 16 ? INT16_MAX : INT8_MAX);
|
||||
values.push(bitsPerSample == 16 ? INT16_MIN : INT8_MIN);
|
||||
minValues[i] = bitsPerSample == 16 ? INT16_MAX : INT8_MAX;
|
||||
maxValues[i] = bitsPerSample == 16 ? INT16_MIN : INT8_MIN;
|
||||
}
|
||||
|
||||
var rangeStart = pointIndex * samplesPerPoint;
|
||||
var rangeEnd = rangeStart + samplesPerPoint;
|
||||
if (rangeEnd > soundDataSampleCount) rangeEnd = soundDataSampleCount;
|
||||
|
||||
// Process all samples in this range
|
||||
for (sampleIndex in rangeStart...rangeEnd)
|
||||
{
|
||||
for (channelIndex in 0...channels)
|
||||
{
|
||||
var sampleIndex:Int = sampleIndex * channels + channelIndex;
|
||||
var sampleValue = soundData[sampleIndex];
|
||||
var sampleValue:Int = soundData[sampleIndex * channels + channelIndex];
|
||||
|
||||
if (sampleValue < values[channelIndex * 2]) values[(channelIndex * 2)] = sampleValue;
|
||||
if (sampleValue > values[channelIndex * 2 + 1]) values[(channelIndex * 2) + 1] = sampleValue;
|
||||
if (sampleValue < minValues[channelIndex]) minValues[channelIndex] = sampleValue;
|
||||
if (sampleValue > maxValues[channelIndex]) maxValues[channelIndex] = sampleValue;
|
||||
}
|
||||
}
|
||||
|
||||
// We now have the min and max values for the range.
|
||||
for (value in values)
|
||||
outputData.push(value);
|
||||
// Write directly to final positions in output Vector
|
||||
var baseIndex:Int = pointIndex * channels * 2;
|
||||
for (channelIndex in 0...channels)
|
||||
{
|
||||
outputData[baseIndex + channelIndex * 2] = minValues[channelIndex];
|
||||
outputData[baseIndex + channelIndex * 2 + 1] = maxValues[channelIndex];
|
||||
}
|
||||
}
|
||||
|
||||
var outputDataLength:Int = Std.int(outputData.length / channels / 2);
|
||||
var result = new WaveformData(null, channels, sampleRate, samplesPerPoint, bitsPerSample, outputPointCount, outputData);
|
||||
|
||||
trace('[WAVEFORM] Interpreted audio buffer in ${TimerUtil.seconds(perfStart)}.');
|
||||
var result = new WaveformData(null, channels, soundBuffer.sampleRate, samplesPerPoint, bitsPerSample, outputPointCount, outputData.toArray());
|
||||
|
||||
return result;
|
||||
}
|
||||
|
|
|
@ -57,7 +57,11 @@ class PlayerRegistry extends BaseRegistry<PlayableCharacter, PlayerData> impleme
|
|||
var player = fetchEntry(charId);
|
||||
if (player == null) continue;
|
||||
|
||||
#if UNLOCK_EVERYTHING
|
||||
count++;
|
||||
#else
|
||||
if (player.isUnlocked()) count++;
|
||||
#end
|
||||
}
|
||||
|
||||
return count;
|
||||
|
@ -65,6 +69,7 @@ class PlayerRegistry extends BaseRegistry<PlayableCharacter, PlayerData> impleme
|
|||
|
||||
public function hasNewCharacter():Bool
|
||||
{
|
||||
#if (!UNLOCK_EVERYTHING)
|
||||
var charactersSeen = Save.instance.charactersSeen.clone();
|
||||
|
||||
for (charId in listEntryIds())
|
||||
|
@ -78,6 +83,7 @@ class PlayerRegistry extends BaseRegistry<PlayableCharacter, PlayerData> impleme
|
|||
// This character is unlocked but we haven't seen them in Freeplay yet.
|
||||
return true;
|
||||
}
|
||||
#end
|
||||
|
||||
// Fallthrough case.
|
||||
return false;
|
||||
|
@ -85,9 +91,10 @@ class PlayerRegistry extends BaseRegistry<PlayableCharacter, PlayerData> impleme
|
|||
|
||||
public function listNewCharacters():Array<String>
|
||||
{
|
||||
var charactersSeen = Save.instance.charactersSeen.clone();
|
||||
var result = [];
|
||||
|
||||
#if (!UNLOCK_EVERYTHING)
|
||||
var charactersSeen = Save.instance.charactersSeen.clone();
|
||||
for (charId in listEntryIds())
|
||||
{
|
||||
var player = fetchEntry(charId);
|
||||
|
@ -99,6 +106,7 @@ class PlayerRegistry extends BaseRegistry<PlayableCharacter, PlayerData> impleme
|
|||
// This character is unlocked but we haven't seen them in Freeplay yet.
|
||||
result.push(charId);
|
||||
}
|
||||
#end
|
||||
|
||||
return result;
|
||||
}
|
||||
|
@ -117,6 +125,7 @@ class PlayerRegistry extends BaseRegistry<PlayableCharacter, PlayerData> impleme
|
|||
/**
|
||||
* Return true if the given stage character is associated with a specific playable character.
|
||||
* If so, the level should only appear if that character is selected in Freeplay.
|
||||
* NOTE: This is NOT THE SAME as `player.isUnlocked()`!
|
||||
* @param characterId The stage character ID.
|
||||
* @return Whether the character is owned by any one character.
|
||||
*/
|
||||
|
@ -124,4 +133,17 @@ class PlayerRegistry extends BaseRegistry<PlayableCharacter, PlayerData> impleme
|
|||
{
|
||||
return ownedCharacterIds.exists(characterId);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param characterId The character ID to check.
|
||||
* @return Whether the player saw the character unlock animation in Character Select.
|
||||
*/
|
||||
public function isCharacterSeen(characterId:String):Bool
|
||||
{
|
||||
#if UNLOCK_EVERYTHING
|
||||
return true;
|
||||
#else
|
||||
return Save.instance.charactersSeen.contains(characterId);
|
||||
#end
|
||||
}
|
||||
}
|
||||
|
|
|
@ -140,7 +140,7 @@ class SongMetadata implements ICloneable<SongMetadata>
|
|||
*/
|
||||
public function toString():String
|
||||
{
|
||||
return 'SongMetadata(${this.songName} by ${this.artist} and ${this.charter}, variation ${this.variation})';
|
||||
return 'SongMetadata(${this.songName} by ${this.artist}, charted by ${this.charter}, variation ${this.variation})';
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -1117,6 +1117,23 @@ class SongNoteDataRaw implements ICloneable<SongNoteDataRaw>
|
|||
return 'SongNoteData(${this.time}ms, ' + (this.length > 0 ? '[${this.length}ms hold]' : '') + ' ${this.data}'
|
||||
+ (this.kind != '' ? ' [kind: ${this.kind}])' : ')');
|
||||
}
|
||||
|
||||
public function buildTooltip():String
|
||||
{
|
||||
if ((this.kind?.length ?? 0) == 0) return "";
|
||||
|
||||
var result:String = 'Kind: ${this.kind}';
|
||||
if (this.params.length == 0) return result;
|
||||
|
||||
result += "\nParams:";
|
||||
|
||||
for (param in params)
|
||||
{
|
||||
result += '\n- ${param.name}: ${param.value}';
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
|
@ -37,7 +37,7 @@ class FNFLegacyImporter
|
|||
{
|
||||
trace('Migrating song metadata from FNF Legacy.');
|
||||
|
||||
var songMetadata:SongMetadata = new SongMetadata('Import', Constants.DEFAULT_ARTIST, 'default');
|
||||
var songMetadata:SongMetadata = new SongMetadata('Import', Constants.DEFAULT_ARTIST, Constants.DEFAULT_CHARTER, Constants.DEFAULT_VARIATION);
|
||||
|
||||
// Set generatedBy string for debugging.
|
||||
songMetadata.generatedBy = 'Chart Editor Import (FNF Legacy)';
|
||||
|
|
|
@ -24,7 +24,7 @@ class SongDataMigrator
|
|||
|
||||
public static function migrate_SongMetadata_v2_1_0(input:SongData_v2_1_0.SongMetadata_v2_1_0):SongMetadata
|
||||
{
|
||||
var result:SongMetadata = new SongMetadata(input.songName, input.artist, input.variation);
|
||||
var result:SongMetadata = new SongMetadata(input.songName, input.artist, Constants.DEFAULT_CHARTER, input.variation);
|
||||
result.version = SongRegistry.SONG_METADATA_VERSION;
|
||||
result.timeFormat = input.timeFormat;
|
||||
result.divisions = input.divisions;
|
||||
|
@ -66,7 +66,7 @@ class SongDataMigrator
|
|||
|
||||
public static function migrate_SongMetadata_v2_0_0(input:SongData_v2_0_0.SongMetadata_v2_0_0):SongMetadata
|
||||
{
|
||||
var result:SongMetadata = new SongMetadata(input.songName, input.artist, input.variation);
|
||||
var result:SongMetadata = new SongMetadata(input.songName, input.artist, Constants.DEFAULT_CHARTER, input.variation);
|
||||
result.version = SongRegistry.SONG_METADATA_VERSION;
|
||||
result.timeFormat = input.timeFormat;
|
||||
result.divisions = input.divisions;
|
||||
|
|
|
@ -13,7 +13,7 @@ class StageRegistry extends BaseRegistry<Stage, StageData> implements ISingleton
|
|||
* Handle breaking changes by incrementing this value
|
||||
* and adding migration to the `migrateStageData()` function.
|
||||
*/
|
||||
public static final STAGE_DATA_VERSION:thx.semver.Version = "1.0.3";
|
||||
public static final STAGE_DATA_VERSION:thx.semver.Version = "1.0.2";
|
||||
|
||||
public static final STAGE_DATA_VERSION_RULE:thx.semver.VersionRule = ">=1.0.0 <1.1.0";
|
||||
|
||||
|
|
70
source/funkin/external/android/CallbackUtil.hx
vendored
Normal file
70
source/funkin/external/android/CallbackUtil.hx
vendored
Normal file
|
@ -0,0 +1,70 @@
|
|||
package funkin.external.android;
|
||||
|
||||
#if android
|
||||
import lime.system.JNI;
|
||||
import flixel.util.FlxSignal;
|
||||
|
||||
/**
|
||||
* A Utility class to handle Android API level callbacks and events.
|
||||
*/
|
||||
@:unreflective
|
||||
class CallbackUtil
|
||||
{
|
||||
/**
|
||||
* The result code for `DATA_FOLDER_CLOSED` activity.
|
||||
*/
|
||||
public static var DATA_FOLDER_CLOSED(get, never):Int;
|
||||
|
||||
@:noCompletion
|
||||
static function get_DATA_FOLDER_CLOSED():Int
|
||||
{
|
||||
final field:Null<Dynamic> = JNIUtil.createStaticField('funkin/extensions/CallbackUtil', 'DATA_FOLDER_CLOSED', 'I');
|
||||
|
||||
return field != null ? field.get() : 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* Signal triggered when an activity result is received.
|
||||
*
|
||||
* First argument is the request code, second is the result code.
|
||||
*/
|
||||
public static var onActivityResult:FlxTypedSignal<Int->Int->Void> = new FlxTypedSignal<Int->Int->Void>();
|
||||
|
||||
/**
|
||||
* Initializes the callback utility.
|
||||
*/
|
||||
public static function init():Void
|
||||
{
|
||||
final initCallBackJNI:Null<Dynamic> = JNIUtil.createStaticMethod('funkin/extensions/CallbackUtil', 'initCallBack', '(Lorg/haxe/lime/HaxeObject;)V');
|
||||
|
||||
if (initCallBackJNI != null)
|
||||
{
|
||||
initCallBackJNI(new CallbackHandler());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Internal class to handle native callback events.
|
||||
*/
|
||||
class CallbackHandler #if (lime >= "8.0.0") implements JNISafety #end
|
||||
{
|
||||
@:allow(funkin.external.android.CallbackUtil)
|
||||
function new():Void {}
|
||||
|
||||
/**
|
||||
* Handles the activity result callback from native code.
|
||||
*
|
||||
* @param requestCode The request code of the acitivty.
|
||||
* @param resultCode The result code of the acitivty.
|
||||
*/
|
||||
@:keep
|
||||
#if (lime >= "8.0.0")
|
||||
@:runOnMainThread
|
||||
#end
|
||||
public function onActivityResult(requestCode:Int, resultCode:Int):Void
|
||||
{
|
||||
if (CallbackUtil.onActivityResult != null) CallbackUtil.onActivityResult.dispatch(requestCode, resultCode);
|
||||
}
|
||||
}
|
||||
#end
|
23
source/funkin/external/android/DataFolderUtil.hx
vendored
Normal file
23
source/funkin/external/android/DataFolderUtil.hx
vendored
Normal file
|
@ -0,0 +1,23 @@
|
|||
package funkin.external.android;
|
||||
|
||||
#if android
|
||||
/**
|
||||
* A Utility class to manage the Application's Data folder on Android.
|
||||
*/
|
||||
@:unreflective
|
||||
class DataFolderUtil
|
||||
{
|
||||
/**
|
||||
* Opens the data folder on an Android device using JNI.
|
||||
*/
|
||||
public static function openDataFolder():Void
|
||||
{
|
||||
final openDataFolderJNI:Null<Dynamic> = JNIUtil.createStaticMethod('funkin/util/DataFolderUtil', 'openDataFolder', '(I)V');
|
||||
|
||||
if (openDataFolderJNI != null)
|
||||
{
|
||||
openDataFolderJNI(CallbackUtil.DATA_FOLDER_CLOSED);
|
||||
}
|
||||
}
|
||||
}
|
||||
#end
|
111
source/funkin/external/android/JNIUtil.hx
vendored
Normal file
111
source/funkin/external/android/JNIUtil.hx
vendored
Normal file
|
@ -0,0 +1,111 @@
|
|||
package funkin.external.android;
|
||||
|
||||
#if android
|
||||
import lime.system.JNI;
|
||||
|
||||
/**
|
||||
* A utility class for caching JNI method and field references.
|
||||
*/
|
||||
class JNIUtil
|
||||
{
|
||||
@:noCompletion
|
||||
private static var staticMethodCache:Map<String, Dynamic> = [];
|
||||
|
||||
@:noCompletion
|
||||
private static var memberMethodCache:Map<String, Dynamic> = [];
|
||||
|
||||
@:noCompletion
|
||||
private static var staticFieldCache:Map<String, JNIStaticField> = [];
|
||||
|
||||
@:noCompletion
|
||||
private static var memberFieldCache:Map<String, JNIMemberField> = [];
|
||||
|
||||
/**
|
||||
* Retrieves or creates a cached static method reference.
|
||||
*
|
||||
* @param className The name of the Java class containing the method.
|
||||
* @param methodName The name of the method.
|
||||
* @param signature The method signature in JNI format.
|
||||
* @param cache Whether to cache the result (default true).
|
||||
* @return A dynamic reference to the static method.
|
||||
*/
|
||||
public static function createStaticMethod(className:String, methodName:String, signature:String, cache:Bool = true):Null<Dynamic>
|
||||
{
|
||||
@:privateAccess
|
||||
className = JNI.transformClassName(className);
|
||||
|
||||
final key:String = '$className::$methodName::$signature';
|
||||
|
||||
if (cache && !staticMethodCache.exists(key)) staticMethodCache.set(key, JNI.createStaticMethod(className, methodName, signature));
|
||||
else if (!cache) return JNI.createStaticMethod(className, methodName, signature);
|
||||
|
||||
return staticMethodCache.get(key);
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieves or creates a cached member method reference.
|
||||
*
|
||||
* @param className The name of the Java class containing the method.
|
||||
* @param methodName The name of the method.
|
||||
* @param signature The method signature in JNI format.
|
||||
* @param cache Whether to cache the result (default true).
|
||||
* @return A dynamic reference to the member method.
|
||||
*/
|
||||
public static function createMemberMethod(className:String, methodName:String, signature:String, cache:Bool = true):Null<Dynamic>
|
||||
{
|
||||
@:privateAccess
|
||||
className = JNI.transformClassName(className);
|
||||
|
||||
final key:String = '$className::$methodName::$signature';
|
||||
|
||||
if (cache && !memberMethodCache.exists(key)) memberMethodCache.set(key, JNI.createMemberMethod(className, methodName, signature));
|
||||
else if (!cache) return JNI.createMemberMethod(className, methodName, signature);
|
||||
|
||||
return memberMethodCache.get(key);
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieves or creates a cached static field reference.
|
||||
*
|
||||
* @param className The name of the Java class containing the field.
|
||||
* @param fieldName The name of the field.
|
||||
* @param signature The field signature in JNI format.
|
||||
* @param cache Whether to cache the result (default true).
|
||||
* @return A reference to the static field.
|
||||
*/
|
||||
public static function createStaticField(className:String, fieldName:String, signature:String, cache:Bool = true):Null<JNIStaticField>
|
||||
{
|
||||
@:privateAccess
|
||||
className = JNI.transformClassName(className);
|
||||
|
||||
final key:String = '$className::$fieldName::$signature';
|
||||
|
||||
if (cache && !staticFieldCache.exists(key)) staticFieldCache.set(key, JNI.createStaticField(className, fieldName, signature));
|
||||
else if (!cache) return JNI.createStaticField(className, fieldName, signature);
|
||||
|
||||
return staticFieldCache.get(key);
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieves or creates a cached member field reference.
|
||||
*
|
||||
* @param className The name of the Java class containing the field.
|
||||
* @param fieldName The name of the field.
|
||||
* @param signature The field signature in JNI format.
|
||||
* @param cache Whether to cache the result (default true).
|
||||
* @return A reference to the member field.
|
||||
*/
|
||||
public static function createMemberField(className:String, fieldName:String, signature:String, cache:Bool = true):Null<JNIMemberField>
|
||||
{
|
||||
@:privateAccess
|
||||
className = JNI.transformClassName(className);
|
||||
|
||||
final key:String = '$className::$fieldName::$signature';
|
||||
|
||||
if (cache && !memberFieldCache.exists(key)) memberFieldCache.set(key, JNI.createMemberField(className, fieldName, signature));
|
||||
else if (!cache) return JNI.createMemberField(className, fieldName, signature);
|
||||
|
||||
return memberFieldCache.get(key);
|
||||
}
|
||||
}
|
||||
#end
|
52
source/funkin/external/android/ScreenUtil.hx
vendored
Normal file
52
source/funkin/external/android/ScreenUtil.hx
vendored
Normal file
|
@ -0,0 +1,52 @@
|
|||
package funkin.external.android;
|
||||
|
||||
#if android
|
||||
import lime.math.Rectangle;
|
||||
import lime.system.JNI;
|
||||
|
||||
/**
|
||||
* A Utility class to get Android screen related informations.
|
||||
*/
|
||||
@:unreflective
|
||||
class ScreenUtil
|
||||
{
|
||||
/**
|
||||
* Retrieves the dimensions of display cutouts (such as notches) on Android devices.
|
||||
*
|
||||
* @return An array of `Rectangle` objects, each representing a display cutout's position and size.
|
||||
*/
|
||||
public static function getCutoutDimensions():Array<Rectangle>
|
||||
{
|
||||
final getCutoutDimensionsJNI:Null<Dynamic> = JNIUtil.createStaticMethod('funkin/util/ScreenUtil', 'getCutoutDimensions', '()[Landroid/graphics/Rect;');
|
||||
|
||||
if (getCutoutDimensionsJNI != null)
|
||||
{
|
||||
final rectangles:Array<Rectangle> = [];
|
||||
|
||||
for (rectangle in cast(getCutoutDimensionsJNI(), Array<Dynamic>))
|
||||
{
|
||||
if (rectangle == null) continue;
|
||||
|
||||
final topJNI:Null<JNIMemberField> = JNIUtil.createMemberField('android/graphics/Rect', 'top', 'I');
|
||||
final leftJNI:Null<JNIMemberField> = JNIUtil.createMemberField('android/graphics/Rect', 'left', 'I');
|
||||
final rightJNI:Null<JNIMemberField> = JNIUtil.createMemberField('android/graphics/Rect', 'right', 'I');
|
||||
final bottomJNI:Null<JNIMemberField> = JNIUtil.createMemberField('android/graphics/Rect', 'bottom', 'I');
|
||||
|
||||
if (topJNI != null && leftJNI != null && rightJNI != null && bottomJNI != null)
|
||||
{
|
||||
final top:Int = topJNI.get(rectangle);
|
||||
final left:Int = leftJNI.get(rectangle);
|
||||
final right:Int = rightJNI.get(rectangle);
|
||||
final bottom:Int = bottomJNI.get(rectangle);
|
||||
|
||||
rectangles.push(new Rectangle(left, top, right - left, bottom - top));
|
||||
}
|
||||
}
|
||||
|
||||
return rectangles;
|
||||
}
|
||||
|
||||
return [];
|
||||
}
|
||||
}
|
||||
#end
|
36
source/funkin/external/android/java/funkin/extension/CallbackUtil.java
vendored
Normal file
36
source/funkin/external/android/java/funkin/extension/CallbackUtil.java
vendored
Normal file
|
@ -0,0 +1,36 @@
|
|||
package funkin.extensions;
|
||||
|
||||
import android.content.Intent;
|
||||
|
||||
import org.haxe.extension.Extension;
|
||||
|
||||
import org.haxe.lime.HaxeObject;
|
||||
|
||||
public class CallbackUtil extends Extension
|
||||
{
|
||||
/**
|
||||
* Constant representing the event when the data folder is closed.
|
||||
*/
|
||||
public static int DATA_FOLDER_CLOSED = 0x01;
|
||||
|
||||
private static HaxeObject haxeObject;
|
||||
|
||||
/**
|
||||
* Initializes the callback object for handling Haxe callbacks.
|
||||
*
|
||||
* @param haxeObject The HaxeObject instance to handle callbacks.
|
||||
*/
|
||||
public static void initCallBack(final HaxeObject haxeObject)
|
||||
{
|
||||
CallbackUtil.haxeObject = haxeObject;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean onActivityResult(int requestCode, int resultCode, Intent data)
|
||||
{
|
||||
if (haxeObject != null)
|
||||
haxeObject.call2("onActivityResult", requestCode, resultCode);
|
||||
|
||||
return true;
|
||||
}
|
||||
}
|
346
source/funkin/external/android/java/funkin/provider/DataFolderProvider.java
vendored
Normal file
346
source/funkin/external/android/java/funkin/provider/DataFolderProvider.java
vendored
Normal file
|
@ -0,0 +1,346 @@
|
|||
package funkin.provider;
|
||||
|
||||
import android.content.res.AssetFileDescriptor;
|
||||
import android.database.Cursor;
|
||||
import android.database.MatrixCursor;
|
||||
import android.graphics.Point;
|
||||
import android.os.CancellationSignal;
|
||||
import android.os.ParcelFileDescriptor;
|
||||
import android.provider.DocumentsContract.Document;
|
||||
import android.provider.DocumentsContract.Root;
|
||||
import android.provider.DocumentsProvider;
|
||||
import android.webkit.MimeTypeMap;
|
||||
import java.io.File;
|
||||
import java.io.FileNotFoundException;
|
||||
import java.io.IOException;
|
||||
import java.util.Collections;
|
||||
import java.util.LinkedList;
|
||||
|
||||
/**
|
||||
* @see https://github.com/termux/termux-app/blob/7bceab88e2272f961d1b94ef736f1a9e20173247/app/src/main/java/com/termux/filepicker/TermuxDocumentsProvider.java
|
||||
*/
|
||||
public class DataFolderProvider extends DocumentsProvider
|
||||
{
|
||||
private static File BASE_DIR;
|
||||
|
||||
private static String BASE_DIR_PATH;
|
||||
|
||||
private static final String[] DEFAULT_ROOT_PROJECTION = new String[] {
|
||||
Root.COLUMN_ROOT_ID,
|
||||
Root.COLUMN_MIME_TYPES,
|
||||
Root.COLUMN_FLAGS,
|
||||
Root.COLUMN_ICON,
|
||||
Root.COLUMN_TITLE,
|
||||
Root.COLUMN_SUMMARY,
|
||||
Root.COLUMN_DOCUMENT_ID,
|
||||
Root.COLUMN_AVAILABLE_BYTES
|
||||
};
|
||||
|
||||
private static final String[] DEFAULT_DOCUMENT_PROJECTION = new String[] {
|
||||
Document.COLUMN_DOCUMENT_ID,
|
||||
Document.COLUMN_MIME_TYPE,
|
||||
Document.COLUMN_DISPLAY_NAME,
|
||||
Document.COLUMN_LAST_MODIFIED,
|
||||
Document.COLUMN_FLAGS,
|
||||
Document.COLUMN_SIZE
|
||||
};
|
||||
|
||||
@Override
|
||||
public Cursor queryRoots(String[] projection)
|
||||
{
|
||||
final MatrixCursor result = new MatrixCursor(projection != null ? projection : DEFAULT_ROOT_PROJECTION);
|
||||
|
||||
if (BASE_DIR == null)
|
||||
return result;
|
||||
|
||||
final MatrixCursor.RowBuilder row = result.newRow();
|
||||
|
||||
row.add(Root.COLUMN_ROOT_ID, getDocIdForFile(BASE_DIR));
|
||||
row.add(Root.COLUMN_DOCUMENT_ID, getDocIdForFile(BASE_DIR));
|
||||
row.add(Root.COLUMN_SUMMARY, "Data Folder");
|
||||
row.add(Root.COLUMN_FLAGS, Root.FLAG_SUPPORTS_CREATE | Root.FLAG_SUPPORTS_SEARCH | Root.FLAG_SUPPORTS_IS_CHILD);
|
||||
row.add(Root.COLUMN_TITLE, "::APP_TITLE::");
|
||||
row.add(Root.COLUMN_MIME_TYPES, "*/*");
|
||||
row.add(Root.COLUMN_AVAILABLE_BYTES, BASE_DIR.getFreeSpace());
|
||||
::if (APP_PACKAGE != "")::
|
||||
row.add(Root.COLUMN_ICON, ::APP_PACKAGE::.R.mipmap.ic_launcher);
|
||||
::end::
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Cursor queryDocument(String documentId, String[] projection) throws FileNotFoundException
|
||||
{
|
||||
final MatrixCursor result = new MatrixCursor(projection != null ? projection : DEFAULT_DOCUMENT_PROJECTION);
|
||||
includeFile(result, documentId, null);
|
||||
return result;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Cursor queryChildDocuments(String parentDocumentId, String[] projection, String sortOrder) throws FileNotFoundException
|
||||
{
|
||||
final MatrixCursor result = new MatrixCursor(projection != null ? projection : DEFAULT_DOCUMENT_PROJECTION);
|
||||
|
||||
File parent = getFileForDocId(parentDocumentId);
|
||||
|
||||
if (parent != null)
|
||||
{
|
||||
File[] children = null;
|
||||
|
||||
try {
|
||||
children = parent.listFiles();
|
||||
} catch (SecurityException e) {
|
||||
children = new File[0];
|
||||
}
|
||||
|
||||
if (children != null)
|
||||
{
|
||||
for (File file : children)
|
||||
includeFile(result, null, file);
|
||||
}
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
@Override
|
||||
public ParcelFileDescriptor openDocument(String documentId, String mode, CancellationSignal signal) throws FileNotFoundException
|
||||
{
|
||||
return ParcelFileDescriptor.open(getFileForDocId(documentId), ParcelFileDescriptor.parseMode(mode));
|
||||
}
|
||||
|
||||
@Override
|
||||
public AssetFileDescriptor openDocumentThumbnail(String documentId, Point sizeHint, CancellationSignal signal) throws FileNotFoundException
|
||||
{
|
||||
final File file = getFileForDocId(documentId);
|
||||
final ParcelFileDescriptor pfd = ParcelFileDescriptor.open(file, ParcelFileDescriptor.MODE_READ_ONLY);
|
||||
return new AssetFileDescriptor(pfd, 0, file.length());
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean onCreate()
|
||||
{
|
||||
BASE_DIR = getContext().getExternalFilesDir(null);
|
||||
|
||||
if (BASE_DIR == null)
|
||||
return false;
|
||||
|
||||
try
|
||||
{
|
||||
BASE_DIR_PATH = BASE_DIR.getCanonicalPath();
|
||||
}
|
||||
catch (IOException e)
|
||||
{
|
||||
BASE_DIR_PATH = BASE_DIR.getAbsolutePath();
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String createDocument(String parentDocumentId, String mimeType, String displayName) throws FileNotFoundException
|
||||
{
|
||||
File parentFile = getFileForDocId(parentDocumentId);
|
||||
|
||||
File newFile = new File(parentFile, displayName);
|
||||
|
||||
int noConflictId = 2;
|
||||
|
||||
while (newFile.exists())
|
||||
{
|
||||
newFile = new File(parentFile, displayName + " (" + noConflictId++ + ")");
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
boolean succeeded;
|
||||
|
||||
if (Document.MIME_TYPE_DIR.equals(mimeType))
|
||||
succeeded = newFile.mkdir();
|
||||
else
|
||||
succeeded = newFile.createNewFile();
|
||||
|
||||
if (!succeeded)
|
||||
throw new FileNotFoundException("Failed to create document with id " + newFile.getPath());
|
||||
}
|
||||
catch (IOException e)
|
||||
{
|
||||
throw new FileNotFoundException("Failed to create document with id " + newFile.getPath());
|
||||
}
|
||||
|
||||
return newFile.getPath();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void deleteDocument(String documentId) throws FileNotFoundException
|
||||
{
|
||||
if (!deleteRecursive(getFileForDocId(documentId)))
|
||||
throw new FileNotFoundException("Failed to delete document with id " + documentId);
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getDocumentType(String documentId) throws FileNotFoundException
|
||||
{
|
||||
return getMimeType(getFileForDocId(documentId));
|
||||
}
|
||||
|
||||
@Override
|
||||
public Cursor querySearchDocuments(String rootId, String query, String[] projection) throws FileNotFoundException
|
||||
{
|
||||
final MatrixCursor result = new MatrixCursor(projection != null ? projection : DEFAULT_DOCUMENT_PROJECTION);
|
||||
|
||||
final LinkedList<File> pending = new LinkedList<>();
|
||||
|
||||
pending.add(getFileForDocId(rootId));
|
||||
|
||||
final int MAX_SEARCH_RESULTS = 50;
|
||||
|
||||
while (!pending.isEmpty() && result.getCount() < MAX_SEARCH_RESULTS)
|
||||
{
|
||||
final File file = pending.removeFirst();
|
||||
|
||||
boolean isInsideHome;
|
||||
|
||||
try
|
||||
{
|
||||
isInsideHome = file.getCanonicalPath().startsWith(BASE_DIR_PATH);
|
||||
}
|
||||
catch (IOException e)
|
||||
{
|
||||
isInsideHome = true;
|
||||
}
|
||||
|
||||
if (isInsideHome)
|
||||
{
|
||||
if (file.isDirectory())
|
||||
{
|
||||
try {
|
||||
Collections.addAll(pending, file.listFiles());
|
||||
} catch (SecurityException e) {
|
||||
// ignore
|
||||
}
|
||||
}
|
||||
else if (file.getName().toLowerCase().contains(query))
|
||||
{
|
||||
includeFile(result, null, file);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isChildDocument(String parentDocumentId, String documentId)
|
||||
{
|
||||
try
|
||||
{
|
||||
File parent = getFileForDocId(parentDocumentId).getCanonicalFile();
|
||||
File child = getFileForDocId(documentId).getCanonicalFile();
|
||||
return child.getPath().startsWith(parent.getPath() + "/");
|
||||
}
|
||||
catch (IOException e)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
private boolean deleteRecursive(File file)
|
||||
{
|
||||
if (file.isDirectory())
|
||||
{
|
||||
File[] children = file.listFiles();
|
||||
|
||||
if (children != null)
|
||||
{
|
||||
for (File child : children)
|
||||
{
|
||||
if (!deleteRecursive(child))
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return file.delete();
|
||||
}
|
||||
|
||||
private static String getDocIdForFile(File file)
|
||||
{
|
||||
return file.getAbsolutePath();
|
||||
}
|
||||
|
||||
private static File getFileForDocId(String docId) throws FileNotFoundException
|
||||
{
|
||||
if (BASE_DIR == null)
|
||||
throw new FileNotFoundException("Base directory not available");
|
||||
|
||||
final File f = (docId == null || docId.isEmpty()) ? BASE_DIR : new File(docId);
|
||||
|
||||
if (!f.exists())
|
||||
throw new FileNotFoundException(f.getAbsolutePath() + " not found");
|
||||
|
||||
return f;
|
||||
}
|
||||
|
||||
private static String getMimeType(File file)
|
||||
{
|
||||
if (file == null || file.isDirectory())
|
||||
return Document.MIME_TYPE_DIR;
|
||||
|
||||
String name = file.getName();
|
||||
|
||||
int lastDot = name.lastIndexOf('.');
|
||||
|
||||
if (lastDot >= 0)
|
||||
{
|
||||
String extension = name.substring(lastDot + 1).toLowerCase();
|
||||
|
||||
String mime = MimeTypeMap.getSingleton().getMimeTypeFromExtension(extension);
|
||||
|
||||
if (mime != null)
|
||||
return mime;
|
||||
}
|
||||
|
||||
return "application/octet-stream";
|
||||
}
|
||||
|
||||
private void includeFile(MatrixCursor result, String docId, File file) throws FileNotFoundException
|
||||
{
|
||||
if (docId == null)
|
||||
docId = getDocIdForFile(file);
|
||||
else
|
||||
file = getFileForDocId(docId);
|
||||
|
||||
int flags = 0;
|
||||
|
||||
if (file.isDirectory())
|
||||
{
|
||||
if (file.canWrite())
|
||||
flags |= Document.FLAG_DIR_SUPPORTS_CREATE;
|
||||
}
|
||||
else if (file.canWrite())
|
||||
{
|
||||
flags |= Document.FLAG_SUPPORTS_WRITE;
|
||||
}
|
||||
|
||||
if (file.getParentFile() != null && file.getParentFile().canWrite())
|
||||
flags |= Document.FLAG_SUPPORTS_DELETE;
|
||||
|
||||
final String mimeType = getMimeType(file);
|
||||
|
||||
if (mimeType.startsWith("image/"))
|
||||
flags |= Document.FLAG_SUPPORTS_THUMBNAIL;
|
||||
|
||||
final MatrixCursor.RowBuilder row = result.newRow();
|
||||
row.add(Document.COLUMN_DOCUMENT_ID, docId);
|
||||
row.add(Document.COLUMN_DISPLAY_NAME, file.getName());
|
||||
row.add(Document.COLUMN_SIZE, file.length());
|
||||
row.add(Document.COLUMN_MIME_TYPE, mimeType);
|
||||
row.add(Document.COLUMN_LAST_MODIFIED, file.lastModified());
|
||||
row.add(Document.COLUMN_FLAGS, flags);
|
||||
::if (APP_PACKAGE != "")::
|
||||
row.add(Document.COLUMN_ICON, ::APP_PACKAGE::.R.mipmap.ic_launcher);
|
||||
::end::
|
||||
}
|
||||
}
|
34
source/funkin/external/android/java/funkin/util/DataFolderUtil.java
vendored
Normal file
34
source/funkin/external/android/java/funkin/util/DataFolderUtil.java
vendored
Normal file
|
@ -0,0 +1,34 @@
|
|||
package funkin.util;
|
||||
|
||||
import android.content.Intent;
|
||||
import android.content.ContentResolver;
|
||||
import android.net.Uri;
|
||||
import android.os.Build;
|
||||
import android.provider.DocumentsContract;
|
||||
import android.content.pm.PackageInfo;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
import org.haxe.extension.Extension;
|
||||
|
||||
public class DataFolderUtil
|
||||
{
|
||||
/**
|
||||
* A method that opens the Application's data folder for browsing through the Storage Access Framework.
|
||||
* It's highly based on some code borrowed from Mterial Files
|
||||
* https://github.com/zhanghai/MaterialFiles
|
||||
*/
|
||||
public static void openDataFolder(int requestCode)
|
||||
{
|
||||
::if (APP_PACKAGE != "")::
|
||||
if (Extension.mainActivity != null)
|
||||
{
|
||||
Intent intent = new Intent(Intent.ACTION_VIEW);
|
||||
intent.setDataAndType(DocumentsContract.buildRootUri("::APP_PACKAGE::.docprovider", ""), "vnd.android.document/directory");
|
||||
intent.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION);
|
||||
intent.addFlags(Intent.FLAG_GRANT_WRITE_URI_PERMISSION);
|
||||
Extension.mainActivity.startActivityForResult(intent, requestCode);
|
||||
}
|
||||
::end::
|
||||
}
|
||||
}
|
39
source/funkin/external/android/java/funkin/util/ScreenUtil.java
vendored
Normal file
39
source/funkin/external/android/java/funkin/util/ScreenUtil.java
vendored
Normal file
|
@ -0,0 +1,39 @@
|
|||
package funkin.util;
|
||||
|
||||
import android.graphics.Rect;
|
||||
import android.os.Build;
|
||||
import android.view.DisplayCutout;
|
||||
import android.view.WindowInsets;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
import org.haxe.extension.Extension;
|
||||
|
||||
public class ScreenUtil
|
||||
{
|
||||
public static Rect[] getCutoutDimensions()
|
||||
{
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P)
|
||||
{
|
||||
if (Extension.mainActivity != null)
|
||||
{
|
||||
WindowInsets insets = Extension.mainActivity.getWindow().getDecorView().getRootWindowInsets();
|
||||
|
||||
if (insets != null)
|
||||
{
|
||||
DisplayCutout cutout = insets.getDisplayCutout();
|
||||
|
||||
if (cutout != null)
|
||||
{
|
||||
List<Rect> boundingRects = cutout.getBoundingRects();
|
||||
|
||||
if (boundingRects != null && !boundingRects.isEmpty())
|
||||
return boundingRects.toArray(new Rect[0]);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return new Rect[0];
|
||||
}
|
||||
}
|
17
source/funkin/external/ios/AudioSession.hx
vendored
Normal file
17
source/funkin/external/ios/AudioSession.hx
vendored
Normal file
|
@ -0,0 +1,17 @@
|
|||
package funkin.external.ios;
|
||||
|
||||
#if ios
|
||||
/**
|
||||
* A Utility class to manage iOS audio.
|
||||
*/
|
||||
@:build(funkin.util.macro.LinkerMacro.xml('project/Build.xml'))
|
||||
@:include('AudioSession.hpp')
|
||||
@:unreflective
|
||||
extern class AudioSession
|
||||
{
|
||||
@:native('initialize')
|
||||
static function initialize():Void;
|
||||
@:native('setActive')
|
||||
static function setActive(active:Bool):Void;
|
||||
}
|
||||
#end
|
18
source/funkin/external/ios/ScreenUtil.hx
vendored
Normal file
18
source/funkin/external/ios/ScreenUtil.hx
vendored
Normal file
|
@ -0,0 +1,18 @@
|
|||
package funkin.external.ios;
|
||||
|
||||
#if ios
|
||||
/**
|
||||
* A Utility class to get iOS screen related informations.
|
||||
*/
|
||||
@:build(funkin.util.macro.LinkerMacro.xml('project/Build.xml'))
|
||||
@:include('ScreenUtil.hpp')
|
||||
@:unreflective
|
||||
extern class ScreenUtil
|
||||
{
|
||||
@:native('getSafeAreaInsets')
|
||||
static function getSafeAreaInsets(top:cpp.RawPointer<Float>, bottom:cpp.RawPointer<Float>, left:cpp.RawPointer<Float>, right:cpp.RawPointer<Float>):Void;
|
||||
|
||||
@:native('getScreenSize')
|
||||
static function getScreenSize(width:cpp.RawPointer<Float>, height:cpp.RawPointer<Float>):Void;
|
||||
}
|
||||
#end
|
29
source/funkin/external/ios/project/Build.xml
vendored
Normal file
29
source/funkin/external/ios/project/Build.xml
vendored
Normal file
|
@ -0,0 +1,29 @@
|
|||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<xml>
|
||||
<pragma once="true" />
|
||||
|
||||
<files id="haxe">
|
||||
<compilerflag value="-I${this_dir}/ios/include" if="iphoneos || iphonesim" />
|
||||
</files>
|
||||
|
||||
<files id="__main__">
|
||||
<compilerflag value="-I${this_dir}/ios/include" if="iphoneos || iphonesim" />
|
||||
</files>
|
||||
|
||||
<files id="external-ios" dir="${this_dir}/ios" if="iphoneos || iphonesim">
|
||||
<compilerflag value="-I${this_dir}/ios/include" />
|
||||
|
||||
<file name="src/ScreenUtil.mm" />
|
||||
<file name="src/AudioSession.mm" />
|
||||
</files>
|
||||
|
||||
<target id="haxe">
|
||||
<section if="iphoneos || iphonesim">
|
||||
<vflag name="-framework" value="UIKit" />
|
||||
<vflag name="-framework" value="Foundation" />
|
||||
<vflag name="-framework" value="AVFAudio" />
|
||||
</section>
|
||||
|
||||
<files id="external-ios" if="iphoneos || iphonesim" />
|
||||
</target>
|
||||
</xml>
|
4
source/funkin/external/ios/project/ios/include/AudioSession.hpp
vendored
Normal file
4
source/funkin/external/ios/project/ios/include/AudioSession.hpp
vendored
Normal file
|
@ -0,0 +1,4 @@
|
|||
#pragma once
|
||||
|
||||
void initialize();
|
||||
void setActive(bool active);
|
4
source/funkin/external/ios/project/ios/include/ScreenUtil.hpp
vendored
Normal file
4
source/funkin/external/ios/project/ios/include/ScreenUtil.hpp
vendored
Normal file
|
@ -0,0 +1,4 @@
|
|||
#pragma once
|
||||
|
||||
void getSafeAreaInsets(double* top, double* bottom, double* left, double* right);
|
||||
void getScreenSize(double* width, double* height);
|
41
source/funkin/external/ios/project/ios/src/AudioSession.mm
vendored
Normal file
41
source/funkin/external/ios/project/ios/src/AudioSession.mm
vendored
Normal file
|
@ -0,0 +1,41 @@
|
|||
#import <Foundation/Foundation.h>
|
||||
#import <AVFAudio/AVFAudio.h>
|
||||
|
||||
void initialize()
|
||||
{
|
||||
AVAudioSession *session = [AVAudioSession sharedInstance];
|
||||
|
||||
NSError *error;
|
||||
[session setCategory:AVAudioSessionCategoryPlayback
|
||||
mode:AVAudioSessionModeDefault
|
||||
options:AVAudioSessionCategoryOptionAllowBluetoothA2DP|AVAudioSessionCategoryOptionInterruptSpokenAudioAndMixWithOthers
|
||||
error:&error];
|
||||
|
||||
if (@available(iOS 17.0, *))
|
||||
{
|
||||
[session setPrefersInterruptionOnRouteDisconnect:false error:nil];
|
||||
}
|
||||
|
||||
if (@available(iOS 14.5, *))
|
||||
{
|
||||
[session setPrefersNoInterruptionsFromSystemAlerts:true error:nil];
|
||||
}
|
||||
|
||||
if (error)
|
||||
{
|
||||
NSLog(@"Unable to set category of audio session: %@", error);
|
||||
}
|
||||
}
|
||||
|
||||
void setActive(bool active)
|
||||
{
|
||||
AVAudioSession *session = [AVAudioSession sharedInstance];
|
||||
NSError *error;
|
||||
|
||||
[session setActive:YES error:&error];
|
||||
|
||||
if (error)
|
||||
{
|
||||
NSLog(@"Unable to set active of audio session: %@", error);
|
||||
}
|
||||
}
|
36
source/funkin/external/ios/project/ios/src/ScreenUtil.mm
vendored
Normal file
36
source/funkin/external/ios/project/ios/src/ScreenUtil.mm
vendored
Normal file
|
@ -0,0 +1,36 @@
|
|||
#import "ScreenUtil.hpp"
|
||||
|
||||
#import <UIKit/UIKit.h>
|
||||
#import <Foundation/Foundation.h>
|
||||
|
||||
void getSafeAreaInsets(double* top, double* bottom, double* left, double* right)
|
||||
{
|
||||
if (@available(iOS 11, *))
|
||||
{
|
||||
UIWindow* window = [UIApplication sharedApplication].windows[0];
|
||||
UIEdgeInsets safeAreaInsets = window.safeAreaInsets;
|
||||
|
||||
float scale = [UIScreen mainScreen].scale;
|
||||
|
||||
(*top) = safeAreaInsets.top * scale;
|
||||
(*bottom) = safeAreaInsets.bottom * scale;
|
||||
(*left) = safeAreaInsets.left * scale;
|
||||
(*right) = safeAreaInsets.right * scale;
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
(*top) = 0.0;
|
||||
(*bottom) = 0.0;
|
||||
(*left) = 0.0;
|
||||
(*right) = 0.0;
|
||||
}
|
||||
|
||||
void getScreenSize(double* width, double* height)
|
||||
{
|
||||
CGRect screenRect = [[UIScreen mainScreen] bounds];
|
||||
float scale = [UIScreen mainScreen].scale;
|
||||
|
||||
(*width) = (double)screenRect.size.width * scale;
|
||||
(*height) = (double)screenRect.size.height * scale;
|
||||
}
|
|
@ -11,6 +11,9 @@ import flixel.math.FlxRect;
|
|||
import flixel.math.FlxPoint;
|
||||
import flixel.graphics.frames.FlxFrame.FlxFrameAngle;
|
||||
import flixel.FlxCamera;
|
||||
import openfl.system.System;
|
||||
|
||||
using StringTools;
|
||||
|
||||
/**
|
||||
* An FlxSprite with additional functionality.
|
||||
|
@ -32,6 +35,8 @@ class FunkinSprite extends FlxSprite
|
|||
*/
|
||||
static var previousCachedTextures:Map<String, FlxGraphic> = [];
|
||||
|
||||
static var permanentCachedTextures:Map<String, FlxGraphic> = [];
|
||||
|
||||
/**
|
||||
* @param x Starting X position
|
||||
* @param y Starting Y position
|
||||
|
@ -243,6 +248,27 @@ class FunkinSprite extends FlxSprite
|
|||
}
|
||||
}
|
||||
|
||||
public static function permanentCacheTexture(key:String):Void
|
||||
{
|
||||
// We don't want to cache the same texture twice.
|
||||
if (permanentCachedTextures.exists(key)) return;
|
||||
|
||||
// Else, texture is currently uncached.
|
||||
var graphic:FlxGraphic = FlxGraphic.fromAssetKey(key, false, null, true);
|
||||
if (graphic == null)
|
||||
{
|
||||
FlxG.log.warn('Failed to cache graphic: $key');
|
||||
}
|
||||
else
|
||||
{
|
||||
trace('Successfully cached graphic: $key');
|
||||
graphic.persist = true;
|
||||
permanentCachedTextures.set(key, graphic);
|
||||
}
|
||||
|
||||
currentCachedTextures = permanentCachedTextures;
|
||||
}
|
||||
|
||||
public static function cacheSparrow(key:String):Void
|
||||
{
|
||||
cacheTexture(Paths.image(key));
|
||||
|
@ -259,7 +285,14 @@ class FunkinSprite extends FlxSprite
|
|||
public static function preparePurgeCache():Void
|
||||
{
|
||||
previousCachedTextures = currentCachedTextures;
|
||||
currentCachedTextures = [];
|
||||
|
||||
for (graphicKey in previousCachedTextures.keys())
|
||||
{
|
||||
if (!permanentCachedTextures.exists(graphicKey)) continue;
|
||||
previousCachedTextures.remove(graphicKey);
|
||||
}
|
||||
|
||||
currentCachedTextures = permanentCachedTextures;
|
||||
}
|
||||
|
||||
public static function purgeCache():Void
|
||||
|
@ -273,6 +306,32 @@ class FunkinSprite extends FlxSprite
|
|||
graphic.destroy();
|
||||
previousCachedTextures.remove(graphicKey);
|
||||
}
|
||||
@:privateAccess
|
||||
if (FlxG.bitmap._cache == null)
|
||||
{
|
||||
@:privateAccess
|
||||
FlxG.bitmap._cache = new Map();
|
||||
System.gc();
|
||||
return;
|
||||
}
|
||||
|
||||
@:privateAccess
|
||||
for (key in FlxG.bitmap._cache.keys())
|
||||
{
|
||||
var obj:Null<FlxGraphic> = FlxG.bitmap.get(key);
|
||||
if (obj == null) continue;
|
||||
if (obj.persist) continue;
|
||||
if (permanentCachedTextures.exists(key)) continue;
|
||||
if (!(obj.useCount <= 0 || key.contains("characters") || key.contains("charSelect") || key.contains("results"))) continue;
|
||||
|
||||
FlxG.bitmap.removeKey(key);
|
||||
obj.destroy();
|
||||
}
|
||||
openfl.Assets.cache.clear("songs");
|
||||
openfl.Assets.cache.clear("sounds");
|
||||
openfl.Assets.cache.clear("music");
|
||||
|
||||
System.gc();
|
||||
}
|
||||
|
||||
static function isGraphicCached(graphic:FlxGraphic):Bool
|
||||
|
|
|
@ -136,6 +136,9 @@ class DropShadowScreenspace extends DropShadowShader
|
|||
// essentially just stole this from the AngleMask shader but repurposed it to smooth
|
||||
// the threshold because without any sort of smoothing it produces horrible edges
|
||||
float antialias(vec2 fragCoord, float curThreshold, bool useMask) {
|
||||
if (AA_STAGES == 0.0) {
|
||||
return intensityPass(fragCoord, curThreshold, useMask);
|
||||
}
|
||||
|
||||
// In GLSL 100, we need to use constant loop bounds
|
||||
// Well assume a reasonable maximum for AA_STAGES and use a fixed loop
|
||||
|
|
|
@ -198,7 +198,7 @@ class DropShadowShader extends FlxShader
|
|||
|
||||
/*
|
||||
Loads an image for the mask.
|
||||
While you *could* directly set the value of the mask, this function works for both HTML5 and desktop targets.
|
||||
While you *could* directly set the value of the mask, this function works for both HTML5 and native targets.
|
||||
*/
|
||||
public function loadAltMask(path:String)
|
||||
{
|
||||
|
@ -362,6 +362,9 @@ class DropShadowShader extends FlxShader
|
|||
// essentially just stole this from the AngleMask shader but repurposed it to smooth
|
||||
// the threshold because without any sort of smoothing it produces horrible edges
|
||||
float antialias(vec2 fragCoord, float curThreshold, bool useMask) {
|
||||
if (AA_STAGES == 0.0) {
|
||||
return intensityPass(fragCoord, curThreshold, useMask);
|
||||
}
|
||||
|
||||
// In GLSL 100, we need to use constant loop bounds
|
||||
// Well assume a reasonable maximum for AA_STAGES and use a fixed loop
|
||||
|
|
|
@ -11,7 +11,7 @@ import openfl.net.NetStream;
|
|||
/**
|
||||
* Plays a video via a NetStream. Only works on HTML5.
|
||||
* This does NOT replace hxvlc, nor does hxvlc replace this.
|
||||
* hxvlc only works on desktop and does not work on HTML5!
|
||||
* hxvlc only works on native and does not work on HTML5!
|
||||
*/
|
||||
@:nullSafety
|
||||
class FlxVideo extends FunkinSprite
|
||||
|
|
|
@ -2,6 +2,7 @@ package funkin.graphics.video;
|
|||
|
||||
#if hxvlc
|
||||
import hxvlc.flixel.FlxVideoSprite;
|
||||
import funkin.Preferences;
|
||||
|
||||
/**
|
||||
* Not to be confused with FlxVideo, this is a hxvlc based video class
|
||||
|
@ -13,6 +14,13 @@ class FunkinVideoSprite extends FlxVideoSprite
|
|||
public function new(x:Float = 0, y:Float = 0)
|
||||
{
|
||||
super(x, y);
|
||||
// null safety fucking SUCKS
|
||||
if (bitmap != null)
|
||||
{
|
||||
bitmap.onOpening.add(function():Void {
|
||||
if (bitmap != null) bitmap.audioDelay = Preferences.globalOffset * 1000; // Microseconds
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
#end
|
||||
|
|
|
@ -13,7 +13,6 @@ import flixel.input.gamepad.FlxGamepadInputID;
|
|||
import flixel.input.keyboard.FlxKey;
|
||||
import flixel.math.FlxAngle;
|
||||
import flixel.math.FlxPoint;
|
||||
import lime.ui.Haptic;
|
||||
|
||||
/**
|
||||
* A core class which handles receiving player input and interpreting it into game actions.
|
||||
|
@ -64,7 +63,9 @@ class Controls extends FlxActionSet
|
|||
var _freeplay_jump_to_top = new FunkinAction(Action.FREEPLAY_JUMP_TO_TOP);
|
||||
var _freeplay_jump_to_bottom = new FunkinAction(Action.FREEPLAY_JUMP_TO_BOTTOM);
|
||||
var _cutscene_advance = new FunkinAction(Action.CUTSCENE_ADVANCE);
|
||||
#if FEATURE_DEBUG_MENU
|
||||
var _debug_menu = new FunkinAction(Action.DEBUG_MENU);
|
||||
#end
|
||||
#if FEATURE_CHART_EDITOR
|
||||
var _debug_chart = new FunkinAction(Action.DEBUG_CHART);
|
||||
#end
|
||||
|
@ -287,10 +288,12 @@ class Controls extends FlxActionSet
|
|||
inline function get_CUTSCENE_ADVANCE()
|
||||
return _cutscene_advance.check();
|
||||
|
||||
#if FEATURE_DEBUG_MENU
|
||||
public var DEBUG_MENU(get, never):Bool;
|
||||
|
||||
inline function get_DEBUG_MENU()
|
||||
return _debug_menu.check();
|
||||
#end
|
||||
|
||||
#if FEATURE_CHART_EDITOR
|
||||
public var DEBUG_CHART(get, never):Bool;
|
||||
|
@ -321,7 +324,7 @@ class Controls extends FlxActionSet
|
|||
inline function get_VOLUME_MUTE()
|
||||
return _volume_mute.check();
|
||||
|
||||
public function new(name, scheme:KeyboardScheme = null)
|
||||
public function new(name, ?scheme:KeyboardScheme)
|
||||
{
|
||||
super(name);
|
||||
|
||||
|
@ -346,7 +349,7 @@ class Controls extends FlxActionSet
|
|||
add(_freeplay_jump_to_top);
|
||||
add(_freeplay_jump_to_bottom);
|
||||
add(_cutscene_advance);
|
||||
add(_debug_menu);
|
||||
#if FEATURE_DEBUG_MENU add(_debug_menu); #end
|
||||
#if FEATURE_CHART_EDITOR add(_debug_chart); #end
|
||||
#if FEATURE_STAGE_EDITOR add(_debug_stage); #end
|
||||
add(_volume_up);
|
||||
|
@ -369,7 +372,7 @@ class Controls extends FlxActionSet
|
|||
setKeyboardScheme(scheme, false);
|
||||
}
|
||||
|
||||
override function update()
|
||||
override function update():Void
|
||||
{
|
||||
super.update();
|
||||
}
|
||||
|
@ -474,7 +477,7 @@ class Controls extends FlxActionSet
|
|||
case FREEPLAY_JUMP_TO_TOP: _freeplay_jump_to_top;
|
||||
case FREEPLAY_JUMP_TO_BOTTOM: _freeplay_jump_to_bottom;
|
||||
case CUTSCENE_ADVANCE: _cutscene_advance;
|
||||
case DEBUG_MENU: _debug_menu;
|
||||
#if FEATURE_DEBUG_MENU case DEBUG_MENU: _debug_menu; #end
|
||||
#if FEATURE_CHART_EDITOR case DEBUG_CHART: _debug_chart; #end
|
||||
#if FEATURE_STAGE_EDITOR case DEBUG_STAGE: _debug_stage; #end
|
||||
case VOLUME_UP: _volume_up;
|
||||
|
@ -485,8 +488,7 @@ class Controls extends FlxActionSet
|
|||
|
||||
static function init():Void
|
||||
{
|
||||
var actions = new FlxActionManager();
|
||||
FlxG.inputs.add(actions);
|
||||
FlxG.inputs.addUniqueType(new FlxActionManager());
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -559,8 +561,10 @@ class Controls extends FlxActionSet
|
|||
func(_freeplay_jump_to_bottom, JUST_PRESSED);
|
||||
case CUTSCENE_ADVANCE:
|
||||
func(_cutscene_advance, JUST_PRESSED);
|
||||
#if FEATURE_DEBUG_MENU
|
||||
case DEBUG_MENU:
|
||||
func(_debug_menu, JUST_PRESSED);
|
||||
#end
|
||||
#if FEATURE_CHART_EDITOR
|
||||
case DEBUG_CHART:
|
||||
func(_debug_chart, JUST_PRESSED);
|
||||
|
@ -726,11 +730,6 @@ class Controls extends FlxActionSet
|
|||
forEachBound(control, function(action, state) addKeys(action, keys, state));
|
||||
}
|
||||
|
||||
public function bindSwipe(control:Control, swipeDir:FlxDirectionFlags = FlxDirectionFlags.UP, ?swpLength:Float = 90)
|
||||
{
|
||||
forEachBound(control, function(action, press) action.add(new FlxActionInputDigitalMobileSwipeGameplay(swipeDir, press, swpLength)));
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets all actions that pertain to the binder to trigger when the supplied keys are used.
|
||||
* If binder is a literal you can inline this
|
||||
|
@ -788,7 +787,9 @@ class Controls extends FlxActionSet
|
|||
bindKeys(Control.FREEPLAY_JUMP_TO_TOP, getDefaultKeybinds(scheme, Control.FREEPLAY_JUMP_TO_TOP));
|
||||
bindKeys(Control.FREEPLAY_JUMP_TO_BOTTOM, getDefaultKeybinds(scheme, Control.FREEPLAY_JUMP_TO_BOTTOM));
|
||||
bindKeys(Control.CUTSCENE_ADVANCE, getDefaultKeybinds(scheme, Control.CUTSCENE_ADVANCE));
|
||||
#if FEATURE_DEBUG_MENU
|
||||
bindKeys(Control.DEBUG_MENU, getDefaultKeybinds(scheme, Control.DEBUG_MENU));
|
||||
#end
|
||||
#if FEATURE_CHART_EDITOR
|
||||
bindKeys(Control.DEBUG_CHART, getDefaultKeybinds(scheme, Control.DEBUG_CHART));
|
||||
#end
|
||||
|
@ -798,8 +799,6 @@ class Controls extends FlxActionSet
|
|||
bindKeys(Control.VOLUME_UP, getDefaultKeybinds(scheme, Control.VOLUME_UP));
|
||||
bindKeys(Control.VOLUME_DOWN, getDefaultKeybinds(scheme, Control.VOLUME_DOWN));
|
||||
bindKeys(Control.VOLUME_MUTE, getDefaultKeybinds(scheme, Control.VOLUME_MUTE));
|
||||
|
||||
bindMobileLol();
|
||||
}
|
||||
|
||||
function getDefaultKeybinds(scheme:KeyboardScheme, control:Control):Array<FlxKey>
|
||||
|
@ -830,7 +829,7 @@ class Controls extends FlxActionSet
|
|||
case Control.FREEPLAY_JUMP_TO_TOP: return [HOME];
|
||||
case Control.FREEPLAY_JUMP_TO_BOTTOM: return [END];
|
||||
case Control.CUTSCENE_ADVANCE: return [Z, ENTER];
|
||||
case Control.DEBUG_MENU: return [GRAVEACCENT];
|
||||
#if FEATURE_DEBUG_MENU case Control.DEBUG_MENU: return [GRAVEACCENT]; #end
|
||||
#if FEATURE_CHART_EDITOR case Control.DEBUG_CHART: return []; #end
|
||||
#if FEATURE_STAGE_EDITOR case Control.DEBUG_STAGE: return []; #end
|
||||
case Control.VOLUME_UP: return [PLUS, NUMPADPLUS];
|
||||
|
@ -861,7 +860,7 @@ class Controls extends FlxActionSet
|
|||
case Control.FREEPLAY_JUMP_TO_TOP: return [HOME];
|
||||
case Control.FREEPLAY_JUMP_TO_BOTTOM: return [END];
|
||||
case Control.CUTSCENE_ADVANCE: return [G, Z];
|
||||
case Control.DEBUG_MENU: return [GRAVEACCENT];
|
||||
#if FEATURE_DEBUG_MENU case Control.DEBUG_MENU: return [GRAVEACCENT]; #end
|
||||
#if FEATURE_CHART_EDITOR case Control.DEBUG_CHART: return []; #end
|
||||
#if FEATURE_STAGE_EDITOR case Control.DEBUG_STAGE: return []; #end
|
||||
case Control.VOLUME_UP: return [PLUS];
|
||||
|
@ -892,7 +891,7 @@ class Controls extends FlxActionSet
|
|||
case Control.FREEPLAY_JUMP_TO_TOP: return [];
|
||||
case Control.FREEPLAY_JUMP_TO_BOTTOM: return [];
|
||||
case Control.CUTSCENE_ADVANCE: return [ENTER];
|
||||
case Control.DEBUG_MENU: return [];
|
||||
#if FEATURE_DEBUG_MENU case Control.DEBUG_MENU: return []; #end
|
||||
#if FEATURE_CHART_EDITOR case Control.DEBUG_CHART: return []; #end
|
||||
#if FEATURE_STAGE_EDITOR case Control.DEBUG_STAGE: return []; #end
|
||||
case Control.VOLUME_UP: return [NUMPADPLUS];
|
||||
|
@ -906,30 +905,6 @@ class Controls extends FlxActionSet
|
|||
return [];
|
||||
}
|
||||
|
||||
function bindMobileLol()
|
||||
{
|
||||
#if FLX_TOUCH
|
||||
// MAKE BETTER TOUCH BIND CODE
|
||||
|
||||
bindSwipe(Control.NOTE_UP, FlxDirectionFlags.UP, 40);
|
||||
bindSwipe(Control.NOTE_DOWN, FlxDirectionFlags.DOWN, 40);
|
||||
bindSwipe(Control.NOTE_LEFT, FlxDirectionFlags.LEFT, 40);
|
||||
bindSwipe(Control.NOTE_RIGHT, FlxDirectionFlags.RIGHT, 40);
|
||||
|
||||
// feels more like drag when up/down are inversed
|
||||
bindSwipe(Control.UI_UP, FlxDirectionFlags.DOWN);
|
||||
bindSwipe(Control.UI_DOWN, FlxDirectionFlags.UP);
|
||||
bindSwipe(Control.UI_LEFT, FlxDirectionFlags.LEFT);
|
||||
bindSwipe(Control.UI_RIGHT, FlxDirectionFlags.RIGHT);
|
||||
#end
|
||||
|
||||
#if android
|
||||
forEachBound(Control.BACK, function(action, pres) {
|
||||
action.add(new FlxActionInputDigitalAndroid(FlxAndroidKey.BACK, JUST_PRESSED));
|
||||
});
|
||||
#end
|
||||
}
|
||||
|
||||
function removeKeyboard()
|
||||
{
|
||||
for (action in this.digitalActions)
|
||||
|
@ -1012,7 +987,9 @@ class Controls extends FlxActionSet
|
|||
Control.VOLUME_UP => getDefaultGamepadBinds(Control.VOLUME_UP),
|
||||
Control.VOLUME_DOWN => getDefaultGamepadBinds(Control.VOLUME_DOWN),
|
||||
Control.VOLUME_MUTE => getDefaultGamepadBinds(Control.VOLUME_MUTE),
|
||||
#if FEATURE_DEBUG_MENU
|
||||
Control.DEBUG_MENU => getDefaultGamepadBinds(Control.DEBUG_MENU),
|
||||
#end
|
||||
#if FEATURE_CHART_EDITOR
|
||||
Control.DEBUG_CHART => getDefaultGamepadBinds(Control.DEBUG_CHART),
|
||||
#end
|
||||
|
@ -1076,8 +1053,10 @@ class Controls extends FlxActionSet
|
|||
[];
|
||||
case Control.VOLUME_MUTE:
|
||||
[];
|
||||
#if FEATURE_DEBUG_MENU
|
||||
case Control.DEBUG_MENU:
|
||||
[];
|
||||
#end
|
||||
#if FEATURE_CHART_EDITOR
|
||||
case Control.DEBUG_CHART:
|
||||
[];
|
||||
|
@ -1101,13 +1080,6 @@ class Controls extends FlxActionSet
|
|||
forEachBound(control, function(action, state) addButtons(action, buttons, state, id));
|
||||
}
|
||||
|
||||
public function touchShit(control:Control, id)
|
||||
{
|
||||
forEachBound(control, function(action, state) {
|
||||
// action
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets all actions that pertain to the binder to trigger when the supplied keys are used.
|
||||
* If binder is a literal you can inline this
|
||||
|
@ -1469,163 +1441,6 @@ class FunkinAction extends FlxActionDigital
|
|||
}
|
||||
}
|
||||
|
||||
class FlxActionInputDigitalMobileSwipeGameplay extends FlxActionInputDigital
|
||||
{
|
||||
var touchMap:Map<Int, Swipes> = new Map();
|
||||
|
||||
var vibrationSteps:Int = 5;
|
||||
var curStep:Int = 5;
|
||||
var activateLength:Float = 90;
|
||||
var hapticPressure:Int = 100;
|
||||
|
||||
public function new(swipeDir:FlxDirectionFlags = FlxDirectionFlags.ANY, Trigger:FlxInputState, ?swipeLength:Float = 90)
|
||||
{
|
||||
super(OTHER, swipeDir.toInt(), Trigger);
|
||||
|
||||
activateLength = swipeLength;
|
||||
}
|
||||
|
||||
// fix right swipe
|
||||
// make so cant double swipe during gameplay
|
||||
// hold notes?
|
||||
|
||||
override function update():Void
|
||||
{
|
||||
super.update();
|
||||
|
||||
#if FLX_TOUCH
|
||||
for (touch in FlxG.touches.list)
|
||||
{
|
||||
if (touch.justPressed)
|
||||
{
|
||||
var pos:FlxPoint = new FlxPoint(touch.screenX, touch.screenY);
|
||||
var pos2:FlxPoint = new FlxPoint(touch.screenX, touch.screenY);
|
||||
|
||||
var swp:Swipes =
|
||||
{
|
||||
initTouchPos: pos,
|
||||
curTouchPos: pos2,
|
||||
touchAngle: 0,
|
||||
touchLength: 0
|
||||
};
|
||||
touchMap[touch.touchPointID] = swp;
|
||||
|
||||
curStep = 1;
|
||||
Haptic.vibrate(40, 70);
|
||||
}
|
||||
if (touch.pressed)
|
||||
{
|
||||
var daSwipe = touchMap[touch.touchPointID];
|
||||
|
||||
daSwipe.curTouchPos.set(touch.screenX, touch.screenY);
|
||||
|
||||
var dx = daSwipe.initTouchPos.x - touch.screenX;
|
||||
var dy = daSwipe.initTouchPos.y - touch.screenY;
|
||||
|
||||
daSwipe.touchAngle = Math.atan2(dy, dx);
|
||||
daSwipe.touchLength = Math.sqrt(dx * dx + dy * dy);
|
||||
|
||||
FlxG.watch.addQuick("LENGTH", daSwipe.touchLength);
|
||||
FlxG.watch.addQuick("ANGLE", FlxAngle.asDegrees(daSwipe.touchAngle));
|
||||
|
||||
if (daSwipe.touchLength >= (activateLength / vibrationSteps) * curStep)
|
||||
{
|
||||
curStep += 1;
|
||||
// Haptic.vibrate(Std.int(hapticPressure / (curStep * 1.5)), 50);
|
||||
}
|
||||
}
|
||||
|
||||
if (touch.justReleased)
|
||||
{
|
||||
touchMap.remove(touch.touchPointID);
|
||||
}
|
||||
|
||||
/* switch (inputID)
|
||||
{
|
||||
case FlxDirectionFlags.UP:
|
||||
return
|
||||
case FlxDirectionFlags.DOWN:
|
||||
}
|
||||
*/
|
||||
}
|
||||
#end
|
||||
}
|
||||
|
||||
override public function check(Action:FlxAction):Bool
|
||||
{
|
||||
for (swp in touchMap)
|
||||
{
|
||||
var degAngle = FlxAngle.asDegrees(swp.touchAngle);
|
||||
|
||||
switch (trigger)
|
||||
{
|
||||
case JUST_PRESSED:
|
||||
if (swp.touchLength >= activateLength)
|
||||
{
|
||||
if (inputID == FlxDirectionFlags.UP.toInt())
|
||||
{
|
||||
if (degAngle >= 45 && degAngle <= 90 + 45) return properTouch(swp);
|
||||
}
|
||||
else if (inputID == FlxDirectionFlags.DOWN.toInt())
|
||||
{
|
||||
if (-degAngle >= 45 && -degAngle <= 90 + 45) return properTouch(swp);
|
||||
}
|
||||
else if (inputID == FlxDirectionFlags.LEFT.toInt())
|
||||
{
|
||||
if (degAngle <= 45 && -degAngle <= 45) return properTouch(swp);
|
||||
}
|
||||
else if (inputID == FlxDirectionFlags.RIGHT.toInt())
|
||||
{
|
||||
if (degAngle >= 90 + 45 && degAngle <= -90 + -45) return properTouch(swp);
|
||||
}
|
||||
}
|
||||
default:
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
function properTouch(swipe:Swipes):Bool
|
||||
{
|
||||
curStep = 1;
|
||||
Haptic.vibrate(100, 30);
|
||||
swipe.initTouchPos.set(swipe.curTouchPos.x, swipe.curTouchPos.y);
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
// Maybe this can be committed to main HaxeFlixel repo?
|
||||
#if android
|
||||
class FlxActionInputDigitalAndroid extends FlxActionInputDigital
|
||||
{
|
||||
/**
|
||||
* Android buttons action input
|
||||
* @param androidKeyID Key identifier (FlxAndroidKey.BACK, FlxAndroidKey.MENU... those are the only 2 android specific ones)
|
||||
* @param Trigger What state triggers this action (PRESSED, JUST_PRESSED, RELEASED, JUST_RELEASED)
|
||||
*/
|
||||
public function new(androidKeyID:FlxAndroidKey, Trigger:FlxInputState)
|
||||
{
|
||||
super(FlxInputDevice.OTHER, androidKeyID, Trigger);
|
||||
}
|
||||
|
||||
override public function check(Action:FlxAction):Bool
|
||||
{
|
||||
return switch (trigger)
|
||||
{
|
||||
#if android
|
||||
case PRESSED: FlxG.android.checkStatus(inputID, PRESSED) || FlxG.android.checkStatus(inputID, PRESSED);
|
||||
case RELEASED: FlxG.android.checkStatus(inputID, RELEASED) || FlxG.android.checkStatus(inputID, JUST_RELEASED);
|
||||
case JUST_PRESSED: FlxG.android.checkStatus(inputID, JUST_PRESSED);
|
||||
case JUST_RELEASED: FlxG.android.checkStatus(inputID, JUST_RELEASED);
|
||||
#end
|
||||
|
||||
default: false;
|
||||
}
|
||||
}
|
||||
}
|
||||
#end
|
||||
|
||||
/**
|
||||
* Since, in many cases multiple actions should use similar keys, we don't want the
|
||||
* rebinding UI to list every action. ActionBinders are what the user percieves as
|
||||
|
@ -1664,7 +1479,7 @@ enum Control
|
|||
VOLUME_DOWN;
|
||||
VOLUME_MUTE;
|
||||
// DEBUG
|
||||
DEBUG_MENU;
|
||||
#if FEATURE_DEBUG_MENU DEBUG_MENU; #end
|
||||
#if FEATURE_CHART_EDITOR DEBUG_CHART; #end
|
||||
#if FEATURE_STAGE_EDITOR DEBUG_STAGE; #end
|
||||
}
|
||||
|
@ -1720,7 +1535,9 @@ enum abstract Action(String) to String from String
|
|||
var VOLUME_DOWN = "volume_down";
|
||||
var VOLUME_MUTE = "volume_mute";
|
||||
// DEBUG
|
||||
#if FEATURE_DEBUG_MENU
|
||||
var DEBUG_MENU = "debug_menu";
|
||||
#end
|
||||
#if FEATURE_CHART_EDITOR
|
||||
var DEBUG_CHART = "debug_chart";
|
||||
#end
|
||||
|
|
|
@ -294,7 +294,7 @@ class PreciseInputManager extends FlxKeyManager<FlxKey, PreciseInputList>
|
|||
// TODO: Remove this line with SDL3 when timestamps change meaning.
|
||||
// This is because SDL3's timestamps are measured in nanoseconds, not milliseconds.
|
||||
timestamp *= Constants.NS_PER_MS; // 18126000000 38367000000
|
||||
timestamp -= Conductor.instance.inputOffset * Constants.NS_PER_MS;
|
||||
// timestamp -= globalOffset * Constants.NS_PER_MS;
|
||||
// trace(timestamp);
|
||||
updateKeyStates(key, true);
|
||||
|
||||
|
|
|
@ -13,6 +13,7 @@ import funkin.input.Controls.Action;
|
|||
*
|
||||
* Example: Pressing Ctrl+Z will undo, while holding Ctrl+Z will start to undo repeatedly.
|
||||
*/
|
||||
@:nullSafety
|
||||
class TurboActionHandler extends FlxBasic
|
||||
{
|
||||
/**
|
||||
|
|
|
@ -12,6 +12,7 @@ import flixel.FlxBasic;
|
|||
*
|
||||
* Example: Pressing Ctrl+Z will undo, while holding Ctrl+Z will start to undo repeatedly.
|
||||
*/
|
||||
@:nullSafety
|
||||
class TurboButtonHandler extends FlxBasic
|
||||
{
|
||||
/**
|
||||
|
|
|
@ -11,6 +11,7 @@ import flixel.FlxBasic;
|
|||
*
|
||||
* Example: Pressing Ctrl+Z will undo, while holding Ctrl+Z will start to undo repeatedly.
|
||||
*/
|
||||
@:nullSafety
|
||||
class TurboKeyHandler extends FlxBasic
|
||||
{
|
||||
/**
|
||||
|
|
136
source/funkin/mobile/input/ControlsHandler.hx
Normal file
136
source/funkin/mobile/input/ControlsHandler.hx
Normal file
|
@ -0,0 +1,136 @@
|
|||
package funkin.mobile.input;
|
||||
|
||||
import funkin.input.Controls;
|
||||
import flixel.input.FlxInput;
|
||||
import flixel.input.actions.FlxAction;
|
||||
import flixel.input.actions.FlxActionInput;
|
||||
import flixel.input.actions.FlxActionInputDigital;
|
||||
import funkin.mobile.ui.FunkinButton;
|
||||
import funkin.mobile.ui.FunkinHitbox;
|
||||
import funkin.play.notes.NoteDirection;
|
||||
import openfl.events.KeyboardEvent;
|
||||
import openfl.events.TouchEvent;
|
||||
|
||||
/**
|
||||
* Handles setting up and managing input controls for the game.
|
||||
*/
|
||||
class ControlsHandler
|
||||
{
|
||||
/**
|
||||
* Returns wether the last input was sent through touch.
|
||||
*/
|
||||
public static var lastInputTouch(default, null):Bool = true;
|
||||
|
||||
/**
|
||||
* Returns wether there's a gamepad or keyboard devices connected and active.
|
||||
*/
|
||||
public static var hasExternalInputDevice(get, never):Bool;
|
||||
|
||||
/**
|
||||
* Returns wether an external input device is currently used as the main input.
|
||||
*/
|
||||
public static var usingExternalInputDevice(get, never):Bool;
|
||||
|
||||
/**
|
||||
* Initialize input trackers used to get the current status of the `lastInputTouch` field.
|
||||
*/
|
||||
public static function initInputTrackers():Void
|
||||
{
|
||||
FlxG.stage.addEventListener(KeyboardEvent.KEY_DOWN, (_) -> lastInputTouch = false);
|
||||
FlxG.stage.addEventListener(TouchEvent.TOUCH_BEGIN, (_) -> lastInputTouch = true);
|
||||
}
|
||||
|
||||
/**
|
||||
* Adds a button input to a given FlxActionDigital and caches it.
|
||||
*
|
||||
* @param action The FlxActionDigital to add the button input to.
|
||||
* @param button The FunkinButton associated with the action.
|
||||
* @param state The input state to associate with the action.
|
||||
* @param cachedInput The array of FlxActionInput objects to cache the input.
|
||||
*/
|
||||
public static function addButton(action:FlxActionDigital, button:FunkinButton, state:FlxInputState, cachedInput:Array<FlxActionInput>):Void
|
||||
{
|
||||
if (action == null || button == null || cachedInput == null) return;
|
||||
|
||||
final input:FlxActionInputDigitalIFlxInput = new FlxActionInputDigitalIFlxInput(button, state);
|
||||
cachedInput.push(input);
|
||||
action.add(input);
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets up hitbox controls based on game controls and hitbox hints.
|
||||
*
|
||||
* @param controls The controls instance defining game controls.
|
||||
* @param hitbox The hitbox to associate with the controls.
|
||||
* @param cachedInput The array of action input objects to cache the input.
|
||||
*/
|
||||
@:access(funkin.input.Controls)
|
||||
public static function setupHitbox(controls:Controls, hitbox:FunkinHitbox, cachedInput:Array<FlxActionInput>):Void
|
||||
{
|
||||
if (controls == null || hitbox == null) return;
|
||||
|
||||
for (hint in hitbox.members)
|
||||
{
|
||||
@:privateAccess
|
||||
switch (hint.noteDirection)
|
||||
{
|
||||
case NoteDirection.LEFT:
|
||||
controls.forEachBound(Control.NOTE_LEFT, function(action:FlxActionDigital, state:FlxInputState):Void {
|
||||
addButton(action, hint, state, cachedInput);
|
||||
});
|
||||
case NoteDirection.DOWN:
|
||||
controls.forEachBound(Control.NOTE_DOWN, function(action:FlxActionDigital, state:FlxInputState):Void {
|
||||
addButton(action, hint, state, cachedInput);
|
||||
});
|
||||
case NoteDirection.UP:
|
||||
controls.forEachBound(Control.NOTE_UP, function(action:FlxActionDigital, state:FlxInputState):Void {
|
||||
addButton(action, hint, state, cachedInput);
|
||||
});
|
||||
case NoteDirection.RIGHT:
|
||||
controls.forEachBound(Control.NOTE_RIGHT, function(action:FlxActionDigital, state:FlxInputState):Void {
|
||||
addButton(action, hint, state, cachedInput);
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Removes cached input associated with game controls.
|
||||
*
|
||||
* @param controls The Controls instance defining game controls.
|
||||
* @param cachedInput The array of action input objects to clear cached input from.
|
||||
*/
|
||||
public static function removeCachedInput(controls:Controls, cachedInput:Array<FlxActionInput>):Void
|
||||
{
|
||||
for (action in controls.digitalActions)
|
||||
{
|
||||
var i:Int = action.inputs.length;
|
||||
|
||||
while (i-- > 0)
|
||||
{
|
||||
var j:Int = cachedInput.length;
|
||||
|
||||
while (j-- > 0)
|
||||
{
|
||||
if (cachedInput[j] == action.inputs[i])
|
||||
{
|
||||
action.remove(action.inputs[i]);
|
||||
cachedInput.remove(cachedInput[j]);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@:noCompletion
|
||||
private static function get_hasExternalInputDevice():Bool
|
||||
{
|
||||
return FlxG.gamepads.numActiveGamepads > 0;
|
||||
}
|
||||
|
||||
@:noCompletion
|
||||
private static function get_usingExternalInputDevice():Bool
|
||||
{
|
||||
return ControlsHandler.hasExternalInputDevice && !ControlsHandler.lastInputTouch;
|
||||
}
|
||||
}
|
57
source/funkin/mobile/input/PreciseInputHandler.hx
Normal file
57
source/funkin/mobile/input/PreciseInputHandler.hx
Normal file
|
@ -0,0 +1,57 @@
|
|||
package funkin.mobile.input;
|
||||
|
||||
import flixel.input.FlxInput;
|
||||
import funkin.input.PreciseInputManager;
|
||||
import funkin.mobile.ui.FunkinHitbox;
|
||||
import funkin.play.notes.NoteDirection;
|
||||
import haxe.Int64;
|
||||
|
||||
/**
|
||||
* Handles setting up and managing precise input controls for the game.
|
||||
*/
|
||||
@:access(funkin.input.PreciseInputManager)
|
||||
class PreciseInputHandler
|
||||
{
|
||||
/**
|
||||
* Initializes the hitbox with the relevant hints and event handlers.
|
||||
*
|
||||
* @param hitbox The hitbox to initialize.
|
||||
*/
|
||||
public static function initializeHitbox(hitbox:FunkinHitbox):Void
|
||||
{
|
||||
hitbox.onHintDown.add(handleHintDown);
|
||||
hitbox.onHintUp.add(handleHintUp);
|
||||
}
|
||||
|
||||
/**
|
||||
* Handles the event when a hint is pressed.
|
||||
*
|
||||
* @param hint The hint that was pressed.
|
||||
*/
|
||||
static function handleHintDown(hint:FunkinHint):Void
|
||||
{
|
||||
final timestamp:Int64 = PreciseInputManager.getCurrentTimestamp();
|
||||
@:privateAccess
|
||||
if (hint.input?.justPressed ?? false)
|
||||
{
|
||||
PreciseInputManager.instance.onInputPressed.dispatch({noteDirection: hint.noteDirection, timestamp: timestamp});
|
||||
PreciseInputManager.instance._dirPressTimestamps.set(hint.noteDirection, timestamp);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Handles the event when a hint is released.
|
||||
*
|
||||
* @param hint The hint that was released.
|
||||
*/
|
||||
static function handleHintUp(hint:FunkinHint):Void
|
||||
{
|
||||
final timestamp:Int64 = PreciseInputManager.getCurrentTimestamp();
|
||||
@:privateAccess
|
||||
if (hint.input?.justReleased ?? false)
|
||||
{
|
||||
PreciseInputManager.instance.onInputReleased.dispatch({noteDirection: hint.noteDirection, timestamp: timestamp});
|
||||
PreciseInputManager.instance._dirPressTimestamps.set(hint.noteDirection, timestamp);
|
||||
}
|
||||
}
|
||||
}
|
156
source/funkin/mobile/ui/FunkinBackButton.hx
Normal file
156
source/funkin/mobile/ui/FunkinBackButton.hx
Normal file
|
@ -0,0 +1,156 @@
|
|||
package funkin.mobile.ui;
|
||||
|
||||
import flixel.FlxG;
|
||||
import flixel.tweens.FlxEase;
|
||||
import flixel.tweens.FlxTween;
|
||||
import flixel.util.FlxColor;
|
||||
import flixel.util.FlxSignal;
|
||||
import funkin.audio.FunkinSound;
|
||||
import funkin.util.HapticUtil;
|
||||
|
||||
class FunkinBackButton extends FunkinButton
|
||||
{
|
||||
public var onConfirmStart(default, null):FlxSignal = new FlxSignal();
|
||||
public var onConfirmEnd(default, null):FlxSignal = new FlxSignal();
|
||||
|
||||
public var enabled:Bool = true;
|
||||
|
||||
var confirming:Bool = false;
|
||||
|
||||
public var restingOpacity:Float;
|
||||
|
||||
var instant:Bool = false;
|
||||
var held:Bool = false;
|
||||
|
||||
/**
|
||||
* Creates a new FunkinBackButton instance.
|
||||
*
|
||||
* @param x The x position of the object.
|
||||
* @param y The y position of the object.
|
||||
* @param color Button's optional color.
|
||||
* @param confirmCallback An optional callback function that will be triggered when the object is clicked.
|
||||
* @param restingOpacity An optional float that is the alpha the button will be when not selected/hovered over.
|
||||
* @param instant An optional flag that makes the button not play the full animation before calling the callback.
|
||||
*/
|
||||
public function new(?x:Float = 0, ?y:Float = 0, ?color:FlxColor = FlxColor.WHITE, ?confirmCallback:Void->Void, ?restingOpacity:Float = 0.3,
|
||||
instant:Bool = false):Void
|
||||
{
|
||||
super(x, y);
|
||||
|
||||
frames = Paths.getSparrowAtlas("backButton");
|
||||
animation.addByIndices('idle', 'back', [0], "", 24, false);
|
||||
animation.addByIndices('hold', 'back', [5], "", 24, false);
|
||||
animation.addByIndices('confirm', 'back', [6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22], "", 24, false);
|
||||
animation.play("idle");
|
||||
|
||||
scale.set(0.7, 0.7);
|
||||
updateHitbox();
|
||||
|
||||
this.color = color;
|
||||
this.restingOpacity = restingOpacity;
|
||||
this.instant = instant;
|
||||
this.alpha = restingOpacity;
|
||||
this.ignoreDownHandler = true;
|
||||
|
||||
onUp.add(playConfirmAnim);
|
||||
onDown.add(playHoldAnim);
|
||||
onOut.add(playOutAnim);
|
||||
|
||||
onConfirmEnd.add(confirmCallback);
|
||||
}
|
||||
|
||||
function playHoldAnim():Void
|
||||
{
|
||||
if (confirming || held || !enabled) return;
|
||||
|
||||
held = true;
|
||||
|
||||
FlxTween.cancelTweensOf(this);
|
||||
HapticUtil.vibrate(0, 0.01, 0.5);
|
||||
animation.play('hold');
|
||||
|
||||
alpha = 1;
|
||||
}
|
||||
|
||||
function playConfirmAnim():Void
|
||||
{
|
||||
if (!enabled) return;
|
||||
|
||||
if (instant)
|
||||
{
|
||||
onConfirmEnd.dispatch();
|
||||
return;
|
||||
}
|
||||
else if (confirming)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
confirming = true;
|
||||
|
||||
FlxTween.cancelTweensOf(this);
|
||||
HapticUtil.vibrate(0, 0.05, 0.5);
|
||||
animation.play('confirm');
|
||||
|
||||
FunkinSound.playOnce(Paths.sound('cancelMenu'));
|
||||
|
||||
onConfirmStart.dispatch();
|
||||
|
||||
animation.onFinish.addOnce(function(name:String) {
|
||||
if (name != 'confirm') return;
|
||||
confirming = false;
|
||||
held = false;
|
||||
onConfirmEnd.dispatch();
|
||||
});
|
||||
}
|
||||
|
||||
function playOutAnim():Void
|
||||
{
|
||||
if (confirming || !enabled) return;
|
||||
|
||||
FlxTween.cancelTweensOf(this);
|
||||
HapticUtil.vibrate(0, 0.01, 0.2);
|
||||
animation.play('idle');
|
||||
|
||||
FlxTween.tween(this, {alpha: restingOpacity}, 0.5,
|
||||
{
|
||||
ease: FlxEase.expoOut,
|
||||
onComplete: function(tween:FlxTween):Void {
|
||||
held = false;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
public function resetCallbacks():Void
|
||||
{
|
||||
onUp.removeAll();
|
||||
onDown.removeAll();
|
||||
onOut.removeAll();
|
||||
|
||||
confirming = false;
|
||||
held = false;
|
||||
|
||||
onUp.add(playConfirmAnim);
|
||||
onDown.add(playHoldAnim);
|
||||
onOut.add(playOutAnim);
|
||||
}
|
||||
|
||||
override public function update(elapsed:Float):Void
|
||||
{
|
||||
#if android
|
||||
if (FlxG.android.justReleased.BACK) onConfirmEnd.dispatch();
|
||||
#end
|
||||
|
||||
super.update(elapsed);
|
||||
}
|
||||
|
||||
override function destroy():Void
|
||||
{
|
||||
super.destroy();
|
||||
|
||||
onConfirmStart.removeAll();
|
||||
onConfirmEnd.removeAll();
|
||||
|
||||
if (animation != null && animation.onFinish != null) animation.onFinish.removeAll();
|
||||
}
|
||||
}
|
464
source/funkin/mobile/ui/FunkinButton.hx
Normal file
464
source/funkin/mobile/ui/FunkinButton.hx
Normal file
|
@ -0,0 +1,464 @@
|
|||
package funkin.mobile.ui;
|
||||
|
||||
import flixel.FlxCamera;
|
||||
import flixel.FlxG;
|
||||
import flixel.util.FlxColor;
|
||||
import funkin.graphics.FunkinSprite;
|
||||
import flixel.input.FlxInput;
|
||||
import flixel.input.IFlxInput;
|
||||
import flixel.input.touch.FlxTouch;
|
||||
import flixel.math.FlxPoint;
|
||||
import flixel.util.FlxDestroyUtil;
|
||||
import flixel.util.FlxSignal;
|
||||
import openfl.display.Graphics;
|
||||
import haxe.ds.Map;
|
||||
|
||||
/**
|
||||
* Enum representing the status of the button.
|
||||
*/
|
||||
enum abstract FunkinButtonStatus(Int) from Int to Int
|
||||
{
|
||||
var NORMAL = 0;
|
||||
var PRESSED = 1;
|
||||
}
|
||||
|
||||
/**
|
||||
* A simple button class that calls a function when touched.
|
||||
*/
|
||||
#if !display
|
||||
@:generic
|
||||
#end
|
||||
@:allow(funkin.mobile.ui.FunkinHitbox)
|
||||
@:allow(funkin.mobile.ui.FunkinButton)
|
||||
class FunkinButton extends FunkinSprite implements IFlxInput
|
||||
{
|
||||
/**
|
||||
* A map that's storing every active touch's ID that's pressing a button.
|
||||
*/
|
||||
public static var buttonsTouchID:Map<Int, FunkinButton> = new Map();
|
||||
|
||||
/**
|
||||
* The current state of the button, either `FunkinButtonStatus.NORMAL` or `FunkinButtonStatus.PRESSED`.
|
||||
*/
|
||||
public var status:FunkinButtonStatus;
|
||||
|
||||
/**
|
||||
* The callback function to call when the button is released.
|
||||
*/
|
||||
public var onUp(default, null):FlxSignal = new FlxSignal();
|
||||
|
||||
/**
|
||||
* The callback function to call when the button is pressed down.
|
||||
*/
|
||||
public var onDown(default, null):FlxSignal = new FlxSignal();
|
||||
|
||||
/**
|
||||
* The callback function to call when the button is no longer hovered over.
|
||||
*/
|
||||
public var onOut(default, null):FlxSignal = new FlxSignal();
|
||||
|
||||
/**
|
||||
* Whether the button was just released.
|
||||
*/
|
||||
public var justReleased(get, never):Bool;
|
||||
|
||||
/**
|
||||
* Whether the button is currently released.
|
||||
*/
|
||||
public var released(get, never):Bool;
|
||||
|
||||
/**
|
||||
* Whether the button is currently pressed.
|
||||
*/
|
||||
public var pressed(get, never):Bool;
|
||||
|
||||
/**
|
||||
* Whether the button was just pressed.
|
||||
*/
|
||||
public var justPressed(get, never):Bool;
|
||||
|
||||
/**
|
||||
* The touch instance that pressed this button.
|
||||
*/
|
||||
public var currentTouch(get, never):Null<FlxTouch>;
|
||||
|
||||
/**
|
||||
* An array of objects that blocks your input.
|
||||
*/
|
||||
public var deadZones:Array<FunkinSprite> = [];
|
||||
|
||||
/**
|
||||
* Whether the button should be released if you swiped over somwhere else.
|
||||
*/
|
||||
public var limitToBounds:Bool = true;
|
||||
|
||||
/**
|
||||
* A radius for circular buttons.
|
||||
* If this radius is larger than 0 then the overlap check will look if the touch point is inside this raius.
|
||||
*/
|
||||
public var radius:Float = 0;
|
||||
|
||||
/**
|
||||
* The vertices of the polygon defining the button's hitbox.
|
||||
* The array should contain points in the format: [x1, y1, x2, y2, ...].
|
||||
* If the array is empty, the polygon is ignored, and the default hitbox is used.
|
||||
*/
|
||||
public var polygon:Null<Array<Float>> = null;
|
||||
|
||||
/**
|
||||
* The input associated with the button, using `Int` as the type.
|
||||
*/
|
||||
var input:FlxInput<Int>;
|
||||
|
||||
/**
|
||||
* The input currently pressing this button, if none, it's `null`.
|
||||
* Needed to check for its release.
|
||||
*/
|
||||
var currentInput:IFlxInput;
|
||||
|
||||
/**
|
||||
* The ID of the touch object that pressed this button.
|
||||
*/
|
||||
var touchID:Int = -1;
|
||||
|
||||
/**
|
||||
* Whether the button should skip calling onDownHandler() on touch.pressed.
|
||||
*/
|
||||
public var ignoreDownHandler:Bool = false;
|
||||
|
||||
/**
|
||||
* Creates a new `FunkinButton` object.
|
||||
*
|
||||
* @param x The x position of the button.
|
||||
* @param y The y position of the button.
|
||||
*/
|
||||
public function new(x:Float = 0, y:Float = 0):Void
|
||||
{
|
||||
super(x, y);
|
||||
|
||||
status = FunkinButtonStatus.NORMAL;
|
||||
solid = false;
|
||||
immovable = true;
|
||||
#if FLX_DEBUG
|
||||
ignoreDrawDebug = true;
|
||||
#end
|
||||
scrollFactor.set();
|
||||
input = new FlxInput(0);
|
||||
}
|
||||
|
||||
/**
|
||||
* Called by the game state when the state is changed (if this object belongs to the state).
|
||||
*/
|
||||
public override function destroy():Void
|
||||
{
|
||||
deadZones = FlxDestroyUtil.destroyArray(deadZones);
|
||||
currentInput = null;
|
||||
input = null;
|
||||
|
||||
buttonsTouchID.remove(touchID);
|
||||
|
||||
touchID = -1;
|
||||
|
||||
super.destroy();
|
||||
}
|
||||
|
||||
/**
|
||||
* Called by the game loop automatically, handles touch over and click detection.
|
||||
*/
|
||||
public override function update(elapsed:Float):Void
|
||||
{
|
||||
super.update(elapsed);
|
||||
|
||||
#if FLX_POINTER_INPUT
|
||||
// Update the button, but only if touches are enabled
|
||||
if (visible)
|
||||
{
|
||||
final overlapFound:Bool = checkTouchOverlap();
|
||||
final touchReleased:Bool = (currentTouch != null && currentTouch.justReleased);
|
||||
|
||||
if ((currentInput != null && currentInput.justReleased || (!limitToBounds && touchReleased)) && overlapFound)
|
||||
{
|
||||
onUpHandler();
|
||||
}
|
||||
|
||||
if (status != FunkinButtonStatus.NORMAL && (!overlapFound || (currentInput != null && currentInput.justReleased)))
|
||||
{
|
||||
if (limitToBounds || (!limitToBounds && touchReleased)) onOutHandler();
|
||||
}
|
||||
}
|
||||
#end
|
||||
|
||||
input.update();
|
||||
}
|
||||
|
||||
function checkTouchOverlap(?touch:FlxTouch):Bool
|
||||
{
|
||||
final touches:Array<FlxTouch> = touch == null ? FlxG.touches.list : [touch];
|
||||
|
||||
for (camera in cameras)
|
||||
{
|
||||
for (touch in touches)
|
||||
{
|
||||
final worldPos:FlxPoint = touch.getWorldPosition(camera, _point);
|
||||
|
||||
for (zone in deadZones)
|
||||
{
|
||||
if (zone != null && zone.overlapsPoint(worldPos, true, camera)) return false;
|
||||
}
|
||||
|
||||
function updateTouchID():Void
|
||||
{
|
||||
touchID = touch.touchPointID;
|
||||
if (buttonsTouchID.exists(touchID) && buttonsTouchID.get(touchID) != this)
|
||||
{
|
||||
final prevButton:Null<FunkinButton> = buttonsTouchID.get(touchID);
|
||||
|
||||
if (input != null && prevButton != null && prevButton.input != null && !prevButton.limitToBounds) prevButton.onOutHandler();
|
||||
}
|
||||
buttonsTouchID.set(touchID, this);
|
||||
|
||||
updateStatus(touch);
|
||||
}
|
||||
|
||||
if (polygon != null && polygon.length >= 6 && polygon.length % 2 == 0)
|
||||
{
|
||||
if (polygonOverlapsPoint(worldPos, false, camera))
|
||||
{
|
||||
updateTouchID();
|
||||
return true;
|
||||
}
|
||||
}
|
||||
else if (radius > 0)
|
||||
{
|
||||
if (circleOverlapsPoint(worldPos, camera))
|
||||
{
|
||||
updateTouchID();
|
||||
return true;
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
if (overlapsPoint(worldPos, true, camera))
|
||||
{
|
||||
updateTouchID();
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
function circleOverlapsPoint(point:FlxPoint, ?camera:FlxCamera):Bool
|
||||
{
|
||||
if (camera == null) camera = FlxG.camera;
|
||||
|
||||
final xPos = point.x - camera.scroll.x;
|
||||
final yPos = point.y - camera.scroll.y;
|
||||
getScreenPosition(_point, camera);
|
||||
point.putWeak();
|
||||
|
||||
final distanceX = xPos - (_point.x + (width / 2));
|
||||
final distanceY = yPos - (_point.y + (height / 2));
|
||||
final distance = Math.sqrt((distanceX * distanceX) + (distanceY * distanceY));
|
||||
|
||||
return distance <= radius;
|
||||
}
|
||||
|
||||
function polygonOverlapsPoint(point:FlxPoint, inScreenSpace:Bool = false, ?camera:FlxCamera):Bool
|
||||
{
|
||||
if (polygon == null || polygon.length < 6 || polygon.length % 2 != 0) return false;
|
||||
|
||||
if (!inScreenSpace) return isPointInPolygon(polygon, point, FlxPoint.weak(x, y));
|
||||
|
||||
if (camera == null) camera = FlxG.camera;
|
||||
|
||||
final pos:FlxPoint = FlxPoint.weak(point.x - camera.scroll.x, point.y - camera.scroll.y);
|
||||
|
||||
point.putWeak();
|
||||
|
||||
return isPointInPolygon(polygon, pos, getScreenPosition(_point, camera));
|
||||
}
|
||||
|
||||
static function isPointInPolygon(vertices:Array<Float>, point:FlxPoint, ?offset:FlxPoint):Bool
|
||||
{
|
||||
if (offset == null) offset = FlxPoint.weak();
|
||||
|
||||
var inside:Bool = false;
|
||||
|
||||
final numsPoints:Int = Math.floor(vertices.length / 2);
|
||||
|
||||
for (i in 0...numsPoints)
|
||||
{
|
||||
final vertex1:FlxPoint = FlxPoint.weak(vertices[i * 2] + offset.x, vertices[i * 2 + 1] + offset.y);
|
||||
final vertex2:FlxPoint = FlxPoint.weak(vertices[(i + 1) % numsPoints * 2] + offset.x, vertices[(i + 1) % numsPoints * 2 + 1] + offset.y);
|
||||
|
||||
if (checkRayIntersection(vertex1, vertex2, point))
|
||||
{
|
||||
inside = !inside;
|
||||
}
|
||||
}
|
||||
|
||||
point.putWeak();
|
||||
offset.putWeak();
|
||||
|
||||
return inside;
|
||||
}
|
||||
|
||||
static inline function checkRayIntersection(vertex1:FlxPoint, vertex2:FlxPoint, point:FlxPoint):Bool
|
||||
{
|
||||
final result:Bool = (vertex1.y > point.y) != (vertex2.y > point.y)
|
||||
&& point.x < (vertex1.x + ((point.y - vertex1.y) / (vertex2.y - vertex1.y)) * (vertex2.x - vertex1.x));
|
||||
|
||||
vertex1.putWeak();
|
||||
vertex2.putWeak();
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
function isPressed(check:Bool):Bool
|
||||
{
|
||||
return !(status != FunkinButtonStatus.NORMAL && (!check || (currentInput != null && currentInput.justReleased)));
|
||||
}
|
||||
|
||||
function updateStatus(newInput:IFlxInput):Void
|
||||
{
|
||||
if (newInput.justPressed)
|
||||
{
|
||||
currentInput = newInput;
|
||||
|
||||
onDownHandler();
|
||||
}
|
||||
else if (status == FunkinButtonStatus.NORMAL && !ignoreDownHandler)
|
||||
{
|
||||
if (newInput.pressed)
|
||||
{
|
||||
onDownHandler();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function onUpHandler():Void
|
||||
{
|
||||
status = FunkinButtonStatus.NORMAL;
|
||||
|
||||
input.release();
|
||||
|
||||
buttonsTouchID.remove(touchID);
|
||||
|
||||
touchID = -1;
|
||||
|
||||
currentInput = null;
|
||||
|
||||
onUp.dispatch();
|
||||
}
|
||||
|
||||
function onDownHandler():Void
|
||||
{
|
||||
status = FunkinButtonStatus.PRESSED;
|
||||
|
||||
input.press();
|
||||
|
||||
onDown.dispatch();
|
||||
}
|
||||
|
||||
function onOutHandler():Void
|
||||
{
|
||||
status = FunkinButtonStatus.NORMAL;
|
||||
|
||||
input.release();
|
||||
|
||||
buttonsTouchID.remove(touchID);
|
||||
|
||||
touchID = -1;
|
||||
|
||||
onOut.dispatch();
|
||||
}
|
||||
|
||||
#if FLX_DEBUG
|
||||
public override function drawDebugOnCamera(camera:FlxCamera):Void
|
||||
{
|
||||
if (polygon != null && polygon.length >= 6 && polygon.length % 2 == 0)
|
||||
{
|
||||
if (!camera.visible || !camera.exists || !isOnScreen(camera)) return;
|
||||
|
||||
getScreenPosition(_point, camera);
|
||||
|
||||
final gfx:Graphics = beginDrawDebug(camera);
|
||||
|
||||
final boundingBoxColor:Null<FlxColor> = getDebugBoundingBoxColor(allowCollisions);
|
||||
|
||||
if (boundingBoxColor != null) drawDebugPolygonColor(gfx, polygon, boundingBoxColor);
|
||||
|
||||
endDrawDebug(camera);
|
||||
}
|
||||
else if (radius > 0)
|
||||
{
|
||||
if (!camera.visible || !camera.exists || !isOnScreen(camera)) return;
|
||||
|
||||
getScreenPosition(_point, camera);
|
||||
|
||||
final gfx:Graphics = beginDrawDebug(camera);
|
||||
|
||||
final boundingBoxColor:Null<FlxColor> = getDebugBoundingBoxColor(allowCollisions);
|
||||
|
||||
if (boundingBoxColor != null) drawDebugCircleColor(gfx, boundingBoxColor);
|
||||
|
||||
endDrawDebug(camera);
|
||||
}
|
||||
else
|
||||
{
|
||||
super.drawDebugOnCamera(camera);
|
||||
}
|
||||
}
|
||||
|
||||
function drawDebugCircleColor(gfx:Graphics, color:FlxColor):Void
|
||||
{
|
||||
gfx.lineStyle(2, color, 0.75);
|
||||
gfx.drawCircle(radius, radius, radius);
|
||||
}
|
||||
|
||||
function drawDebugPolygonColor(gfx:Graphics, vertices:Array<Float>, color:FlxColor):Void
|
||||
{
|
||||
gfx.lineStyle(2, color, 0.75);
|
||||
|
||||
for (i in 0...Math.floor(vertices.length / 2))
|
||||
{
|
||||
if (i == 0)
|
||||
{
|
||||
gfx.moveTo(vertices[i * 2] + _point.x, vertices[i * 2 + 1] + _point.y);
|
||||
}
|
||||
else
|
||||
{
|
||||
gfx.lineTo(vertices[i * 2] + _point.x, vertices[i * 2 + 1] + _point.y);
|
||||
}
|
||||
}
|
||||
}
|
||||
#end
|
||||
|
||||
inline function get_justReleased():Bool
|
||||
{
|
||||
return input.justReleased;
|
||||
}
|
||||
|
||||
inline function get_released():Bool
|
||||
{
|
||||
return input.released;
|
||||
}
|
||||
|
||||
inline function get_pressed():Bool
|
||||
{
|
||||
return input.pressed;
|
||||
}
|
||||
|
||||
inline function get_justPressed():Bool
|
||||
{
|
||||
return input.justPressed;
|
||||
}
|
||||
|
||||
inline function get_currentTouch():Null<FlxTouch>
|
||||
{
|
||||
return FlxG.touches.getByID(touchID);
|
||||
}
|
||||
}
|
683
source/funkin/mobile/ui/FunkinHitbox.hx
Normal file
683
source/funkin/mobile/ui/FunkinHitbox.hx
Normal file
|
@ -0,0 +1,683 @@
|
|||
package funkin.mobile.ui;
|
||||
|
||||
import flixel.FlxG;
|
||||
import flixel.graphics.FlxGraphic;
|
||||
import flixel.group.FlxSpriteGroup;
|
||||
import flixel.input.actions.FlxActionInput;
|
||||
import flixel.tweens.FlxEase;
|
||||
import flixel.tweens.FlxTween;
|
||||
import flixel.util.FlxColor;
|
||||
import flixel.util.FlxDestroyUtil;
|
||||
import flixel.util.FlxSignal;
|
||||
import funkin.graphics.shaders.HSVShader;
|
||||
import funkin.graphics.FunkinSprite;
|
||||
import funkin.mobile.input.ControlsHandler;
|
||||
import funkin.play.notes.NoteDirection;
|
||||
import openfl.display.BitmapData;
|
||||
import openfl.display.Shape;
|
||||
import openfl.geom.Matrix;
|
||||
import openfl.Vector;
|
||||
import funkin.data.notestyle.NoteStyleRegistry;
|
||||
import funkin.play.notes.notestyle.NoteStyle;
|
||||
import funkin.data.animation.AnimationData;
|
||||
import funkin.util.assets.FlxAnimationUtil;
|
||||
import funkin.ui.FullScreenScaleMode;
|
||||
|
||||
enum FunkinHintAlphaStyle
|
||||
{
|
||||
INVISIBLE_TILL_PRESS;
|
||||
VISIBLE_TILL_PRESS;
|
||||
}
|
||||
|
||||
/**
|
||||
* The `FunkinHint` class represents a button with HSV color properties, allowing hue and saturation adjustments.
|
||||
*/
|
||||
@:nullSafety
|
||||
class FunkinHint extends FunkinButton
|
||||
{
|
||||
/**
|
||||
* A map defining different alpha styles for hint visibility during press and release states.
|
||||
*
|
||||
* Each style is represented as a key with an associated array of two alpha values:
|
||||
* - The first value corresponds to the alpha when the hint is pressed.
|
||||
* - The second value corresponds to the alpha when the hint is not pressed.
|
||||
* - The third value corresponds to the duratuon it'll take to tween between the two values.
|
||||
*/
|
||||
static final HINT_ALPHA_STYLE:Map<FunkinHintAlphaStyle, Array<Float>> = [
|
||||
INVISIBLE_TILL_PRESS => [0.3, 0.00001, 0.01],
|
||||
VISIBLE_TILL_PRESS => [0.4, 0.2, 0.08]
|
||||
];
|
||||
|
||||
/**
|
||||
* Indicates whether the hint is pixel.
|
||||
*/
|
||||
public var isPixel:Bool = false;
|
||||
|
||||
/**
|
||||
* The direction of the note associated with the button.
|
||||
*/
|
||||
var noteDirection:NoteDirection;
|
||||
|
||||
/**
|
||||
* The label associated with the button.
|
||||
*/
|
||||
var label:Null<FunkinSprite>;
|
||||
|
||||
/**
|
||||
* The tween used to animate the alpha changes of the button.
|
||||
*/
|
||||
var labelAlphaTween:Null<FlxTween>;
|
||||
|
||||
/**
|
||||
* The HSV shader used to adjust the hue and saturation of the button.
|
||||
*/
|
||||
var hsvShader:HSVShader;
|
||||
|
||||
/**
|
||||
* The tween used to animate the alpha changes of the button.
|
||||
*/
|
||||
var alphaTween:Null<FlxTween>;
|
||||
|
||||
var followTarget:Null<FunkinSprite>;
|
||||
|
||||
var followTargetSize:Bool = false;
|
||||
|
||||
/**
|
||||
* Creates a new `FunkinHint` object.
|
||||
*
|
||||
* @param x The x position of the button.
|
||||
* @param y The y position of the button.
|
||||
* @param noteDirection The direction of the note the button represents (e.g. left, right).
|
||||
* @param label An graphic to display as the label on the button.
|
||||
*/
|
||||
public function new(x:Float, y:Float, noteDirection:NoteDirection, label:Null<FlxGraphic>):Void
|
||||
{
|
||||
super(x, y);
|
||||
|
||||
this.noteDirection = noteDirection;
|
||||
|
||||
if (label != null)
|
||||
{
|
||||
this.label = new FunkinSprite(x, y);
|
||||
this.label.loadGraphic(label);
|
||||
}
|
||||
|
||||
hsvShader = new HSVShader();
|
||||
hsvShader.hue = 1.0;
|
||||
hsvShader.saturation = 1.0;
|
||||
hsvShader.value = 1.0;
|
||||
shader = hsvShader;
|
||||
}
|
||||
|
||||
/**
|
||||
* Initializes alpha tween animations for the button.
|
||||
*
|
||||
* @param style The alpha style to use.
|
||||
*/
|
||||
public function initTween(style:FunkinHintAlphaStyle):Void
|
||||
{
|
||||
final hintAlpha:Null<Array<Float>> = HINT_ALPHA_STYLE.get(style);
|
||||
final swapValues:Bool = style == VISIBLE_TILL_PRESS;
|
||||
|
||||
if (hintAlpha == null || hintAlpha.length < 2) return;
|
||||
|
||||
function createTween(targetAlpha:Float, transitionTime:Float, isPressed:Bool):Void
|
||||
{
|
||||
alphaTween?.cancel();
|
||||
alphaTween = FlxTween.tween(this, {alpha: targetAlpha}, transitionTime, {ease: FlxEase.circInOut});
|
||||
|
||||
if (label != null)
|
||||
{
|
||||
labelAlphaTween?.cancel();
|
||||
labelAlphaTween = FlxTween.tween(label, {alpha: (hintAlpha[0] + hintAlpha[1]) - targetAlpha}, transitionTime, {ease: FlxEase.circInOut});
|
||||
}
|
||||
}
|
||||
|
||||
onDown.add(createTween.bind(hintAlpha[swapValues ? 1 : 0], hintAlpha[2], true));
|
||||
onUp.add(createTween.bind(hintAlpha[swapValues ? 0 : 1], hintAlpha[2], false));
|
||||
onOut.add(createTween.bind(hintAlpha[swapValues ? 0 : 1], hintAlpha[2], false));
|
||||
|
||||
alpha = hintAlpha[swapValues ? 0 : 1];
|
||||
|
||||
if (label != null && hintAlpha != null) label.alpha = hintAlpha[0];
|
||||
}
|
||||
|
||||
/**
|
||||
* Makes the hitbox follow the specified sprite.
|
||||
*
|
||||
* @param sprite The FunkinSprite instance that the hitbox should follow.
|
||||
* @param followTargetSize A boolean indicating whether the hitbox should adjust to the target's size. Default is true.
|
||||
*/
|
||||
public function follow(sprite:FunkinSprite, followTargetSize:Bool = true):Void
|
||||
{
|
||||
this.followTargetSize = followTargetSize;
|
||||
followTarget = sprite;
|
||||
}
|
||||
|
||||
/**
|
||||
* Desaturates the button, setting its saturation to 0.2.
|
||||
*/
|
||||
public function desaturate():Void
|
||||
{
|
||||
hsvShader.saturation = 0.2;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the hue of the button.
|
||||
*
|
||||
* @param hue The new hue value.
|
||||
*/
|
||||
public function setHue(hue:Float):Void
|
||||
{
|
||||
hsvShader.hue = hue;
|
||||
}
|
||||
|
||||
public override function update(elapsed:Float):Void
|
||||
{
|
||||
super.update(elapsed);
|
||||
|
||||
if (followTarget != null)
|
||||
{
|
||||
final widthMultiplier:Float = isPixel ? 1.35 : 1.35;
|
||||
final heightMultiplier:Float = 8;
|
||||
|
||||
final xOffset:Float = isPixel ? 43.265 : 0;
|
||||
final yOffset:Float = isPixel ? 57.65 : 0;
|
||||
|
||||
// TODO: THIS feels off when playing on regular notes but it's fine for pixel notes? Hard to explain needs more testing
|
||||
if (followTargetSize)
|
||||
{
|
||||
setSize(followTarget.width * widthMultiplier + (isPixel ? 93.05 : 0), followTarget.height * heightMultiplier + (isPixel ? 118 : 0));
|
||||
}
|
||||
|
||||
setPosition((followTarget.x - (followTarget.width * ((widthMultiplier - 1) / 2))) - xOffset, (followTarget.y - 220) - yOffset);
|
||||
}
|
||||
}
|
||||
|
||||
public override function draw():Void
|
||||
{
|
||||
super.draw();
|
||||
|
||||
if (label != null && label.visible)
|
||||
{
|
||||
label.cameras = _cameras;
|
||||
label.draw();
|
||||
}
|
||||
}
|
||||
|
||||
#if FLX_DEBUG
|
||||
public override function drawDebug():Void
|
||||
{
|
||||
super.drawDebug();
|
||||
|
||||
if (label != null) label.drawDebug();
|
||||
}
|
||||
#end
|
||||
|
||||
/**
|
||||
* Cleans up memory used by the `FunkinHint`.
|
||||
*/
|
||||
public override function destroy():Void
|
||||
{
|
||||
if (alphaTween != null) alphaTween = FlxDestroyUtil.destroy(alphaTween);
|
||||
|
||||
if (labelAlphaTween != null) labelAlphaTween = FlxDestroyUtil.destroy(labelAlphaTween);
|
||||
|
||||
if (label != null) label = FlxDestroyUtil.destroy(label);
|
||||
|
||||
super.destroy();
|
||||
}
|
||||
|
||||
override function set_x(v:Float):Float
|
||||
{
|
||||
super.set_x(v);
|
||||
|
||||
if (label != null) label.x = x;
|
||||
|
||||
return x;
|
||||
}
|
||||
|
||||
override function set_y(v:Float):Float
|
||||
{
|
||||
super.set_y(v);
|
||||
|
||||
if (label != null) label.y = y;
|
||||
|
||||
return y;
|
||||
}
|
||||
}
|
||||
|
||||
enum abstract FunkinHitboxControlSchemes(String) from String to String
|
||||
{
|
||||
var FourLanes = 'Four Lanes';
|
||||
var DoubleThumbTriangle = 'Double Thumb Triangle';
|
||||
var DoubleThumbSquare = 'Double Thumb Square';
|
||||
var DoubleThumbDPad = 'Double Thumb DPad';
|
||||
var Arrows = 'Arrows';
|
||||
}
|
||||
|
||||
/**
|
||||
* This class represents a zone with four buttons, designed to be easily customizable in layout.
|
||||
*/
|
||||
@:nullSafety
|
||||
class FunkinHitbox extends FlxTypedSpriteGroup<FunkinHint>
|
||||
{
|
||||
/**
|
||||
* Indicates whether the hitbox is pixel.
|
||||
*/
|
||||
public var isPixel(default, set):Bool = false;
|
||||
|
||||
/**
|
||||
* A `FlxTypedSignal` that triggers every time a button is pressed.
|
||||
*/
|
||||
public var onHintDown:FlxTypedSignal<FunkinHint->Void> = new FlxTypedSignal<FunkinHint->Void>();
|
||||
|
||||
/**
|
||||
* A `FlxTypedSignal` that triggers every time a button is released.
|
||||
*/
|
||||
public var onHintUp:FlxTypedSignal<FunkinHint->Void> = new FlxTypedSignal<FunkinHint->Void>();
|
||||
|
||||
/**
|
||||
* The list of tracked inputs for the hitbox.
|
||||
*/
|
||||
var trackedInputs:Array<FlxActionInput> = [];
|
||||
|
||||
/**
|
||||
* Creates a new `FunkinHitbox` object.
|
||||
*/
|
||||
public function new(?schemeOverride:String, ?showGradint:Bool = true, ?directionsOverride:Array<NoteDirection>, ?colorsOverride:Array<FlxColor>):Void
|
||||
{
|
||||
super();
|
||||
|
||||
final hintsColors:Array<FlxColor> = (colorsOverride == null || colorsOverride.length == 0) ? [0xFFC34B9A, 0xFF00FFFF, 0xFF12FB06, 0xFFF9393F] : colorsOverride;
|
||||
final hintsNoteDirections:Array<NoteDirection> = (directionsOverride == null || directionsOverride.length == 0) ? [NoteDirection.LEFT, NoteDirection.DOWN, NoteDirection.UP, NoteDirection.RIGHT] : directionsOverride;
|
||||
|
||||
#if mobile
|
||||
final controlsScheme:String = (schemeOverride == null || schemeOverride.length == 0) ? Preferences.controlsScheme : schemeOverride;
|
||||
|
||||
switch (controlsScheme)
|
||||
{
|
||||
case FunkinHitboxControlSchemes.FourLanes:
|
||||
final hintWidth:Int = Math.floor(FlxG.width / hintsNoteDirections.length);
|
||||
final hintHeight:Int = FlxG.height;
|
||||
|
||||
for (i in 0...hintsNoteDirections.length)
|
||||
{
|
||||
add(createHintLane(i * hintWidth, 0, hintsNoteDirections[i % hintsNoteDirections.length], hintWidth, hintHeight,
|
||||
hintsColors[i % hintsColors.length], true, showGradint));
|
||||
}
|
||||
case FunkinHitboxControlSchemes.DoubleThumbTriangle:
|
||||
final screenHalf:Int = Math.floor(FlxG.width / 2);
|
||||
|
||||
for (i in 0...2)
|
||||
{
|
||||
final xOffset:Int = (i == 1) ? screenHalf : 0;
|
||||
|
||||
add(createHintTriangle(xOffset, 0, hintsNoteDirections[0], Math.floor(FlxG.width / 4), FlxG.height, hintsColors[0], showGradint));
|
||||
add(createHintTriangle(xOffset, FlxG.height / 2, hintsNoteDirections[1], Math.floor(FlxG.width / 2), Math.floor(FlxG.height / 2), hintsColors[1],
|
||||
showGradint));
|
||||
add(createHintTriangle(xOffset, 0, hintsNoteDirections[2], Math.floor(FlxG.width / 2), Math.floor(FlxG.height / 2), hintsColors[2], showGradint));
|
||||
add(createHintTriangle(xOffset + Math.floor(FlxG.width / 4), 0, hintsNoteDirections[3], Math.floor(FlxG.width / 4), FlxG.height, hintsColors[3],
|
||||
showGradint));
|
||||
}
|
||||
case FunkinHitboxControlSchemes.DoubleThumbSquare:
|
||||
final screenHalf:Int = Math.floor(FlxG.width / 2);
|
||||
|
||||
final hintWidth:Int = Math.floor((FlxG.width / hintsNoteDirections.length) / 2);
|
||||
final hintHeight:Int = FlxG.height;
|
||||
|
||||
final boxWidth:Int = Math.floor(hintWidth * 2);
|
||||
final boxHeight:Int = Math.floor(hintHeight / 2);
|
||||
|
||||
for (i in 0...2)
|
||||
{
|
||||
final xOffset:Int = (i == 1) ? screenHalf : 0;
|
||||
|
||||
for (j in 0...hintsNoteDirections.length)
|
||||
{
|
||||
if (j == 1 || j == 2)
|
||||
{
|
||||
add(createHintLane(xOffset + hintWidth, (j == 1) ? boxHeight : 0, hintsNoteDirections[j], boxWidth, boxHeight,
|
||||
hintsColors[j % hintsColors.length], false, showGradint));
|
||||
}
|
||||
else
|
||||
{
|
||||
add(createHintLane(xOffset + (j == 0 ? 0 : hintWidth + boxWidth), 0, hintsNoteDirections[j], hintWidth, hintHeight,
|
||||
hintsColors[j % hintsColors.length], false, showGradint));
|
||||
}
|
||||
}
|
||||
}
|
||||
case FunkinHitboxControlSchemes.DoubleThumbDPad:
|
||||
final hintSize:Int = 75;
|
||||
final outlineThickness:Int = 5;
|
||||
final hintsAngles:Array<Float> = [Math.PI, Math.PI / 2, Math.PI * 1.5, 0];
|
||||
final hintsZoneRadius:Int = 115;
|
||||
|
||||
for (i in 0...2)
|
||||
{
|
||||
for (j in 0...hintsAngles.length)
|
||||
{
|
||||
final x:Float = ((i == 1) ? FlxG.width - (hintSize * 4) : hintSize * 2) + Math.cos(hintsAngles[j]) * hintsZoneRadius;
|
||||
final y:Float = (FlxG.height - (hintSize * 3.75)) + Math.sin(hintsAngles[j]) * hintsZoneRadius;
|
||||
|
||||
add(createHintCircle(i == 0 ? x + FullScreenScaleMode.gameNotchSize.x : x - FullScreenScaleMode.gameNotchSize.x, y,
|
||||
hintsNoteDirections[j % hintsNoteDirections.length], hintSize, outlineThickness, hintsColors[j % hintsColors.length]));
|
||||
}
|
||||
}
|
||||
case FunkinHitboxControlSchemes.Arrows:
|
||||
final hintWidth:Int = 146;
|
||||
final hintHeight:Int = 149;
|
||||
final noteSpacing:Int = 80;
|
||||
|
||||
final xPos:Int = Math.floor((FlxG.width - (hintWidth + noteSpacing) * hintsNoteDirections.length) / 2);
|
||||
final yPos:Int = Math.floor(FlxG.height - hintHeight * 2 - 24);
|
||||
|
||||
for (i in 0...hintsNoteDirections.length)
|
||||
{
|
||||
add(createHintTransparentNote(xPos + i * hintWidth + noteSpacing * i, yPos, hintsNoteDirections[i % hintsNoteDirections.length], hintWidth,
|
||||
hintHeight));
|
||||
}
|
||||
}
|
||||
#end
|
||||
|
||||
scrollFactor.set();
|
||||
|
||||
ControlsHandler.setupHitbox(PlayerSettings.player1.controls, this, trackedInputs);
|
||||
}
|
||||
|
||||
public function getFirstHintByDirection(direction:NoteDirection):Null<FunkinHint>
|
||||
{
|
||||
var result:Null<FunkinHint> = null;
|
||||
forEachOfType(FunkinHint, function(hint:FunkinHint):Void {
|
||||
@:privateAccess
|
||||
if (hint.noteDirection == direction) result = hint;
|
||||
});
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a new `FunkinHint` lane button along side a graphic label with specified properties.
|
||||
*
|
||||
* @param x The x position of the button.
|
||||
* @param y The y position of the button.
|
||||
* @param noteDirection The direction of the note the button represents (e.g. left, right).
|
||||
* @param width The width of the button.
|
||||
* @param height The height of the button.
|
||||
* @param id The ID of the button.
|
||||
* @param color The color of the button.
|
||||
* @return A new `FunkinHint` object.
|
||||
*/
|
||||
function createHintLane(x:Float, y:Float, noteDirection:NoteDirection, width:Int, height:Int, color:FlxColor = 0xFFFFFFFF, label:Bool = true,
|
||||
gradient:Bool = true):FunkinHint
|
||||
{
|
||||
final hint:FunkinHint = new FunkinHint(x, y, noteDirection, label ? createHintLaneLabelGraphic(width, height, Math.floor(height * 0.035), color) : null);
|
||||
hint.loadGraphic(createHintLaneGraphic(width, height, color, gradient));
|
||||
hint.onDown.add(onHintDown.dispatch.bind(hint));
|
||||
hint.onUp.add(onHintUp.dispatch.bind(hint));
|
||||
hint.onOut.add(onHintUp.dispatch.bind(hint));
|
||||
hint.initTween(INVISIBLE_TILL_PRESS);
|
||||
return hint;
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a new `FunkinHint` triangle button with specified properties.
|
||||
*
|
||||
* @param x The x position of the triangle button.
|
||||
* @param y The y position of the triangle button.
|
||||
* @param noteDirection The direction of the note the button represents (e.g. left, right).
|
||||
* @param size The size of the triangle (base length).
|
||||
* @param upright A boolean indicating if the triangle is upright (true) or inverted (false).
|
||||
* @param id The unique ID of the triangle button.
|
||||
* @param color The color of the triangle button (default is white).
|
||||
* @return A new `FunkinHint` triangle object.
|
||||
*/
|
||||
function createHintTriangle(x:Float, y:Float, noteDirection:NoteDirection, width:Int, height:Int, color:FlxColor = 0xFFFFFFFF,
|
||||
gradient:Bool = true):FunkinHint
|
||||
{
|
||||
final hint:FunkinHint = new FunkinHint(x, y, noteDirection, null);
|
||||
hint.loadGraphic(createHintTriangleGraphic(width, height, noteDirection, color, gradient));
|
||||
hint.onDown.add(onHintDown.dispatch.bind(hint));
|
||||
hint.onUp.add(onHintUp.dispatch.bind(hint));
|
||||
hint.onOut.add(onHintUp.dispatch.bind(hint));
|
||||
hint.initTween(INVISIBLE_TILL_PRESS);
|
||||
hint.polygon = getTriangleVertices(width, height, noteDirection);
|
||||
return hint;
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a new `FunkinHint` circular button with specified properties.
|
||||
*
|
||||
* @param x The x position of the circular button.
|
||||
* @param y The y position of the circular button.
|
||||
* @param noteDirection The direction of the note the button represents (e.g., left, right).
|
||||
* @param radius The radius of the circular button.
|
||||
* @param outlineThickness The thickness of the outline for the circle.
|
||||
* @param color The color of the circular button (default is white).
|
||||
* @return A new `FunkinHint` circular object.
|
||||
*/
|
||||
function createHintCircle(x:Float, y:Float, noteDirection:NoteDirection, radius:Float, outlineThickness:Int, color:FlxColor = 0xFFFFFFFF):FunkinHint
|
||||
{
|
||||
final hint:FunkinHint = new FunkinHint(x, y, noteDirection, null);
|
||||
hint.loadGraphic(createHintCircleGraphic(radius, outlineThickness, color));
|
||||
hint.limitToBounds = false;
|
||||
hint.radius = radius;
|
||||
hint.onDown.add(onHintDown.dispatch.bind(hint));
|
||||
hint.onUp.add(onHintUp.dispatch.bind(hint));
|
||||
hint.onOut.add(onHintUp.dispatch.bind(hint));
|
||||
hint.initTween(VISIBLE_TILL_PRESS);
|
||||
return hint;
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a new `FunkinHint` representing a transparent note corresponding to the note from the scene.
|
||||
* @param x The x position of the button.
|
||||
* @param y The y position of the button.
|
||||
* @param noteDirection The direction of the note the button represents (e.g. left, right).
|
||||
* @param width The width of the button.
|
||||
* @param height The height of the button.
|
||||
* @return A new `FunkinHint` object.
|
||||
*/
|
||||
function createHintTransparentNote(x:Float, y:Float, noteDirection:NoteDirection, width:Int, height:Int):FunkinHint
|
||||
{
|
||||
final hint:FunkinHint = new FunkinHint(x, y, noteDirection, null);
|
||||
hint.alpha = 0;
|
||||
hint.setSize(width, height);
|
||||
hint.onDown.add(onHintDown.dispatch.bind(hint));
|
||||
hint.onUp.add(onHintUp.dispatch.bind(hint));
|
||||
hint.onOut.add(onHintUp.dispatch.bind(hint));
|
||||
|
||||
var noteStyle:NoteStyle = NoteStyleRegistry.instance.fetchDefault();
|
||||
@:privateAccess
|
||||
@:nullSafety(Off)
|
||||
{
|
||||
hint.frames = Paths.getSparrowAtlas(noteStyle.getStrumlineAssetPath() ?? '', noteStyle.getAssetLibrary(noteStyle.getStrumlineAssetPath(true)));
|
||||
FlxAnimationUtil.addAtlasAnimations(hint, noteStyle.getStrumlineAnimationData(noteDirection));
|
||||
}
|
||||
|
||||
hint.animation.play('static', true);
|
||||
|
||||
hint.onDown.add(() -> {
|
||||
hint.animation.play('press', true);
|
||||
hint.centerOrigin();
|
||||
hint.centerOffsets();
|
||||
});
|
||||
|
||||
hint.onUp.add(() -> {
|
||||
hint.animation.play('static', true);
|
||||
hint.centerOrigin();
|
||||
hint.centerOffsets();
|
||||
});
|
||||
|
||||
hint.onOut.add(() -> {
|
||||
hint.animation.play('static', true);
|
||||
hint.centerOrigin();
|
||||
hint.centerOffsets();
|
||||
});
|
||||
|
||||
hint.centerOffsets();
|
||||
hint.centerOrigin();
|
||||
|
||||
return hint;
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a lane graphic for a hint button.
|
||||
*
|
||||
* @param width The width of the graphic.
|
||||
* @param height The height of the graphic.
|
||||
* @param baseColor The base color of the graphic.
|
||||
* @return A `FlxGraphic` object representing the button graphic.
|
||||
*/
|
||||
function createHintLaneGraphic(width:Int, height:Int, baseColor:FlxColor = 0xFFFFFFFF, gradient:Bool = true):FlxGraphic
|
||||
{
|
||||
final shape:Shape = new Shape();
|
||||
|
||||
if (gradient)
|
||||
{
|
||||
final matrix:Matrix = new Matrix();
|
||||
matrix.createGradientBox(width, height, 0, 0, 0);
|
||||
shape.graphics.beginGradientFill(RADIAL, [baseColor.to24Bit(), baseColor.to24Bit()], [0, baseColor.alphaFloat], [60, 255], matrix, PAD, RGB, 0);
|
||||
}
|
||||
else
|
||||
{
|
||||
shape.graphics.beginFill(baseColor.to24Bit(), baseColor.alphaFloat);
|
||||
}
|
||||
|
||||
shape.graphics.drawRect(0, 0, width, height);
|
||||
shape.graphics.endFill();
|
||||
|
||||
final graphicData:BitmapData = new BitmapData(width, height, true, 0);
|
||||
graphicData.draw(shape, true);
|
||||
return FlxGraphic.fromBitmapData(graphicData, false, null, false);
|
||||
}
|
||||
|
||||
function createHintLaneLabelGraphic(width:Int, height:Int, labelHeight:Int, baseColor:FlxColor = 0xFFFFFFFF):FlxGraphic
|
||||
{
|
||||
final shape:Shape = new Shape();
|
||||
shape.graphics.beginFill(0, 0);
|
||||
shape.graphics.drawRect(0, 0, width, height);
|
||||
shape.graphics.endFill();
|
||||
|
||||
final matrix:Matrix = new Matrix();
|
||||
matrix.createGradientBox(width, labelHeight, Math.PI / 2, 0, 0);
|
||||
shape.graphics.beginGradientFill(LINEAR, [baseColor.to24Bit(), baseColor.to24Bit()], [baseColor.alphaFloat, 0], [0, 255], matrix);
|
||||
shape.graphics.drawRect(0, 0, width, labelHeight);
|
||||
shape.graphics.endFill();
|
||||
|
||||
final matrix:Matrix = new Matrix();
|
||||
matrix.createGradientBox(width, labelHeight, Math.PI / 2, 0, height - labelHeight);
|
||||
shape.graphics.beginGradientFill(LINEAR, [baseColor.to24Bit(), baseColor.to24Bit()], [0, baseColor.alphaFloat], [0, 255], matrix);
|
||||
shape.graphics.drawRect(0, height - labelHeight, width, labelHeight);
|
||||
shape.graphics.endFill();
|
||||
|
||||
final graphicData:BitmapData = new BitmapData(width, height, true, 0);
|
||||
graphicData.draw(shape, true);
|
||||
return FlxGraphic.fromBitmapData(graphicData, false, null, false);
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a triangle graphic for a hint button.
|
||||
*
|
||||
* @param size The base length of the triangle.
|
||||
* @param upright A boolean indicating if the triangle is upright (true) or inverted (false).
|
||||
* @param baseColor The base color of the triangle graphic (default is white).
|
||||
* @return A `FlxGraphic` object representing the triangle button graphic.
|
||||
*/
|
||||
function createHintTriangleGraphic(width:Int, height:Int, facing:NoteDirection, baseColor:FlxColor = 0xFFFFFFFF, gradient:Bool = true):FlxGraphic
|
||||
{
|
||||
final shape:Shape = new Shape();
|
||||
|
||||
if (gradient)
|
||||
{
|
||||
final matrix:Matrix = new Matrix();
|
||||
matrix.createGradientBox(width, height, 0, 0, 0);
|
||||
shape.graphics.beginGradientFill(RADIAL, [baseColor.to24Bit(), baseColor.to24Bit()], [0, baseColor.alphaFloat], [60, 255], matrix, PAD, RGB, 0);
|
||||
}
|
||||
else
|
||||
{
|
||||
shape.graphics.beginFill(baseColor.to24Bit(), baseColor.alphaFloat);
|
||||
}
|
||||
|
||||
shape.graphics.drawRect(width / 2, height / 2, width / 2, height / 2);
|
||||
shape.graphics.drawTriangles(Vector.ofArray(getTriangleVertices(width, height, facing)), Vector.ofArray([0, 1, 2]));
|
||||
shape.graphics.endFill();
|
||||
|
||||
final graphicData:BitmapData = new BitmapData(width, height, true, 0);
|
||||
graphicData.draw(shape, true);
|
||||
return FlxGraphic.fromBitmapData(graphicData, false, null, false);
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a circular graphic for a hint button.
|
||||
*
|
||||
* @param radius The radius of the circle.
|
||||
* @param baseColor The base color of the circle graphic (default is white).
|
||||
* @param outlineThickness The thickness of the outline for the circle.
|
||||
* @return A `FlxGraphic` object representing the circular button graphic.
|
||||
*/
|
||||
function createHintCircleGraphic(radius:Float, outlineThickness:Int, baseColor:FlxColor = 0xFFFFFFFF):FlxGraphic
|
||||
{
|
||||
var brightColor:FlxColor = baseColor;
|
||||
brightColor.brightness += 0.6;
|
||||
|
||||
if (baseColor.brightness >= 0.75) baseColor.alphaFloat -= baseColor.brightness * 0.35;
|
||||
|
||||
final shape:Shape = new Shape();
|
||||
shape.graphics.beginFill(baseColor.to24Bit(), baseColor.alphaFloat);
|
||||
shape.graphics.lineStyle(outlineThickness, brightColor.to24Bit(), brightColor.alpha);
|
||||
shape.graphics.drawCircle(radius, radius, radius);
|
||||
shape.graphics.endFill();
|
||||
|
||||
final matrix:Matrix = new Matrix();
|
||||
matrix.translate(outlineThickness, outlineThickness);
|
||||
|
||||
final graphicData:BitmapData = new BitmapData(Math.floor((radius + outlineThickness) * 2), Math.floor((radius + outlineThickness) * 2), true, 0);
|
||||
graphicData.draw(shape, matrix, true);
|
||||
return FlxGraphic.fromBitmapData(graphicData, false, null, false);
|
||||
}
|
||||
|
||||
/**
|
||||
* Сalculates vertices in a given direction
|
||||
* @param width width of triangle
|
||||
* @param height height of triangle
|
||||
* @param facing The side the triangle faces
|
||||
* @return array of vertices
|
||||
*/
|
||||
function getTriangleVertices(width:Int, height:Int, facing:NoteDirection):Array<Float>
|
||||
{
|
||||
if (facing == UP) facing = DOWN;
|
||||
else if (facing == DOWN) facing = UP;
|
||||
|
||||
return switch (facing)
|
||||
{
|
||||
case UP: [width / 2, 0, 0, height, width, height];
|
||||
case DOWN: [0, 0, width, 0, width / 2, height];
|
||||
case LEFT: [0, 0, width, height / 2, 0, height];
|
||||
case RIGHT: [width, 0, 0, height / 2, width, height];
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Cleans up memory used by the `FunkinHitbox`.
|
||||
*/
|
||||
public override function destroy():Void
|
||||
{
|
||||
if (trackedInputs != null && trackedInputs.length > 0) ControlsHandler.removeCachedInput(PlayerSettings.player1.controls, trackedInputs);
|
||||
|
||||
FlxDestroyUtil.destroy(onHintDown);
|
||||
FlxDestroyUtil.destroy(onHintUp);
|
||||
|
||||
super.destroy();
|
||||
}
|
||||
|
||||
@:noCompletion
|
||||
function set_isPixel(value:Bool):Bool
|
||||
{
|
||||
isPixel = value;
|
||||
forEachOfType(FunkinHint, function(hint:FunkinHint):Void {
|
||||
hint.isPixel = value;
|
||||
});
|
||||
return value;
|
||||
}
|
||||
}
|
125
source/funkin/mobile/ui/mainmenu/FunkinOptionsButton.hx
Normal file
125
source/funkin/mobile/ui/mainmenu/FunkinOptionsButton.hx
Normal file
|
@ -0,0 +1,125 @@
|
|||
package funkin.mobile.ui.mainmenu;
|
||||
|
||||
import flixel.FlxG;
|
||||
import flixel.tweens.FlxTween;
|
||||
import flixel.util.FlxSignal;
|
||||
import flixel.util.FlxTimer;
|
||||
import funkin.audio.FunkinSound;
|
||||
import funkin.util.HapticUtil;
|
||||
|
||||
class FunkinOptionsButton extends FunkinButton
|
||||
{
|
||||
public var onConfirmStart(default, null):FlxSignal = new FlxSignal();
|
||||
public var onConfirmEnd(default, null):FlxSignal = new FlxSignal();
|
||||
|
||||
var confirming:Bool = false;
|
||||
var instant:Bool = false;
|
||||
var held:Bool = false;
|
||||
|
||||
/**
|
||||
* Creates a new FunkinOptionsButton instance.
|
||||
*
|
||||
* @param x The x position of the object.
|
||||
* @param y The y position of the object.
|
||||
* @param confirmCallback An optional callback function that will be triggered when the object is clicked.
|
||||
* @param instant An optional flag that makes the button not play the full animation before calling the callback.
|
||||
*/
|
||||
public function new(?x:Float = 0, ?y:Float = 0, ?confirmCallback:Void->Void, instant:Bool = false):Void
|
||||
{
|
||||
super(x, y);
|
||||
|
||||
frames = Paths.getSparrowAtlas("mainmenu/optionsButton");
|
||||
animation.addByIndices('idle', 'options', [0], "", 24, false);
|
||||
animation.addByIndices('hold', 'options', [3], "", 24, false);
|
||||
animation.addByIndices('confirm', 'options', [4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16], "", 24, false);
|
||||
animation.play("idle");
|
||||
|
||||
scale.set(0.7, 0.7);
|
||||
updateHitbox();
|
||||
|
||||
this.instant = instant;
|
||||
this.ignoreDownHandler = true;
|
||||
|
||||
onUp.add(playConfirmAnim);
|
||||
onDown.add(playHoldAnim);
|
||||
onOut.add(playOutAnim);
|
||||
|
||||
onConfirmEnd.add(confirmCallback);
|
||||
}
|
||||
|
||||
function playHoldAnim():Void
|
||||
{
|
||||
if (confirming || held) return;
|
||||
|
||||
held = true;
|
||||
|
||||
FlxTween.cancelTweensOf(this);
|
||||
HapticUtil.vibrate(0, 0.01, 0.2);
|
||||
animation.play('hold');
|
||||
}
|
||||
|
||||
function playConfirmAnim():Void
|
||||
{
|
||||
if (instant)
|
||||
{
|
||||
onConfirmEnd.dispatch();
|
||||
return;
|
||||
}
|
||||
else if (confirming)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
confirming = true;
|
||||
|
||||
FlxTween.cancelTweensOf(this);
|
||||
HapticUtil.vibrate(0, 0.05, 0.5);
|
||||
animation.play('confirm');
|
||||
|
||||
FunkinSound.playOnce(Paths.sound('confirmMenu'));
|
||||
|
||||
new FlxTimer().start(0.05, function(_) {
|
||||
HapticUtil.vibrate(0, 0.01, 0.2);
|
||||
}, 4);
|
||||
|
||||
onConfirmStart.dispatch();
|
||||
|
||||
animation.onFinish.addOnce(function(name:String) {
|
||||
if (name != 'confirm') return;
|
||||
onConfirmEnd.dispatch();
|
||||
});
|
||||
}
|
||||
|
||||
function playOutAnim():Void
|
||||
{
|
||||
if (confirming) return;
|
||||
|
||||
FlxTween.cancelTweensOf(this);
|
||||
HapticUtil.vibrate(0, 0.01, 0.2);
|
||||
animation.play('idle');
|
||||
}
|
||||
|
||||
public function resetCallbacks():Void
|
||||
{
|
||||
onUp.removeAll();
|
||||
onDown.removeAll();
|
||||
onOut.removeAll();
|
||||
|
||||
confirming = false;
|
||||
held = false;
|
||||
|
||||
onUp.add(playConfirmAnim);
|
||||
onDown.add(playHoldAnim);
|
||||
onOut.add(playOutAnim);
|
||||
}
|
||||
|
||||
override function destroy():Void
|
||||
{
|
||||
super.destroy();
|
||||
|
||||
onConfirmStart.removeAll();
|
||||
onConfirmEnd.removeAll();
|
||||
|
||||
if (animation != null && animation.onFinish != null) animation.onFinish.removeAll();
|
||||
}
|
||||
}
|
363
source/funkin/mobile/ui/options/ControlsSchemeMenu.hx
Normal file
363
source/funkin/mobile/ui/options/ControlsSchemeMenu.hx
Normal file
|
@ -0,0 +1,363 @@
|
|||
package funkin.mobile.ui.options;
|
||||
|
||||
import flixel.addons.transition.FlxTransitionableState;
|
||||
import flixel.math.FlxMath;
|
||||
import flixel.group.FlxSpriteGroup.FlxTypedSpriteGroup;
|
||||
import flixel.tweens.FlxTween;
|
||||
import flixel.util.FlxColor;
|
||||
import flixel.FlxG;
|
||||
import funkin.mobile.ui.options.objects.SchemeMenuButton;
|
||||
import funkin.mobile.ui.options.objects.HitboxShowcase;
|
||||
import funkin.mobile.ui.FunkinHitbox;
|
||||
import funkin.util.TouchUtil;
|
||||
import funkin.util.MathUtil;
|
||||
import funkin.ui.MusicBeatSubState;
|
||||
import funkin.ui.AtlasText;
|
||||
import funkin.ui.FullScreenScaleMode;
|
||||
import funkin.graphics.shaders.HSVShader;
|
||||
import funkin.graphics.FunkinSprite;
|
||||
import funkin.graphics.FunkinCamera;
|
||||
import funkin.audio.FunkinSound;
|
||||
import funkin.Preferences;
|
||||
|
||||
/**
|
||||
* Represents the controls scheme menu.
|
||||
* In this menu, you can change your controls scheme.
|
||||
*/
|
||||
class ControlsSchemeMenu extends MusicBeatSubState
|
||||
{
|
||||
/**
|
||||
* Text that displays current scheme's name.
|
||||
*/
|
||||
var schemeNameText:AtlasText;
|
||||
|
||||
/**
|
||||
* A camera for buttons.
|
||||
*/
|
||||
var camButtons:FunkinCamera;
|
||||
|
||||
/**
|
||||
* A camera for hitbox showcases group.
|
||||
*/
|
||||
var camHitboxes:FunkinCamera;
|
||||
|
||||
/**
|
||||
* A button that is changed depending if you are in the hitbox demo or not.
|
||||
*/
|
||||
var currentButton:SchemeMenuButton;
|
||||
|
||||
/**
|
||||
* Group of hitbox showcase selection items.
|
||||
*/
|
||||
var hitboxShowcases:FlxTypedSpriteGroup<HitboxShowcase>;
|
||||
|
||||
/**
|
||||
* An object used for selecting the current hitbox scheme.
|
||||
*/
|
||||
var itemNavHitbox:FunkinSprite;
|
||||
|
||||
/**
|
||||
* Returns true, if player is currently in hitbox demonstration.
|
||||
*/
|
||||
var isInDemo:Bool;
|
||||
|
||||
/**
|
||||
* An array of every single scheme.
|
||||
*/
|
||||
final availableSchemes:Array<String> = [
|
||||
FunkinHitbox.FunkinHitboxControlSchemes.Arrows,
|
||||
FunkinHitbox.FunkinHitboxControlSchemes.FourLanes,
|
||||
FunkinHitbox.FunkinHitboxControlSchemes.DoubleThumbTriangle,
|
||||
FunkinHitbox.FunkinHitboxControlSchemes.DoubleThumbSquare,
|
||||
FunkinHitbox.FunkinHitboxControlSchemes.DoubleThumbDPad
|
||||
];
|
||||
|
||||
/**
|
||||
* Current selected index
|
||||
*/
|
||||
var currentIndex:Int = 0;
|
||||
|
||||
/**
|
||||
* Touch X position when touch was just pressed. Resets on release.
|
||||
*/
|
||||
var dragStartingX:Int;
|
||||
|
||||
/**
|
||||
* Touch X distance between dragStartingX and current touch position. Resets on release.
|
||||
*/
|
||||
var dragDistance:Int;
|
||||
|
||||
/**
|
||||
* Represents the background shader for the menu, utilizing HSV color adjustments.
|
||||
*/
|
||||
var hsv:HSVShader = new HSVShader();
|
||||
|
||||
public override function create():Void
|
||||
{
|
||||
super.create();
|
||||
|
||||
FlxG.state.persistentDraw = false;
|
||||
FlxG.state.persistentUpdate = false;
|
||||
|
||||
hsv.hue = -0.6;
|
||||
hsv.saturation = 0.9;
|
||||
hsv.value = 3.6;
|
||||
|
||||
final menuBG:FunkinSprite = FunkinSprite.create('menuBG');
|
||||
menuBG.shader = hsv;
|
||||
menuBG.setGraphicSize(Std.int(FlxG.width * 1.1));
|
||||
menuBG.updateHitbox();
|
||||
menuBG.screenCenter();
|
||||
menuBG.scrollFactor.set(0, 0);
|
||||
add(menuBG);
|
||||
|
||||
for (i in 0...availableSchemes.length)
|
||||
{
|
||||
if (availableSchemes[i] == Preferences.controlsScheme)
|
||||
{
|
||||
currentIndex = i;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
schemeNameText = new AtlasText(FlxG.width * 0.05, FlxG.height * 0.05, availableSchemes[currentIndex], AtlasFont.BOLD);
|
||||
add(schemeNameText);
|
||||
|
||||
setupCameras();
|
||||
|
||||
setupHitboxShowcases();
|
||||
|
||||
createButton(false);
|
||||
}
|
||||
|
||||
/**
|
||||
* Setups every needed camera.
|
||||
*/
|
||||
function setupCameras():Void
|
||||
{
|
||||
final mainCamera:FunkinCamera = new FunkinCamera('SchemeMenuCamera');
|
||||
mainCamera.bgColor = FlxColor.BLACK;
|
||||
FlxG.cameras.add(mainCamera);
|
||||
|
||||
if (camControls != null) FlxG.cameras.remove(camControls);
|
||||
|
||||
camControls = new FunkinCamera('camControls');
|
||||
camControls.bgColor = 0x0;
|
||||
FlxG.cameras.add(camControls, false);
|
||||
|
||||
camButtons = new FunkinCamera('camButtons');
|
||||
camButtons.bgColor = 0x0;
|
||||
FlxG.cameras.add(camButtons, false);
|
||||
|
||||
camHitboxes = new FunkinCamera('camHitboxes');
|
||||
camHitboxes.setScale(0.5, 0.5);
|
||||
camHitboxes.bgColor = 0x0;
|
||||
FlxG.cameras.add(camHitboxes, false);
|
||||
}
|
||||
|
||||
/**
|
||||
* Setups the hitbox showcase items.
|
||||
*/
|
||||
function setupHitboxShowcases():Void
|
||||
{
|
||||
hitboxShowcases = new FlxTypedSpriteGroup<HitboxShowcase>();
|
||||
hitboxShowcases.x = (-1500 * currentIndex) + (-1500 / (availableSchemes.length + 1) * currentIndex);
|
||||
|
||||
for (i in 0...availableSchemes.length)
|
||||
{
|
||||
final hitboxShowcase:HitboxShowcase = new HitboxShowcase(0, 0, i, currentIndex, availableSchemes[i], onSelectHitbox);
|
||||
hitboxShowcase.x = Math.floor(FlxG.width * -0.16 + (1500 * (i * FullScreenScaleMode.wideScale.x)));
|
||||
hitboxShowcases.add(hitboxShowcase);
|
||||
}
|
||||
|
||||
hitboxShowcases.cameras = [camHitboxes];
|
||||
add(hitboxShowcases);
|
||||
|
||||
itemNavHitbox = new FunkinSprite(FlxG.width * 0.295).makeSolidColor(Std.int(FlxG.width * 0.25), Std.int(FlxG.height * 0.25), FlxColor.GREEN);
|
||||
itemNavHitbox.cameras = [camButtons];
|
||||
itemNavHitbox.updateHitbox();
|
||||
itemNavHitbox.screenCenter(Y);
|
||||
itemNavHitbox.visible = false;
|
||||
add(itemNavHitbox);
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates or recreates a scheme menu button.
|
||||
* @param isDemoScreen Returns true, if player is currently in hitbox demo.
|
||||
*/
|
||||
function createButton(isDemoScreen:Bool):Void
|
||||
{
|
||||
if (currentButton != null) remove(currentButton);
|
||||
|
||||
if (isDemoScreen)
|
||||
{
|
||||
currentButton = new SchemeMenuButton(FlxG.width * 0.83, FlxG.height * 0.03, 'BACK', onHitboxDemoBack);
|
||||
currentButton.text.x -= 5;
|
||||
}
|
||||
else
|
||||
{
|
||||
currentButton = new SchemeMenuButton(FlxG.width * 0.83, FlxG.height * 0.83, 'DEMO', onHitboxDemo);
|
||||
currentButton.text.x -= 10;
|
||||
}
|
||||
|
||||
add(currentButton);
|
||||
}
|
||||
|
||||
/**
|
||||
* Called when current hitbox has been selected.
|
||||
*/
|
||||
function onSelectHitbox():Void
|
||||
{
|
||||
currentButton.busy = true;
|
||||
|
||||
Preferences.controlsScheme = availableSchemes[currentIndex];
|
||||
|
||||
FlxTransitionableState.skipNextTransIn = true;
|
||||
FlxTransitionableState.skipNextTransOut = true;
|
||||
|
||||
FlxG.switchState(() -> new funkin.ui.options.OptionsState());
|
||||
}
|
||||
|
||||
/**
|
||||
* Called when the current button is pressed and player is not in demo right now.
|
||||
*/
|
||||
function onHitboxDemo():Void
|
||||
{
|
||||
isInDemo = true;
|
||||
|
||||
FlxTween.tween(hsv, {hue: 0, saturation: 0, value: 0.5}, 0.5);
|
||||
|
||||
hitboxShowcases.forEach(function(hitboxShowcase:HitboxShowcase) {
|
||||
hitboxShowcase.visible = false;
|
||||
});
|
||||
|
||||
schemeNameText.visible = false;
|
||||
|
||||
createButton(true);
|
||||
|
||||
addHitbox(true, false, availableSchemes[currentIndex]);
|
||||
|
||||
hitbox.forEachAlive(function(hint:FunkinHint) {
|
||||
if (availableSchemes[currentIndex] == FunkinHitboxControlSchemes.Arrows) hint.alpha = 1;
|
||||
|
||||
if (!hint.deadZones.contains(cast(currentButton.body, FunkinSprite))) hint.deadZones.push(cast(currentButton.body, FunkinSprite));
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Called when the current button is pressed and player is in demo right now.
|
||||
*/
|
||||
function onHitboxDemoBack():Void
|
||||
{
|
||||
isInDemo = false;
|
||||
|
||||
FlxTween.tween(hsv, {hue: -0.6, saturation: 0.9, value: 3.6}, 0.5);
|
||||
|
||||
hitboxShowcases.forEach(function(hitboxShowcase:HitboxShowcase) {
|
||||
hitboxShowcase.visible = true;
|
||||
});
|
||||
|
||||
schemeNameText.visible = true;
|
||||
|
||||
createButton(false);
|
||||
|
||||
if (hitbox != null) hitbox.exists = false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Updates selection using currentIndex.
|
||||
* @param change Used to change currentIndex.
|
||||
*/
|
||||
function setSelection(index:Int):Void
|
||||
{
|
||||
final newIndex:Int = Math.floor(FlxMath.bound(index, 0, hitboxShowcases.length - 1));
|
||||
|
||||
if (currentIndex != newIndex)
|
||||
{
|
||||
currentIndex = newIndex;
|
||||
}
|
||||
else
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
FunkinSound.playOnce(Paths.sound('scrollMenu'), 0.4);
|
||||
|
||||
schemeNameText.text = availableSchemes[currentIndex];
|
||||
|
||||
hitboxShowcases.forEach(function(hitboxShowcase:HitboxShowcase) {
|
||||
hitboxShowcase.selectionIndex = currentIndex;
|
||||
});
|
||||
}
|
||||
|
||||
#if FEATURE_TOUCH_CONTROLS
|
||||
/**
|
||||
* Handles touch dragging.
|
||||
*/
|
||||
function handleDrag():Void
|
||||
{
|
||||
if (TouchUtil.justPressed && TouchUtil.touch != null) dragStartingX = TouchUtil.touch.x;
|
||||
|
||||
if (TouchUtil.pressed && TouchUtil.touch != null) dragDistance = TouchUtil.touch.x - dragStartingX;
|
||||
|
||||
if (TouchUtil.justReleased)
|
||||
{
|
||||
dragStartingX = 0;
|
||||
dragDistance = 0;
|
||||
}
|
||||
|
||||
// trace(dragDistance);
|
||||
}
|
||||
|
||||
/**
|
||||
* Handles all the touch inputs.
|
||||
*/
|
||||
function handleInputs():Void
|
||||
{
|
||||
if (isInDemo) return;
|
||||
|
||||
if (currentButton.busy) return;
|
||||
|
||||
handleDrag();
|
||||
|
||||
if (TouchUtil.pressAction(itemNavHitbox))
|
||||
{
|
||||
hitboxShowcases.members[currentIndex].onPress();
|
||||
|
||||
currentButton.busy = true;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* HitboxShowcases X position when the player just pressed on the screen.
|
||||
* Used for dragging.
|
||||
*/
|
||||
var originX:Float;
|
||||
|
||||
public override function update(elapsed:Float):Void
|
||||
{
|
||||
super.update(elapsed);
|
||||
|
||||
handleInputs();
|
||||
|
||||
if (TouchUtil.justPressed) originX = hitboxShowcases.x;
|
||||
|
||||
if (TouchUtil.pressed && dragDistance != 0)
|
||||
{
|
||||
final showcasesTargetX:Float = originX + dragDistance * 10;
|
||||
hitboxShowcases.x = MathUtil.smoothLerpPrecision(hitboxShowcases.x, showcasesTargetX, elapsed, 0.5);
|
||||
|
||||
final minShowcasesX:Float = -1500 * availableSchemes.length;
|
||||
hitboxShowcases.x = FlxMath.bound(hitboxShowcases.x, minShowcasesX, 400);
|
||||
|
||||
final targetIndex:Int = Math.round(hitboxShowcases.x / -1500);
|
||||
|
||||
if (currentIndex != targetIndex) setSelection(targetIndex);
|
||||
}
|
||||
else
|
||||
{
|
||||
hitboxShowcases.x = MathUtil.smoothLerpPrecision(hitboxShowcases.x, (-1500 * currentIndex) + (-1500 / (availableSchemes.length + 1) * currentIndex), elapsed, 0.5);
|
||||
}
|
||||
#end
|
||||
}
|
||||
}
|
120
source/funkin/mobile/ui/options/objects/HitboxShowcase.hx
Normal file
120
source/funkin/mobile/ui/options/objects/HitboxShowcase.hx
Normal file
|
@ -0,0 +1,120 @@
|
|||
package funkin.mobile.ui.options.objects;
|
||||
|
||||
import flixel.addons.display.shapes.FlxShapeBox;
|
||||
import flixel.group.FlxSpriteGroup;
|
||||
import flixel.effects.FlxFlicker;
|
||||
import flixel.util.FlxSignal;
|
||||
import flixel.util.FlxColor;
|
||||
import flixel.FlxG;
|
||||
import funkin.mobile.ui.FunkinHitbox;
|
||||
import funkin.audio.FunkinSound;
|
||||
import funkin.util.MathUtil;
|
||||
|
||||
/**
|
||||
* Represents a showcase hitbox in the scheme menu.
|
||||
*/
|
||||
class HitboxShowcase extends FlxSpriteGroup
|
||||
{
|
||||
/**
|
||||
* An array of values for lerping object's alpha.
|
||||
*/
|
||||
static final HITBOX_SHOWCASE_ALPHA:Array<Float> = [0.3, 1];
|
||||
|
||||
/**
|
||||
* Object's own index.
|
||||
*/
|
||||
public var index:Int;
|
||||
|
||||
/**
|
||||
* Current selection's index from menu where this object is used.
|
||||
*/
|
||||
public var selectionIndex:Int;
|
||||
|
||||
/**
|
||||
* Indicates if object's index is equal to current selection's index.
|
||||
*/
|
||||
public var selected(get, never):Bool;
|
||||
|
||||
/**
|
||||
* Signal dispatched when the object is selected. Additional behavior can be added by subscribing to this signal.
|
||||
*/
|
||||
public var onSelect(default, null):FlxSignal = new FlxSignal();
|
||||
|
||||
/**
|
||||
* Indicates if the object is currently processing a selection (to avoid multiple triggers).
|
||||
*/
|
||||
public var busy:Bool = false;
|
||||
|
||||
/**
|
||||
* Creates a new HitboxShowcase instance.
|
||||
*
|
||||
* @param x The x position of the object.
|
||||
* @param y The y position of the object.
|
||||
* @param index An integer used as object's index.
|
||||
* @param selectionIndex Menu's current selection index.
|
||||
* @param controlsScheme Hitbox's controls scheme.
|
||||
* @param onClick An optional callback function that will be triggered when the object is clicked.
|
||||
*/
|
||||
public function new(x:Int = 0, y:Int = 0, index:Int, selectionIndex:Int = 0, controlsScheme:String, ?onClick:Void->Void):Void
|
||||
{
|
||||
super(x, y);
|
||||
|
||||
this.index = index;
|
||||
this.selectionIndex = selectionIndex;
|
||||
|
||||
setupObjects(controlsScheme);
|
||||
|
||||
alpha = HITBOX_SHOWCASE_ALPHA[selected ? 1 : 0];
|
||||
|
||||
if (onClick != null) onSelect.add(onClick);
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates and setups every needed object.
|
||||
*
|
||||
* @param controlsScheme Hitbox's controls scheme.
|
||||
*/
|
||||
function setupObjects(controlsScheme:String):Void
|
||||
{
|
||||
final bg:FlxShapeBox = new FlxShapeBox(0, 0, FlxG.width + 2, FlxG.height + 2, {thickness: 6, color: FlxColor.BLACK}, FlxColor.GRAY);
|
||||
bg.screenCenter();
|
||||
add(bg);
|
||||
|
||||
final hitbox:FunkinHitbox = new FunkinHitbox(controlsScheme, false);
|
||||
hitbox.forEachAlive(function(hint:FunkinHint):Void {
|
||||
if (controlsScheme != FunkinHitbox.FunkinHitboxControlSchemes.FourLanes) hint.alpha = 0.3;
|
||||
});
|
||||
hitbox.active = false;
|
||||
add(hitbox);
|
||||
}
|
||||
|
||||
/**
|
||||
* Called when the object is both selected and pressed.
|
||||
*/
|
||||
public function onPress():Void
|
||||
{
|
||||
if (!busy)
|
||||
{
|
||||
busy = true;
|
||||
|
||||
FunkinSound.playOnce(Paths.sound('confirmMenu'));
|
||||
|
||||
FlxFlicker.flicker(this, 1, 0.06, true, false, function(_) {
|
||||
busy = false;
|
||||
onSelect.dispatch();
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
public override function update(elapsed:Float):Void
|
||||
{
|
||||
super.update(elapsed);
|
||||
|
||||
alpha = MathUtil.smoothLerpPrecision(alpha, HITBOX_SHOWCASE_ALPHA[selected ? 1 : 0], elapsed, 0.2);
|
||||
}
|
||||
|
||||
function get_selected():Bool
|
||||
{
|
||||
return index == selectionIndex;
|
||||
}
|
||||
}
|
78
source/funkin/mobile/ui/options/objects/SchemeMenuButton.hx
Normal file
78
source/funkin/mobile/ui/options/objects/SchemeMenuButton.hx
Normal file
|
@ -0,0 +1,78 @@
|
|||
package funkin.mobile.ui.options.objects;
|
||||
|
||||
import flixel.addons.display.shapes.FlxShapeBox;
|
||||
import flixel.effects.FlxFlicker;
|
||||
import flixel.group.FlxSpriteGroup;
|
||||
import flixel.util.FlxSignal;
|
||||
import flixel.util.FlxColor;
|
||||
import funkin.util.TouchUtil;
|
||||
import funkin.util.SwipeUtil;
|
||||
import funkin.audio.FunkinSound;
|
||||
import funkin.ui.AtlasText;
|
||||
|
||||
/**
|
||||
* Represents a button in the scheme menu, specifically designed for mobile touch input.
|
||||
* The button displays text and allows selection through touch or an external callback.
|
||||
*/
|
||||
class SchemeMenuButton extends FlxSpriteGroup
|
||||
{
|
||||
/**
|
||||
* The visual body of the button.
|
||||
*/
|
||||
public var body:Null<FlxShapeBox>;
|
||||
|
||||
/**
|
||||
* The text displayed on the button.
|
||||
*/
|
||||
public var text:Null<AtlasText>;
|
||||
|
||||
/**
|
||||
* Signal dispatched when the button is selected. Additional behavior can be added by subscribing to this signal.
|
||||
*/
|
||||
public var onSelect(default, null):FlxSignal = new FlxSignal();
|
||||
|
||||
/**
|
||||
* Indicates if the button is currently processing a selection (to avoid multiple triggers).
|
||||
*/
|
||||
public var busy:Bool = false;
|
||||
|
||||
/**
|
||||
* Creates a new SchemeMenuButton instance.
|
||||
*
|
||||
* @param xPos The x position of the button.
|
||||
* @param yPos The y position of the button.
|
||||
* @param labelText The text displayed on the button.
|
||||
* @param onClick An optional callback function that will be triggered when the button is clicked.
|
||||
*/
|
||||
public function new(?xPos:Float = 0, ?yPos:Float = 0, labelText:String, ?onClick:Void->Void):Void
|
||||
{
|
||||
super(xPos, yPos);
|
||||
|
||||
body = new FlxShapeBox(0, 0, 200, 100, {thickness: 4, color: FlxColor.BLACK}, FlxColor.WHITE);
|
||||
add(body);
|
||||
|
||||
text = new AtlasText(-150, -75, labelText, AtlasFont.DEFAULT);
|
||||
add(text);
|
||||
|
||||
updateHitbox();
|
||||
|
||||
if (onClick != null) onSelect.add(onClick);
|
||||
}
|
||||
|
||||
public override function update(elapsed:Float):Void
|
||||
{
|
||||
super.update(elapsed);
|
||||
|
||||
if (!busy && (TouchUtil.pressAction(this) && !SwipeUtil.swipeAny))
|
||||
{
|
||||
busy = true;
|
||||
|
||||
FunkinSound.playOnce(Paths.sound('confirmMenu'));
|
||||
|
||||
FlxFlicker.flicker(this, 1, 0.06, true, false, function(_) {
|
||||
busy = false;
|
||||
onSelect.dispatch();
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
309
source/funkin/mobile/util/AdMobUtil.hx
Normal file
309
source/funkin/mobile/util/AdMobUtil.hx
Normal file
|
@ -0,0 +1,309 @@
|
|||
package funkin.mobile.util;
|
||||
|
||||
#if FEATURE_MOBILE_ADVERTISEMENTS
|
||||
import extension.admob.Admob;
|
||||
import extension.admob.AdmobBannerAlign;
|
||||
import extension.admob.AdmobBannerSize;
|
||||
import extension.admob.AdmobEvent;
|
||||
import flixel.FlxG;
|
||||
import funkin.play.cutscene.VideoCutscene;
|
||||
import funkin.util.macro.EnvironmentConfigMacro;
|
||||
|
||||
/**
|
||||
* Provides utility functions for working with admob advertisements.
|
||||
*/
|
||||
@:nullSafety
|
||||
class AdMobUtil
|
||||
{
|
||||
/**
|
||||
* Counter that tracks the number of times a blueball event or a victory occurs.
|
||||
*/
|
||||
public static var PLAYING_COUNTER:UInt = 0;
|
||||
|
||||
/**
|
||||
* The maximum number of actions or events allowed before an advertisement is shown.
|
||||
*/
|
||||
public static final MAX_BEFORE_AD:UInt = 3;
|
||||
|
||||
#if NO_TESTING_ADS
|
||||
/**
|
||||
* AdMob publisher ID used for the application.
|
||||
*/
|
||||
static final ADMOB_PUBLISHER:String = EnvironmentConfigMacro.environmentConfig.get("MOBILE_GLOBAL_ADMOB_PUBLISHER");
|
||||
|
||||
/**
|
||||
* Test ad unit IDs for development and testing purposes.
|
||||
* These IDs are provided by Google AdMob for testing ads without incurring costs.
|
||||
* They should not be used in production applications.
|
||||
*/
|
||||
/**
|
||||
* Ad unit ID for displaying banner ads.
|
||||
*/
|
||||
static final BANNER_AD_UNIT_ID:String = #if mobile EnvironmentConfigMacro.environmentConfig.get(#if android "ANDROID_ADMOB_BANNER_ID" #else "IOS_ADMOB_BANNER_ID" #end) #else "" #end;
|
||||
|
||||
/**
|
||||
* Ad unit ID for displaying interstitial ads.
|
||||
*/
|
||||
static final INTERSTITIAL_AD_UNIT_ID:String = #if mobile EnvironmentConfigMacro.environmentConfig.get(#if android "ANDROID_ADMOB_INTERSTITIAL_ID" #else "IOS_ADMOB_INTERSTITIAL_ID" #end) #else "" #end;
|
||||
|
||||
/**
|
||||
* Ad unit ID for displaying rewarded ads.
|
||||
*/
|
||||
static final REWARDED_AD_UNIT_ID:String = "";
|
||||
#else
|
||||
|
||||
/**
|
||||
* AdMob publisher ID used for the application.
|
||||
* This ID is a test publisher ID provided by Google AdMob.
|
||||
* Replace with your actual publisher ID for production.
|
||||
*/
|
||||
static final ADMOB_PUBLISHER:String = "ca-app-pub-3940256099942544";
|
||||
|
||||
/**
|
||||
* Ad unit ID for displaying banner ads.
|
||||
* Test IDs are used for Android and iOS platforms, while non-supported platforms default to an empty string.
|
||||
* Replace with your actual banner ad unit ID for production.
|
||||
*
|
||||
* - Android: "9214589741" (test ad unit ID)
|
||||
* - iOS: "2435281174" (test ad unit ID)
|
||||
*/
|
||||
static final BANNER_AD_UNIT_ID:String = #if android "9214589741" #elseif ios "2435281174" #else "" #end;
|
||||
|
||||
/**
|
||||
* Ad unit ID for displaying interstitial ads.
|
||||
* Test IDs are used for Android and iOS platforms, while non-supported platforms default to an empty string.
|
||||
* Replace with your actual interstitial ad unit ID for production.
|
||||
*
|
||||
* - Android: "1033173712" (test ad unit ID)
|
||||
* - iOS: "4411468910" (test ad unit ID)
|
||||
*/
|
||||
static final INTERSTITIAL_AD_UNIT_ID:String = #if android "1033173712" #elseif ios "4411468910" #else "" #end;
|
||||
|
||||
/**
|
||||
* Ad unit ID for displaying rewarded ads.
|
||||
* Test IDs are used for Android and iOS platforms, while non-supported platforms default to an empty string.
|
||||
* Replace with your actual interstitial video ad unit ID for production.
|
||||
*
|
||||
* - Android: "8691691433" (test ad unit ID)
|
||||
* - iOS: "5135589807" (test ad unit ID)
|
||||
*/
|
||||
static final REWARDED_AD_UNIT_ID:String = #if android "8691691433" #elseif ios "5135589807" #else "" #end;
|
||||
#end
|
||||
|
||||
/**
|
||||
* Initializes the AdMob SDK and sets up event listeners for interstitial and rewarded ads.
|
||||
*
|
||||
* The listeners display ads automatically when they are loaded.
|
||||
*/
|
||||
public static function init():Void
|
||||
{
|
||||
Admob.onEvent.add(function(event:AdmobEvent):Void {
|
||||
#if ios
|
||||
if (event.name == AdmobEvent.AVM_WILL_PLAY_AUDIO)
|
||||
{
|
||||
if (FlxG.sound.music != null) FlxG.sound.music.pause();
|
||||
|
||||
for (sound in FlxG.sound.list)
|
||||
{
|
||||
if (sound != null) sound.pause();
|
||||
}
|
||||
|
||||
#if hxvlc
|
||||
@:privateAccess
|
||||
if (VideoCutscene.vid != null) VideoCutscene.vid.pause();
|
||||
#end
|
||||
}
|
||||
else if (event.name == AdmobEvent.AVM_DID_STOP_PLAYING_AUDIO)
|
||||
{
|
||||
if (FlxG.sound.music != null) FlxG.sound.music.resume();
|
||||
|
||||
for (sound in FlxG.sound.list)
|
||||
{
|
||||
if (sound != null) sound.resume();
|
||||
}
|
||||
|
||||
#if hxvlc
|
||||
@:privateAccess
|
||||
if (VideoCutscene.vid != null) VideoCutscene.vid.resume();
|
||||
#end
|
||||
}
|
||||
#end
|
||||
|
||||
trace(event.toString());
|
||||
});
|
||||
|
||||
Admob.configureConsentMetadata(Admob.getTCFConsentForPurpose(0) == 1, StringTools.startsWith(Admob.getUSPrivacy(), '1Y'));
|
||||
|
||||
Admob.init(#if TESTING_ADS true #else false #end);
|
||||
}
|
||||
|
||||
/**
|
||||
* Adds a banner ad at the specified size and alignment.
|
||||
* @param size The size of the banner ad, defaulting to the standard banner size.
|
||||
* @param align The alignment of the banner ad, defaulting to the bottom of the screen.
|
||||
*/
|
||||
public static inline function addBanner(size:Int = AdmobBannerSize.BANNER, align:Int = AdmobBannerAlign.BOTTOM_CENTER):Void
|
||||
{
|
||||
#if FEATURE_MOBILE_IAP
|
||||
if (InAppPurchasesUtil.isPurchased(InAppPurchasesUtil.UPGRADE_PRODUCT_ID)) return;
|
||||
#end
|
||||
Admob.showBanner([AdMobUtil.ADMOB_PUBLISHER, AdMobUtil.BANNER_AD_UNIT_ID].join('/'), size, align);
|
||||
}
|
||||
|
||||
/**
|
||||
* Removes the currently displayed banner ad, if any.
|
||||
*/
|
||||
public static inline function removeBanner():Void
|
||||
{
|
||||
#if FEATURE_MOBILE_IAP
|
||||
if (InAppPurchasesUtil.isPurchased(InAppPurchasesUtil.UPGRADE_PRODUCT_ID)) return;
|
||||
#end
|
||||
Admob.hideBanner();
|
||||
}
|
||||
|
||||
/**
|
||||
* Loads an interstitial ad using AdMob.
|
||||
*
|
||||
* @param onInterstitialFinish Callback function to be called when the rewarded ad has been completed by the user.
|
||||
*/
|
||||
public static function loadInterstitial(onInterstitialFinish:Void->Void):Void
|
||||
{
|
||||
#if FEATURE_MOBILE_IAP
|
||||
if (InAppPurchasesUtil.isPurchased(InAppPurchasesUtil.UPGRADE_PRODUCT_ID))
|
||||
{
|
||||
if (onInterstitialFinish != null) onInterstitialFinish();
|
||||
|
||||
return;
|
||||
}
|
||||
#end
|
||||
|
||||
function interstitialEvent(event:AdmobEvent):Void
|
||||
{
|
||||
if (event.name == AdmobEvent.INTERSTITIAL_LOADED)
|
||||
{
|
||||
Admob.showInterstitial();
|
||||
}
|
||||
else if (event.name == AdmobEvent.INTERSTITIAL_DISMISSED
|
||||
|| event.name == AdmobEvent.INTERSTITIAL_FAILED_TO_LOAD
|
||||
|| event.name == AdmobEvent.INTERSTITIAL_FAILED_TO_SHOW)
|
||||
{
|
||||
if (onInterstitialFinish != null) onInterstitialFinish();
|
||||
|
||||
Admob.onEvent.remove(interstitialEvent);
|
||||
}
|
||||
}
|
||||
|
||||
Admob.onEvent.add(interstitialEvent);
|
||||
|
||||
Admob.loadInterstitial([AdMobUtil.ADMOB_PUBLISHER, AdMobUtil.INTERSTITIAL_AD_UNIT_ID].join('/'));
|
||||
}
|
||||
|
||||
/**
|
||||
* Loads a rewarded ad using Admob.
|
||||
*
|
||||
* @param onRewardedFinish Callback function to be called when the rewarded ad has been completed by the user.
|
||||
*/
|
||||
public static function loadRewarded(onRewardedFinish:Void->Void):Void
|
||||
{
|
||||
#if FEATURE_MOBILE_IAP
|
||||
if (InAppPurchasesUtil.isPurchased(InAppPurchasesUtil.UPGRADE_PRODUCT_ID))
|
||||
{
|
||||
if (onRewardedFinish != null) onRewardedFinish();
|
||||
|
||||
return;
|
||||
}
|
||||
#end
|
||||
|
||||
function rewardedEvent(event:AdmobEvent):Void
|
||||
{
|
||||
if (event.name == AdmobEvent.REWARDED_LOADED)
|
||||
{
|
||||
Admob.showRewarded();
|
||||
}
|
||||
else if (event.name == AdmobEvent.REWARDED_DISMISSED
|
||||
|| event.name == AdmobEvent.REWARDED_FAILED_TO_LOAD
|
||||
|| event.name == AdmobEvent.REWARDED_FAILED_TO_SHOW)
|
||||
{
|
||||
if (onRewardedFinish != null) onRewardedFinish();
|
||||
|
||||
Admob.onEvent.remove(rewardedEvent);
|
||||
}
|
||||
}
|
||||
|
||||
Admob.onEvent.add(rewardedEvent);
|
||||
|
||||
Admob.loadRewarded([AdMobUtil.ADMOB_PUBLISHER, AdMobUtil.REWARDED_AD_UNIT_ID].join('/'));
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the volume level for ads with sound, allowing control over ad audio playback.
|
||||
* @param volume A Float representing the desired volume (0 = mute, 1 = full volume).
|
||||
*/
|
||||
public static inline function setVolume(volume:Float):Void
|
||||
{
|
||||
Admob.setVolume(volume);
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks whether consent for a specific advertising purpose has been granted.
|
||||
* @param purpose The purpose for which consent is required.
|
||||
* @return An Int indicating consent status (-1 for no consent, 1 for granted).
|
||||
*/
|
||||
public static inline function getTCFConsentForPurpose(purpose:Int):Int
|
||||
{
|
||||
return Admob.getTCFConsentForPurpose(purpose);
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if the user has given consent for all ad purposes.
|
||||
* This is typically required for GDPR compliance, where each purpose (0-9) needs to be individually consented.
|
||||
* @return Bool indicating whether the user has consented to all purposes.
|
||||
*/
|
||||
public static function hasFullTCFConsent():Bool
|
||||
{
|
||||
for (purpose in 0...Admob.getTCFPurposeConsent().length)
|
||||
{
|
||||
if (Admob.getTCFConsentForPurpose(purpose) != 1) return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieves the current user's consent status as a string.
|
||||
* Useful for GDPR compliance to understand if ads can be personalized.
|
||||
* @return A String with the consent status.
|
||||
*/
|
||||
public static inline function getTCFPurposeConsent():String
|
||||
{
|
||||
return Admob.getTCFPurposeConsent();
|
||||
}
|
||||
|
||||
/**
|
||||
* Determines if showing a privacy options form is required based on regional laws.
|
||||
* @return A Bool indicating if a privacy options form is required (true if required, false if not required).
|
||||
*/
|
||||
public static inline function isPrivacyOptionsRequired():Bool
|
||||
{
|
||||
return Admob.isPrivacyOptionsRequired();
|
||||
}
|
||||
|
||||
/**
|
||||
* Displays the privacy options form to the user, allowing them to adjust consent.
|
||||
* Useful for GDPR and other privacy regulations compliance.
|
||||
*/
|
||||
public static inline function showPrivacyOptionsForm():Void
|
||||
{
|
||||
Admob.showPrivacyOptionsForm();
|
||||
}
|
||||
|
||||
/**
|
||||
* Opens the Ad Inspector interface.
|
||||
* This method works for test devices registered programmatically or in the AdMob UI.
|
||||
*/
|
||||
public static inline function openAdInspector():Void
|
||||
{
|
||||
Admob.openAdInspector();
|
||||
}
|
||||
}
|
||||
#end
|
345
source/funkin/mobile/util/InAppPurchasesUtil.hx
Normal file
345
source/funkin/mobile/util/InAppPurchasesUtil.hx
Normal file
|
@ -0,0 +1,345 @@
|
|||
package funkin.mobile.util;
|
||||
|
||||
#if FEATURE_MOBILE_IAP
|
||||
#if android
|
||||
import extension.androidtools.widget.Toast;
|
||||
import extension.iapcore.android.IAPAndroid;
|
||||
import extension.iapcore.android.IAPProductDetails;
|
||||
import extension.iapcore.android.IAPPurchase;
|
||||
import extension.iapcore.android.IAPPurchaseState;
|
||||
import extension.iapcore.android.IAPResponseCode;
|
||||
import extension.iapcore.android.IAPResult;
|
||||
#elseif ios
|
||||
import extension.iapcore.ios.IAPError;
|
||||
import extension.iapcore.ios.IAPIOS;
|
||||
import extension.iapcore.ios.IAPProductDetails;
|
||||
import extension.iapcore.ios.IAPPurchase;
|
||||
import extension.iapcore.ios.IAPPurchaseState;
|
||||
#end
|
||||
|
||||
/**
|
||||
* Provides utility functions for working with in-app purchases.
|
||||
*/
|
||||
@:nullSafety
|
||||
class InAppPurchasesUtil
|
||||
{
|
||||
/**
|
||||
* The product ID used for the "No Ads" in-app purchase upgrade.
|
||||
*/
|
||||
public static final UPGRADE_PRODUCT_ID:String = 'no_ads';
|
||||
|
||||
public static var hasInitialized:Bool = false;
|
||||
|
||||
/**
|
||||
* A static variable that holds an array of currently loaded product details for in-app purchases.
|
||||
*/
|
||||
static var currentProductDetails:Array<IAPProductDetails> = [];
|
||||
|
||||
/**
|
||||
* A static variable that holds an array of currently purchased for in-app purchases.
|
||||
*/
|
||||
static var currentPurchased:Array<IAPPurchase> = [];
|
||||
|
||||
/**
|
||||
* Initializes the in-app purchases utility.
|
||||
*/
|
||||
public static function init():Void
|
||||
{
|
||||
#if android
|
||||
IAPAndroid.onLog.add(function(message:String):Void {
|
||||
trace(message);
|
||||
});
|
||||
|
||||
IAPAndroid.onBillingSetupFinished.add(function(result:IAPResult):Void {
|
||||
if (result.getResponseCode() != IAPResponseCode.OK)
|
||||
{
|
||||
trace('Billing setup failed "$result"!');
|
||||
return;
|
||||
}
|
||||
|
||||
IAPAndroid.queryPurchases();
|
||||
|
||||
IAPAndroid.queryProductDetails([UPGRADE_PRODUCT_ID]);
|
||||
});
|
||||
|
||||
IAPAndroid.onBillingServiceDisconnected.add(function():Void {
|
||||
trace("Billing service disconnected!");
|
||||
});
|
||||
|
||||
IAPAndroid.onProductDetailsResponse.add(function(result:IAPResult, productDetails:Array<IAPProductDetails>):Void {
|
||||
if (result.getResponseCode() == IAPResponseCode.OK)
|
||||
{
|
||||
hasInitialized = true;
|
||||
currentProductDetails = productDetails;
|
||||
}
|
||||
else
|
||||
{
|
||||
hasInitialized = false;
|
||||
trace('Failed to fetch product details: "$result"');
|
||||
}
|
||||
});
|
||||
|
||||
IAPAndroid.onQueryPurchasesResponse.add(function(result:IAPResult, purchases:Array<IAPPurchase>):Void {
|
||||
if (result.getResponseCode() == IAPResponseCode.OK) handlePurchases(purchases);
|
||||
else
|
||||
{
|
||||
trace('Failed to query purchases: "$result"');
|
||||
}
|
||||
});
|
||||
|
||||
IAPAndroid.onPurchasesUpdated.add(function(result:IAPResult, purchases:Array<IAPPurchase>):Void {
|
||||
if (result.getResponseCode() == IAPResponseCode.OK) handlePurchases(purchases);
|
||||
else
|
||||
{
|
||||
trace('Failed to update purchases: "$result"');
|
||||
}
|
||||
});
|
||||
|
||||
IAPAndroid.onAcknowledgePurchaseResponse.add(function(result:IAPResult):Void {
|
||||
if (result.getResponseCode() == IAPResponseCode.OK) trace('Purchase acknowledged successfully!');
|
||||
else
|
||||
{
|
||||
trace('Failed to acknowledge purchase: $result');
|
||||
}
|
||||
});
|
||||
|
||||
IAPAndroid.init();
|
||||
|
||||
IAPAndroid.startConnection();
|
||||
#else
|
||||
IAPIOS.onProductDetailsReceived.add(function(productDetails:Array<IAPProductDetails>):Void {
|
||||
if (productDetails != null)
|
||||
{
|
||||
currentProductDetails = productDetails;
|
||||
}
|
||||
});
|
||||
|
||||
IAPIOS.onProductDetailsFailed.add(function(error:IAPError):Void {
|
||||
hasInitialized = false;
|
||||
});
|
||||
|
||||
IAPIOS.onPurchasesUpdated.add(function(purchases:Array<IAPPurchase>):Void {
|
||||
handlePurchases(purchases);
|
||||
trace("iOS purchases updated: " + purchases.length);
|
||||
hasInitialized = true;
|
||||
trace("hasInitialized: " + hasInitialized);
|
||||
});
|
||||
|
||||
IAPIOS.init();
|
||||
|
||||
IAPIOS.restorePurchases();
|
||||
|
||||
IAPIOS.requestProducts([UPGRADE_PRODUCT_ID]);
|
||||
#end
|
||||
}
|
||||
|
||||
/**
|
||||
* Restores previously made in-app purchases for the current user.
|
||||
*/
|
||||
public static function restorePurchases():Void
|
||||
{
|
||||
#if android
|
||||
IAPAndroid.queryPurchases();
|
||||
#else
|
||||
IAPIOS.restorePurchases();
|
||||
#end
|
||||
}
|
||||
|
||||
/**
|
||||
* Initiates the purchase process for the specified item.
|
||||
*
|
||||
* @param id The identifier of the item to be purchased.
|
||||
* @param onPurchased The function to be called when the the product is purchased.
|
||||
*/
|
||||
public static function purchase(id:String, onPurchased:Void->Void):Void
|
||||
{
|
||||
for (product in currentProductDetails)
|
||||
{
|
||||
#if android
|
||||
if (product.getProductId() == id)
|
||||
{
|
||||
function purchasesUpdatedEvent(result:IAPResult, purchases:Array<IAPPurchase>):Void
|
||||
{
|
||||
if (result.getResponseCode() == IAPResponseCode.OK)
|
||||
{
|
||||
for (purchase in purchases)
|
||||
{
|
||||
if (purchase.getProducts().contains(id))
|
||||
{
|
||||
if (purchase.getPurchaseState() == IAPPurchaseState.PURCHASED)
|
||||
{
|
||||
if (onPurchased != null) onPurchased();
|
||||
|
||||
IAPAndroid.onPurchasesUpdated.remove(purchasesUpdatedEvent);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
IAPAndroid.onPurchasesUpdated.remove(purchasesUpdatedEvent);
|
||||
}
|
||||
|
||||
final debugMessage:Null<String> = result.getDebugMessage();
|
||||
|
||||
if (debugMessage != null && debugMessage.length > 0)
|
||||
{
|
||||
Toast.makeText(debugMessage, Toast.LENGTH_SHORT);
|
||||
}
|
||||
}
|
||||
|
||||
if (!IAPAndroid.onPurchasesUpdated.has(purchasesUpdatedEvent))
|
||||
{
|
||||
IAPAndroid.onPurchasesUpdated.add(purchasesUpdatedEvent);
|
||||
}
|
||||
|
||||
IAPAndroid.launchPurchaseFlow(product);
|
||||
return;
|
||||
}
|
||||
#elseif ios
|
||||
if (product.getProductIdentifier() == id)
|
||||
{
|
||||
function purchasesUpdatedEvent(purchases:Array<IAPPurchase>):Void
|
||||
{
|
||||
for (purchase in purchases)
|
||||
{
|
||||
if (purchase.getPaymentProductIdentifier() == id)
|
||||
{
|
||||
switch (purchase.getTransactionState())
|
||||
{
|
||||
case IAPPurchaseState.PURCHASED:
|
||||
if (onPurchased != null) onPurchased();
|
||||
|
||||
IAPIOS.onPurchasesUpdated.remove(purchasesUpdatedEvent);
|
||||
case IAPPurchaseState.FAILED:
|
||||
IAPIOS.onPurchasesUpdated.remove(purchasesUpdatedEvent);
|
||||
default:
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (!IAPIOS.onPurchasesUpdated.has(purchasesUpdatedEvent))
|
||||
{
|
||||
IAPIOS.onPurchasesUpdated.add(purchasesUpdatedEvent);
|
||||
}
|
||||
|
||||
IAPIOS.purchaseProduct(product);
|
||||
return;
|
||||
}
|
||||
#end
|
||||
}
|
||||
|
||||
trace("Didn't find product details for ID: " + id);
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if the specified product ID is already purchased.
|
||||
*
|
||||
* @param id The product ID to check.
|
||||
*
|
||||
* @return `true` if the product is already purchased and acknowledged, false otherwise.
|
||||
*/
|
||||
public static function isPurchased(id:String):Bool
|
||||
{
|
||||
for (purchase in currentPurchased)
|
||||
{
|
||||
#if android
|
||||
if (purchase.getProducts().contains(id))
|
||||
{
|
||||
return true;
|
||||
}
|
||||
#elseif ios
|
||||
if (purchase.getPaymentProductIdentifier() == id)
|
||||
{
|
||||
return true;
|
||||
}
|
||||
#end
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
@:noCompletion
|
||||
private static function handlePurchases(purchases:Array<IAPPurchase>):Void
|
||||
{
|
||||
for (purchase in purchases)
|
||||
{
|
||||
#if android
|
||||
if (purchase.getPurchaseState() == IAPPurchaseState.PURCHASED)
|
||||
{
|
||||
if (!purchase.isAcknowledged())
|
||||
{
|
||||
IAPAndroid.acknowledgePurchase(purchase.getPurchaseToken());
|
||||
}
|
||||
|
||||
var alreadyTracked:Bool = false;
|
||||
|
||||
for (existing in currentPurchased)
|
||||
{
|
||||
if (existing.getPurchaseToken() == purchase.getPurchaseToken())
|
||||
{
|
||||
alreadyTracked = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (!alreadyTracked)
|
||||
{
|
||||
currentPurchased.push(purchase);
|
||||
trace('Android purchase tracked: ${purchase.getPurchaseToken()}');
|
||||
}
|
||||
else
|
||||
{
|
||||
trace('Android purchase already tracked: ${purchase.getPurchaseToken()}');
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
trace('Android purchase not completed: ${purchase.getPurchaseState()}');
|
||||
}
|
||||
#elseif ios
|
||||
trace('Transaction ID: ${purchase.getTransactionIdentifier()}');
|
||||
trace('Transaction Date: ${purchase.getTransactionDate()}');
|
||||
trace('Transaction Payment Product ID: ${purchase.getPaymentProductIdentifier()}');
|
||||
|
||||
var alreadyTracked:Bool = false;
|
||||
|
||||
for (existing in currentPurchased)
|
||||
{
|
||||
if (existing.getTransactionIdentifier() == purchase.getTransactionIdentifier())
|
||||
{
|
||||
alreadyTracked = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
switch (purchase.getTransactionState())
|
||||
{
|
||||
case IAPPurchaseState.PURCHASING:
|
||||
trace('iOS purchase is in progress.');
|
||||
case IAPPurchaseState.DEFERRED:
|
||||
trace('iOS purchase is deferred.');
|
||||
case IAPPurchaseState.FAILED:
|
||||
trace('iOS purchase failed: ${purchase.getTransactionError()}.');
|
||||
case IAPPurchaseState.PURCHASED | IAPPurchaseState.RESTORED:
|
||||
trace('iOS purchase successful or restored.');
|
||||
|
||||
if (!alreadyTracked)
|
||||
{
|
||||
currentPurchased.push(purchase);
|
||||
|
||||
trace('iOS purchase tracked: ${purchase.getTransactionIdentifier()}');
|
||||
|
||||
IAPIOS.finishPurchase(purchase);
|
||||
}
|
||||
else
|
||||
{
|
||||
trace('iOS purchase already tracked: ${purchase.getTransactionIdentifier()}');
|
||||
}
|
||||
}
|
||||
#end
|
||||
}
|
||||
}
|
||||
}
|
||||
#end
|
72
source/funkin/mobile/util/InAppReviewUtil.hx
Normal file
72
source/funkin/mobile/util/InAppReviewUtil.hx
Normal file
|
@ -0,0 +1,72 @@
|
|||
package funkin.mobile.util;
|
||||
|
||||
#if FEATURE_MOBILE_IAR
|
||||
#if android
|
||||
import extension.iarcore.android.IARAndroid as IAR;
|
||||
#elseif ios
|
||||
import extension.iarcore.ios.IARIOS as IAR;
|
||||
#end
|
||||
#end
|
||||
|
||||
/**
|
||||
* Provides utility functions for working with in-app reviews.
|
||||
* @see https://developer.android.com/guide/playcore/in-app-review
|
||||
*/
|
||||
@:nullSafety
|
||||
class InAppReviewUtil
|
||||
{
|
||||
/**
|
||||
* Chance for exiting the Results screen to display a prompt to review the game, as a percent.
|
||||
*/
|
||||
public static var ODDS:UInt = 5;
|
||||
|
||||
/**
|
||||
* Initializes callbacks tied to the In-App Review functionality.
|
||||
*/
|
||||
public static function init():Void
|
||||
{
|
||||
#if FEATURE_MOBILE_IAR
|
||||
#if android
|
||||
trace('[IAR] Initializing callbacks...');
|
||||
|
||||
IAR.onLog.add(function(message:String):Void {
|
||||
trace('[IAR] Error occurred: "$message"');
|
||||
});
|
||||
IAR.onReviewCompleted.add(function(success:Bool):Void {
|
||||
trace('[IAR] Review completed: "${success ? 'Success' : 'Failure'}"');
|
||||
});
|
||||
IAR.onReviewError.add(function(message:String):Void {
|
||||
trace('[IAR] Review failed: "$message"');
|
||||
});
|
||||
#end
|
||||
#else
|
||||
trace('[IAR] IAR is disabled...');
|
||||
#end
|
||||
}
|
||||
|
||||
/**
|
||||
* When called, displays a card which prompts the user to provide a review of the game,
|
||||
* which will be posted to the respective app store.
|
||||
*
|
||||
* Google Play will throttle this for us t
|
||||
*/
|
||||
public static function requestReview():Void
|
||||
{
|
||||
#if FEATURE_MOBILE_IAR
|
||||
trace('[IAR] Sending in-app review request...');
|
||||
#if android
|
||||
IAR.init();
|
||||
|
||||
#if FEATURE_DEBUG_FUNCTIONS
|
||||
IAR.requestAndLaunchFakeReviewFlow();
|
||||
#else
|
||||
IAR.requestAndLaunchReviewFlow();
|
||||
#end
|
||||
#else
|
||||
IAR.requestReview();
|
||||
#end
|
||||
#else
|
||||
trace('[IAR] IAR is disabled...');
|
||||
#end
|
||||
}
|
||||
}
|
101
source/funkin/mobile/util/ScreenUtil.hx
Normal file
101
source/funkin/mobile/util/ScreenUtil.hx
Normal file
|
@ -0,0 +1,101 @@
|
|||
package funkin.mobile.util;
|
||||
|
||||
#if ios
|
||||
import funkin.external.ios.ScreenUtil as NativeScreenUtil;
|
||||
#elseif android
|
||||
import funkin.external.android.ScreenUtil as NativeScreenUtil;
|
||||
#end
|
||||
import lime.math.Rectangle;
|
||||
import lime.system.System;
|
||||
|
||||
/**
|
||||
* A Utility class to get mobile screen related informations.
|
||||
*/
|
||||
class ScreenUtil
|
||||
{
|
||||
/**
|
||||
* Get `Rectangle` Object that contains the dimensions of the screen's Notch.
|
||||
* Scales the dimensions to return coords in pixels, not points
|
||||
* @return Rectangle
|
||||
*/
|
||||
public static function getNotchRect():Rectangle
|
||||
{
|
||||
final notchRect:Rectangle = new Rectangle();
|
||||
|
||||
#if android
|
||||
final rectDimensions:Array<Array<Float>> = [[], [], [], []];
|
||||
|
||||
// Push all the dimensions of the cutouts into an array
|
||||
for (rect in NativeScreenUtil.getCutoutDimensions())
|
||||
{
|
||||
rectDimensions[0].push(rect.x);
|
||||
rectDimensions[1].push(rect.y);
|
||||
rectDimensions[2].push(rect.width);
|
||||
rectDimensions[3].push(rect.height);
|
||||
}
|
||||
|
||||
// Put all the dimensions into the rectangle
|
||||
for (i => dimensions in rectDimensions)
|
||||
{
|
||||
for (dimension in dimensions)
|
||||
{
|
||||
switch (i)
|
||||
{
|
||||
case 0:
|
||||
notchRect.x += dimension;
|
||||
case 1:
|
||||
notchRect.y += dimension;
|
||||
case 2:
|
||||
notchRect.width += dimension;
|
||||
case 3:
|
||||
notchRect.height += dimension;
|
||||
}
|
||||
}
|
||||
}
|
||||
#elseif ios
|
||||
var topInset:Float = -1;
|
||||
var leftInset:Float = -1;
|
||||
var rightInset:Float = -1;
|
||||
var bottomInset:Float = -1;
|
||||
var deviceWidth:Float = -1;
|
||||
var deviceHeight:Float = -1;
|
||||
var displayOrientation:DisplayOrientation = System.getDisplayOrientation(0);
|
||||
|
||||
NativeScreenUtil.getSafeAreaInsets(cpp.RawPointer.addressOf(topInset), cpp.RawPointer.addressOf(bottomInset), cpp.RawPointer.addressOf(leftInset),
|
||||
cpp.RawPointer.addressOf(rightInset));
|
||||
|
||||
NativeScreenUtil.getScreenSize(cpp.RawPointer.addressOf(deviceWidth), cpp.RawPointer.addressOf(deviceHeight));
|
||||
|
||||
notchRect.x = 0;
|
||||
notchRect.y = 0.0;
|
||||
|
||||
// Calculate the rectangle dimensions for the notch
|
||||
// Note: iOS only spits out *insets* for "safe areas", so we can only get a broad position for the notch
|
||||
// left + right insets are the same, so we can use either
|
||||
// Note: *inset* is the distance from the edge of the screen where a safe area gets defined
|
||||
// see: https://developer.apple.com/documentation/uikit/uiview/safeareainsets
|
||||
switch (displayOrientation)
|
||||
{
|
||||
case DISPLAY_ORIENTATION_LANDSCAPE: // landscape
|
||||
notchRect.width = leftInset + rightInset;
|
||||
notchRect.height = bottomInset - topInset;
|
||||
notchRect.y = topInset;
|
||||
case DISPLAY_ORIENTATION_LANDSCAPE_FLIPPED: // landscape
|
||||
notchRect.width = leftInset + rightInset;
|
||||
notchRect.height = bottomInset - topInset;
|
||||
notchRect.y = topInset;
|
||||
notchRect.x = deviceWidth - notchRect.width; // move notchRect if we are flipped, notch is at the right of screen
|
||||
case DISPLAY_ORIENTATION_PORTRAIT: // portrait
|
||||
notchRect.width = deviceWidth;
|
||||
notchRect.height = topInset;
|
||||
case DISPLAY_ORIENTATION_PORTRAIT_FLIPPED: // portrait
|
||||
notchRect.width = deviceWidth;
|
||||
notchRect.height = bottomInset;
|
||||
notchRect.y = deviceHeight - notchRect.height; // move notchRect if we are flipped, the notch is at the bottom of screen
|
||||
default: // display orientation unknown? perhaps this occurs on desktop
|
||||
}
|
||||
#end
|
||||
|
||||
return notchRect;
|
||||
}
|
||||
}
|
54
source/funkin/modding/ModStore.hx
Normal file
54
source/funkin/modding/ModStore.hx
Normal file
|
@ -0,0 +1,54 @@
|
|||
package funkin.modding;
|
||||
|
||||
import haxe.ds.StringMap;
|
||||
|
||||
/**
|
||||
* Temporary persistent data storage for mods to use.
|
||||
*/
|
||||
@:nullSafety
|
||||
class ModStore
|
||||
{
|
||||
/**
|
||||
* All registered stores for this session.
|
||||
*/
|
||||
public static final stores:StringMap<Dynamic> = new StringMap<Dynamic>();
|
||||
|
||||
/**
|
||||
* Attempts to register a new store with the given ID and return it.
|
||||
* If a store with the same ID already exists, that store will be returned instead (discards `data`).
|
||||
*
|
||||
* @id The unique ID for this store.
|
||||
* @data Optional initial data, uses an empty object by default.
|
||||
* @return The store data at the given ID.
|
||||
*/
|
||||
public static function register(id:String, ?data:Dynamic):Dynamic
|
||||
{
|
||||
if (stores.exists(id)) return stores.get(id);
|
||||
stores.set(id, data ??= {});
|
||||
return data;
|
||||
}
|
||||
|
||||
/**
|
||||
* Helper function to get a store by ID.
|
||||
*
|
||||
* @id The target ID of the store.
|
||||
* @return The store data, or `null` if the store did not exist.
|
||||
*/
|
||||
public static function get(id:String):Null<Dynamic>
|
||||
{
|
||||
return stores.get(id);
|
||||
}
|
||||
|
||||
/**
|
||||
* Helper function to remove a store by ID and return it.
|
||||
*
|
||||
* @id The target ID of the store.
|
||||
* @return The store data, or `null` if the store did not exist.
|
||||
*/
|
||||
public static function remove(id:String):Null<Dynamic>
|
||||
{
|
||||
var data:Null<Dynamic> = stores.get(id);
|
||||
stores.remove(id);
|
||||
return data;
|
||||
}
|
||||
}
|
|
@ -2,6 +2,7 @@ package funkin.modding;
|
|||
|
||||
import polymod.Polymod;
|
||||
|
||||
@:nullSafety
|
||||
class PolymodErrorHandler
|
||||
{
|
||||
/**
|
||||
|
@ -47,7 +48,7 @@ class PolymodErrorHandler
|
|||
logError(error.message);
|
||||
|
||||
// Last word is the class name.
|
||||
var className:String = error.message.split(' ').pop();
|
||||
var className:Null<String> = error.message.split(' ').pop();
|
||||
var msg:String = 'Import error in ${error.origin}';
|
||||
msg += '\nCould not import unknown class ${className}';
|
||||
msg += '\nCheck to ensure the class exists and is spelled correctly.';
|
||||
|
|
|
@ -26,13 +26,19 @@ import polymod.Polymod;
|
|||
/**
|
||||
* A class for interacting with Polymod, the atomic modding framework for Haxe.
|
||||
*/
|
||||
@:nullSafety
|
||||
class PolymodHandler
|
||||
{
|
||||
/**
|
||||
* The API version for the current version of the game. Since 0.5.0, we've just made this the game version!
|
||||
* Minor updates rarely impact mods but major versions often do.
|
||||
* Minor updates rarely impact mods but major versions sometimes do.
|
||||
*/
|
||||
// static final API_VERSION:String = Constants.VERSION;
|
||||
public static var API_VERSION(get, never):String;
|
||||
|
||||
static function get_API_VERSION():String
|
||||
{
|
||||
return Constants.VERSION;
|
||||
}
|
||||
|
||||
/**
|
||||
* The Semantic Versioning rule
|
||||
|
@ -40,7 +46,7 @@ class PolymodHandler
|
|||
* Using more complex rules allows mods from older compatible versions to stay functioning,
|
||||
* while preventing mods made for future versions from being installed.
|
||||
*/
|
||||
static final API_VERSION_RULE:String = ">=0.6.3 <0.7.0";
|
||||
public static final API_VERSION_RULE:String = ">=0.6.3 <0.8.0";
|
||||
|
||||
/**
|
||||
* Where relative to the executable that mods are located.
|
||||
|
@ -65,7 +71,7 @@ class PolymodHandler
|
|||
|
||||
public static var loadedModIds:Array<String> = [];
|
||||
|
||||
// Use SysZipFileSystem on desktop and MemoryZipFilesystem on web.
|
||||
// Use SysZipFileSystem on native and MemoryZipFilesystem on web.
|
||||
static var modFileSystem:Null<ZipFileSystem> = null;
|
||||
|
||||
/**
|
||||
|
@ -266,18 +272,19 @@ class PolymodHandler
|
|||
|
||||
#if FEATURE_NEWGROUNDS
|
||||
// `funkin.api.newgrounds.Leaderboards` allows for submitting cheated scores.
|
||||
// We still grant read-only access.
|
||||
Polymod.addImportAlias('funkin.api.newgrounds.Leaderboards', funkin.api.newgrounds.Leaderboards.LeaderboardsSandboxed);
|
||||
|
||||
// `funkin.api.newgrounds.Medals` allows for unfair granting of medals.
|
||||
// We still grant read-only access.
|
||||
Polymod.addImportAlias('funkin.api.newgrounds.Medals', funkin.api.newgrounds.Medals.MedalsSandboxed);
|
||||
|
||||
// `funkin.api.newgrounds.NewgroundsClientSandboxed` allows for submitting cheated data.
|
||||
// We still grant read-only access.
|
||||
Polymod.addImportAlias('funkin.api.newgrounds.NewgroundsClient', funkin.api.newgrounds.NewgroundsClient.NewgroundsClientSandboxed);
|
||||
#end
|
||||
|
||||
#if FEATURE_DISCORD_RPC
|
||||
Polymod.addImportAlias('funkin.api.discord.DiscordClient', funkin.api.discord.DiscordClient.DiscordClientSandboxed);
|
||||
#end
|
||||
|
||||
// Add blacklisting for prohibited classes and packages.
|
||||
|
||||
|
@ -301,6 +308,48 @@ class PolymodHandler
|
|||
// Unserializer.DEFAULT_RESOLVER.resolveClass() can access blacklisted packages
|
||||
Polymod.blacklistImport('haxe.Unserializer');
|
||||
|
||||
// `flixel.util.FlxSave`
|
||||
// FlxSave.resolveFlixelClasses() can access blacklisted packages
|
||||
Polymod.blacklistImport('flixel.util.FlxSave');
|
||||
|
||||
// Disable access to AdMob Util
|
||||
Polymod.blacklistImport('funkin.mobile.util.AdMobUtil');
|
||||
|
||||
// Disable access to In-App Purchases Util
|
||||
Polymod.blacklistImport('funkin.mobile.util.InAppPurchasesUtil');
|
||||
|
||||
// Disable access to Admob Extension
|
||||
for (cls in ClassMacro.listClassesInPackage('extension.admob'))
|
||||
{
|
||||
if (cls == null) continue;
|
||||
var className:String = Type.getClassName(cls);
|
||||
Polymod.blacklistImport(className);
|
||||
}
|
||||
|
||||
// Disable access to AndroidTools Extension
|
||||
for (cls in ClassMacro.listClassesInPackage('extension.androidtools'))
|
||||
{
|
||||
if (cls == null) continue;
|
||||
var className:String = Type.getClassName(cls);
|
||||
Polymod.blacklistImport(className);
|
||||
}
|
||||
|
||||
// Disable access to IAPCore Extension
|
||||
for (cls in ClassMacro.listClassesInPackage('extension.iapcore'))
|
||||
{
|
||||
if (cls == null) continue;
|
||||
var className:String = Type.getClassName(cls);
|
||||
Polymod.blacklistImport(className);
|
||||
}
|
||||
|
||||
// Disable access to Haptics Extension
|
||||
for (cls in ClassMacro.listClassesInPackage('extension.haptics'))
|
||||
{
|
||||
if (cls == null) continue;
|
||||
var className:String = Type.getClassName(cls);
|
||||
Polymod.blacklistImport(className);
|
||||
}
|
||||
|
||||
// `lime.system.CFFI`
|
||||
// Can load and execute compiled binaries.
|
||||
Polymod.blacklistImport('lime.system.CFFI');
|
||||
|
@ -325,6 +374,9 @@ class PolymodHandler
|
|||
// Can load native processes on the host operating system.
|
||||
Polymod.blacklistImport('openfl.desktop.NativeProcess');
|
||||
|
||||
// Contains critical private environment variables.
|
||||
Polymod.blacklistImport('funkin.util.macro.EnvironmentConfigMacro');
|
||||
|
||||
// `funkin.api.*`
|
||||
// Contains functions which may allow for cheating and such.
|
||||
for (cls in ClassMacro.listClassesInPackage('funkin.api'))
|
||||
|
@ -344,6 +396,24 @@ class PolymodHandler
|
|||
Polymod.blacklistImport(className);
|
||||
}
|
||||
|
||||
// `hscript.*
|
||||
// Contains functions which may allow for interpreting unsanitized strings.
|
||||
for (cls in ClassMacro.listClassesInPackage('hscript'))
|
||||
{
|
||||
if (cls == null) continue;
|
||||
var className:String = Type.getClassName(cls);
|
||||
Polymod.blacklistImport(className);
|
||||
}
|
||||
|
||||
// `funkin.api.newgrounds.*`
|
||||
// Contains functions which allow for cheating medals and leaderboards.
|
||||
for (cls in ClassMacro.listClassesInPackage('funkin.api.newgrounds'))
|
||||
{
|
||||
if (cls == null) continue;
|
||||
var className:String = Type.getClassName(cls);
|
||||
Polymod.blacklistImport(className);
|
||||
}
|
||||
|
||||
// `io.newgrounds.*`
|
||||
// Contains functions which allow for cheating medals and leaderboards.
|
||||
for (cls in ClassMacro.listClassesInPackage('io.newgrounds'))
|
||||
|
|
|
@ -3,11 +3,13 @@ package funkin.modding.base;
|
|||
/**
|
||||
* An empty base class meant to be extended by scripts.
|
||||
*/
|
||||
|
||||
class Object {
|
||||
@:nullSafety
|
||||
class Object
|
||||
{
|
||||
public function new() {}
|
||||
|
||||
public function toString():String {
|
||||
public function toString():String
|
||||
{
|
||||
return "(Object)";
|
||||
}
|
||||
}
|
||||
|
|
|
@ -15,6 +15,7 @@ import openfl.events.KeyboardEvent;
|
|||
* This is a base class for all events that are issued to scripted classes.
|
||||
* It can be used to identify the type of event called, store data, and cancel event propagation.
|
||||
*/
|
||||
@:nullSafety
|
||||
class ScriptEvent
|
||||
{
|
||||
/**
|
||||
|
@ -255,7 +256,8 @@ class HoldNoteScriptEvent extends NoteScriptEvent
|
|||
*/
|
||||
public var doesNotesplash:Bool = false;
|
||||
|
||||
public function new(type:ScriptEventType, holdNote:SustainTrail, healthChange:Float, score:Int, isComboBreak:Bool, comboCount:Int = 0, cancelable:Bool = false):Void
|
||||
public function new(type:ScriptEventType, holdNote:SustainTrail, healthChange:Float, score:Int, isComboBreak:Bool, comboCount:Int = 0,
|
||||
cancelable:Bool = false):Void
|
||||
{
|
||||
super(type, null, healthChange, comboCount, true);
|
||||
this.holdNote = holdNote;
|
||||
|
|
|
@ -6,6 +6,7 @@ import funkin.modding.IScriptedClass;
|
|||
/**
|
||||
* Utility functions to assist with handling scripted classes.
|
||||
*/
|
||||
@:nullSafety
|
||||
class ScriptEventDispatcher
|
||||
{
|
||||
/**
|
||||
|
|
|
@ -1,5 +1,6 @@
|
|||
package funkin.modding.events;
|
||||
|
||||
@:nullSafety
|
||||
enum abstract ScriptEventType(String) from String to String
|
||||
{
|
||||
/**
|
||||
|
|
|
@ -8,6 +8,7 @@ import funkin.modding.events.ScriptEvent;
|
|||
* A module is a scripted class which receives all events without requiring a specific context.
|
||||
* You may have the module active at all times, or only when another script enables it.
|
||||
*/
|
||||
@:nullSafety
|
||||
class Module implements IPlayStateScriptedClass implements IStateChangingScriptedClass
|
||||
{
|
||||
/**
|
||||
|
@ -28,7 +29,7 @@ class Module implements IPlayStateScriptedClass implements IStateChangingScripte
|
|||
*
|
||||
* Priority 1 is processed before Priority 1000, etc.
|
||||
*/
|
||||
public var priority(default, set):Int;
|
||||
public var priority(default, set):Int = 1000;
|
||||
|
||||
function set_priority(value:Int):Int
|
||||
{
|
||||
|
|
|
@ -10,6 +10,7 @@ import funkin.modding.module.ScriptedModule;
|
|||
/**
|
||||
* Utility functions for loading and manipulating active modules.
|
||||
*/
|
||||
@:nullSafety
|
||||
class ModuleHandler
|
||||
{
|
||||
static final moduleCache:Map<String, Module> = new Map<String, Module>();
|
||||
|
@ -74,11 +75,15 @@ class ModuleHandler
|
|||
* Given two module IDs, sort them by priority.
|
||||
* @return 1 or -1 depending on which module has a higher priority.
|
||||
*/
|
||||
static function sortByPriority(a:String, b:String)
|
||||
static function sortByPriority(a:String, b:String):Int
|
||||
{
|
||||
var aModule:Module = moduleCache.get(a);
|
||||
var bModule:Module = moduleCache.get(b);
|
||||
var aModule:Null<Module> = getModule(a);
|
||||
var bModule:Null<Module> = getModule(b);
|
||||
|
||||
if (aModule == null || bModule == null)
|
||||
{
|
||||
return 0;
|
||||
}
|
||||
if (aModule.priority != bModule.priority)
|
||||
{
|
||||
return aModule.priority - bModule.priority;
|
||||
|
@ -89,14 +94,14 @@ class ModuleHandler
|
|||
}
|
||||
}
|
||||
|
||||
public static function getModule(moduleId:String):Module
|
||||
public static function getModule(moduleId:String):Null<Module>
|
||||
{
|
||||
return moduleCache.get(moduleId);
|
||||
}
|
||||
|
||||
public static function activateModule(moduleId:String):Void
|
||||
{
|
||||
var module:Module = getModule(moduleId);
|
||||
var module:Null<Module> = getModule(moduleId);
|
||||
if (module != null)
|
||||
{
|
||||
module.active = true;
|
||||
|
@ -105,7 +110,7 @@ class ModuleHandler
|
|||
|
||||
public static function deactivateModule(moduleId:String):Void
|
||||
{
|
||||
var module:Module = getModule(moduleId);
|
||||
var module:Null<Module> = getModule(moduleId);
|
||||
if (module != null)
|
||||
{
|
||||
module.active = false;
|
||||
|
@ -136,7 +141,7 @@ class ModuleHandler
|
|||
{
|
||||
for (moduleId in modulePriorityOrder)
|
||||
{
|
||||
var module:Module = moduleCache.get(moduleId);
|
||||
var module:Null<Module> = moduleCache.get(moduleId);
|
||||
// The module needs to be active to receive events.
|
||||
if (module != null && module.active)
|
||||
{
|
||||
|
|
|
@ -1,13 +1,14 @@
|
|||
package funkin.play;
|
||||
|
||||
import funkin.ui.freeplay.charselect.PlayableCharacter;
|
||||
import flixel.FlxState;
|
||||
import funkin.data.freeplay.player.PlayerRegistry;
|
||||
import flixel.FlxG;
|
||||
import flixel.FlxObject;
|
||||
import flixel.FlxSprite;
|
||||
import flixel.input.touch.FlxTouch;
|
||||
import flixel.util.FlxColor;
|
||||
import flixel.util.FlxTimer;
|
||||
import funkin.util.HapticUtil;
|
||||
import funkin.audio.FunkinSound;
|
||||
import funkin.graphics.FunkinSprite;
|
||||
import funkin.modding.events.ScriptEvent;
|
||||
|
@ -19,6 +20,10 @@ import funkin.ui.story.StoryMenuState;
|
|||
import funkin.util.MathUtil;
|
||||
import funkin.effects.RetroCameraFade;
|
||||
import flixel.math.FlxPoint;
|
||||
import funkin.util.TouchUtil;
|
||||
#if FEATURE_MOBILE_ADVERTISEMENTS
|
||||
import funkin.mobile.util.AdMobUtil;
|
||||
#end
|
||||
|
||||
/**
|
||||
* A substate which renders over the PlayState when the player dies.
|
||||
|
@ -94,6 +99,8 @@ class GameOverSubState extends MusicBeatSubState
|
|||
|
||||
var targetCameraZoom:Float = 1.0;
|
||||
|
||||
var canInput:Bool = false;
|
||||
|
||||
public function new(params:GameOverParams)
|
||||
{
|
||||
super();
|
||||
|
@ -101,9 +108,19 @@ class GameOverSubState extends MusicBeatSubState
|
|||
this.isChartingMode = params?.isChartingMode ?? false;
|
||||
transparent = params.transparent;
|
||||
|
||||
cameraFollowPoint = new FlxObject(PlayState.instance.cameraFollowPoint.x, PlayState.instance.cameraFollowPoint.y, 1, 1);
|
||||
cameraFollowPoint = new FlxObject(0, 0, 1, 1);
|
||||
if (parentPlayState != null)
|
||||
{
|
||||
cameraFollowPoint.x = parentPlayState.cameraFollowPoint.x;
|
||||
cameraFollowPoint.y = parentPlayState.cameraFollowPoint.y;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* The PlayState that this GameOverSubState is displaying on top of.
|
||||
*/
|
||||
public var parentPlayState:Null<PlayState>;
|
||||
|
||||
/**
|
||||
* Reset the game over configuration to the default.
|
||||
*/
|
||||
|
@ -119,19 +136,18 @@ class GameOverSubState extends MusicBeatSubState
|
|||
{
|
||||
if (instance != null)
|
||||
{
|
||||
// TODO: Do something in this case? IDK.
|
||||
FlxG.log.warn('WARNING: GameOverSubState instance already exists. This should not happen.');
|
||||
}
|
||||
instance = this;
|
||||
|
||||
super.create();
|
||||
|
||||
parentPlayState = cast _parentState;
|
||||
|
||||
//
|
||||
// Set up the visuals
|
||||
//
|
||||
|
||||
var playState = PlayState.instance;
|
||||
|
||||
// Add a black background to the screen.
|
||||
var bg:FunkinSprite = new FunkinSprite().makeSolidColor(FlxG.width * 2, FlxG.height * 2, FlxColor.BLACK);
|
||||
// We make this transparent so that we can see the stage underneath during debugging,
|
||||
|
@ -143,10 +159,10 @@ class GameOverSubState extends MusicBeatSubState
|
|||
|
||||
// Pluck Boyfriend from the PlayState and place him (in the same position) in the GameOverSubState.
|
||||
// We can then play the character's `firstDeath` animation.
|
||||
if (PlayState.instance.isMinimalMode) {}
|
||||
if ((parentPlayState?.isMinimalMode ?? true)) {}
|
||||
else
|
||||
{
|
||||
boyfriend = PlayState.instance.currentStage.getBoyfriend(true);
|
||||
boyfriend = parentPlayState?.currentStage.getBoyfriend(true);
|
||||
if (boyfriend != null)
|
||||
{
|
||||
boyfriend.canPlayOtherAnims = true;
|
||||
|
@ -164,15 +180,26 @@ class GameOverSubState extends MusicBeatSubState
|
|||
|
||||
// The conductor now represents the BPM of the game over music.
|
||||
Conductor.instance.update(0);
|
||||
|
||||
#if mobile
|
||||
addBackButton(FlxG.width - 230, FlxG.height - 200, FlxColor.WHITE, goBack);
|
||||
#end
|
||||
|
||||
HapticUtil.vibrate(0, Constants.DEFAULT_VIBRATION_DURATION);
|
||||
|
||||
// Allow input a second later to prevent accidental gameover skips.
|
||||
new FlxTimer().start(1, function(tmr:FlxTimer) {
|
||||
canInput = true;
|
||||
});
|
||||
}
|
||||
|
||||
@:nullSafety(Off)
|
||||
function setCameraTarget():Void
|
||||
{
|
||||
if (PlayState.instance.isMinimalMode || boyfriend == null) return;
|
||||
if ((parentPlayState?.isMinimalMode ?? true) || boyfriend == null) return;
|
||||
|
||||
// Assign a camera follow point to the boyfriend's position.
|
||||
cameraFollowPoint = new FlxObject(PlayState.instance.cameraFollowPoint.x, PlayState.instance.cameraFollowPoint.y, 1, 1);
|
||||
cameraFollowPoint = new FlxObject(parentPlayState.cameraFollowPoint.x, parentPlayState.cameraFollowPoint.y, 1, 1);
|
||||
cameraFollowPoint.x = getMidPointOld(boyfriend).x;
|
||||
cameraFollowPoint.y = getMidPointOld(boyfriend).y;
|
||||
var offsets:Array<Float> = boyfriend.getDeathCameraOffsets();
|
||||
|
@ -182,7 +209,7 @@ class GameOverSubState extends MusicBeatSubState
|
|||
|
||||
FlxG.camera.target = null;
|
||||
FlxG.camera.follow(cameraFollowPoint, LOCKON, Constants.DEFAULT_CAMERA_FOLLOW_RATE / 2);
|
||||
targetCameraZoom = (PlayState?.instance?.currentStage?.camZoom ?? 1.0) * boyfriend.getDeathCameraZoom();
|
||||
targetCameraZoom = (parentPlayState?.currentStage?.camZoom ?? 1.0) * boyfriend.getDeathCameraZoom();
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -207,7 +234,7 @@ class GameOverSubState extends MusicBeatSubState
|
|||
public function resetCameraZoom():Void
|
||||
{
|
||||
// Apply camera zoom level from stage data.
|
||||
FlxG.camera.zoom = PlayState?.instance?.currentStage?.camZoom ?? 1.0;
|
||||
FlxG.camera.zoom = parentPlayState?.currentStage?.camZoom ?? 1.0;
|
||||
}
|
||||
|
||||
var hasStartedAnimation:Bool = false;
|
||||
|
@ -218,7 +245,7 @@ class GameOverSubState extends MusicBeatSubState
|
|||
{
|
||||
hasStartedAnimation = true;
|
||||
|
||||
if (boyfriend == null || PlayState.instance.isMinimalMode)
|
||||
if (boyfriend == null || (parentPlayState?.isMinimalMode ?? true))
|
||||
{
|
||||
// Play the "blue balled" sound. May play a variant if one has been assigned.
|
||||
playBlueBalledSFX();
|
||||
|
@ -245,76 +272,16 @@ class GameOverSubState extends MusicBeatSubState
|
|||
// Handle user inputs.
|
||||
//
|
||||
|
||||
// MOBILE ONLY: Restart the level when tapping Boyfriend.
|
||||
if (FlxG.onMobile)
|
||||
{
|
||||
var touch:FlxTouch = FlxG.touches.getFirst();
|
||||
if (touch != null)
|
||||
{
|
||||
if (boyfriend == null || touch.overlaps(boyfriend))
|
||||
{
|
||||
confirmDeath();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// KEYBOARD ONLY: Restart the level when pressing the assigned key.
|
||||
if (controls.ACCEPT && blueballed && !mustNotExit)
|
||||
// Restart the level when pressing the assigned key.
|
||||
if ((controls.ACCEPT #if mobile || (TouchUtil.pressAction() && !TouchUtil.overlaps(backButton) && canInput) #end)
|
||||
&& blueballed
|
||||
&& !mustNotExit)
|
||||
{
|
||||
blueballed = false;
|
||||
confirmDeath();
|
||||
}
|
||||
|
||||
// KEYBOARD ONLY: Return to the menu when pressing the assigned key.
|
||||
if (controls.BACK && !mustNotExit && !isEnding)
|
||||
{
|
||||
isEnding = true;
|
||||
blueballed = false;
|
||||
PlayState.instance.deathCounter = 0;
|
||||
// PlayState.seenCutscene = false; // old thing...
|
||||
if (gameOverMusic != null) gameOverMusic.stop();
|
||||
|
||||
// Stop death quotes immediately.
|
||||
hasPlayedDeathQuote = true;
|
||||
if (deathQuoteSound != null)
|
||||
{
|
||||
deathQuoteSound.stop();
|
||||
deathQuoteSound = null;
|
||||
}
|
||||
|
||||
if (isChartingMode)
|
||||
{
|
||||
this.close();
|
||||
if (FlxG.sound.music != null) FlxG.sound.music.pause(); // Don't reset song position!
|
||||
PlayState.instance.close(); // This only works because PlayState is a substate!
|
||||
return;
|
||||
}
|
||||
else
|
||||
{
|
||||
var targetState:funkin.ui.transition.stickers.StickerSubState->FlxState = (PlayStatePlaylist.isStoryMode) ? (sticker) ->
|
||||
new StoryMenuState(sticker) : (sticker) -> FreeplayState.build(sticker);
|
||||
|
||||
if (PlayStatePlaylist.isStoryMode)
|
||||
{
|
||||
PlayStatePlaylist.reset();
|
||||
}
|
||||
|
||||
var stickerPackId:Null<String> = PlayState.instance.currentChart.stickerPack;
|
||||
|
||||
if (stickerPackId == null)
|
||||
{
|
||||
var playerCharacterId = PlayerRegistry.instance.getCharacterOwnerId(PlayState.instance.currentChart.characters.player);
|
||||
var playerCharacter = PlayerRegistry.instance.fetchEntry(playerCharacterId ?? Constants.DEFAULT_CHARACTER);
|
||||
|
||||
if (playerCharacter != null)
|
||||
{
|
||||
stickerPackId = playerCharacter.getStickerPackID();
|
||||
}
|
||||
}
|
||||
|
||||
openSubState(new funkin.ui.transition.stickers.StickerSubState({targetState: targetState, stickerPack: stickerPackId}));
|
||||
}
|
||||
}
|
||||
if (controls.BACK && !mustNotExit && !isEnding) goBack();
|
||||
|
||||
if (gameOverMusic != null && gameOverMusic.playing)
|
||||
{
|
||||
|
@ -324,9 +291,9 @@ class GameOverSubState extends MusicBeatSubState
|
|||
}
|
||||
else if (boyfriend != null)
|
||||
{
|
||||
if (PlayState.instance.isMinimalMode)
|
||||
if ((parentPlayState?.isMinimalMode ?? true))
|
||||
{
|
||||
// startDeathMusic(1.0, false);
|
||||
// Do nothing?
|
||||
}
|
||||
else
|
||||
{
|
||||
|
@ -360,9 +327,11 @@ class GameOverSubState extends MusicBeatSubState
|
|||
|
||||
function playDeathQuote():Void
|
||||
{
|
||||
if (isEnding) return;
|
||||
if (boyfriend == null) return;
|
||||
if (parentPlayState == null) return;
|
||||
|
||||
var deathQuote = boyfriend.getDeathQuote();
|
||||
var deathQuote:Null<String> = boyfriend.getDeathQuote();
|
||||
if (deathQuote == null) return;
|
||||
|
||||
if (deathQuoteSound != null)
|
||||
|
@ -402,7 +371,7 @@ class GameOverSubState extends MusicBeatSubState
|
|||
|
||||
startDeathMusic(1.0, true); // isEnding changes this function's behavior.
|
||||
|
||||
if (PlayState.instance.isMinimalMode || boyfriend == null) {}
|
||||
if ((parentPlayState?.isMinimalMode ?? true) || boyfriend == null) {}
|
||||
else
|
||||
{
|
||||
boyfriend.playAnimation('deathConfirm' + animationSuffix, true);
|
||||
|
@ -417,15 +386,15 @@ class GameOverSubState extends MusicBeatSubState
|
|||
if (pixel) RetroCameraFade.fadeBlack(FlxG.camera, 10, 1);
|
||||
else
|
||||
FlxG.camera.fade(FlxColor.BLACK, 1, true, null, true);
|
||||
PlayState.instance.needsReset = true;
|
||||
if (parentPlayState != null) parentPlayState.needsReset = true;
|
||||
|
||||
if (PlayState.instance.isMinimalMode || boyfriend == null) {}
|
||||
if ((parentPlayState?.isMinimalMode ?? true) || boyfriend == null) {}
|
||||
else
|
||||
{
|
||||
// Readd Boyfriend to the stage.
|
||||
boyfriend.isDead = false;
|
||||
remove(boyfriend);
|
||||
PlayState.instance.currentStage.addCharacter(boyfriend, BF);
|
||||
parentPlayState?.currentStage.addCharacter(boyfriend, BF);
|
||||
}
|
||||
|
||||
// Snap reset the camera which may have changed because of the player character data.
|
||||
|
@ -440,13 +409,37 @@ class GameOverSubState extends MusicBeatSubState
|
|||
RetroCameraFade.fadeToBlack(FlxG.camera, 10, 2);
|
||||
new FlxTimer().start(2, _ -> {
|
||||
FlxG.camera.filters = [];
|
||||
#if FEATURE_MOBILE_ADVERTISEMENTS
|
||||
if (AdMobUtil.PLAYING_COUNTER >= AdMobUtil.MAX_BEFORE_AD)
|
||||
{
|
||||
AdMobUtil.loadInterstitial(function():Void {
|
||||
AdMobUtil.PLAYING_COUNTER = 0;
|
||||
resetPlaying(true);
|
||||
});
|
||||
}
|
||||
else
|
||||
resetPlaying(true);
|
||||
#else
|
||||
resetPlaying(true);
|
||||
#end
|
||||
});
|
||||
}
|
||||
else
|
||||
{
|
||||
FlxG.camera.fade(FlxColor.BLACK, 2, false, function() {
|
||||
#if FEATURE_MOBILE_ADVERTISEMENTS
|
||||
if (AdMobUtil.PLAYING_COUNTER >= AdMobUtil.MAX_BEFORE_AD)
|
||||
{
|
||||
AdMobUtil.loadInterstitial(function():Void {
|
||||
AdMobUtil.PLAYING_COUNTER = 0;
|
||||
resetPlaying();
|
||||
});
|
||||
}
|
||||
else
|
||||
resetPlaying();
|
||||
#else
|
||||
resetPlaying();
|
||||
#end
|
||||
});
|
||||
}
|
||||
});
|
||||
|
@ -489,7 +482,7 @@ class GameOverSubState extends MusicBeatSubState
|
|||
public function startDeathMusic(startingVolume:Float = 1, force:Bool = false):Void
|
||||
{
|
||||
var musicPath:Null<String> = resolveMusicPath(musicSuffix, isStarting, isEnding);
|
||||
var onComplete:() -> Void = () -> {};
|
||||
var onComplete:Void->Void = () -> {};
|
||||
|
||||
if (isStarting)
|
||||
{
|
||||
|
@ -504,7 +497,7 @@ class GameOverSubState extends MusicBeatSubState
|
|||
onComplete = function() {
|
||||
isStarting = true;
|
||||
// We need to force to ensure that the non-starting music plays.
|
||||
startDeathMusic(1.0, true);
|
||||
startDeathMusic(0.0, true);
|
||||
};
|
||||
}
|
||||
}
|
||||
|
@ -533,6 +526,61 @@ class GameOverSubState extends MusicBeatSubState
|
|||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Pressing BACK from the Game Over screen should return the player to the Story/Freeplay menu as appropriate.
|
||||
*/
|
||||
public function goBack():Void
|
||||
{
|
||||
if (blueballed == false) return;
|
||||
isEnding = true;
|
||||
blueballed = false;
|
||||
if (parentPlayState != null) parentPlayState.deathCounter = 0;
|
||||
// PlayState.seenCutscene = false; // old thing...
|
||||
if (gameOverMusic != null) gameOverMusic.stop();
|
||||
|
||||
// Stop death quotes immediately.
|
||||
hasPlayedDeathQuote = true;
|
||||
if (deathQuoteSound != null)
|
||||
{
|
||||
deathQuoteSound.stop();
|
||||
deathQuoteSound = null;
|
||||
}
|
||||
|
||||
if (isChartingMode)
|
||||
{
|
||||
this.close();
|
||||
if (FlxG.sound.music != null) FlxG.sound.music.pause(); // Don't reset song position!
|
||||
if (parentPlayState != null) parentPlayState.close(); // This only works because PlayState is a substate!
|
||||
parentPlayState = null;
|
||||
return;
|
||||
}
|
||||
else
|
||||
{
|
||||
var targetState:funkin.ui.transition.stickers.StickerSubState->FlxState = (PlayStatePlaylist.isStoryMode) ? (sticker) ->
|
||||
new StoryMenuState(sticker) : (sticker) -> FreeplayState.build(sticker);
|
||||
|
||||
if (PlayStatePlaylist.isStoryMode)
|
||||
{
|
||||
PlayStatePlaylist.reset();
|
||||
}
|
||||
|
||||
var stickerPackId:Null<String> = parentPlayState?.currentChart.stickerPack;
|
||||
|
||||
if (stickerPackId == null)
|
||||
{
|
||||
var playerCharacterId:Null<String> = PlayerRegistry.instance.getCharacterOwnerId(parentPlayState?.currentChart.characters.player);
|
||||
var playerCharacter:Null<PlayableCharacter> = PlayerRegistry.instance.fetchEntry(playerCharacterId ?? Constants.DEFAULT_CHARACTER);
|
||||
|
||||
if (playerCharacter != null)
|
||||
{
|
||||
stickerPackId = playerCharacter.getStickerPackID();
|
||||
}
|
||||
}
|
||||
|
||||
openSubState(new funkin.ui.transition.stickers.StickerSubState({targetState: targetState, stickerPack: stickerPackId}));
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Play the sound effect that occurs when
|
||||
* boyfriend's testicles get utterly annihilated.
|
||||
|
@ -540,6 +588,7 @@ class GameOverSubState extends MusicBeatSubState
|
|||
public static function playBlueBalledSFX():Void
|
||||
{
|
||||
blueballed = true;
|
||||
|
||||
if (Assets.exists(Paths.sound('gameplay/gameover/fnf_loss_sfx' + blueBallSuffix)))
|
||||
{
|
||||
FunkinSound.playOnce(Paths.sound('gameplay/gameover/fnf_loss_sfx' + blueBallSuffix));
|
||||
|
|
|
@ -6,6 +6,10 @@ import funkin.graphics.FunkinSprite;
|
|||
import funkin.ui.MusicBeatState;
|
||||
import flixel.addons.transition.FlxTransitionableState;
|
||||
import funkin.ui.mainmenu.MainMenuState;
|
||||
#if mobile
|
||||
import funkin.util.TouchUtil;
|
||||
import funkin.util.SwipeUtil;
|
||||
#end
|
||||
|
||||
class GitarooPause extends MusicBeatState
|
||||
{
|
||||
|
@ -32,15 +36,18 @@ class GitarooPause extends MusicBeatState
|
|||
}
|
||||
|
||||
var bg:FunkinSprite = FunkinSprite.create('pauseAlt/pauseBG');
|
||||
bg.setGraphicSize(Std.int(FlxG.width));
|
||||
bg.updateHitbox();
|
||||
bg.screenCenter();
|
||||
add(bg);
|
||||
|
||||
var bf:FunkinSprite = FunkinSprite.createSparrow(0, 30, 'pauseAlt/bfLol');
|
||||
bf.animation.addByPrefix('lol', "funnyThing", 13);
|
||||
bf.animation.play('lol');
|
||||
add(bf);
|
||||
bf.screenCenter(X);
|
||||
add(bf);
|
||||
|
||||
replayButton = FunkinSprite.createSparrow(FlxG.width * 0.28, FlxG.height * 0.7, 'pauseAlt/pauseUI');
|
||||
replayButton = FunkinSprite.createSparrow(FlxG.width * 0.25, FlxG.height * 0.7, 'pauseAlt/pauseUI');
|
||||
replayButton.animation.addByPrefix('selected', 'bluereplay', 0, false);
|
||||
replayButton.animation.appendByPrefix('selected', 'yellowreplay');
|
||||
replayButton.animation.play('selected');
|
||||
|
@ -57,11 +64,19 @@ class GitarooPause extends MusicBeatState
|
|||
super.create();
|
||||
}
|
||||
|
||||
#if mobile
|
||||
function checkSelectionPress():Bool
|
||||
{
|
||||
var buttonAcceptCheck:Bool = replaySelect ? TouchUtil.pressAction(replayButton) : TouchUtil.pressAction(cancelButton);
|
||||
return buttonAcceptCheck && !SwipeUtil.swipeAny;
|
||||
}
|
||||
#end
|
||||
|
||||
override function update(elapsed:Float):Void
|
||||
{
|
||||
if (controls.UI_LEFT_P || controls.UI_RIGHT_P) changeThing();
|
||||
if (controls.UI_LEFT_P || controls.UI_RIGHT_P #if mobile || SwipeUtil.justSwipedLeft || SwipeUtil.justSwipedRight #end) changeThing();
|
||||
|
||||
if (controls.ACCEPT)
|
||||
if (controls.ACCEPT #if mobile || checkSelectionPress() #end)
|
||||
{
|
||||
if (replaySelect)
|
||||
{
|
||||
|
|
Some files were not shown because too many files have changed in this diff Show more
Loading…
Reference in a new issue