From 9fb625c18daab018fb1ba2bfaa2551b1e38c59b0 Mon Sep 17 00:00:00 2001 From: ItzBlack <100058443+ItzBlack6093@users.noreply.github.com> Date: Sun, 13 Oct 2024 11:50:11 +0700 Subject: [PATCH] Merged Changes by ItzBlack6093 and squashed changes, and removed whitespace changes Previous commit messages: added hyperspeed-related things (the pbpace, the splits (i hate css), the sfx) clear delay reworked splits upload the file manually cus i'm stupid skins Delete assets/skins directory Delete skins directory fk me moved to gamemode_extended missed a fucikng 's' Update gamemode_extended.js --- .gitignore | 3 +- LICENSE | 42 +- README.md | 100 +-- assets/icons/cleargarb.svg | 10 +- assets/icons/download.svg | 2 +- assets/icons/info.svg | 14 +- assets/icons/next.svg | 10 +- assets/sfx/zenith_speedrun_end.mp3 | Bin 0 -> 44137 bytes assets/sfx/zenith_speedrun_start.mp3 | Bin 0 -> 44137 bytes assets/skins/tgm_c.png | Bin 0 -> 9481 bytes assets/skins/tgm_w.png | Bin 0 -> 17885 bytes index.html | 2 + info.html | 174 +++--- info.md | 46 +- src/data/attacktable.json | 28 +- src/data/data.js | 280 ++++----- src/data/defaultSettings.json | 15 +- src/data/gamemodes.json | 293 ++++----- src/data/kicks.js | 48 +- src/data/pieces.json | 92 +-- src/data/sfxlist.json | 386 ++++++------ src/display/boardEffects.js | 222 +++---- src/display/particles.js | 452 +++++++------- src/display/renderBoard.js | 318 +++++----- src/display/renderer.js | 738 +++++++++++------------ src/features/editboard.js | 236 ++++---- src/features/history.js | 380 ++++++------ src/features/modes.js | 280 ++++----- src/features/profileStats.js | 170 +++--- src/features/settings.js | 146 ++--- src/features/sounds.js | 254 ++++---- src/features/stats.js | 269 +++++---- src/game.js | 334 +++++----- src/main.js | 123 ++-- src/mechanics/bag.js | 156 ++--- src/mechanics/board.js | 254 ++++---- src/mechanics/clearlines.js | 2 + src/mechanics/fallingpiece.js | 126 ++-- src/mechanics/gamemode_extended.js | 277 +++++++++ src/mechanics/hold.js | 84 +-- src/mechanics/locking.js | 262 ++++---- src/mechanics/mechanics.js | 281 +++++---- src/mechanics/zenith.js | 116 ---- src/menus/generate.js | 432 ++++++------- src/menus/menuactions.js | 480 +++++++-------- src/menus/modals.js | 254 ++++---- src/movement/controls.js | 2 +- src/movement/movement.js | 323 +++++----- styles/boards.css | 872 +++++++++++++-------------- styles/menus.css | 696 ++++++++++----------- styles/miscmenus.css | 290 ++++----- styles/settings.css | 452 +++++++------- styles/style.css | 480 +++++++-------- tauri/package.json | 28 +- tauri/src-tauri/Cargo.toml | 40 +- tauri/src-tauri/build.rs | 6 +- tauri/src-tauri/src/main.rs | 16 +- tauri/src-tauri/tauri.conf.json | 86 +-- 58 files changed, 5847 insertions(+), 5635 deletions(-) create mode 100644 assets/sfx/zenith_speedrun_end.mp3 create mode 100644 assets/sfx/zenith_speedrun_start.mp3 create mode 100644 assets/skins/tgm_c.png create mode 100644 assets/skins/tgm_w.png create mode 100644 src/mechanics/gamemode_extended.js delete mode 100644 src/mechanics/zenith.js diff --git a/.gitignore b/.gitignore index 414c49a..ab39c6e 100644 --- a/.gitignore +++ b/.gitignore @@ -1,3 +1,4 @@ .vscode/ package-lock.json -Cargo.lock \ No newline at end of file +Cargo.lock +README.md \ No newline at end of file diff --git a/LICENSE b/LICENSE index 85a0235..c11413b 100644 --- a/LICENSE +++ b/LICENSE @@ -1,21 +1,21 @@ -MIT License - -Copyright (c) 2024 TitanPlayz - -Permission is hereby granted, free of charge, to any person obtaining a copy -of this software and associated documentation files (the "Software"), to deal -in the Software without restriction, including without limitation the rights -to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -copies of the Software, and to permit persons to whom the Software is -furnished to do so, subject to the following conditions: - -The above copyright notice and this permission notice shall be included in all -copies or substantial portions of the Software. - -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE -SOFTWARE. +MIT License + +Copyright (c) 2024 TitanPlayz + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/README.md b/README.md index 60af4c1..973b966 100644 --- a/README.md +++ b/README.md @@ -1,50 +1,50 @@ -# Teti README - -Hosted on github pages [here](https://titanplayz100.github.io/teti/) - -The info page can be found [here](https://titanplayz100.github.io/teti/info.html) - -> [!WARNING] -> Firefox is not supported (due to import assertions) - -## Desktop App -### NEW! - -Releases are now run through a workflow. They are **up to date** and contain all the latest features. - -App build using Tauri, feel free to open issues and PRs. - -## Data Formats (for my convenience) -### Gamemode Structure (gamemodes.json) -```js -gamemodes = { - "gamemode_name": { - settings: {}, // settings that override the default * settings - displayName: "", // name shown on gamemode selection - objectiveText: "", // subtext displayed on right side - goalStat: "", // stat being tracked (valid property in stats class) - target: "", // target (valid target in settings) - result: "", // displayed as result (another valid stat in stats class) - - // TO BE ADDED LATER - music: "", // custom song that can play - compmusic: "", // custom song that played on pb pace - startBoard: "", // starting board, tetrio map format - effects: [], // custom background / effects - } -} -``` - -Add functionality mainly in `features/modes.js`. -You can modify existing modules as well from other files - -### Adding Audio (sfxlist.json) -```json -{ - { - "name": "", - "path": "assets/sfx/." - } -} -``` -Use with `this.game.sounds.playSound()` +# Teti README + +Hosted on github pages [here](https://titanplayz100.github.io/teti/) + +The info page can be found [here](https://titanplayz100.github.io/teti/info.html) + +> [!WARNING] +> Firefox is not supported (due to import assertions) + +## Desktop App +### NEW! + +Releases are now run through a workflow. They are **up to date** and contain all the latest features. + +App build using Tauri, feel free to open issues and PRs. + +## Data Formats (for my convenience) +### Gamemode Structure (gamemodes.json) +```js +gamemodes = { + "gamemode_name": { + settings: {}, // settings that override the default * settings + displayName: "", // name shown on gamemode selection + objectiveText: "", // subtext displayed on right side + goalStat: "", // stat being tracked (valid property in stats class) + target: "", // target (valid target in settings) + result: "", // displayed as result (another valid stat in stats class) + + // TO BE ADDED LATER + music: "", // custom song that can play + compmusic: "", // custom song that played on pb pace + startBoard: "", // starting board, tetrio map format + effects: [], // custom background / effects + } +} +``` + +Add functionality mainly in `features/modes.js`. +You can modify existing modules as well from other files + +### Adding Audio (sfxlist.json) +```json +{ + { + "name": "", + "path": "assets/sfx/." + } +} +``` +Use with `this.game.sounds.playSound()` diff --git a/assets/icons/cleargarb.svg b/assets/icons/cleargarb.svg index 0bf1351..ba06cdb 100644 --- a/assets/icons/cleargarb.svg +++ b/assets/icons/cleargarb.svg @@ -1,6 +1,6 @@ - - - - - + + + + + \ No newline at end of file diff --git a/assets/icons/download.svg b/assets/icons/download.svg index 1cd579b..5c4fd7d 100644 --- a/assets/icons/download.svg +++ b/assets/icons/download.svg @@ -1 +1 @@ - + diff --git a/assets/icons/info.svg b/assets/icons/info.svg index a4d9d1a..dbcf9fc 100644 --- a/assets/icons/info.svg +++ b/assets/icons/info.svg @@ -1,8 +1,8 @@ - - - - - - - + + + + + + + \ No newline at end of file diff --git a/assets/icons/next.svg b/assets/icons/next.svg index 4e2db95..ebb3fce 100644 --- a/assets/icons/next.svg +++ b/assets/icons/next.svg @@ -1,6 +1,6 @@ - - - - - + + + + + \ No newline at end of file diff --git a/assets/sfx/zenith_speedrun_end.mp3 b/assets/sfx/zenith_speedrun_end.mp3 new file mode 100644 index 0000000000000000000000000000000000000000..27eaddf499b198b3d3b41f8fd46b4d233a13a8d4 GIT binary patch literal 44137 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

^BiCN9<^ulPF{OGKFBjbkeK~edH>bWDJ%rD z@aUNX|6kJ)tucF(A$Zh&qKhuY0E=7-x3bi^LETs8Jp~pao&AcEi$3W?tccnht)`GC z@KU`%qIH5dO0%NL>l(%=+u!G=;|^LwKl)k|O@i%a2AUJY!O^Ifnpj0o-&JN2!_J-* zW>CV8%Pz+s{dNoeg+Kx*zhk>5{;^E4i^f|add~QxMoJ$4tjm(5eFdw2Dqe4f1!chk z$ehWuKBuY=Yz7fTD0oH?j7xOja`M|ZS0XS|#zMH{r|}VFUh; zR=>}AqcRAB24&V2g{~h2%dy8}$;5;1IQuuh$b#z1CHP!f!|6-T2NMLP5L>r#iqFj) z1d=RPU*HX`K8HzHz2{_G=d-?%UC&ZVcgU5}_Ds?+*7lLe`N(!_OydH|v+y461vc0M z=BeI<=L~ zVhiQxSLvku$B#E!j$Q^Ku;|7zwaNMNPp8Hy$EZwzN+U)#=a^|!j*yJ5;y=0E6$pHc zN>WxDGhg3q9MAly5z%UzkddM_V7}Z`JGn|K6(FKpSG}XD1a3aLDj0Fco#IO?4h_yX z*a5KQO*F`a0y+~OFMTo#4=R{G3AQcq*xUwG05#jvyKiHu=QuEw%bDg=_&^IQcUZU%L}4 zWZJddg3UTE=9M(w;UtkH+VXMTsSO_PEsAE!XuwvYy;OfaqZH(x^Y^~X=5|&C8=7~n zpTm}F5Qszwl)zm#p;9!&CFfdr5)^<6j*KMM1%+Ou62l*3LJ$HaL?%0J{ImLqD(e1^ ziF=U*7Om8xPV`RlPFUtMgH!FgBA&$(Yc~Xk=|~)KkLeKQ4)DMBKKh+6ei)E|Lx9~g z|FJz6>K~tVwPMZT3&;^q@0vd(*=bsJh3_@91gFv0UL{~nZ7ZwEeFRfW8KyiV{40en z3GW@)y{FZ3i{TbmY;iY%D^MH}zuE*rx@Gt6 zCVc&Kj4X9zdBcR+OQM-XrZwDr1f9D=hZa9{FgoMnzZJm8{~OFflrgf?=e(P!$!2QH z!GpsoJ)R<18G_JidnbMo`V3<%gj6NbH0`uL|c|&eW}e_r#L9ZAr-E{db!6Zkpwn8e z+QfWdMNag|1iLmQxARGaLGZG#c}e^RCM3gwI2ivB*XrT<9;I9g9Ob!W$_c$KpVPaS zzT-<8q2m;7Gg+S18Y+T`-+T1_;=rKCp~Bp#_CD1DH(AXfd2=fV;QJ9|lVan>qg@0~ zn0_zK-nj!GpJVL`(~jgR6Cb8Dl1J4iOZV?c1V*HRNKh=EyW7^$@hHMZeXez)-6-6i z&B(DKfBl9074aX>0%Mzw*BSiKuf~KxBXQ$CkJv)5NphwBJAa>0GC_vmGg-4s2!W9**wHIwPwfC-v zJdvzpcS@k|yU$Nj@;E)eAxnDDp*tIJ1U5JL>Nh-fe&;6%0Ik>1{}F|2&}{_I5W6S8 zkz%P;y#1~>G9@)O!-Z@BSE5OOSn62u18z@4Vm@3hOopKGbPgzB35_tY=&&nxB`=N5 z{xv5z?20)lapz{Gk(}K|quJ1t8qvt$C~uj9fUnKSEOzhtcM{U#o{$&8xJKHd?mRnz zS^IwPhYU6FHU-r*=2r(Sfjq<@uC9oy>m}6CMmS8NgS5g(<67o8C|=5pHX0Q1OQsuB zxct>AI&!>Z>q*h$Q(7s9Z4K|gR9mC9|M`tGcNC}kynNcnWs<3SkB|0iEQW9zxIs(F z5=C@4(0^+OfN0D2%mEOVO`f{-`8YTP6(}S12?vs7A(K5Rwc;@pr`NFJxa-0_ocy?{ zpiGAmGZ_j@g4Q%aEd5=Wjy!Mz0yr7QX9$Mh+) z0nYB#Nw{l{)QdR()(>ulcS(?+BR|4J@W{>oETP5oLfor7X}vwP`nBk0uKbw9D>d$s zdQZMdJ9yt9QanUU>Ch@jsLS$=UV8Ya2nbTOh9dPt55}o+o;b$tzZ$v%4czfaG|4id znp(mZI565ER`1|^o-P4C>0}Yp;w&$IOF60k;;~W{Pd`2X<)MC`7rXrBo{!va%~Sa^ zQaCoSDpgcnJ@;^=?0IUZIbk&k1y9<-Jfbu?u-3ff4fqtEqa&mjEK^7467Cof^W7jOq^}MOA5SeLs53Q8;nJOvpD= z;&^~P4y*6f^+^iZXRn81mA9vy{}=Gpd7TyU9zI4(i8qtO2Hj(heQ0!L1ampw|2Z#c zt@pD@tq2_FGtxu7ZdHCeP{M+DJCmX4sOP_+=_p!|DIN&#Ow0B*5HH8&wmG|^I;c#z zG2T8F}*Q~{*nF_afQ z+UfxD(DJE69#;%4-mS1nNa(*jen#|SWJVQxM1)0*DMI68a?d`D?aGaa!k@`{xOiT| z3`g`CBQyG*7Z-AiTN~aN%E3cK1EK50U}s;@IOD|6E>sPQ`}=6^zcB(ypDkDQPcj1b zGK(Y~Oe&wTgO=>TJ`PTYbN#Pq?dzrJGTEM1Ouhb6Eg+=!X7%bedqq~lsg82pU{Y9X?e{l z9N@egf41eyN3o_@p1`uw3>!>y0C2-SYeD3n-|baEu@ll=`rhjFqT_jenJKRTNPL z8gVe_&Jj6IQWPKAH4CFo#&#^!IKfhhqwHSO!?*N^reUOO6@cM3?w>6k=f0Trm}t`A z;eKl2k}gj@fRD!ZN0;xXZoS=p^T{OsATrEwzcc28yG;ll6767rWi+0li?F%g1FU0< z`UcyKe#f^5UNP%Ygh<()7~-)vUv0R_;Mm8HTTu=b{{|c{rG z;1k6BUy|7KwqNQ6I2hn!VCC?TWhsGBXva#`hsvBCo^F9FDOu;lo zEhN8d=m`NxArLG)sMl*xXL7;wPf}R1iC{*4<&GM%H^yzW=rVi)ICXJ&YgS^&ni4OS)V$&C^q+Mh^5DO2&MYD%KVIgM<)CARpg0dSo`Dhe+0u z*`A7=ANr8?*~ap{pSAstdz_u}g&qK|UG=4iaRFQ^rtBXHaI7g=>K6V}Y}4>EVzxNW z22LpDD}0PrY$(7>gvC#Valb>~kbj-{OXK&eQs^Mx6A$0DCP-bDocX_LcrtBbsF@*S zZ*Nl0UHi2b1?A8*N$Nfmt~|si_H3In95LHeP?7cr5>jCl@Tdk*7O>4VSpZrqs?%V& zFOTuvk2=419YNr{5E6t&0k?AYaV8;e3Z@SQDqGBjRs~lwKC+vVt;WL}^fcAW66yjM zfos-_b{n>SgyhwPD}C8YulFM5Hf_wcL*(i3U*^y4gy|7aOu;1E{xnD9L zhw%Q~F+%C(3$5X7g(<0pq`$a|zp?sme@5!&0Z-rVfHOW%q}2ZqIu3_`W)+*nvoSJ0 zq^UGzv_gQ)Lzd^n5|;V0TT<3ij?1!-9Z8X8YD7D8ogZYt8&9+DZiGpW(7s=sTvy77 z8{pxL4n81;{8p-Q=Q!+NMJIos9G@(jy%yN#Z309Dc_z!**Z^g!Ii)$mq6D*9l|b18 z1Mir)kHcgh%lZu6*74`tY*wK#lw5kl!lf8QPO%?i7kj3Qz}kV&G3hdUE{XM`!7MDF zaeHwL3>J=AQW@ynt@xA~%aMOh4NzJAsiw{1>Tn1?;s7rBdyzY~tlCmREd2EkaiZRB z0aL)uDHLG?jK(>}o$DHt3grI43R)-I82yNusl2FpNU_0nzq7aQgCvbh}qf zZB)MRb87Ow*CVrH#wDO$B#I!dHt-k4qmR#&jm67{-D--raFFf!2P@sgG*@jcT-E$j zi9^i>4jR}WFBZejCN6_X&5xh@;!Hn)sX@oCLz*UX#dkD8sQ(bUj=4FZ9;A2p8gB#3 zV`M|!h@e)4jJN~WUsO&S5JUT!&++NFLHw8|D$aL!-Qx40cE*;yOb`XXEM#=%>3Tp5vx%vq?+D&o*ol8K zD)W<-$+UYKLI$%J#ujHG2d_Q0RNzP3Z7U0#FT6 z_R$3WuG^*l6F5IZ1fc*`x0GVh5>w5I=eUmI6J|6NJ1j7)crvK!BR-aDyS|A?GNZ_~ zkJ0^V^asgkf`*S%OT~8NoKC6in>M6iQZ)-L+{dK90V>cKS7EcYAnI!tAA92M>^%FkVbz4UhJ#^@%S?>m{HIPqPLiO_9R zx%k`8dY||lyhD(xWvboUu6(?pHIjaYf82j2LUYf88PDPL^gZBSKlKNJIKc%Cr%c4fvy%muq`~b$|0uN_6J;rDsi#Eb zlXyBgk;-kBbDLfo>u7tu!8FAAKMMVjX+-W`=5&fggOYWW)$EZ*?L3)MQYwPcNnZK&Y&D|q>{;FS|9clikp zsAGamF~bfg-$lBpf>2ZkpbAO!PH2C(s{~$B%D3;Nj(Yfyiy> zU#g}Jg+mm;@KbT5*-}KujLhdnhED#@RTL|oc|^{e-S=`=YSll5L!4NeV|tkK{|r?N zA*%3tns{^?R4+!qC%0@W$StH788j<9^Xm~T0@gL=F8-^b)3|}ZlA;j&D%S=Qo?fQ* z1_vBWm`%s1YRTNg%*+W>+cd{3j?AQlwQ>$Q}OzGrS-hZVUbpi;z*{1lTv?#T!AL13nh;q6%X|w zQs2Kx>0BT<&;-!YF4MW#9-7`9N`t5teRPe8Dq^!p>twGa>bN8#w{l@lvsC*rK_Afe zT|v&}l@f$L&HG%11hyl622I$(=eCmzSavwP=K+glKqZdvWra1Rk#^rZsfBQ#9BMJ# z0vn022+=co$z*>r1P&zvTD@r8Fo8^Cz=E{2e;cjVT&jKTLT7GsxDuC57U5es${#B& z1M^qh#@MdsvcK2UI{V)$o3-6*&u_6;I$ZEk!~Hrn9}D=<_uXI5$~T2bPN@9;&Fnoy6dM(RDwKHm z(wHc!hlF$>?AhrWzQ9ArtCpcoQ$sD@ha8fThD2fMzoqB2^}gK9h!5Y>7SR-k8)&JpXJ23Q7tW)U>E2TPzw{)Jj2EkEo zrP35Fj6vuVkUrSP(t>81CMu$01OjRN9M@~uGlxe0@MQe2hHm^JU}y%T6m^yK#c@(U z8>jHcy_k#wvi(*VWZG@HB+&4g!$VN`V6@(C8I->iC%)<0A;#mLu7sJhxOFuDuK3zG zy?cS#FFQNgfFYKOoH|rM!==GKBN#!J*ACZ`TeRzO{XGqJ9;kc&E=T9eU3S2?p&*oU z5`SnBEv4EMsLmOhpmbi(dg!<>gsj?^Fq?Z_&X1`R6Ot-Bs0#Dg!zVHht!0(wffAo6 zl%lyjga6}%Yo7$%pDsgL$HLFi#TzenYC(Yz^)_b(A0FAqlUT97i& ztV};Vimj`AdwKZ>9A}5IJ;Z+EkYl0hB#dvF-RpF5631%yWR zUr-&P_=p>vv8nnS@#O~J(lcCd4DbJiPX58B=bP_eynDhC`>%$cV+Pr227*32r^X?$ zq7HNl2=PgXq=)452Faw;T^vuvF4ze**)xszCxT!6rl!qMnYN_(89Y)n&Tk~<&?zs@ zfW6Shq44M~M?ABNEdj+i#FvQ8qkG1$`coV_c#gj$)T@j`PnGMZ2eppFE1?x=2r&^x z7h%#ADVpgzHf`ES63d~s=+qtazm61}TEH9al9VO3YixuLZA2_Brnvn~R+Sv6as^O9 zL;Lj9L#;fnR3zU%c#Z*K`=4|pb|&~2^(F}F{D~4f(+Olp!@7i34D#irxOiy(}GO#1?Yq=_rB(??0kKBz&{CeircTXH>`t&>^r!v z3>cA*Jb0|S>Q_Xu68^3ULe(kB(M#kx{FAbuiLo$TXAlBxp(6B*L`x^lLY~vGO$iMq zrBFsww>U2vRoam&m!&=^;hQ@cwR(5{#`RVG9U_|62I%k+P(_BlW1ij!AtC?vYX8dS z^%>{*y1RovviK4#DV(kH0eD?9JWLm$Co@$0?S1U8h#{P={#QdU;i~+U!h_z{*VZ94 zEDkU>2qY~HQkt6j{{g=bh&~9i+6Fd zS?Fb5U$_rm$Gnm4mw_5YaBy_Tv`?AZ074%6CZ;Z=wHE|bVr!VSq!DpqjGdfaK=V#{ zgSEcN5tQ@uZez}L!&8n~V|ZL#qsH4hTUwlMlV3ykM}pGYRfT#kC$|n!>*3@r6pdmu zy%6YgY0OESp8!cYvg?&Fj5Pc)PtU9<(ZbFcit1~OFB%KFQoJ%9}_T%wPGK7>aUV`j=w`rvH|PL z^R~L{4m_Uw-pSz2?nyP~*y`Cw>Z|JBePlISme;UHNeRF$D8 z7VE%b*`Yy1#&1Yq+DSVt)xs>Okg<_tsL9-=rA=H+&P!>X$ox60m|4k*h=Qa+A|fT% zQ_DyD7qd@>XIeDT)k8iXSSC`DLT3OwU$1S8_p9Gdj4OYG)ns&nV$qe?y~9pfC5iGH zZi?=dh_<#0w%w3a8OtoqYo7>Au$3e1ZRq0tnDb<7^V0LmniDqk3Q*rJ@m(9Zi?mi! zq?ixb3F$WCg>1D(ofJ|pZnr?fQD!}efW!Uf3SHK$L?>aAlx2$7`=K&se87e5iZd}` zBt){4jNZ0T!bpbl85Z^hOIzj2*)T}<7r{TmOm#+^5i3#Evi>7q=a!g(0xEJAv->CT z{t9aylqdM$+3y+OaP_{|jFrD?Pwb|O1}907z%6&Hkj{Vu+qY1H`vRa=7sG*G1S9jU02@JW<@_mIkkkDPva(1o~*;l zJMPnc;bImw@enip)h5ULZ-5&nC!?f6Pyh8$&P)lxW9ZYr`X55KF+p@JB74jN^(upZ zBC!>jqLqAL!OZu-GSavErx|$yNf4;7w0F32!b43N>61MY6x1BIEn9`R&o>Mkl@tG_ z==0i^LVlZ1LQ*edWU}jcOt@Lm0Z4Mejm#^wc$8Q`7fnf z*Dywdlq}M4Xwzj(;Wonfk~+**Ljgj;UytD>yg~MhzU1IwtP=MWNUG6g?be%QkbYOc z%{PD*V8rA1t?SzBX}si*i3(}tsc21NUtPh{zBdc`wdV(GpolsV1o~V}AnTViupu0l zV+OlvF=N6h$`~F@#ZDXEPKUJ&e^~+#W-nanH$nIB%Shnto`|gZ8A@sLsRDyHg1_Dp z#a#XGL;*5ZwG&-n3kXxmEi6J_F-eNF?bogZEqFCe-y#|tX(u+Pp`2WA(XAQetl^zKM*UdYvpwvCv}x#KalJ%UwKO4UQdhN_=e4& zhFFl6Le8Zo-kDJ0oxx5YC?wrZWk0qIo69=Xu`8CAFSjUb`X53!ND#POA_yq-CQ7D% zY^+Zc5v@KEGB%HJKruQ=E67jCtwt(4c3%Gszx{li{k-yM<4;{_7=A6L1RM2QQr*sabca*|K^@6YqSVLjG~N!ZKOhY|0sJ!L(vy1c12gtqf5|sCqi-;}*EYEG z)g=7LTmMi+^5(x-0lL=;{o~6gqQa&YlyLI@GXL}E8dGnuT+nKM*Kblu@9>NNE6}$l zfwPGxCYF5=-xjo}@tkhLf{I9`!CrfDxd;VL8@aMA)*Y*IbEZPx6Yi2k;1c=U+8kYq zhzTAP)V~nL^CN%hI^CH6X8XR4w2hQs?|{Oag~x>#ft5v=gdCAs1jE)&;q~-#@LxF| zt|iNlFaoL_XwPONf13ImzWQlyLFbe#U*)4SE{OfI>QnPe*}5E+3r+(awdIV#;&U-k z%8Ot%Yh?t%jL)#ej1#3MG>j#T2)d;e)C%buXIjP)i}4z%4E99=&?$5g={J%r;@<^O zB0wVOt)!;+v4$_a_vIc5RGTDC5q-9`kgnO8*de}49QRa`(GAY8C{g2-oI)z%<|db!P&Pe=CrZ*JeyL$ zBTg^YGMz{@msWLb@he1x#L}dd#|fRq3V}lp4|kze6Bhm+f;|7WV9b4~7IM|b%+x;n zM%&M|u$J3Z)-u4@*6ab5_$_m;_T)bAM-S8XOD~XobTuIoOt*BtEyL6Q@gqo7jK1-` zmpGe4J$dl;(baqtzz5u4F)3i7sEk7(5{izk&7gZpidahJ)`gne**f2+of)QFHo?dF zVDY9*WD!^!4)7pS)XxY~u|NtXd6tl6>RDg6eU779RBUL{cmIy3!N{_f(a)E50Gll> z!1kWd9YaybFvajtb=l8>0wG+DNjH7`@@mHY8UN8>43<@_bt=f@=3Mh|oHQ|ruNW=O zo3N5acNrb?DQH%+)=fk=%a4>ynzYL4wXU+=_Ib}fxH(EE{~>f3ck{$Q0Ktf1q*Ka$ zZC%xe2$=*4YhE^PlT6%SS~}KrWNthIOP#2D{J$asIeoOeE@@+o*&7Q5!e#x?S)f-D zZf#k2Uf*12vq$TyBiOqw1WmyUoQP)+y>jAQokqq z@$MKHyb{}ysWhRR2}np#=DUkdJ(Z?v&k@*or=%$Sxf;j(#GoOFN%uV;~dJRX0IgSD-@XdW}xduv-q<@p&ZWpT5ZOIpGCgXqx-BC ziGGJTv&ZXhB&1);{PY{WnSpn6^Ub{btje$jJ=+9U4 z!g|E79?*!{9$*wPJFWym`5;*>2pnX+iwzs2C{(GgKWyNVQI<&MeYIT6y3s7qDAP;L zW^K*X;Z2?tHgv<96Bjox+Jed5853-08Z)S3|HgcMILa$bW>_2K%Qcd61akN!Kc|w9 zejA@-qU+|7NH94MZ=Qn%wx&8rCJjtNzukY-Md~cYlrfyVEmN{}zqR40MF*VAWmNgT z7wgsdlA&3$9k^=T>9n+okv^C;VN@1t6?S%+q&4#PfTH}``W~Hr!;bcz{BFCOwp%AC z`)HWxT!02+2~a~KVGLm@7UpdUAR!B&kaI9|G53e^`+1l;xYGV?QF(D$$V}A&WZFX# zbcQJt3fRf(c6-QOEp+^5KGVAzpt#(7b7DX^r8%xHb6ZH6$_y8rq`&IO~ zA5@TJBERCg5x?}ddVcTKf+cq0ip`SDQ>A`=Pil%@;>Sl*$48YWu zlwL~|Uhhdn=p=D#831lowYs?S!_Pov)iI(aSXQ1ul(L{omFvY-RA+kSKU9cXU1j@| zXdj~BZzt28)-UaxOeTKnj!eClz?A?Ii*hl2Gd5m`W2?8H6ss3Tqi7x(e_hb2n96K2 z?0&Kgy8MQvPP?JSU- zW06->1xco`(bq+Qy;o&()lXjZ8@Mq=L5#VRYq4qfw5w<<1OMh2pK&lpX@=BcRn8o( z!qsga5D&b2;A!4<)(pXNPQ=n!`Dnq7$2%}o9NjKM6K=noE#|kahn<(hFNilxD}TY+ zqiXUWGmy%z!hcG7QwJe{xvFtNd*7DJ*hdorj*JWsIihQ4wv0#-@6(gqgpU%UU3q#Gp+jFjh`mgCqDT4OYu!L;mC=E|{jLd?JA}-=7%I`6Fc49PV4fCge z{jZ3!(>`$iq)->}G7UOukvMT7@UVN64JvxXIyBx~?cXyhp{Yl&_3Y`ps`4}Z z(616Qn!wSpn_Vxd&OKl$R72oHE+h#^4&7kC424I>8hdsCCIFxtIB&d@>p5uBpd2cZ z*TpX8t+lJ_8rg8*TT0YEuikp8EKqH2=LxohdalVEuTKJ`5vte)F|_j#bN5{@#Zxm? z4U+f^j{Wk~*S{hx>jyCN=ybE)-;#SSrkhY)v?u(LB{=^Np`$PeNrS#18NztUAWwN1 zBpSpB7bwJh`vkr8ynwWVzpobJ3Vt7-%ZdJbuzrU%h)gdr#7~y1UE3}6(r@uEv(tSID9>t7^-HRcX$?v9k=_Y8}8Sp!ulwL%arTphxD zuU`hVN@k9}hSzYO&o(#xX%^fKpO|yL}J?{bim>t#=#clVl*~o6^WD;7tXe*S0au0uo+n_1N6;4T6ReZ5u`72jyk7UW_~2?*nJzRX z8TRTd&(2kv4%H>!iIIKWPFFvdNuKBszr6+j(p&loA7JX~f`3%Zg(oN#^d(4P<1X$f zotr*{RVZubj}UpozN?U-oaYTr2ieq;c7o8V-g>IkepG?z*Z>(?RE=7WaIgqu@y8#h z7#W{jBC_HjO5=%mvSAAh_qUsV0{hh%*#J+BAHP1|6M>umeg8TfHgiB}PNYKoo>q39PJ?j#m|`kaieCcOX&Yfk-S%Ja7;#TYjgtXX!xV3l%Pa zM7W*fVOyH`5=&FjQ>e$EuXAc9g^DFIP9gE@uAl?L3I@*I$)5CUhBNb9+Hd>00p`>9 zW2sQ8bz?*8v-VPns2=!Kn*;5!@$d+~RZZ*hPAYy@c#nihDD`5ZOr9ZEHi%S>*p{WR z3Pj85Eq440k-U8(tZg6?_+MBFh$vDCdX)86JgR#VlChQz!(uzx7PYnL$7Mb23ec zj$sGP|E(+McIx(j&@rWRIJHvfR36&#l=osBPU^VyRc3Q(v=(bW!O&MmQCN>S5Iz(i zN?Gu@i}99aPoT%Fh?l$d8YG2m2PH!@Qb-q7fmezpkyxRF&03|E?^{FWlt~wOXB@XQ z(__9{3!CT3&*Lcg;a|N>WGmmCf zE4n<`ReiwDT*fM+&A#)%#j^2g_bP6nItI*MA_y?EY8Q+)4x%+iy|$?XFU>94RLi`7 z-NkmlBO-b4RY`POBdq!qY~o|t9ibC9Nj63Fd6hfI(#}W|<@Pv3PU*Pas8Iaqm-<5Y zplgls#i{l3c}M$9t~qHOy6MX%QNYE?h8Yx&y8f8+|ESz6im9Nn5I6(AlCDJ93j#C) z%BtB5V*RwRHtzO)a7=wsoMSRl#zAJJwo=umd7YQqL3Zidb@n%4)vsQX0XP3E%n&Mq zt8vssq&i891o>fH*e&EdcdOmcg0*{AY@Jb&Fk~ag6D%$4uj8RxDHuVeSn#9qK3L+U zO9y|QQqWCE2^NAM8`Z``tGERpr(U@SPz9nmZI`c)8;%0Ps%rwP*i&Zh8Xdjt zQvTg{0J=Ba2qf@+rl03%q3~0U25W7oqe2I>*4=NHm9`RU06xmW!hD=XO?zaPko8i4 z(?|5U?gF>M`My&uzY)`B9nHHo=Ry7^B~VvgVJ23B&}>kSEODoP3b^|y`Ow0xri3Nk zEG^O~&Le~ykLva=Jhimv%$SN|Ng)a9E01(gA`@YQWg)Rtj*YiRxM9B?{9+E*vlpuQ zKDAT0jfdNiL^nyqV!@@!H6HyNtwK#tDO`zapOD?9Tj_s9A1e0&qZ;WS18J6*BcLE`!Y@`4qmUhL+Vi`^*%Lk}x`}?g9iB;*da%AZJe6?Rv z!H+A%&lZ+`#Rm~a*74n_GK`elRZD--d!~sfErD^4&~K;$*C|MpOM?*1`x}bkjzhv= z`ikYVczc!b9$+i~Jr5nzG1xvKm#|5VFL#m1^B1vfy#W~HXl~C>)TduF7_IdotIxaH z59*WYUL+afEt!q;;%lW7JyAcl41TuTdu^UN>))5(Do9}Ij^`a6*>pI)GM+tOhFb~& z=28>s3VlJE@23JhI zUQ-}PLV4)PfFN6)xMqafs%dDjfE)1QS$qG`m5ZRtwZ@>w@kWr~-n{j9= zq8G;lO#OJjIK}qeztk$b22sORVKyW5TLT7)eDt~;c{8KvT`{SuEQ?2u{uQ~W1o}~O znM<&YyK&p+nS>&rsh?h{5GcJ@r6YJjcYYPlf|t5hjIgjEwoZI#%lvk@G{l@#lz6;| z!J{Tz!}Nu#N=-sb_sCeseJ2$)%Cawm=$r`J!{}RfnP6f^4B8+wL?Dhs9XcANf7dXU44Hu5Zc>(#fRoZ~V#UG$y*=Cv ztRY2ZC4ymn%Qd5IxbQ72$(tE>rxeN(+aC$jh!*N)T3%=(TZXkwL$&F{s}at8B}_P?Hj1u%t_4*iQCm zX#;OY#JQ)$4%qBf8P2D-^CRKc8{ z5>Qv}CFdb8ut9-0_`k`s(WOO7SpDC)rmeG7oqQ|)BC9Gwd4N(|5q;hv8f80M*Lsmr4)Ge|FO09|6eZiarAXH z)-yCPSVbEEilO%$k0pJLR<=1g#d3xj+8o`{eNv`NWl8qQ+L-|j-y{ubWAhL(mDUkJ zFc?5FH|3J5r0G=>NS(11&i#?XLA0Rab$#t?x38gS{+7#i>>AmD(} z7$Oir11>!pLjxWK1RO9LLj(e7z@w2A%r3&frMTJ5s@a+RX`v}5fP9oA_~%! z8bo@P-VtfiZ{Y27znMGl%>Dj*Gm|{$IcN8`yT7x$b50_S3^ZvdIVb@D0FAbmnh5|v zge8o*&XE&-X9win+;imM4hp$X;$Kt1nH~nv=YHS4Nj|cFE2DRX#?E_M~bWJtU?=iwX@Y@C|Zwv zebzm8X@5uy>w(|lLU7Ogg}E5ZYOXh&L;)V)BhGI{b>JBmo2ji@2NdK3>!N>%?qcpf z3WlP@>n~r>e43Zmbg`*CEw9BdQ?^M5rx$1-|FV}XlDkF>de~CqRG#YiU`y}Lb6GV} zCOL~G+Z$h*r`V@Tiwcs~J7J=j@u?!SJ4IfG$DeF%thF&2bAnSA*NC;xJvLHhXMXY! zUzlLWqkaHpJ$blln4rsN&K`4#s^D6v(?P?@q))X5+`mnmvL$4VwSAwB5gP_q2C_nW$;}4ncTR;B&%a(J&Cm$Hzy^M^X&s z=?DTtp-_;x1V};xNRR+x{M@l{U!XhY@+rh`7-~oi!qXXzbw;`KoMOW5P+nLCK0d-a z&maD|q4o9ugm=gM&H{lCkS`n!0*i@*+}uEa*T7)ay$K+{JM=$lV9W?*3Nk@rP+p!0 zq`EiK9eerj5cY^a_0e9Qu4m!cBS1)3q#HpLL$C_|he=IseWO1$PAPD7c0-?O5y<|B zB-YvCFS7pO+v&<#IDdD9p#CTBKcxSN{Y;o3rLPZDLm|9Q-P2Z6;5)SsvqvGE?O|s_ zJ9{a6BpfOQL^#+%fe-{#76?beC4n+f2e72Qq=OVf7W_9TZFdY7?v6m7LJ`2loC!D( zC=@I!1(gKaNlDlNAyRe{Ks!4q1SkQQB|PmQva)c*-yjS;C z16WEH3>1e*H~=AFDKO9uE-nd_m6S%xNQsLhkus8JQ1%Fz2FlY7PS{RoH@G7bgm!m4 zTR0^gredV6z$YOl{@09=D;(=UP$1NRv%5XY2lLmGnX?nTL>7qH`4yhPXg9&O9&^p zyCag&KYmZxKjxkPhhz{3%fRg&2vsd^=O6)uAip!~w6$AfD_`mG>|3(+(UzaJQJK+T6L%1q2CFE5Ru36-Ex|(Wd7cRh_YVBRZ z=sa4>5(5BGF`j;j0MF9d2$K|8ZGCl$1xi*bVJ^X#n|1&IQ-!vgika{5s&#kVrB<)w z@f97jD=n-?j5VKAd6bz*@m!>`rMyj#RkcG=MEB0Y$P^dnW>|O}bt6fB@wI1@nrJyu zW&vkyxm?M}1J5&Ylb+k*9m>tN?yc2b!8Dgm7TRY+T-yYa&GcP{Hn(vGns^-q4GrKzn9 z+g>9ahd)zKV#C5n0QHjH@?j-+k3b7@6ML;u&wX|i(I^#AoMnASPt7m7pc=qrqM>5L zSgp@Nx6;vKH+SI*Drk8-g72U!3MAm z;M|`1P}b;2RzQQ9>-?oDZPwqPMq_8In$m{Vjy&5nBG!dt^ zjIN9&N`?p{n~XlZR%6}f{dFM?Dwv|5Z1M#$6u18-O_t$8n%Pyn&_kirh3@XAm&8u5 z#Z#<|?G3NK_@=3*ekJMN6-abmB$mF2Xc*ZSdapdvHIf~m|52HNBhLgiKwQO_#_r@6 z?!7UK=tN(^>gg!Z#L{N*T@(%RLM*9nTBk!i_bn+mb5e{~e-y4z&q^d@v<;NqvaKn4 z=wOk%@YPf>BqV5?jC$=Q^No{;WqO;+;s%`ht&~-^{u)w|rd?}6)qE_jAaq=Jcb@mF zabV$5I>KTiujOqATjKG-!{7^2BIkDJlz~myu>PKc@&0T#ePrNmqoIkzSJnO>{fH)W zl@87q2TsH2hmxu5K8v}J`LnMdjc|4F#El<~UqmWbD+g3?u211DE+ubY>LSX|A9-`L zHKP6tO1KP&_OIX^a@4F}RM|V|C#JZS5UUDc9xA1k9X83~W}nOok}ad73k|D9_SkuR zbI3ZSnk2E=-<8kP76{Jr1RFYE%=gAtWesA!>uV3Ft>IY{l`oI7Bb?m(q z6iBD3**fcgqLgwpGVxsRG0%q*Kcd4Qtsvj%jbojAito226yLs~m}YwO$&2o2W8;@^ zpzHCuXw;ai5x$D0t0uM$-G%g>Y;Dc)AT^p_o1N1P_}QDQ294X(LKTz`J9B>b0 z&wH9@wKWyM{bnPJNkR3MtBZ1Hg7uU>*AfUp}*R@aVOCf(+}Ha6I+rIY2Jfn%2zkt(ouS zx{FI7hM42)vgd#t*9WwUbn`Yl*)uI%9DC^EltI68;g7e9{TnKr2JCbxYnWk_i8J1I zOe4b}F{4SP<_da2HD{ntlR<7G)Z0dH{F4tHy>c+paI<$~;i7zzl(hx&>*6PdP!7YL zT`M4HWGPxq4pw})XNW$UxUGzMpZbAqFoFB{7nxCb@S-!z$t@z~W3FhjAC;`A>5O%O zr085Wd~8R7FY$F=7`*9#CiV)M5lv$}Cr>f;!nH4h{mRuET2!^)1;j+7gu8Dztsu=C ziG{>;-)UXkEKsd0idKq#RF)+_-gAFfRB9V*5Gs5y__OIg{j6NbwytOh2Dg5t#1vZV5jLo=#H#fg>H655_@AUQq#dMD*JH?SKtwn>tUL5`HdNS}|IUw2IhWsNb0*Pdw>H)YKcr zpC;Ig9z(jc^qlglt(C08lDH_Haz3}xp=Nq%^u)X)d)D>_{GMn1t>F=4yjq-d}JO3z+=u!Guh)k%O=CI@wX)$-S_s*I+& z_v)sFH__eC#J;!3j|Ln!xT`Z8%OYE~uYKHl?l;+?P6U0s*^zmYbAlVNsd;wKPqSt) zyWlIs+rH-R#5Sp0(p(6$H#m}@#;#t47ruzqjH}OOT{x~`I=<+=ulUjmRS&SJAC7!^ z!t~lS%eRc{9q0a+oAra8`lOfNX;O0e3+7PuhGFl3F+kqwFE?IC&VIKOA<2rfPs-y+ z;EtD?uVY=^Z+~rJ0rvXvF>jPCY=jxQ4lF}V%+R*DQs6NapLilB)5E-~%G#6_oMuB_ z=dJJg-_{$l?Ax2W{Q<}w{7blaB9KU55&&VzNMy#{{(xoY?3^m$<+^}+L-l?ud1R|i zrZ{D0MMo~_2~C4^Q42rPp^hPdACuA7LHy)xai^MH8~=SaMrM|yih-M1Y!CtEwvI5; z;l_?lqueH)qWU+L-fvMZ^qqxW+KcK+$qA{Vh4e)kp`+}juZ06cJ;4$dHq-K11HW<@ z!p+kdj2aM_I>D>74+fmD+EMK_$6aC3o0N0usv{K_DYmLA$&|Sr>>HjG73n?MnJlbm zB}=r)vAuz|LPA0^#7mja}&o_@f{VIuQ_Gut8X zf)cs^aEv5&i`EbNP(?}#mSNF7x*r4-~;ID7?k4~ z)4fc{hRm+UM{td&bv-dte_i`St{HLb6(E>AYoMRaL5;;LDf*-gne}~prr3BxtT4AW z8yTI_39)9W5ogM+%)-`@c%i`Pg)UfyTlt21PnBB(J6+iIbIBN@!4^sZwVcX%*!1I$ z5llIbN;rV)$aA@mcb}gKy;=kE4o$UruRzfepuAo;^I$SXYC#6UerFE~eopnZirn;ul zbE9rc#vG2EKCb=Il;-#AQCzoJpV~EL&3boPiFzQJ>R<_aQI9W?_yf@MqR(C%i2(Dzoe@Em>t{kDVIes75W_y)yS*LjLa8It)IB6FH5=CAAGHB zQa8yl+BYR)&B2q4-}B>z=%P|w;OOtePhM80|7sFW;w9kxyB5ZgR7 zB+?PPYto_3l<9Qn0$X@q~Y=0>49T?QD3ztltY@^a&j)tVLo^$`N2LX zYP4(~KaWm@{B*lB zQE9L^dxYXo{44Ex`t{Ep3*o`UMe5)+)&w*#xYaX2<7 zFWc9^C`w_E1wq%%67`f9sd~m@?rXRO*+9^zQg)VH$A@{eZf~&E}*63KuseQ@kWCPBw@re2eIV z^=FaqwgdJMOeRi2EMaAee%3*|A+Y)u;OGdXoqnKh4GRm7lwqkP9hjr&2c$PDVmV2~ znyGErxMJe{C8b+ZUN+g9O}=)CaaqKFd7D0NVTNWiy$Xriz0poCl&5bjCLsEmp`WT( zWF}B0QZ-H<%T;yE`Z|vs_*CWN^*jm<1v37z{g2xHSC_K`)>WJaEnei^!a3T)^vu?; zFskkS@Z?4pw4^ZuWv%da z{%nW^<$yWa60R*Ze?oEm>=ES)-i+@$no{Ua+ZzRuWG!>zAJo$%Eat}4Lp3}*_+*e8 zip<{0*4dw%(qFOvTIfmLjtw*Js7+&k6e}fO&9TBOqCqPVBE5ai@M)B)kJ!%OtDh#t z&aqK)m6B$tqlvBfX*|-R^oQdi$J(NE48H%y7fEcRG`D`$Iw5GKA^?-xShEHMysxCqwhHQ&Zbhtg62IxLoVw zN>o>s)4SD^%c&5rr!S>}nd?%{%BeT2n%ST4x0P92(dwV*`Q4{?Gk7J}$D!Iu*<$Y% zOt-B;R}+vsg)@~G5i!U%S$c6tFEC2CoL?E_zo1gF8JCqa_;Y!o$dc|Zmxz`Sl(m*u z;GOeuEash?OBuL_V`d1P?&b1UxHq72Im`cz{eUyeb_K6tIo@SjUv+vE^hEt5b%+?? zR;Z+~v3~X=34d8|sIZN{&;V2C53)ix`Dv zdLsY17U%sWj>!()E@tVIx4zk){1q>}jHH55vQLk`P~LcVf$vs<>@MB?TrqA~?fX|3 z<_0A5ze4;)uN@Py@&<{?Y4k$+=8cLm*dZ27QoF5nZBg7ZV8m*&5iztFpJNabD%t&_ zel^Z+jg{`E%-sqZa%@I)Z5$-Kjpvc6R>~{s)#@nE!LEafE6~=JtS8=C_mh%EAC*6) z2rs#8-TqleD9@5P)4x7^S~k0jayyfBq6lpK(W3}pBRw)*Nnxctd7r|dF00yq!f~H1 z=1|O7$a8@+D}usGe7HYr?1y_9EgrMqx}g_ZUH(*L{-urCkanvr(`_!Nlxviup=(03 z!5j5mPp`75pW`)Sy9!|}aDKf}z4K&zoVqSJKE33v1b?l7E|QbNc#`R;=09!}h zLlAxa2bsz87i_* z91LU%U4BPuup{Xy6g)AJ`oPo3WTQf~HZ>&U#Z-h&l_=4QV~gnZ@c|C&>PCLsZwr~6 zwCky~gQ%arZ=UdSCrel*yji&|IOIrEdN+RxLO6vDdo(L4>|qP4L_Hoh(tF*Dmnr^u znLo1ZUxJuyjlSHK>Z@_I_M^F+2eYg|VZSvQc@2to7!7PRf zeXA@qC7E}R_wb^x8@_*E_k5luOvOL%bZ&jQ^`bfw5R#F>d7+tWT0Kd@=Hi$80{u=b zdUz1`=-@?MSgpJ!5-J_qVEY<5Jw*%vY3Z#12vcgPK0T~h{ z&Q+hv1KOU5_R$Wdf7en8j0eDw)PM&rjMibsWp7CFW`Ll0$O0||1vToH+E>U$>ymSP zY47ryvm e(6>w`=#aZM#anSC+WPDdPi=Jrwc=~G_x=x@SZ3n@ literal 0 HcmV?d00001 diff --git a/assets/skins/tgm_w.png b/assets/skins/tgm_w.png new file mode 100644 index 0000000000000000000000000000000000000000..b4fafec5eb01c3f08e3dd6b3d50194faee3cfdf5 GIT binary patch literal 17885 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 z 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 literal 0 HcmV?d00001 diff --git a/index.html b/index.html index 058ad03..7628220 100644 --- a/index.html +++ b/index.html @@ -182,6 +182,8 @@

Teti

Allow Hold Queue

Infinite Hold

Allow Queue Modification

+

Line Clear Delay

+

Line Clear Delay

Allspin

Allspin's are mini

Allow history (undo/redo)

diff --git a/info.html b/info.html index 8a8665d..34eac65 100644 --- a/info.html +++ b/info.html @@ -1,88 +1,88 @@ - - - - - - - - - Teti - Info - - - - - -
- -
- Back to teti - - - - - - -
-

css using chota

-

md renderer with zero-md

- Top of Page - - - - + + + + + + + + + Teti - Info + + + + + +
+ +
+ Back to teti + + + + + + +
+

css using chota

+

md renderer with zero-md

+ Top of Page + + + + \ No newline at end of file diff --git a/info.md b/info.md index 6ee1f5a..89123ee 100644 --- a/info.md +++ b/info.md @@ -16,11 +16,11 @@ This project started out as a learning experience, because surprisingly it was m ## Acknowledgements Thanks [existancepy](https://github.com/existancepy) for all the fixes. -Thanks [ItzBlack6093](https://github.com/ItzBlack6093) for adding TGM Race mode and other fixes. +Thanks [ItzBlack6093](https://github.com/ItzBlack6093) for adding many modes and fixes. Feel free to contribute with features and fixes, and open issues. -Design inspired by [Strangelinx's 'blocks'](https://strangelinx.github.io/blocks/) and [Tetra Legends.](https://tetralegend.app) +Design inspired by [Strangelinx's 'blocks'](https://strangelinx.github.io/blocks/) and [Tetra Legends.](https://tetralegends.app) Icons from [The Noun Project.](https://thenounproject.com) Sound effects from [TETR.IO.](https://tetr.io) Skins from [YHF](https://you.have.fail/ed/at/tetrioplus/) @@ -67,10 +67,28 @@ Skins from [YHF](https://you.have.fail/ed/at/tetrioplus/) ## TODO list Things that I am working on based on other changes -- wow finished +- ready set go start option +- arcade mode + +Maybe: +- cooler action text (maybe if i switch to pixijs then i can make text better) + +Future thoughts (honestly only if i have time): +- I honestly might want to break my rule and start using a framework for rendering. + - (turns out pixijs is much better for games than canvas 2d, and howler for audio) +- I have mixed feelings, because this would make the project technically no longer purely made by me (though the info page is a bit iffy) +- Maybe I don't care and will switch anyway, and maybe in the end it will pay off. +- this would make my most recent changes irrelivent + - such as piece flash and reset animation cause i have to redo them in pixi + - in the future itll probably pay off +*** ## Updates +#### v1.3.3 +- Thanks to itzblack for the Zenith Tower mode +- added option for line clear delay + #### v1.3.2 - reset animation - toggles with stride mode @@ -80,7 +98,7 @@ Things that I am working on based on other changes - added notifications - displays errors - shows export and import messages -- removed assets loading screen, cause it was unecessary +- removed assets loading screen, cause it was unnecessary - added different grid patterns #### v1.3.1 @@ -159,8 +177,8 @@ Things that I am working on based on other changes - can easily add custom gamemodes now using gamemodes JSON - competitive mode - sets certain gamerules, disables custom game settings - - PBs are onyl saved when used -- seperated goals into its own menu + - PBs are only saved when used +- separated goals into its own menu - view pbs in competitive mode menu ### v1.2.0 => COMPETITIVE MODE @@ -203,8 +221,6 @@ Things that I am working on based on other changes *** ## Feature Wishlist Future wants for game, kinda ordered by ease and desire for feature -- ready set go start option -- cooler action text - finesse detection - allow importing tetrio settings and custom game files - small guide on essential things for game @@ -212,14 +228,20 @@ Future wants for game, kinda ordered by ease and desire for feature - WIKI for technical docs about project - glossary of useful terms - more rotation systems (ars, trs, srs/srsX, none) +- more bag systems (pure random, nes random, 14 bag, 7+2 bag) - replay functionality (either save gamestate or save keystrokes idk yet) + - statistics graph - more unique gamemodes (techmino styled) - misdrop remover mode - holdless and next queueless gamemode kinda like qp2 cards - colourblind gamemode -- guide like progression thing? using custom boards (kinda like tetris tres bien) +- guide like progression using custom maps + - maybe like tetris tres bien - achievements, progression tree -- maybe play around with server api stuff, like adding a leaderboard or connecting tetrio stats + - the jstris map creator looks really nice + - could have user made maps section as well +- maybe play around with api stuff + - leaderboards + - connecting tetrio stats - touch settings -- bot to play against -- statistics graph \ No newline at end of file +- bot to play against \ No newline at end of file diff --git a/src/data/attacktable.json b/src/data/attacktable.json index 53e6e3a..647ecad 100644 --- a/src/data/attacktable.json +++ b/src/data/attacktable.json @@ -1,15 +1,15 @@ -{ - "": [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0], - "TSPIN": [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0], - "TSPIN MINI": [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0], - "SINGLE": [0, 0, 1, 1, 1, 1, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 3, 3, 3, 3, 3], - "DOUBLE": [1, 1, 1, 1, 2, 2, 2, 2, 3, 3, 3, 3, 4, 4, 4, 4, 5, 5, 5, 5, 6], - "TRIPLE": [2, 2, 3, 3, 4, 4, 5, 5, 6, 6, 7, 7, 8, 8, 9, 9, 10, 10, 11, 11, 12], - "QUAD": [4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24], - "TSPIN SINGLE": [2, 2, 3, 3, 4, 4, 5, 5, 6, 6, 7, 7, 8, 8, 9, 9, 10, 10, 11, 11, 12], - "TSPIN DOUBLE": [4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24], - "TSPIN TRIPLE": [6, 7, 9, 10, 12, 13, 15, 16, 18, 19, 21, 22, 24, 25, 27, 28, 30, 31, 33, 34, 36], - "TSPIN MINI SINGLE": [0, 0, 1, 1, 1, 1, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 3, 3, 3, 3, 3], - "TSPIN MINI DOUBLE": [1, 1, 1, 1, 2, 2, 2, 2, 3, 3, 3, 3, 4, 4, 4, 4, 5, 5, 5, 5, 6], - "ALL CLEAR": 10 +{ + "": [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0], + "TSPIN": [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0], + "TSPIN MINI": [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0], + "SINGLE": [0, 0, 1, 1, 1, 1, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 3, 3, 3, 3, 3], + "DOUBLE": [1, 1, 1, 1, 2, 2, 2, 2, 3, 3, 3, 3, 4, 4, 4, 4, 5, 5, 5, 5, 6], + "TRIPLE": [2, 2, 3, 3, 4, 4, 5, 5, 6, 6, 7, 7, 8, 8, 9, 9, 10, 10, 11, 11, 12], + "QUAD": [4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24], + "TSPIN SINGLE": [2, 2, 3, 3, 4, 4, 5, 5, 6, 6, 7, 7, 8, 8, 9, 9, 10, 10, 11, 11, 12], + "TSPIN DOUBLE": [4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24], + "TSPIN TRIPLE": [6, 7, 9, 10, 12, 13, 15, 16, 18, 19, 21, 22, 24, 25, 27, 28, 30, 31, 33, 34, 36], + "TSPIN MINI SINGLE": [0, 0, 1, 1, 1, 1, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 3, 3, 3, 3, 3], + "TSPIN MINI DOUBLE": [1, 1, 1, 1, 2, 2, 2, 2, 3, 3, 3, 3, 4, 4, 4, 4, 5, 5, 5, 5, 6], + "ALL CLEAR": 10 } \ No newline at end of file diff --git a/src/data/data.js b/src/data/data.js index bb5f696..b01804d 100644 --- a/src/data/data.js +++ b/src/data/data.js @@ -1,139 +1,141 @@ -export const cleartypes = { 0: "", 1: "Single", 2: "Double", 3: "Triple", 4: "Quad", 5: "Teti-tris" }; - -export const defaultSkins = ['tetrio', 'jstris', 'plain']; - -export const scoringTable = { - "": 0, - TSPIN: 400, - "TSPIN MINI": 100, - SINGLE: 100, - DOUBLE: 300, - TRIPLE: 500, - QUAD: 800, - "TSPIN SINGLE": 800, - "TSPIN DOUBLE": 1200, - "TSPIN TRIPLE": 1600, - "TSPIN MINI SINGLE": 200, - "TSPIN MINI DOUBLE": 400, - "ALL CLEAR": 3500, -}; - -export const levellingTable = { - 0: 0, - 1: 1, - 2: 2, - 3: 4, - 4: 6, -}; - -export const disabledKeys = [ - "ArrowRight", - "ArrowLeft", - "ArrowUp", - "ArrowDown", - " ", - "Enter", - "Escape", - "Tab", -]; - -export const spinChecks = [ - [ - [0, 2], - [2, 2], - ], - [ - [2, 2], - [2, 0], - ], - [ - [0, 0], - [2, 0], - ], - [ - [0, 0], - [0, 2], - ], -]; - -export const gameoverText = { - clearlines: "Cleared _ lines", - attack: "Sent _ damage", - cleargarbage: "Dug _ lines", - ended: "Survived _ lines", - level: "Reached level _", - time: "Spent _ seconds", - altitude: "Climbed _ meters", -} - -export const gameoverResultText = { - time: " in _ seconds", - score: " to score _ points", - maxCombo: " to get _ combo", -} - -export const lowerIsBetter = { - time: true, - score: false, - maxCombo: false -} - -export const pbTrackingStat = { - clearlines: "pps", - time: "score", - attack: "apm", - cleargarbage: "dss", - combo: "maxCombo", - level: "pps" -} - -export const resultSuffix = { - time: 's', - score: ' Points', - maxCombo: ' Combo' -} - -export const statDecimals = { - 0: ["clearlines", "pieceCount", "score", "pcs", "quads", "allspins", "level", "attack", "cleargarbage", "sent", "recieved", "combo", "maxCombo", "btbCount", "maxBTB", "tpE", "ipE", "inputs", "holds", "rotates", "ppb",], - 1: ["time", "vs", "chzind", "garbeff",], - 2: ["pps", "apm", "lpm", "app", "apl", "appw", "dss", "dsp", "vsOnApm", "kps", "kpp"] -} - -export const statsSecondary = { - pps: "pieceCount", - apm: "attack", - lpm: "clearlines", - app: "attack", - apl: "attack", - appw: "attack", - dss: "cleargarbage", - dsp: "cleargarbage", - kps: "inputs", - kpp: "inputs", - tpE: "tspins", - ipE: "quads", - ppb: "score" -} - - -// const sfx = await fetch("https://api.github.com/repos/titanplayz100/teti/contents/assets/sfx") -// const combo = await fetch("https://api.github.com/repos/titanplayz100/teti/contents/assets/sfx/combo") -// const songs = await fetch("https://api.github.com/repos/titanplayz100/teti/contents/assets/songs") -// manually remove dirs -export const songsobj = [ - { - "name": "Cafe de Touhou 3 - The Girl's Secret Room.mp3", - "path": "assets/songs/Cafe de Touhou 3 - The Girl's Secret Room.mp3", - "type": "file" - }, - { - "name": "ShibayanRecords - Acoustic Image.mp3", - "path": "assets/songs/ShibayanRecords - Acoustic Image.mp3", - "type": "file" - }, - { - "name": "ShibayanRecords - Close to your Mind.mp3", - "path": "assets/songs/ShibayanRecords - Close to your Mind.mp3", - "type": "file" - } -]; +export const cleartypes = { 0: "", 1: "Single", 2: "Double", 3: "Triple", 4: "Quad", 5: "Teti-tris" }; + +export const defaultSkins = ['tetrio', 'jstris', 'plain', 'tgm_c', 'tgm_w']; + +export const scoringTable = { + "": 0, + TSPIN: 400, + "TSPIN MINI": 100, + SINGLE: 100, + DOUBLE: 300, + TRIPLE: 500, + QUAD: 800, + "TSPIN SINGLE": 800, + "TSPIN DOUBLE": 1200, + "TSPIN TRIPLE": 1600, + "TSPIN MINI SINGLE": 200, + "TSPIN MINI DOUBLE": 400, + "ALL CLEAR": 3500, +}; + +export const levellingTable = { + 0: 0, + 1: 1, + 2: 2, + 3: 4, + 4: 6, +}; + +export const disabledKeys = [ + "ArrowRight", + "ArrowLeft", + "ArrowUp", + "ArrowDown", + " ", + "Enter", + "Escape", + "Tab", +]; + +export const spinChecks = [ + [ + [0, 2], + [2, 2], + ], + [ + [2, 2], + [2, 0], + ], + [ + [0, 0], + [2, 0], + ], + [ + [0, 0], + [0, 2], + ], +]; + +export const gameoverText = { + clearlines: "Cleared _ lines", + attack: "Sent _ damage", + cleargarbage: "Dug _ lines", + ended: "Survived _ lines", + tgm_level: "Reached level _", + time: "Spent _ seconds", + altitude: "Climbed _ meters", +} + +export const gameoverResultText = { + time: " in _ seconds", + score: " to score _ points", + maxCombo: " to get _ combo", + grade: " to get grade _" +} + +export const lowerIsBetter = { + time: true, + score: false, + maxCombo: false +} + +export const pbTrackingStat = { + clearlines: "pps", + time: "score", + attack: "apm", + cleargarbage: "dss", + combo: "maxCombo", + level: "pps" +} + +export const resultSuffix = { + time: 's', + score: ' Points', + maxCombo: ' Combo', + grade: "" +} + +export const statDecimals = { + 0: ["clearlines", "pieceCount", "score", "pcs", "quads", "allspins", "level", "attack", "cleargarbage", "sent", "recieved", "combo", "maxCombo", "btbCount", "maxBTB", "tpE", "ipE", "inputs", "holds", "rotates", "ppb",], + 1: ["time", "vs", "chzind", "garbeff",], + 2: ["pps", "apm", "lpm", "app", "apl", "appw", "dss", "dsp", "vsOnApm", "kps", "kpp"] +} + +export const statsSecondary = { + pps: "pieceCount", + apm: "attack", + lpm: "clearlines", + app: "attack", + apl: "attack", + appw: "attack", + dss: "cleargarbage", + dsp: "cleargarbage", + kps: "inputs", + kpp: "inputs", + tpE: "tspins", + ipE: "quads", + ppb: "score" +} + + +// const sfx = await fetch("https://api.github.com/repos/titanplayz100/teti/contents/assets/sfx") +// const combo = await fetch("https://api.github.com/repos/titanplayz100/teti/contents/assets/sfx/combo") +// const songs = await fetch("https://api.github.com/repos/titanplayz100/teti/contents/assets/songs") +// manually remove dirs +export const songsobj = [ + { + "name": "Cafe de Touhou 3 - The Girl's Secret Room.mp3", + "path": "assets/songs/Cafe de Touhou 3 - The Girl's Secret Room.mp3", + "type": "file" + }, + { + "name": "ShibayanRecords - Acoustic Image.mp3", + "path": "assets/songs/ShibayanRecords - Acoustic Image.mp3", + "type": "file" + }, + { + "name": "ShibayanRecords - Close to your Mind.mp3", + "path": "assets/songs/ShibayanRecords - Close to your Mind.mp3", + "type": "file" + } +]; diff --git a/src/data/defaultSettings.json b/src/data/defaultSettings.json index eb5926c..ade8d14 100644 --- a/src/data/defaultSettings.json +++ b/src/data/defaultSettings.json @@ -27,7 +27,14 @@ "preserveARR": true, "allowHold": true, "infiniteHold": true, + "clearDelay": 0, "gamemode": "sprint", + "allspin": true, + "allspinminis": true, + "history": true, + "competitiveMode": false, + "sidebar": ["time", "apm", "pps"], + "stride": false, "requiredLines": 40, "timeLimit": 120, "requiredAttack": 40, @@ -37,13 +44,7 @@ "allowQueueModify": true, "lookAheadPieces": 3, "raceTarget": 500, - "requiredAltitude": 1650, - "allspin": true, - "allspinminis": true, - "history": true, - "competitiveMode": false, - "sidebar": ["time", "apm", "pps"], - "stride": false + "requiredAltitude": 1650 }, "control": { "rightKey": "ArrowRight", diff --git a/src/data/gamemodes.json b/src/data/gamemodes.json index edb901a..3be3c76 100644 --- a/src/data/gamemodes.json +++ b/src/data/gamemodes.json @@ -1,146 +1,147 @@ -{ - "*": { - "settings": { - "gravitySpeed": 950, - "lockDelay": 600, - "maxLockMovements": 15, - "nextPieces": 5, - "allowLockout": false, - "preserveARR": true, - "allowHold": true, - "infiniteHold": false, - "allowQueueModify": false, - "allspin": false, - "allspinminis": false, - "history": false, - "sidebar": ["time", "apm", "pps"], - "stride": false - }, - "displayName": "Unset", - "objectiveText": "", - "goalStat": "", - "target": "", - "result": "", - "music": "", - "compmusic": "", - "startBoard": "", - "effects": [] - }, - "custom": { - "displayName": "Zen / Custom" - }, - "sprint": { - "displayName": "Sprint", - "objectiveText": "Lines", - "goalStat": "clearlines", - "target": "requiredLines", - "result": "time", - "settings": { - "requiredLines": 40, - "stride":true - } - }, - "ultra": { - "displayName": "Ultra", - "objectiveText": "Score", - "goalStat": "time", - "target": "timeLimit", - "result": "score", - "settings": { - "timeLimit": 120, - "sidebar": ["time", "score", "pps"] - } - }, - "attacker": { - "displayName": "Attacker", - "objectiveText": "Damage", - "goalStat": "attack", - "target": "requiredAttack", - "result": "time", - "settings": { - "requiredAttack": 100 - } - }, - "digger": { - "displayName": "Digger", - "objectiveText": "Remaining", - "goalStat": "cleargarbage", - "target": "requiredGarbage", - "result": "time", - "settings": { - "requiredGarbage": 100, - "sidebar": ["time", "dss", "pps"] - } - }, - "survival": { - "displayName": "Survival", - "objectiveText": "received", - "goalStat": "clearlines", - "target": "gameEnd", - "result": "time", - "settings": { - "survivalRate": 60, - "sidebar": ["time", "lpm", "pps"] - } - }, - "backfire": { - "displayName": "Backfire", - "objectiveText": "Sent", - "goalStat": "attack", - "target": "requiredAttack", - "result": "time", - "settings": { - "requiredAttack": 100, - "backfireMulti": 1 - } - }, - "combo": { - "displayName": "4w / Combo", - "objectiveText": "Time", - "goalStat": "time", - "target": "combobreak", - "result": "maxCombo", - "settings": { - "allspin": true, - "allspinminis": false, - "sidebar": ["combo", "pps"] - } - }, - "lookahead": { - "displayName": "Lookahead", - "objectiveText": "Lines", - "goalStat": "clearlines", - "target": "requiredLines", - "result": "time", - "settings": { - "requiredLines": 40, - "lookAheadPieces": 3, - "gravitySpeed": 1001, - "sidebar": ["time", "kpp", "pps"] - } - }, - "race": { - "displayName": "Race", - "objectiveText": "Level", - "goalStat": "level", - "target": "raceTarget", - "result": "time", - "settings": { - "raceTarget": 500, - "gravitySpeed": 0 - } - }, - "zenith": { - "displayName": "Climb", - "objectiveText": "Altitude", - "goalStat": "altitude", - "target": "requiredAltitude", - "result": "time", - "settings": { - "requiredAltitude": 1650, - "gravitySpeed": 950, - "allspin": true, - "allspinminis": true - } - } -} \ No newline at end of file +{ + "*": { + "settings": { + "gravitySpeed": 950, + "lockDelay": 600, + "maxLockMovements": 15, + "nextPieces": 5, + "allowLockout": false, + "preserveARR": true, + "allowHold": true, + "infiniteHold": false, + "allowQueueModify": false, + "allspin": false, + "allspinminis": false, + "history": false, + "sidebar": ["time", "apm", "pps"], + "stride": false + }, + "displayName": "Unset", + "objectiveText": "", + "goalStat": "", + "target": "", + "result": "", + "music": "", + "compmusic": "", + "startBoard": "", + "effects": [] + }, + "custom": { + "displayName": "Zen / Custom" + }, + "sprint": { + "displayName": "Sprint", + "objectiveText": "Lines", + "goalStat": "clearlines", + "target": "requiredLines", + "result": "time", + "settings": { + "requiredLines": 40, + "stride":true + } + }, + "ultra": { + "displayName": "Ultra", + "objectiveText": "Score", + "goalStat": "time", + "target": "timeLimit", + "result": "score", + "settings": { + "timeLimit": 120, + "sidebar": ["time", "score", "pps"] + } + }, + "attacker": { + "displayName": "Attacker", + "objectiveText": "Damage", + "goalStat": "attack", + "target": "requiredAttack", + "result": "time", + "settings": { + "requiredAttack": 100 + } + }, + "digger": { + "displayName": "Digger", + "objectiveText": "Remaining", + "goalStat": "cleargarbage", + "target": "requiredGarbage", + "result": "time", + "settings": { + "requiredGarbage": 100, + "sidebar": ["time", "dss", "pps"] + } + }, + "survival": { + "displayName": "Survival", + "objectiveText": "received", + "goalStat": "clearlines", + "target": "gameEnd", + "result": "time", + "settings": { + "survivalRate": 60, + "sidebar": ["time", "lpm", "pps"] + } + }, + "backfire": { + "displayName": "Backfire", + "objectiveText": "Sent", + "goalStat": "attack", + "target": "requiredAttack", + "result": "time", + "settings": { + "requiredAttack": 100, + "backfireMulti": 1 + } + }, + "combo": { + "displayName": "4w / Combo", + "objectiveText": "Time", + "goalStat": "time", + "target": "combobreak", + "result": "maxCombo", + "settings": { + "allspin": true, + "allspinminis": false, + "sidebar": ["combo", "pps"] + } + }, + "lookahead": { + "displayName": "Lookahead", + "objectiveText": "Lines", + "goalStat": "clearlines", + "target": "requiredLines", + "result": "time", + "settings": { + "requiredLines": 40, + "lookAheadPieces": 3, + "gravitySpeed": 1001, + "sidebar": ["time", "kpp", "pps"] + } + }, + "race": { + "displayName": "Race", + "objectiveText": "Level", + "goalStat": "tgm_level", + "target": "raceTarget", + "result": "grade", + "settings": { + "raceTarget": 999, + "gravitySpeed": 0, + "clearDelay": 420 + } + }, + "zenith": { + "displayName": "Climb", + "objectiveText": "Altitude", + "goalStat": "altitude", + "target": "requiredAltitude", + "result": "time", + "settings": { + "requiredAltitude": 1650, + "gravitySpeed": 950, + "allspin": true, + "allspinminis": true + } + } +} diff --git a/src/data/kicks.js b/src/data/kicks.js index 79e2f33..b56e122 100644 --- a/src/data/kicks.js +++ b/src/data/kicks.js @@ -1,25 +1,25 @@ -export const KickData = [[ // srs+ (tetrio) - [[0, 0], [-1, 0], [-1, -1], [0, 2], [-1, 2]], // 4 -> 1, 1 is north, ccw is 1 -> 4, ccw is * -1 - [[0, 0], [-1, 0], [-1, 1], [0, -2], [-1, -2]], // 1 -> 2 - [[0, 0], [1, 0], [1, -1], [0, 2], [1, 2]], // 2 -> 3 - [[0, 0], [1, 0], [1, 1], [0, -2], [1, -2]] // 3 -> 4 -], [ // I piece kicks - [[0, 0], [1, 0], [-2, 0], [1, -2], [-2, 1]], // 4 -> 1 - [[0, 0], [1, 0], [-2, 0], [1, 2], [-2, -1]], // 1 -> 2 - [[0, 0], [-1, 0], [2, 0], [-1, 2], [2, -1]], // 2 -> 3 - [[0, 0], [-1, 0], [2, 0], [-1, -2], [2, 1]] // 3 -> 4 -]]; - -export const KickData180 = [[ - [[0, 0], [0, -1], [-1, -1], [1, -1], [-1, 0], [1, 0]], // 3 -> 1 - [[0, 0], [-1, 0], [-1, 2], [-1, 1], [0, 2], [0, 1]], // 4 -> 2 - [[0, 0], [0, 1], [1, 1], [-1, 1], [1, 0], [-1, 0]], // 1 -> 3 - [[0, 0], [1, 0], [1, 2], [1, 1], [0, 2], [0, 1]] // 2 -> 4 -], [ // I piece kicks - [[0, 0], [0, -1]], // 3 -> 1 - [[0, 0], [-1, 0]], // 4 -> 2 - [[0, 0], [0, 1]], // 1 -> 3 - [[0, 0], [1, 0]], // 2 -> 4 -]]; - +export const KickData = [[ // srs+ (tetrio) + [[0, 0], [-1, 0], [-1, -1], [0, 2], [-1, 2]], // 4 -> 1, 1 is north, ccw is 1 -> 4, ccw is * -1 + [[0, 0], [-1, 0], [-1, 1], [0, -2], [-1, -2]], // 1 -> 2 + [[0, 0], [1, 0], [1, -1], [0, 2], [1, 2]], // 2 -> 3 + [[0, 0], [1, 0], [1, 1], [0, -2], [1, -2]] // 3 -> 4 +], [ // I piece kicks + [[0, 0], [1, 0], [-2, 0], [1, -2], [-2, 1]], // 4 -> 1 + [[0, 0], [1, 0], [-2, 0], [1, 2], [-2, -1]], // 1 -> 2 + [[0, 0], [-1, 0], [2, 0], [-1, 2], [2, -1]], // 2 -> 3 + [[0, 0], [-1, 0], [2, 0], [-1, -2], [2, 1]] // 3 -> 4 +]]; + +export const KickData180 = [[ + [[0, 0], [0, -1], [-1, -1], [1, -1], [-1, 0], [1, 0]], // 3 -> 1 + [[0, 0], [-1, 0], [-1, 2], [-1, 1], [0, 2], [0, 1]], // 4 -> 2 + [[0, 0], [0, 1], [1, 1], [-1, 1], [1, 0], [-1, 0]], // 1 -> 3 + [[0, 0], [1, 0], [1, 2], [1, 1], [0, 2], [0, 1]] // 2 -> 4 +], [ // I piece kicks + [[0, 0], [0, -1]], // 3 -> 1 + [[0, 0], [-1, 0]], // 4 -> 2 + [[0, 0], [0, 1]], // 1 -> 3 + [[0, 0], [1, 0]], // 2 -> 4 +]]; + // json has no comments :( \ No newline at end of file diff --git a/src/data/pieces.json b/src/data/pieces.json index d908b82..f4af680 100644 --- a/src/data/pieces.json +++ b/src/data/pieces.json @@ -1,47 +1,47 @@ -[{ - "name": "z", - "shape1": [[1, 1, 0], [0, 1, 1], [0, 0, 0]], - "shape2": [[0, 0, 1], [0, 1, 1], [0, 1, 0]], - "shape3": [[0, 0, 0], [1, 1, 0], [0, 1, 1]], - "shape4": [[0, 1, 0], [1, 1, 0], [1, 0, 0]], - "colour": "#D83A28" -}, { - "name": "s", - "shape1": [[0, 1, 1], [1, 1, 0], [0, 0, 0]], - "shape2": [[0, 1, 0], [0, 1, 1], [0, 0, 1]], - "shape3": [[0, 0, 0], [0, 1, 1], [1, 1, 0]], - "shape4": [[1, 0, 0], [1, 1, 0], [0, 1, 0]], - "colour": "#7ACD44" -}, { - "name": "o", - "shape1": [[1, 1], [1, 1]], - "colour": "#F2D74C" -}, { - "name": "t", - "shape1": [[0, 1, 0], [1, 1, 1], [0, 0, 0]], - "shape2": [[0, 1, 0], [0, 1, 1], [0, 1, 0]], - "shape3": [[0, 0, 0], [1, 1, 1], [0, 1, 0]], - "shape4": [[0, 1, 0], [1, 1, 0], [0, 1, 0]], - "colour": "#C132D0" -}, { - "name": "j", - "shape1": [[1, 0, 0], [1, 1, 1], [0, 0, 0]], - "shape2": [[0, 1, 1], [0, 1, 0], [0, 1, 0]], - "shape3": [[0, 0, 0], [1, 1, 1], [0, 0, 1]], - "shape4": [[0, 1, 0], [0, 1, 0], [1, 1, 0]], - "colour": "#3358DD" -}, { - "name": "l", - "shape1": [[0, 0, 1], [1, 1, 1], [0, 0, 0]], - "shape2": [[0, 1, 0], [0, 1, 0], [0, 1, 1]], - "shape3": [[0, 0, 0], [1, 1, 1], [1, 0, 0]], - "shape4": [[1, 1, 0], [0, 1, 0], [0, 1, 0]], - "colour": "#EDA93F" -}, { - "name": "i", - "shape1": [[0, 0, 0, 0], [1, 1, 1, 1], [0, 0, 0, 0], [0, 0, 0, 0]], - "shape2": [[0, 0, 1, 0], [0, 0, 1, 0], [0, 0, 1, 0], [0, 0, 1, 0]], - "shape3": [[0, 0, 0, 0], [0, 0, 0, 0], [1, 1, 1, 1], [0, 0, 0, 0]], - "shape4": [[0, 1, 0, 0], [0, 1, 0, 0], [0, 1, 0, 0], [0, 1, 0, 0]], - "colour": "#65DBC8" +[{ + "name": "z", + "shape1": [[1, 1, 0], [0, 1, 1], [0, 0, 0]], + "shape2": [[0, 0, 1], [0, 1, 1], [0, 1, 0]], + "shape3": [[0, 0, 0], [1, 1, 0], [0, 1, 1]], + "shape4": [[0, 1, 0], [1, 1, 0], [1, 0, 0]], + "colour": "#D83A28" +}, { + "name": "s", + "shape1": [[0, 1, 1], [1, 1, 0], [0, 0, 0]], + "shape2": [[0, 1, 0], [0, 1, 1], [0, 0, 1]], + "shape3": [[0, 0, 0], [0, 1, 1], [1, 1, 0]], + "shape4": [[1, 0, 0], [1, 1, 0], [0, 1, 0]], + "colour": "#7ACD44" +}, { + "name": "o", + "shape1": [[1, 1], [1, 1]], + "colour": "#F2D74C" +}, { + "name": "t", + "shape1": [[0, 1, 0], [1, 1, 1], [0, 0, 0]], + "shape2": [[0, 1, 0], [0, 1, 1], [0, 1, 0]], + "shape3": [[0, 0, 0], [1, 1, 1], [0, 1, 0]], + "shape4": [[0, 1, 0], [1, 1, 0], [0, 1, 0]], + "colour": "#C132D0" +}, { + "name": "j", + "shape1": [[1, 0, 0], [1, 1, 1], [0, 0, 0]], + "shape2": [[0, 1, 1], [0, 1, 0], [0, 1, 0]], + "shape3": [[0, 0, 0], [1, 1, 1], [0, 0, 1]], + "shape4": [[0, 1, 0], [0, 1, 0], [1, 1, 0]], + "colour": "#3358DD" +}, { + "name": "l", + "shape1": [[0, 0, 1], [1, 1, 1], [0, 0, 0]], + "shape2": [[0, 1, 0], [0, 1, 0], [0, 1, 1]], + "shape3": [[0, 0, 0], [1, 1, 1], [1, 0, 0]], + "shape4": [[1, 1, 0], [0, 1, 0], [0, 1, 0]], + "colour": "#EDA93F" +}, { + "name": "i", + "shape1": [[0, 0, 0, 0], [1, 1, 1, 1], [0, 0, 0, 0], [0, 0, 0, 0]], + "shape2": [[0, 0, 1, 0], [0, 0, 1, 0], [0, 0, 1, 0], [0, 0, 1, 0]], + "shape3": [[0, 0, 0, 0], [0, 0, 0, 0], [1, 1, 1, 1], [0, 0, 0, 0]], + "shape4": [[0, 1, 0, 0], [0, 1, 0, 0], [0, 1, 0, 0], [0, 1, 0, 0]], + "colour": "#65DBC8" }] \ No newline at end of file diff --git a/src/data/sfxlist.json b/src/data/sfxlist.json index f7bfecf..af07c37 100644 --- a/src/data/sfxlist.json +++ b/src/data/sfxlist.json @@ -1,194 +1,194 @@ -[ - { - "name": "allclear.mp3", - "path": "assets/sfx/allclear.mp3" - }, - { - "name": "btb_1.mp3", - "path": "assets/sfx/btb_1.mp3" - }, - { - "name": "clearbtb.mp3", - "path": "assets/sfx/clearbtb.mp3" - }, - { - "name": "clearline.mp3", - "path": "assets/sfx/clearline.mp3" - }, - { - "name": "clearquad.mp3", - "path": "assets/sfx/clearquad.mp3" - }, - { - "name": "clearspin.mp3", - "path": "assets/sfx/clearspin.mp3" - }, - { - "name": "damage_alert.mp3", - "path": "assets/sfx/damage_alert.mp3" - }, - { - "name": "failure.mp3", - "path": "assets/sfx/failure.mp3" - }, - { - "name": "finish.mp3", - "path": "assets/sfx/finish.mp3" - }, - { - "name": "garbage_in_large.mp3", - "path": "assets/sfx/garbage_in_large.mp3" - }, - { - "name": "garbage_in_small.mp3", - "path": "assets/sfx/garbage_in_small.mp3" - }, - { - "name": "harddrop.mp3", - "path": "assets/sfx/harddrop.mp3" - }, - { - "name": "hold.mp3", - "path": "assets/sfx/hold.mp3" - }, - { - "name": "menutap.mp3", - "path": "assets/sfx/menutap.mp3" - }, - { - "name": "menuclick.mp3", - "path": "assets/sfx/menuclick.mp3" - }, - { - "name": "move.mp3", - "path": "assets/sfx/move.mp3" - }, - { - "name": "retry.mp3", - "path": "assets/sfx/retry.mp3" - }, - { - "name": "rotate.mp3", - "path": "assets/sfx/rotate.mp3" - }, - { - "name": "spin.mp3", - "path": "assets/sfx/spin.mp3" - }, - { - "name": "thunder.mp3", - "path": "assets/sfx/thunder.mp3" - }, - { - "name": "topout.mp3", - "path": "assets/sfx/topout.mp3" - }, - { - "name": "undo.mp3", - "path": "assets/sfx/undo.mp3" - }, - { - "name": "redo.mp3", - "path": "assets/sfx/redo.mp3" - }, - { - "name": "personalbest.mp3", - "path": "assets/sfx/personalbest.mp3" - }, - { - "name": "pbstart.mp3", - "path": "assets/sfx/pbstart.mp3" - }, - { - "name": "pbend.mp3", - "path": "assets/sfx/pbend.mp3" - }, - { - "name": "speed_up.mp3", - "path": "assets/sfx/speed_up.mp3", - "type": "file" - }, - { - "name": "speed_down.mp3", - "path": "assets/sfx/speed_down.mp3", - "type": "file" - }, - { - "name": "levelup.mp3", - "path": "assets/sfx/levelup.mp3", - "type": "file" - }, - { - "name": "zenith_levelup.mp3", - "path": "assets/sfx/zenith_levelup.mp3", - "type": "file" - }, - { - "name": "combo_1.mp3", - "path": "assets/sfx/combo/combo_1.mp3" - }, - { - "name": "combo_10.mp3", - "path": "assets/sfx/combo/combo_10.mp3" - }, - { - "name": "combo_11.mp3", - "path": "assets/sfx/combo/combo_11.mp3" - }, - { - "name": "combo_12.mp3", - "path": "assets/sfx/combo/combo_12.mp3" - }, - { - "name": "combo_13.mp3", - "path": "assets/sfx/combo/combo_13.mp3" - }, - { - "name": "combo_14.mp3", - "path": "assets/sfx/combo/combo_14.mp3" - }, - { - "name": "combo_15.mp3", - "path": "assets/sfx/combo/combo_15.mp3" - }, - { - "name": "combo_16.mp3", - "path": "assets/sfx/combo/combo_16.mp3" - }, - { - "name": "combo_2.mp3", - "path": "assets/sfx/combo/combo_2.mp3" - }, - { - "name": "combo_3.mp3", - "path": "assets/sfx/combo/combo_3.mp3" - }, - { - "name": "combo_4.mp3", - "path": "assets/sfx/combo/combo_4.mp3" - }, - { - "name": "combo_5.mp3", - "path": "assets/sfx/combo/combo_5.mp3" - }, - { - "name": "combo_6.mp3", - "path": "assets/sfx/combo/combo_6.mp3" - }, - { - "name": "combo_7.mp3", - "path": "assets/sfx/combo/combo_7.mp3" - }, - { - "name": "combo_8.mp3", - "path": "assets/sfx/combo/combo_8.mp3" - }, - { - "name": "combo_9.mp3", - "path": "assets/sfx/combo/combo_9.mp3" - }, - { - "name": "hyperalert", - "path": "assets/sfx/hyperalert.mp3" - } +[ + { + "name": "allclear.mp3", + "path": "assets/sfx/allclear.mp3" + }, + { + "name": "btb_1.mp3", + "path": "assets/sfx/btb_1.mp3" + }, + { + "name": "clearbtb.mp3", + "path": "assets/sfx/clearbtb.mp3" + }, + { + "name": "clearline.mp3", + "path": "assets/sfx/clearline.mp3" + }, + { + "name": "clearquad.mp3", + "path": "assets/sfx/clearquad.mp3" + }, + { + "name": "clearspin.mp3", + "path": "assets/sfx/clearspin.mp3" + }, + { + "name": "damage_alert.mp3", + "path": "assets/sfx/damage_alert.mp3" + }, + { + "name": "failure.mp3", + "path": "assets/sfx/failure.mp3" + }, + { + "name": "finish.mp3", + "path": "assets/sfx/finish.mp3" + }, + { + "name": "garbage_in_large.mp3", + "path": "assets/sfx/garbage_in_large.mp3" + }, + { + "name": "garbage_in_small.mp3", + "path": "assets/sfx/garbage_in_small.mp3" + }, + { + "name": "harddrop.mp3", + "path": "assets/sfx/harddrop.mp3" + }, + { + "name": "hold.mp3", + "path": "assets/sfx/hold.mp3" + }, + { + "name": "menutap.mp3", + "path": "assets/sfx/menutap.mp3" + }, + { + "name": "menuclick.mp3", + "path": "assets/sfx/menuclick.mp3" + }, + { + "name": "move.mp3", + "path": "assets/sfx/move.mp3" + }, + { + "name": "retry.mp3", + "path": "assets/sfx/retry.mp3" + }, + { + "name": "rotate.mp3", + "path": "assets/sfx/rotate.mp3" + }, + { + "name": "spin.mp3", + "path": "assets/sfx/spin.mp3" + }, + { + "name": "thunder.mp3", + "path": "assets/sfx/thunder.mp3" + }, + { + "name": "topout.mp3", + "path": "assets/sfx/topout.mp3" + }, + { + "name": "undo.mp3", + "path": "assets/sfx/undo.mp3" + }, + { + "name": "redo.mp3", + "path": "assets/sfx/redo.mp3" + }, + { + "name": "personalbest.mp3", + "path": "assets/sfx/personalbest.mp3" + }, + { + "name": "pbstart.mp3", + "path": "assets/sfx/pbstart.mp3" + }, + { + "name": "pbend.mp3", + "path": "assets/sfx/pbend.mp3" + }, + { + "name": "speed_up.mp3", + "path": "assets/sfx/speed_up.mp3", + "type": "file" + }, + { + "name": "speed_down.mp3", + "path": "assets/sfx/speed_down.mp3", + "type": "file" + }, + { + "name": "levelup.mp3", + "path": "assets/sfx/levelup.mp3", + "type": "file" + }, + { + "name": "zenith_levelup.mp3", + "path": "assets/sfx/zenith_levelup.mp3", + "type": "file" + }, + { + "name": "combo_1.mp3", + "path": "assets/sfx/combo/combo_1.mp3" + }, + { + "name": "combo_10.mp3", + "path": "assets/sfx/combo/combo_10.mp3" + }, + { + "name": "combo_11.mp3", + "path": "assets/sfx/combo/combo_11.mp3" + }, + { + "name": "combo_12.mp3", + "path": "assets/sfx/combo/combo_12.mp3" + }, + { + "name": "combo_13.mp3", + "path": "assets/sfx/combo/combo_13.mp3" + }, + { + "name": "combo_14.mp3", + "path": "assets/sfx/combo/combo_14.mp3" + }, + { + "name": "combo_15.mp3", + "path": "assets/sfx/combo/combo_15.mp3" + }, + { + "name": "combo_16.mp3", + "path": "assets/sfx/combo/combo_16.mp3" + }, + { + "name": "combo_2.mp3", + "path": "assets/sfx/combo/combo_2.mp3" + }, + { + "name": "combo_3.mp3", + "path": "assets/sfx/combo/combo_3.mp3" + }, + { + "name": "combo_4.mp3", + "path": "assets/sfx/combo/combo_4.mp3" + }, + { + "name": "combo_5.mp3", + "path": "assets/sfx/combo/combo_5.mp3" + }, + { + "name": "combo_6.mp3", + "path": "assets/sfx/combo/combo_6.mp3" + }, + { + "name": "combo_7.mp3", + "path": "assets/sfx/combo/combo_7.mp3" + }, + { + "name": "combo_8.mp3", + "path": "assets/sfx/combo/combo_8.mp3" + }, + { + "name": "combo_9.mp3", + "path": "assets/sfx/combo/combo_9.mp3" + }, + { + "name": "hyperalert", + "path": "assets/sfx/hyperalert.mp3" + } ] \ No newline at end of file diff --git a/src/display/boardEffects.js b/src/display/boardEffects.js index e11887a..72ad0de 100644 --- a/src/display/boardEffects.js +++ b/src/display/boardEffects.js @@ -1,112 +1,112 @@ -import { pbTrackingStat } from "../data/data.js"; -import { Game } from "../game.js"; - -export class BoardEffects { - X = 0; - Y = 0; - dX = 0; - dY = 0; - friction = 0.75; - springConstant = 0.02; - targetX = 0; - targetY = 0; - R = 0; - dR = 0; - targetR = 0; - - hasPace = true; - paceCooldown = 0; - - divBoard = document.getElementById("board"); - divDanger = document.getElementById("dangerOverlay"); - border = document.getElementById('backborder') - backboard = document.getElementById('backboard') - - /** - * - * @param {Game} game - */ - constructor(game) { - this.game = game; - } - - move(forceX, forceY) { - this.dX += forceX; - this.dY += forceY; - - const newdx = this.targetX - this.X; - const newdy = this.targetY - this.Y; - const fX = newdx * this.springConstant; - const fY = newdy * this.springConstant; - - this.dX += fX; - this.dY += fY; - this.dX *= this.friction; - this.dY *= this.friction; - this.X += this.dX; - this.Y += this.dY; - - this.X = this.clamp(this.X, 0.5); - this.Y = this.clamp(this.Y, 0.5); - - if (this.X != 0 || this.Y != 0) { - this.divBoard.style.translate = `${this.X}px ${this.Y}px` - } - } - - rotate(torque) { - this.dR += torque; - let newangle = this.targetR - this.R; - const fangle = newangle * this.springConstant; - - this.dR += fangle; - this.dR *= this.friction; - this.R += this.dR; - this.R = this.clamp(this.R, 0.1); - - if (this.R != 0) { - this.divBoard.style.rotate = `${this.R}deg` - } - } - - clamp(num, min) { - if (num < min && num > -min) return 0; - return num - } - - rainbowBoard() { - const stats = this.game.stats; - const pbs = this.game.profilestats.personalBests; - const gamemode = this.game.settings.game.gamemode; - - if (!this.game.settings.display.rainbowPB || - !this.game.settings.game.competitiveMode || - stats.time < 0.5 || pbs[gamemode] == undefined) return; - if (this.paceCooldown > 0) { this.paceCooldown--; return; } - - const trackingStat = pbTrackingStat[this.game.modes.modeJSON.goalStat]; - const current = stats[trackingStat]; - const pbpace = pbs[gamemode].pbstats[trackingStat]; - if (current < pbpace && this.hasPace) { - this.game.sounds.playSound("pbend"); - this.toggleRainbow(false); - } else if (current >= pbpace && !this.hasPace) { - this.game.sounds.playSound("pbstart"); - this.toggleRainbow(true); - } - - this.paceCooldown = this.game.tickrate * 3; - } - - toggleRainbow(pace) { - this.border.style.setProperty('--blur-size', pace ? `0.3vmin` : `0vmin`) - this.border.style.setProperty('--blur-strength', pace ? '0.7vmin' : '0') - this.backboard.style.setProperty('--blur-strength', pace ? '0.5vmin' : '0') - this.hasPace = pace; - } - - toggleDangerBoard(inDanger) { - this.border.classList.toggle("boardDanger", inDanger); - this.divDanger.style.opacity = inDanger ? "0.1" : "0"; - } +import { pbTrackingStat } from "../data/data.js"; +import { Game } from "../game.js"; + +export class BoardEffects { + X = 0; + Y = 0; + dX = 0; + dY = 0; + friction = 0.75; + springConstant = 0.02; + targetX = 0; + targetY = 0; + R = 0; + dR = 0; + targetR = 0; + + hasPace = true; + paceCooldown = 0; + + divBoard = document.getElementById("board"); + divDanger = document.getElementById("dangerOverlay"); + border = document.getElementById('backborder') + backboard = document.getElementById('backboard') + + /** + * + * @param {Game} game + */ + constructor(game) { + this.game = game; + } + + move(forceX, forceY) { + this.dX += forceX; + this.dY += forceY; + + const newdx = this.targetX - this.X; + const newdy = this.targetY - this.Y; + const fX = newdx * this.springConstant; + const fY = newdy * this.springConstant; + + this.dX += fX; + this.dY += fY; + this.dX *= this.friction; + this.dY *= this.friction; + this.X += this.dX; + this.Y += this.dY; + + this.X = this.clamp(this.X, 0.5); + this.Y = this.clamp(this.Y, 0.5); + + if (this.X != 0 || this.Y != 0) { + this.divBoard.style.translate = `${this.X}px ${this.Y}px` + } + } + + rotate(torque) { + this.dR += torque; + let newangle = this.targetR - this.R; + const fangle = newangle * this.springConstant; + + this.dR += fangle; + this.dR *= this.friction; + this.R += this.dR; + this.R = this.clamp(this.R, 0.1); + + if (this.R != 0) { + this.divBoard.style.rotate = `${this.R}deg` + } + } + + clamp(num, min) { + if (num < min && num > -min) return 0; + return num + } + + rainbowBoard() { + const stats = this.game.stats; + const pbs = this.game.profilestats.personalBests; + const gamemode = this.game.settings.game.gamemode; + + if (!this.game.settings.display.rainbowPB || + !this.game.settings.game.competitiveMode || + stats.time < 0.5 || pbs[gamemode] == undefined) return; + if (this.paceCooldown > 0) { this.paceCooldown--; return; } + + const trackingStat = pbTrackingStat[this.game.modes.modeJSON.goalStat]; + const current = stats[trackingStat]; + const pbpace = pbs[gamemode].pbstats[trackingStat]; + if (current < pbpace && this.hasPace) { + this.game.sounds.playSound("pbend"); + this.toggleRainbow(false); + } else if (current >= pbpace && !this.hasPace) { + this.game.sounds.playSound("pbstart"); + this.toggleRainbow(true); + } + + this.paceCooldown = this.game.tickrate * 3; + } + + toggleRainbow(pace) { + this.border.style.setProperty('--blur-size', pace ? `0.3vmin` : `0vmin`) + this.border.style.setProperty('--blur-strength', pace ? '0.7vmin' : '0') + this.backboard.style.setProperty('--blur-strength', pace ? '0.5vmin' : '0') + this.hasPace = pace; + } + + toggleDangerBoard(inDanger) { + this.border.classList.toggle("boardDanger", inDanger); + this.divDanger.style.opacity = inDanger ? "0.1" : "0"; + } } \ No newline at end of file diff --git a/src/display/particles.js b/src/display/particles.js index f87b165..f2d3f87 100644 --- a/src/display/particles.js +++ b/src/display/particles.js @@ -1,227 +1,227 @@ -import { Game } from "../game.js"; - -class Point { - constructor(particleInfo, ctx) { - const { x, y, colour, size, life, dx, dy, sway, xF, yF, swayF, gravity, twinkle, twinkleTime } = particleInfo; - this.x = x; - this.y = y; - this.size = size; - this.colour = colour; - this.maxLife = life; - this.life = life; - this.dx = dx; - this.dy = dy; - this.sway = sway ?? 0; - this.frictionX = xF ?? 1; - this.frictionY = yF ?? 1; - this.frictionSway = swayF ?? 1; - this.gravity = gravity ?? 0; - this.twinkle = twinkle ?? false; - this.twinkleTime = twinkleTime ?? this.life; - - this.ctx = ctx; - } - - draw() { - this.ctx.globalAlpha = Math.max(0, this.life / this.maxLife); - this.ctx.fillStyle = this.colour; - this.ctx.beginPath(); - this.ctx.arc(this.x, this.y, this.size, 0, Math.PI * 2); - this.ctx.fill(); - } - - update() { - this.life -= 1; - this.x += this.dx + Math.sin(this.sway * this.life); - this.y += this.dy; - this.dx *= this.frictionX; - this.dy *= this.frictionY; - this.sway *= this.frictionSway - this.dy += this.gravity; - if (this.twinkle && this.life < this.twinkleTime) this.size = Math.abs((Math.sin(this.life / 15))) * 2; - } -} - -export class Particles { - particles = []; - - /** - * @param {Game} game - */ - constructor(game) { - this.game = game; - this.ctx = this.game.renderer.ctx; - } - - initBoard() { - this.boardWidth = this.game.renderer.boardWidth; - this.boardHeight = this.game.renderer.boardHeight; - this.minosize = this.game.boardrender.minoSize; - } - - spawnParticles(posX, posY, type, pieceWidth = 1, cw = false, colour = "white") { - if (!this.game.settings.display.particles) return - this.volume = this.game.settings.display.particleVolume; - this.size = this.game.settings.display.particleSize; - const [x, y] = [posX * this.minosize, (40 - posY) * this.minosize]; - if (type == "drop") this.creatDropParticles(x, y, colour, this.minosize * pieceWidth, -this.boardHeight); - if (type == "lock") this.createLockParticles(x, y, colour, this.minosize * pieceWidth, 10); - if (type == "clear") this.createClearParticles(x, y, colour, this.boardWidth, -10); - if (type == "pc") this.createPCParticles(x, y, this.boardWidth, 10); - if (type == "dangerboard") this.createDangerBoardParticles(x, this.boardHeight, colour, this.boardWidth, 10); - if (type == "dangersides") this.createDangerSidesParticles(x, y, "red", this.boardWidth, 0, 1); - if (type == "spin") this.createSpinParticles(x, y, colour, cw, this.minosize * pieceWidth, -this.minosize * pieceWidth); - if (type == "spike") this.createSpikeParticles(x, this.boardHeight, colour, this.boardWidth, -this.boardHeight); - if (type == "BTB") this.createBTBParticle(x, y, "gold", this.boardWidth, 0, this.boardHeight); - } - - creatDropParticles(x, y, colour, len, height) { - for (let i = 0; i < this.volume / 3; i++) { - const posX = x + Math.random() * len - len / 2; - const posY = y + Math.random() * height / 2; - const life = Math.random() * 35 + 70; - const dx = Math.random() * 1 - 0.5; - const dy = Math.random() * -1.2 - 2.4; - const sway = Math.random() * 0.04 - 0.02; - - const placeParticle = { x: posX, y: posY, colour, size: this.size, life, dx, dy, sway, xF: 0.95, yF: 0.95, swayF: 0.96 } - const particle = new Point(placeParticle, this.ctx); - this.particles.push(particle); - } - } - - createLockParticles(x, y, colour, len, height) { - for (let i = 0; i < this.volume / 4; i++) { - const posX = x + Math.random() * len - len / 2; - const posY = y + Math.random() * height; - const life = Math.random() * 15 + 30; - const dx = Math.random() * 1 - 0.5 + (posX - x) / 50; - const dy = Math.random() * -0.7 - 1.4; - - const clearParticle = { x: posX, y: posY, colour, size: this.size, life, dx, dy, xF: 0.96, yF: 0.96, gravity: 0.05 } - const particle = new Point(clearParticle, this.ctx); - this.particles.push(particle); - } - } - - createClearParticles(x, y, colour, len, height) { - for (let i = 0; i < this.volume; i++) { - const posX = x + Math.random() * len - const posY = y + Math.random() * height; - const life = Math.random() * 20 + 40; - const dx = Math.random() * 1.5 - 0.75 + (posX - x) / 200; - const dy = Math.random() * -0.8 - 1.5; - - const clearParticle = { x: posX, y: posY, colour, size: this.size, life, dx, dy, yF: 0.99, gravity: 0.1 } - const particle = new Point(clearParticle, this.ctx); - this.particles.push(particle); - } - } - - createPCParticles(x, y, len, height) { - for (let i = 0; i < this.volume; i++) { - const posX = x + Math.random() * len - const posY = y + Math.random() * height; - const life = Math.random() * 100 + 200; - const dx = Math.random() * 1.5 - 0.75; - const dy = Math.random() * -8 - 2; - const colour = `hsl(${Math.random() * 360}, 80%, 60%)` - - const pcParticle = { x: posX, y: posY, colour, size: this.size, life, dx, dy, xF: 0.98, yF: 0.98, twinkle: true, twinkleTime: 130 } - const particle = new Point(pcParticle, this.ctx); - this.particles.push(particle); - } - } - - createDangerBoardParticles(x, y, colour, len, height) { - for (let i = 0; i < this.volume / 10; i++) { - if (Math.random() > this.volume / 500) continue - const posX = x + Math.random() * len - const posY = y + Math.random() * height / 2; - const life = Math.random() * 100 + 200; - const dx = Math.random() * 1 - 0.5; - const dy = Math.random() * -1.2 - 2.4; - const sway = Math.random() * 0.005 - 0.0025; - - const dangerParticle = { x: posX, y: posY, colour, size: this.size, life, dx, dy, sway, swayF: 0.98 } - const particle = new Point(dangerParticle, this.ctx); - this.particles.push(particle); - } - } - - createDangerSidesParticles(x, y, colour, width, len, height) { - for (let i = 0; i < this.volume / 10; i++) { - if (Math.random() > this.volume / 250) continue - const direction = Math.random() > 0.5; - - const posX = (direction ? 0 : width) + x + Math.random() * len - const posY = y + Math.random() * height; - const life = Math.random() * 15 + 30; - const dx = (direction ? 1 : -1) * (Math.random() * 2 + 2); - const dy = Math.random() * 2 - 1; - - const dangerSideParticle = { x: posX, y: posY, colour, size: this.size, life, dx, dy, gravity: 0.05 } - const particle = new Point(dangerSideParticle, this.ctx); - this.particles.push(particle); - } - } - - createSpinParticles(x, y, colour, cw, len, height) { - len *= 0.5; - height *= 0.5; - for (let i = 0; i < this.volume / 3; i++) { - const posX = x + Math.random() * len - len / 2 - const posY = y + Math.random() * height - height / 2; - const life = Math.random() * 35 + 70; - let dx = Math.random() * 1 - 0.5 + (posY - y) / 30; - let dy = Math.random() * 1 - 0.5 + (posX - x) / 30; - if (cw) { dx *= -1 } else { dy *= -1 } - - const spinParticle = { x: posX, y: posY, colour, size: this.size, life, dx, dy, xF: 0.98, yF: 0.98 } - const particle = new Point(spinParticle, this.ctx); - this.particles.push(particle); - } - } - - createSpikeParticles(x, y, colour, len, height) { - for (let i = 0; i < this.volume / 2; i++) { - const posX = x + Math.random() * len - const posY = y + Math.random() * height / 2; - const life = Math.random() * 35 + 70; - const dx = Math.random() * 1 - 0.5; - const dy = Math.random() * 1 - 0.5; - - const spikeParticle = { x: posX, y: posY, colour, size: this.size, life, dx, dy, xF: 0.96, yF: 0.96, twinkle: true } - const particle = new Point(spikeParticle, this.ctx); - this.particles.push(particle); - } - } - - createBTBParticle(x, y, colour, width, len, height) { - for (let i = 0; i < this.volume * 2; i++) { - const direction = Math.random() > 0.5; - - const posX = (direction ? 0 : width) + x + Math.random() * len - const posY = y + Math.random() * height; - const life = Math.random() * 25 + 50; - const dx = (direction ? 1 : -1) * (Math.random() * 2); - const dy = Math.random() * 2 - 1; - - const BTBParticle = { x: posX, y: posY, colour, size: this.size, life, dx, dy, gravity: 0.15 } - const particle = new Point(BTBParticle, this.ctx); - this.particles.push(particle); - } - } - - clearParticles() { - this.particles = []; - } - - update() { - this.particles = this.particles.filter(p => p.life > 0); - this.particles.forEach(particle => { - particle.update(); - particle.draw(); - }); - } +import { Game } from "../game.js"; + +class Point { + constructor(particleInfo, ctx) { + const { x, y, colour, size, life, dx, dy, sway, xF, yF, swayF, gravity, twinkle, twinkleTime } = particleInfo; + this.x = x; + this.y = y; + this.size = size; + this.colour = colour; + this.maxLife = life; + this.life = life; + this.dx = dx; + this.dy = dy; + this.sway = sway ?? 0; + this.frictionX = xF ?? 1; + this.frictionY = yF ?? 1; + this.frictionSway = swayF ?? 1; + this.gravity = gravity ?? 0; + this.twinkle = twinkle ?? false; + this.twinkleTime = twinkleTime ?? this.life; + + this.ctx = ctx; + } + + draw() { + this.ctx.globalAlpha = Math.max(0, this.life / this.maxLife); + this.ctx.fillStyle = this.colour; + this.ctx.beginPath(); + this.ctx.arc(this.x, this.y, this.size, 0, Math.PI * 2); + this.ctx.fill(); + } + + update() { + this.life -= 1; + this.x += this.dx + Math.sin(this.sway * this.life); + this.y += this.dy; + this.dx *= this.frictionX; + this.dy *= this.frictionY; + this.sway *= this.frictionSway + this.dy += this.gravity; + if (this.twinkle && this.life < this.twinkleTime) this.size = Math.abs((Math.sin(this.life / 15))) * 2; + } +} + +export class Particles { + particles = []; + + /** + * @param {Game} game + */ + constructor(game) { + this.game = game; + this.ctx = this.game.renderer.ctx; + } + + initBoard() { + this.boardWidth = this.game.renderer.boardWidth; + this.boardHeight = this.game.renderer.boardHeight; + this.minosize = this.game.boardrender.minoSize; + } + + spawnParticles(posX, posY, type, pieceWidth = 1, cw = false, colour = "white") { + if (!this.game.settings.display.particles) return + this.volume = this.game.settings.display.particleVolume; + this.size = this.game.settings.display.particleSize; + const [x, y] = [posX * this.minosize, (40 - posY) * this.minosize]; + if (type == "drop") this.creatDropParticles(x, y, colour, this.minosize * pieceWidth, -this.boardHeight); + if (type == "lock") this.createLockParticles(x, y, colour, this.minosize * pieceWidth, 10); + if (type == "clear") this.createClearParticles(x, y, colour, this.boardWidth, -10); + if (type == "pc") this.createPCParticles(x, y, this.boardWidth, 10); + if (type == "dangerboard") this.createDangerBoardParticles(x, this.boardHeight, colour, this.boardWidth, 10); + if (type == "dangersides") this.createDangerSidesParticles(x, y, "red", this.boardWidth, 0, 1); + if (type == "spin") this.createSpinParticles(x, y, colour, cw, this.minosize * pieceWidth, -this.minosize * pieceWidth); + if (type == "spike") this.createSpikeParticles(x, this.boardHeight, colour, this.boardWidth, -this.boardHeight); + if (type == "BTB") this.createBTBParticle(x, y, "gold", this.boardWidth, 0, this.boardHeight); + } + + creatDropParticles(x, y, colour, len, height) { + for (let i = 0; i < this.volume / 3; i++) { + const posX = x + Math.random() * len - len / 2; + const posY = y + Math.random() * height / 2; + const life = Math.random() * 35 + 70; + const dx = Math.random() * 1 - 0.5; + const dy = Math.random() * -1.2 - 2.4; + const sway = Math.random() * 0.04 - 0.02; + + const placeParticle = { x: posX, y: posY, colour, size: this.size, life, dx, dy, sway, xF: 0.95, yF: 0.95, swayF: 0.96 } + const particle = new Point(placeParticle, this.ctx); + this.particles.push(particle); + } + } + + createLockParticles(x, y, colour, len, height) { + for (let i = 0; i < this.volume / 4; i++) { + const posX = x + Math.random() * len - len / 2; + const posY = y + Math.random() * height; + const life = Math.random() * 15 + 30; + const dx = Math.random() * 1 - 0.5 + (posX - x) / 50; + const dy = Math.random() * -0.7 - 1.4; + + const clearParticle = { x: posX, y: posY, colour, size: this.size, life, dx, dy, xF: 0.96, yF: 0.96, gravity: 0.05 } + const particle = new Point(clearParticle, this.ctx); + this.particles.push(particle); + } + } + + createClearParticles(x, y, colour, len, height) { + for (let i = 0; i < this.volume; i++) { + const posX = x + Math.random() * len + const posY = y + Math.random() * height; + const life = Math.random() * 20 + 40; + const dx = Math.random() * 1.5 - 0.75 + (posX - x) / 200; + const dy = Math.random() * -0.8 - 1.5; + + const clearParticle = { x: posX, y: posY, colour, size: this.size, life, dx, dy, yF: 0.99, gravity: 0.1 } + const particle = new Point(clearParticle, this.ctx); + this.particles.push(particle); + } + } + + createPCParticles(x, y, len, height) { + for (let i = 0; i < this.volume; i++) { + const posX = x + Math.random() * len + const posY = y + Math.random() * height; + const life = Math.random() * 100 + 200; + const dx = Math.random() * 1.5 - 0.75; + const dy = Math.random() * -8 - 2; + const colour = `hsl(${Math.random() * 360}, 80%, 60%)` + + const pcParticle = { x: posX, y: posY, colour, size: this.size, life, dx, dy, xF: 0.98, yF: 0.98, twinkle: true, twinkleTime: 130 } + const particle = new Point(pcParticle, this.ctx); + this.particles.push(particle); + } + } + + createDangerBoardParticles(x, y, colour, len, height) { + for (let i = 0; i < this.volume / 10; i++) { + if (Math.random() > this.volume / 500) continue + const posX = x + Math.random() * len + const posY = y + Math.random() * height / 2; + const life = Math.random() * 100 + 200; + const dx = Math.random() * 1 - 0.5; + const dy = Math.random() * -1.2 - 2.4; + const sway = Math.random() * 0.005 - 0.0025; + + const dangerParticle = { x: posX, y: posY, colour, size: this.size, life, dx, dy, sway, swayF: 0.98 } + const particle = new Point(dangerParticle, this.ctx); + this.particles.push(particle); + } + } + + createDangerSidesParticles(x, y, colour, width, len, height) { + for (let i = 0; i < this.volume / 10; i++) { + if (Math.random() > this.volume / 250) continue + const direction = Math.random() > 0.5; + + const posX = (direction ? 0 : width) + x + Math.random() * len + const posY = y + Math.random() * height; + const life = Math.random() * 15 + 30; + const dx = (direction ? 1 : -1) * (Math.random() * 2 + 2); + const dy = Math.random() * 2 - 1; + + const dangerSideParticle = { x: posX, y: posY, colour, size: this.size, life, dx, dy, gravity: 0.05 } + const particle = new Point(dangerSideParticle, this.ctx); + this.particles.push(particle); + } + } + + createSpinParticles(x, y, colour, cw, len, height) { + len *= 0.5; + height *= 0.5; + for (let i = 0; i < this.volume / 3; i++) { + const posX = x + Math.random() * len - len / 2 + const posY = y + Math.random() * height - height / 2; + const life = Math.random() * 35 + 70; + let dx = Math.random() * 1 - 0.5 + (posY - y) / 30; + let dy = Math.random() * 1 - 0.5 + (posX - x) / 30; + if (cw) { dx *= -1 } else { dy *= -1 } + + const spinParticle = { x: posX, y: posY, colour, size: this.size, life, dx, dy, xF: 0.98, yF: 0.98 } + const particle = new Point(spinParticle, this.ctx); + this.particles.push(particle); + } + } + + createSpikeParticles(x, y, colour, len, height) { + for (let i = 0; i < this.volume / 2; i++) { + const posX = x + Math.random() * len + const posY = y + Math.random() * height / 2; + const life = Math.random() * 35 + 70; + const dx = Math.random() * 1 - 0.5; + const dy = Math.random() * 1 - 0.5; + + const spikeParticle = { x: posX, y: posY, colour, size: this.size, life, dx, dy, xF: 0.96, yF: 0.96, twinkle: true } + const particle = new Point(spikeParticle, this.ctx); + this.particles.push(particle); + } + } + + createBTBParticle(x, y, colour, width, len, height) { + for (let i = 0; i < this.volume * 2; i++) { + const direction = Math.random() > 0.5; + + const posX = (direction ? 0 : width) + x + Math.random() * len + const posY = y + Math.random() * height; + const life = Math.random() * 25 + 50; + const dx = (direction ? 1 : -1) * (Math.random() * 2); + const dy = Math.random() * 2 - 1; + + const BTBParticle = { x: posX, y: posY, colour, size: this.size, life, dx, dy, gravity: 0.15 } + const particle = new Point(BTBParticle, this.ctx); + this.particles.push(particle); + } + } + + clearParticles() { + this.particles = []; + } + + update() { + this.particles = this.particles.filter(p => p.life > 0); + this.particles.forEach(particle => { + particle.update(); + particle.draw(); + }); + } } \ No newline at end of file diff --git a/src/display/renderBoard.js b/src/display/renderBoard.js index 5853757..f62d7e1 100644 --- a/src/display/renderBoard.js +++ b/src/display/renderBoard.js @@ -1,160 +1,160 @@ -import { Game } from "../game.js"; - -export class BoardRenderer { - boardAlpha = 1; - queueAlpha = 1; - justPlacedCoords = []; - justPlacedAlpha = 1; - minoSize; - texture; - flashTimes = []; - - divlock = document.getElementById("lockTimer"); - - /** - * - * @param {Game} game - */ - constructor(game) { - this.game = game; - } - - // board rendering - getOpacity(cell, cntx, x, y) { - if (cntx != this.game.renderer.ctx) return; - if (this.divlock.value != 0 && cell.includes("A") && this.game.settings.game.gamemode != "lookahead") { - return 1 - (this.divlock.value / 250); - } - if (this.game.settings.game.gamemode == "lookahead") { - for (let [posX, posY] of this.justPlacedCoords) { - if (posX == x && posY == y && cntx == this.game.renderer.ctx) { - return Math.max(this.justPlacedAlpha, this.boardAlpha).toFixed(2); - } - } - } - - return this.boardAlpha.toFixed(2); - } - - setMinoFlash(cntx, x, y, posX, posY) { - if (cntx != this.game.renderer.ctx) return; - for (let { c, t } of this.flashTimes) { - if (c[0] == x && c[1] == y) { - const dx = (t / 15) * this.minoSize; - posX = posX + this.minoSize; - posY = posY + this.minoSize; - this.drawTriangle(posX, posY, posX - dx, posY - dx, cntx); - } - } - } - - drawTriangle(posX, posY, x, y, cntx) { - cntx.globalAlpha = 0.4; - cntx.fillStyle = "#ffffff" - cntx.beginPath(); - cntx.moveTo(posX, posY); - cntx.lineTo(posX, y); - cntx.lineTo(x, posY); - cntx.lineTo(posX, posY); - cntx.fill(); - } - - toHex(num) { - const hex = Math.round((+num * 255) / 100).toString(16); - return hex.length > 1 ? hex : 0 + hex; - } - - loadImage(src) { - this.texture = new Image(372, 30); - this.texture.src = src; - this.texture.onload = () => { - this.game.renderer.updateNext(); - } - } - - getPiece(cntx, cell) { - return this.game.hold.occured && cntx == this.game.renderer.ctxH ? "hold" : cell; - } - - getTexture(name) { - const pieces = ["z", "l", "o", "s", "i", "j", "t", "shadow", "hold", "g", "darkg", "topout"] - const x = pieces.indexOf(name.toLowerCase()) * 31; - const y = 0; - const width = 30; - const height = 30; - return { x, y, width, height }; - } - - getShadowOpacity() { - const opacity = this.game.settings.display.shadowOpacity / 100; - if (this.game.settings.game.gamemode == "lookahead") return (opacity * this.boardAlpha).toFixed(2); - return opacity; - } - - removeCoords([x, y]) { - this.justPlacedCoords = this.justPlacedCoords.filter(c => !(c[0] == x && c[1] == y)); - this.flashTimes = this.flashTimes.filter(({ c, t }) => !(c[0] == x && c[1] == y)); - } - - renderBorder(ctx, x, y) { - const type = this.game.settings.display.gridType; - if (type == "round") { - ctx.beginPath(); - ctx.roundRect(x, y, this.minoSize - 1, this.minoSize - 1, this.minoSize / 4); - ctx.stroke(); - } else if (type == "square") { - ctx.beginPath(); - ctx.strokeRect(x, y, this.minoSize - 1, this.minoSize - 1, this.minoSize / 4); - ctx.stroke(); - } else if (type == "dot") { - ctx.beginPath(); - ctx.arc(x + this.minoSize, y + this.minoSize, 1, 0, 2 * Math.PI); - ctx.stroke(); - ctx.beginPath(); - ctx.arc(x, y, 1, 0, 2 * Math.PI); - ctx.stroke(); - } - } - - /** - * @param {CanvasRenderingContext2D} cntx - */ - renderToCanvas(cntx, grid, yPosChange, [dx, dy] = [0, 0], width, height) { - cntx.clearRect(0, 0, width, height); - grid.forEach((row, y) => { - row.forEach((col, x) => { - const [posX, posY] = [x * this.minoSize, (yPosChange - y) * this.minoSize]; - const cell = col.split(" "); - cntx.lineWidth = 1; - - if (cell.includes("A") || cell.includes("S")) { // active piece or stopped piece - cntx.globalAlpha = this.getOpacity(cell, cntx, x, y) ?? this.queueAlpha.toFixed(2); - const p = this.getTexture(this.getPiece(cntx, cell[1])); - cntx.drawImage(this.texture, p.x, p.y, p.width, p.height, posX + dx, posY + dy, this.minoSize, this.minoSize); - this.setMinoFlash(cntx, x, y, posX + dx, posY + dy); - } - else if (cell.includes("NP") && this.game.renderer.inDanger) { // next piece overlay - cntx.globalAlpha = 0.32; - const p = this.getTexture("topout"); - cntx.drawImage(this.texture, p.x, p.y, p.width, p.height, posX + dx, posY + dy, this.minoSize, this.minoSize); - } - else if (cell.includes("Sh")) { // shadow piece - cntx.globalAlpha = this.getShadowOpacity(); - const piece = this.game.settings.display.colouredShadow ? this.game.falling.piece.name : "shadow"; - const p = this.getTexture(piece); - cntx.drawImage(this.texture, p.x, p.y, p.width, p.height, posX + dx, posY + dy, this.minoSize, this.minoSize); - } - else if (y < 20 && this.game.settings.display.showGrid && cntx == this.game.renderer.ctx) { // grid - cntx.globalAlpha = 1 - cntx.strokeStyle = "#ffffff" + this.toHex(this.game.settings.display.gridopacity); - this.renderBorder(cntx, posX, posY); - } - }); - }); - - // flash - this.flashTimes = this.flashTimes - .filter(({ c, t }) => t > 0) - .map(({ c, t }) => { return { c, t: t - 1 }; }); - } +import { Game } from "../game.js"; + +export class BoardRenderer { + boardAlpha = 1; + queueAlpha = 1; + justPlacedCoords = []; + justPlacedAlpha = 1; + minoSize; + texture; + flashTimes = []; + + divlock = document.getElementById("lockTimer"); + + /** + * + * @param {Game} game + */ + constructor(game) { + this.game = game; + } + + // board rendering + getOpacity(cell, cntx, x, y) { + if (cntx != this.game.renderer.ctx) return; + if (this.divlock.value != 0 && cell.includes("A") && this.game.settings.game.gamemode != "lookahead") { + return 1 - (this.divlock.value / 250); + } + if (this.game.settings.game.gamemode == "lookahead") { + for (let [posX, posY] of this.justPlacedCoords) { + if (posX == x && posY == y && cntx == this.game.renderer.ctx) { + return Math.max(this.justPlacedAlpha, this.boardAlpha).toFixed(2); + } + } + } + + return this.boardAlpha.toFixed(2); + } + + setMinoFlash(cntx, x, y, posX, posY) { + if (cntx != this.game.renderer.ctx) return; + for (let { c, t } of this.flashTimes) { + if (c[0] == x && c[1] == y) { + const dx = (t / 15) * this.minoSize; + posX = posX + this.minoSize; + posY = posY + this.minoSize; + this.drawTriangle(posX, posY, posX - dx, posY - dx, cntx); + } + } + } + + drawTriangle(posX, posY, x, y, cntx) { + cntx.globalAlpha = 0.4; + cntx.fillStyle = "#ffffff" + cntx.beginPath(); + cntx.moveTo(posX, posY); + cntx.lineTo(posX, y); + cntx.lineTo(x, posY); + cntx.lineTo(posX, posY); + cntx.fill(); + } + + toHex(num) { + const hex = Math.round((+num * 255) / 100).toString(16); + return hex.length > 1 ? hex : 0 + hex; + } + + loadImage(src) { + this.texture = new Image(372, 30); + this.texture.src = src; + this.texture.onload = () => { + this.game.renderer.updateNext(); + } + } + + getPiece(cntx, cell) { + return this.game.hold.occured && cntx == this.game.renderer.ctxH ? "hold" : cell; + } + + getTexture(name) { + const pieces = ["z", "l", "o", "s", "i", "j", "t", "shadow", "hold", "g", "darkg", "topout"] + const x = pieces.indexOf(name.toLowerCase()) * 31; + const y = 0; + const width = 30; + const height = 30; + return { x, y, width, height }; + } + + getShadowOpacity() { + const opacity = this.game.settings.display.shadowOpacity / 100; + if (this.game.settings.game.gamemode == "lookahead") return (opacity * this.boardAlpha).toFixed(2); + return opacity; + } + + removeCoords([x, y]) { + this.justPlacedCoords = this.justPlacedCoords.filter(c => !(c[0] == x && c[1] == y)); + this.flashTimes = this.flashTimes.filter(({ c, t }) => !(c[0] == x && c[1] == y)); + } + + renderBorder(ctx, x, y) { + const type = this.game.settings.display.gridType; + if (type == "round") { + ctx.beginPath(); + ctx.roundRect(x, y, this.minoSize - 1, this.minoSize - 1, this.minoSize / 4); + ctx.stroke(); + } else if (type == "square") { + ctx.beginPath(); + ctx.strokeRect(x, y, this.minoSize - 1, this.minoSize - 1, this.minoSize / 4); + ctx.stroke(); + } else if (type == "dot") { + ctx.beginPath(); + ctx.arc(x + this.minoSize, y + this.minoSize, 1, 0, 2 * Math.PI); + ctx.stroke(); + ctx.beginPath(); + ctx.arc(x, y, 1, 0, 2 * Math.PI); + ctx.stroke(); + } + } + + /** + * @param {CanvasRenderingContext2D} cntx + */ + renderToCanvas(cntx, grid, yPosChange, [dx, dy] = [0, 0], width, height) { + cntx.clearRect(0, 0, width, height); + grid.forEach((row, y) => { + row.forEach((col, x) => { + const [posX, posY] = [x * this.minoSize, (yPosChange - y) * this.minoSize]; + const cell = col.split(" "); + cntx.lineWidth = 1; + + if (cell.includes("A") || cell.includes("S")) { // active piece or stopped piece + cntx.globalAlpha = this.getOpacity(cell, cntx, x, y) ?? this.queueAlpha.toFixed(2); + const p = this.getTexture(this.getPiece(cntx, cell[1])); + cntx.drawImage(this.texture, p.x, p.y, p.width, p.height, posX + dx, posY + dy, this.minoSize, this.minoSize); + this.setMinoFlash(cntx, x, y, posX + dx, posY + dy); + } + else if (cell.includes("NP") && this.game.renderer.inDanger) { // next piece overlay + cntx.globalAlpha = 0.32; + const p = this.getTexture("topout"); + cntx.drawImage(this.texture, p.x, p.y, p.width, p.height, posX + dx, posY + dy, this.minoSize, this.minoSize); + } + else if (cell.includes("Sh")) { // shadow piece + cntx.globalAlpha = this.getShadowOpacity(); + const piece = this.game.settings.display.colouredShadow ? this.game.falling.piece.name : "shadow"; + const p = this.getTexture(piece); + cntx.drawImage(this.texture, p.x, p.y, p.width, p.height, posX + dx, posY + dy, this.minoSize, this.minoSize); + } + else if (y < 20 && this.game.settings.display.showGrid && cntx == this.game.renderer.ctx) { // grid + cntx.globalAlpha = 1 + cntx.strokeStyle = "#ffffff" + this.toHex(this.game.settings.display.gridopacity); + this.renderBorder(cntx, posX, posY); + } + }); + }); + + // flash + this.flashTimes = this.flashTimes + .filter(({ c, t }) => t > 0) + .map(({ c, t }) => { return { c, t: t - 1 }; }); + } } \ No newline at end of file diff --git a/src/display/renderer.js b/src/display/renderer.js index d761704..fd2a6fb 100644 --- a/src/display/renderer.js +++ b/src/display/renderer.js @@ -1,369 +1,369 @@ -import { Game } from "../game.js"; -import pieces from "../data/pieces.json" with { type: "json" }; -import { defaultSkins, statDecimals, statsSecondary as statsSecondaries } from "../data/data.js"; - -export class Renderer { - boardHeight; - boardWidth; - holdHeight; - holdWidth; - nextHeight; - nextWidth; - holdQueueGrid = []; - nextQueueGrid = []; - inDanger; - texttimeouts = {}; - resetAnimLength = 30; - resetAnimCurrent = 30; - - sidebarStats; - sidebarFixed; - sidebarSecondary; - /** @type {CanvasRenderingContext2D} */ - ctx; - ctxN; - ctxH; - - canvasField = document.getElementById("playingfield"); - canvasNext = document.getElementById("next"); - canvasHold = document.getElementById("hold"); - divBoard = document.getElementById("board"); - divBackboard = document.getElementById("backboard"); - divLinesSent = document.getElementById("linessent"); - elementEditPieces = document.getElementById("editMenuPieces"); - - elementStats1 = document.getElementById("stats1"); - elementStats2 = document.getElementById("stats2"); - elementStats3 = document.getElementById("stats3"); - elementStatname1 = document.getElementById("statName1"); - elementStatname2 = document.getElementById("statName2"); - elementStatname3 = document.getElementById("statName3"); - elementSmallStat1 = document.getElementById("smallStat1"); - elementSmallStat2 = document.getElementById("smallStat2"); - elementSmallStat3 = document.getElementById("smallStat3"); - - /** - * @param {Game} game - */ - constructor(game) { - this.game = game; - this.board = game.board; - - this.ctx = this.canvasField.getContext("2d"); - this.ctxN = this.canvasNext.getContext("2d"); - this.ctxH = this.canvasHold.getContext("2d"); - } - - renderingLoop() { - this.game.boardrender.renderToCanvas(this.ctx, this.game.board.boardState, 39, [0, 0], this.boardWidth, this.boardHeight); - this.game.boardeffects.move(0, 0); - this.game.boardeffects.rotate(0); - this.game.particles.update(); - this.dangerParticles(); - this.resetAnimation(); - requestAnimationFrame(this.renderingLoop.bind(this)) - if(this.game.settings.game.gamemode == "ultra" && Math.floor(this.game.stats.time) == 60) this.renderTimeLeft("60S LEFT") - if(this.game.settings.game.gamemode == "ultra" && Math.floor(this.game.stats.time) == 90) this.renderTimeLeft("30S LEFT") - } - - sizeCanvas() { - this.renderStyles(); - [this.canvasField, this.canvasNext, this.canvasHold].forEach(c => { - c.width = Math.round(c.offsetWidth / 10) * 10; - c.height = Math.round(c.offsetHeight / 40) * 40; - }); - this.divBoard.style.width = `${this.canvasField.width}px`; - this.divBoard.style.height = `${this.canvasField.height / 2}px`; - this.game.boardrender.minoSize = this.canvasField.width / 10; - this.boardWidth = this.canvasField.offsetWidth; - this.boardHeight = this.canvasField.offsetHeight; - this.nextWidth = this.canvasNext.offsetWidth; - this.nextHeight = this.canvasNext.offsetHeight; - this.holdWidth = this.canvasHold.offsetWidth; - this.holdHeight = this.canvasHold.offsetHeight; - } - - updateNext() { - this.nextQueueGrid = [...Array(15)].map(() => [...Array(4)].map(() => "")); - - const next5 = this.game.bag.getFirstFive(); - next5.forEach((name, idx) => { - const piece = this.getPiece(name); - let [dx, dy] = [0, 3 * (4 - idx)]; - if (piece.name == "o") [dx, dy] = [dx + 1, dy + 1]; // shift o piece - const coords = this.board.pieceToCoords(piece.shape1); - coords.forEach(([x, y]) => (this.nextQueueGrid[y + dy][x + dx] = "A " + piece.name)); - }); - - this.game.boardrender.renderToCanvas(this.ctxN, this.nextQueueGrid, 15, [0, 0], this.nextWidth, this.nextHeight); - if (this.game.settings.game.gamemode == 'lookahead' || !this.game.settings.display.colouredQueues) return; - this.canvasNext.style.outlineColor = this.game.bag.nextPiece().colour; - } - - getPiece(name) { - if (name == "G") return { colour: "gray" } - return pieces.filter(p => p.name == name)[0]; - } - - updateHold() { - this.holdQueueGrid = [...Array(3)].map(() => [...Array(4)].map(() => "")); - this.clearHold(); - if (this.game.hold.piece == undefined) return; - - const name = this.game.hold.piece.name; - const isO = name == "o", - isI = name == "i"; - const [dx, dy] = [isO ? 1 : 0, isO ? 1 : isI ? -1 : 0]; - const coords = this.board.pieceToCoords(this.game.hold.piece.shape1); - - coords.forEach(([x, y]) => (this.holdQueueGrid[y + dy][x + dx] = "A " + name)); - const len = Math.round(this.game.boardrender.minoSize / 2); - const [shiftX, shiftY] = [isO || isI ? 0 : len, isI ? 0 : len]; - - this.game.boardrender.renderToCanvas(this.ctxH, this.holdQueueGrid, 2, [shiftX, shiftY], this.holdWidth, this.holdHeight); - if (this.game.settings.game.gamemode == 'lookahead' || !this.game.settings.display.colouredQueues) return; - const colour = this.game.hold.occured ? "gray" : this.game.hold.piece.colour - this.canvasHold.style.outline = `0.2vh solid ${colour}`; - } - - clearHold() { - this.ctxH.clearRect(0, 0, this.canvasHold.offsetWidth + 10, this.canvasHold.offsetHeight); - } - - renderDanger() { - const condition = - this.game.board.getMinos("S").some(c => c[1] > 16) && // any mino if above row 16 - this.game.settings.game.gamemode != 'combo'; // not combo mode - if (condition && !this.inDanger) { - this.game.sounds.playSound("damage_alert"); - } - this.game.boardeffects.toggleDangerBoard(condition) - this.inDanger = condition; - } - - renderActionText(damagetype, isBTB, isPC, damage, linecount) { - if (damagetype != "") this.setText("cleartext", damagetype, 2000); - if (this.game.stats.combo > 0) - this.setText("combotext", `Combo ${this.game.stats.combo}`, 2000); - if (isBTB && this.game.stats.btbCount > 0) - this.setText("btbtext", `BTB ${this.game.stats.btbCount} `, 2000); - if (isPC) this.setText("pctext", "Perfect Clear", 2000); - if (damage > 0) this.setText("linessent", `${this.game.mechanics.spikeCounter}`, 1500); - - if (this.game.mechanics.spikeCounter > 0) this.spikePattern("white", 1); - if (this.game.mechanics.spikeCounter >= 10) this.spikePattern("red", 1.1); - if (this.game.mechanics.spikeCounter >= 20) this.spikePattern("lime", 1.2); - - // audio - if (isPC) this.game.sounds.playSound("allclear"); - if (this.game.stats.btbCount == 2 && isBTB) this.game.sounds.playSound("btb_1"); - if (linecount >= 4 && this.game.stats.btbCount > 0) { - this.game.sounds.playSound("clearbtb"); - } else if (linecount >= 4) { - this.game.sounds.playSound("clearquad"); - } else if (linecount > 0 && this.game.mechanics.isTspin) { - this.game.sounds.playSound("clearspin"); - } else if (linecount > 0 && this.game.mechanics.isAllspin && this.game.settings.game.allspin) { - this.game.sounds.playSound("clearspin"); - } else if (linecount > 0) { - this.game.sounds.playSound("clearline"); - } - if (this.game.mechanics.spikeCounter >= 15) this.game.sounds.playSound("thunder", false); - if (this.game.stats.combo > 0) - this.game.sounds.playSound(`combo_${this.game.stats.combo > 16 ? 16 : this.game.stats.combo}`); - } - - resetActionText() { - ['btbtext', 'cleartext', 'combotext', 'pctext', 'linessent'].forEach(id => { - document.getElementById(id).style.opacity = "0"; - }) - } - - spikePattern(colour, size) { - this.divLinesSent.style.color = colour; - this.divLinesSent.style.textShadow = `0 0 1vh ${colour}`; - this.divLinesSent.style.fontSize = `${3.5 * size}vh`; - } - - setText(id, text, duration) { - const textbox = document.getElementById(id); - textbox.textContent = text; - textbox.style.transform = "translateX(-2%)"; - textbox.style.opacity = "1"; - if (this.texttimeouts[id] != 0) this.stopTimeout(id); - this.texttimeouts[id] = setTimeout(() => { - textbox.style.opacity = "0"; - textbox.style.transform = "translateX(2%)"; - this.game.mechanics.spikeCounter = 0; - }, duration); - } - - stopTimeout(name) { - clearTimeout(this.texttimeouts[name]); - this.texttimeouts[name] = 0; - } - - renderStyles() { - // custom background - const bg = this.game.settings.display.background; - if (bg == "") bg = "#080B0C"; - document.body.style.background = (bg[0] == "#") ? bg : `url("${bg}") no-repeat center center` - document.body.style.backgroundSize = "cover"; - - const height = Number(this.game.settings.display.boardHeight) + 10; - this.divBoard.style.transform = `scale(${height}%) translate(-50%, -50%)`; - this.canvasHold.style.outline = `0.2vh solid #dbeaf3`; - - // board opacity - const background = `rgba(0, 0, 0, ${Number(this.game.settings.display.boardOpacity) / 100})`; - this.divBackboard.style.backgroundColor = background; - document.body.style.setProperty('--background', background); - - // skins - let skin = this.game.settings.display.skin; - if (defaultSkins.includes(skin)) skin = `./assets/skins/${skin}.png`; - this.game.boardrender.loadImage(skin); - - // sidebar constants - this.sidebarStats = this.game.settings.game.sidebar; - this.sidebarFixed = this.sidebarStats.map(stat => this.createReverseLookup(statDecimals)[stat]); - this.sidebarSecondary = this.sidebarStats.map(stat => statsSecondaries[stat] ?? "None"); - - this.sidebarStats.forEach((stat, index) => { - if (stat == "None") stat = "" - this[`elementStatname${index + 1}`].textContent = stat; - }) - } - - renderSidebar() { - this.sidebarStats.forEach((stat, index) => { - if (stat == "None") { // no stat - this[`elementStats${index + 1}`].textContent = ""; - return; - }; - const displayStat = this.game.stats[stat].toFixed(this.sidebarFixed[index]); - this[`elementStats${index + 1}`].textContent = displayStat; - - if (this.sidebarSecondary[index]) { - const displaySecond = this.game.stats[this.sidebarSecondary[index]] - this[`elementSmallStat${index + 1}`].textContent = displaySecond; - } - }) - } - - renderTimeLeft(text){ - const e = document.getElementById("timeLeftText") - if (this.texttimeouts["timeLeft"] != 0){ - this.stopTimeout("timeLeft"); - //e.classList.remove("warn"); - } - e.textContent = text - e.classList.add("warn") - this.texttimeouts["timeLeft"] = setTimeout(() => { - e.classList.remove("warn"); - }, 3000); - } - - createReverseLookup(obj) { - const reverseLookup = {} - for (const [key, array] of Object.entries(obj)) { - array.forEach(item => { - reverseLookup[item] = key; - }); - } - return reverseLookup - } - - updateAlpha() { - if (this.game.settings.game.gamemode != 'lookahead') return; - const update = (type, amount) => { - if (this.game.stats.checkInvis()) { - if (this.game.boardrender[type] <= 0) { - this.game.boardrender[type] = 1; - this.updateNext(); - this.updateHold(); - } - } else { - if (this.game.boardrender[type] > 0) { - this.game.boardrender[type] += -amount / this.game.tickrate; - this.updateNext(); - this.updateHold(); - } else { - this.game.boardrender[type] = 0; - } - } - } - update("boardAlpha", 3) - update("queueAlpha", 3) - update("justPlacedAlpha", 6) - - } - - setEditPieceColours() { - const elPieces = [...this.elementEditPieces.children]; - elPieces.forEach(elpiece => { - const pieceid = elpiece.id.split("_")[0]; - elpiece.style.backgroundColor = this.getPiece(pieceid).colour - }) - } - - bounceBoard(direction) { - const force = Number(this.game.settings.display.boardBounce); - const forces = { "LEFT": [-force, 0], "RIGHT": [force, 0], "DOWN": [0, force], }; - this.game.boardeffects.move(...forces[direction]); - } - - rotateBoard(type) { - const force = Number(this.game.settings.display.boardBounce) * 0.5; - const forces = { "CW": force, "CCW": -force } - this.game.boardeffects.rotate(forces[type]); - } - - dangerParticles() { - if (!this.inDanger) return; - this.game.particles.spawnParticles(0, 0, "dangerboard"); - this.game.particles.spawnParticles(0, 20, "dangersides"); - } - - resetAnimation() { - if (this.resetAnimCurrent >= this.resetAnimLength * 2) return; - this.resetAnimCurrent++; - if (this.game.boardrender.boardAlpha < 0.99) this.game.boardrender.boardAlpha += 2 / this.resetAnimLength; - if (this.resetAnimCurrent > this.resetAnimLength) return; - - - const progress = this.resetAnimCurrent / this.resetAnimLength; - const startY = this.boardHeight / 2; - const dx = this.boardWidth; - const dy = dx + this.boardHeight / 2; - - const fillTriangle = (p, colour) => { - this.ctx.globalAlpha = 1; - this.ctx.fillStyle = colour; - this.ctx.beginPath(); - this.ctx.moveTo(0, startY - dy * p); - this.ctx.lineTo(dy * p, startY); - this.ctx.lineTo(0, startY + dy * p); - this.ctx.lineTo(0, 0); - this.ctx.fill(); - } - // fill triangle - const progress1 = this.easeInOutCubic(progress); - fillTriangle(progress1, 'white'); - - // clear smaller triangle - const progress2 = this.easeInOutCubic(progress - 0.1); - fillTriangle(progress2, 'black'); - - - if (this.resetAnimCurrent == this.resetAnimLength) { // finished - this.game.startGame(); - this.game.controls.resetting = false; - this.game.boardrender.boardAlpha = 0; - } - } - - easeInOutCubic(x) { - return -(Math.cos(Math.PI * x) - 1) / 2; - } -} +import { Game } from "../game.js"; +import pieces from "../data/pieces.json" with { type: "json" }; +import { defaultSkins, statDecimals, statsSecondary as statsSecondaries } from "../data/data.js"; + +export class Renderer { + boardHeight; + boardWidth; + holdHeight; + holdWidth; + nextHeight; + nextWidth; + holdQueueGrid = []; + nextQueueGrid = []; + inDanger; + texttimeouts = {}; + resetAnimLength = 30; + resetAnimCurrent = 30; + + sidebarStats; + sidebarFixed; + sidebarSecondary; + /** @type {CanvasRenderingContext2D} */ + ctx; + ctxN; + ctxH; + + canvasField = document.getElementById("playingfield"); + canvasNext = document.getElementById("next"); + canvasHold = document.getElementById("hold"); + divBoard = document.getElementById("board"); + divBackboard = document.getElementById("backboard"); + divLinesSent = document.getElementById("linessent"); + elementEditPieces = document.getElementById("editMenuPieces"); + + elementStats1 = document.getElementById("stats1"); + elementStats2 = document.getElementById("stats2"); + elementStats3 = document.getElementById("stats3"); + elementStatname1 = document.getElementById("statName1"); + elementStatname2 = document.getElementById("statName2"); + elementStatname3 = document.getElementById("statName3"); + elementSmallStat1 = document.getElementById("smallStat1"); + elementSmallStat2 = document.getElementById("smallStat2"); + elementSmallStat3 = document.getElementById("smallStat3"); + + /** + * @param {Game} game + */ + constructor(game) { + this.game = game; + this.board = game.board; + + this.ctx = this.canvasField.getContext("2d"); + this.ctxN = this.canvasNext.getContext("2d"); + this.ctxH = this.canvasHold.getContext("2d"); + } + + renderingLoop() { + this.game.boardrender.renderToCanvas(this.ctx, this.game.board.boardState, 39, [0, 0], this.boardWidth, this.boardHeight); + this.game.boardeffects.move(0, 0); + this.game.boardeffects.rotate(0); + this.game.particles.update(); + this.dangerParticles(); + this.resetAnimation(); + requestAnimationFrame(this.renderingLoop.bind(this)) + if(this.game.settings.game.gamemode == "ultra" && Math.floor(this.game.stats.time) == 60) this.renderTimeLeft("60S LEFT") + if(this.game.settings.game.gamemode == "ultra" && Math.floor(this.game.stats.time) == 90) this.renderTimeLeft("30S LEFT") + } + + sizeCanvas() { + this.renderStyles(); + [this.canvasField, this.canvasNext, this.canvasHold].forEach(c => { + c.width = Math.round(c.offsetWidth / 10) * 10; + c.height = Math.round(c.offsetHeight / 40) * 40; + }); + this.divBoard.style.width = `${this.canvasField.width}px`; + this.divBoard.style.height = `${this.canvasField.height / 2}px`; + this.game.boardrender.minoSize = this.canvasField.width / 10; + this.boardWidth = this.canvasField.offsetWidth; + this.boardHeight = this.canvasField.offsetHeight; + this.nextWidth = this.canvasNext.offsetWidth; + this.nextHeight = this.canvasNext.offsetHeight; + this.holdWidth = this.canvasHold.offsetWidth; + this.holdHeight = this.canvasHold.offsetHeight; + } + + updateNext() { + this.nextQueueGrid = [...Array(15)].map(() => [...Array(4)].map(() => "")); + + const next5 = this.game.bag.getFirstFive(); + next5.forEach((name, idx) => { + const piece = this.getPiece(name); + let [dx, dy] = [0, 3 * (4 - idx)]; + if (piece.name == "o") [dx, dy] = [dx + 1, dy + 1]; // shift o piece + const coords = this.board.pieceToCoords(piece.shape1); + coords.forEach(([x, y]) => (this.nextQueueGrid[y + dy][x + dx] = "A " + piece.name)); + }); + + this.game.boardrender.renderToCanvas(this.ctxN, this.nextQueueGrid, 15, [0, 0], this.nextWidth, this.nextHeight); + if (this.game.settings.game.gamemode == 'lookahead' || !this.game.settings.display.colouredQueues) return; + this.canvasNext.style.outlineColor = this.game.bag.nextPiece().colour; + } + + getPiece(name) { + if (name == "G") return { colour: "gray" } + return pieces.filter(p => p.name == name)[0]; + } + + updateHold() { + this.holdQueueGrid = [...Array(3)].map(() => [...Array(4)].map(() => "")); + this.clearHold(); + if (this.game.hold.piece == undefined) return; + + const name = this.game.hold.piece.name; + const isO = name == "o", + isI = name == "i"; + const [dx, dy] = [isO ? 1 : 0, isO ? 1 : isI ? -1 : 0]; + const coords = this.board.pieceToCoords(this.game.hold.piece.shape1); + + coords.forEach(([x, y]) => (this.holdQueueGrid[y + dy][x + dx] = "A " + name)); + const len = Math.round(this.game.boardrender.minoSize / 2); + const [shiftX, shiftY] = [isO || isI ? 0 : len, isI ? 0 : len]; + + this.game.boardrender.renderToCanvas(this.ctxH, this.holdQueueGrid, 2, [shiftX, shiftY], this.holdWidth, this.holdHeight); + if (this.game.settings.game.gamemode == 'lookahead' || !this.game.settings.display.colouredQueues) return; + const colour = this.game.hold.occured ? "gray" : this.game.hold.piece.colour + this.canvasHold.style.outline = `0.2vh solid ${colour}`; + } + + clearHold() { + this.ctxH.clearRect(0, 0, this.canvasHold.offsetWidth + 10, this.canvasHold.offsetHeight); + } + + renderDanger() { + const condition = + this.game.board.getMinos("S").some(c => c[1] > 16) && // any mino if above row 16 + this.game.settings.game.gamemode != 'combo'; // not combo mode + if (condition && !this.inDanger) { + this.game.sounds.playSound("damage_alert"); + } + this.game.boardeffects.toggleDangerBoard(condition) + this.inDanger = condition; + } + + renderActionText(damagetype, isBTB, isPC, damage, linecount) { + if (damagetype != "") this.setText("cleartext", damagetype, 2000); + if (this.game.stats.combo > 0) + this.setText("combotext", `Combo ${this.game.stats.combo}`, 2000); + if (isBTB && this.game.stats.btbCount > 0) + this.setText("btbtext", `BTB ${this.game.stats.btbCount} `, 2000); + if (isPC) this.setText("pctext", "Perfect Clear", 2000); + if (damage > 0) this.setText("linessent", `${this.game.mechanics.spikeCounter}`, 1500); + + if (this.game.mechanics.spikeCounter > 0) this.spikePattern("white", 1); + if (this.game.mechanics.spikeCounter >= 10) this.spikePattern("red", 1.1); + if (this.game.mechanics.spikeCounter >= 20) this.spikePattern("lime", 1.2); + + // audio + if (isPC) this.game.sounds.playSound("allclear"); + if (this.game.stats.btbCount == 2 && isBTB) this.game.sounds.playSound("btb_1"); + if (linecount >= 4 && this.game.stats.btbCount > 0) { + this.game.sounds.playSound("clearbtb"); + } else if (linecount >= 4) { + this.game.sounds.playSound("clearquad"); + } else if (linecount > 0 && this.game.mechanics.isTspin) { + this.game.sounds.playSound("clearspin"); + } else if (linecount > 0 && this.game.mechanics.isAllspin && this.game.settings.game.allspin) { + this.game.sounds.playSound("clearspin"); + } else if (linecount > 0) { + this.game.sounds.playSound("clearline"); + } + if (this.game.mechanics.spikeCounter >= 15) this.game.sounds.playSound("thunder", false); + if (this.game.stats.combo > 0) + this.game.sounds.playSound(`combo_${this.game.stats.combo > 16 ? 16 : this.game.stats.combo}`); + } + + resetActionText() { + ['btbtext', 'cleartext', 'combotext', 'pctext', 'linessent'].forEach(id => { + document.getElementById(id).style.opacity = "0"; + }) + } + + spikePattern(colour, size) { + this.divLinesSent.style.color = colour; + this.divLinesSent.style.textShadow = `0 0 1vh ${colour}`; + this.divLinesSent.style.fontSize = `${3.5 * size}vh`; + } + + setText(id, text, duration) { + const textbox = document.getElementById(id); + textbox.textContent = text; + textbox.style.transform = "translateX(-2%)"; + textbox.style.opacity = "1"; + if (this.texttimeouts[id] != 0) this.stopTimeout(id); + this.texttimeouts[id] = setTimeout(() => { + textbox.style.opacity = "0"; + textbox.style.transform = "translateX(2%)"; + this.game.mechanics.spikeCounter = 0; + }, duration); + } + + stopTimeout(name) { + clearTimeout(this.texttimeouts[name]); + this.texttimeouts[name] = 0; + } + + renderStyles() { + // custom background + const bg = this.game.settings.display.background; + if (bg == "") bg = "#080B0C"; + document.body.style.background = (bg[0] == "#") ? bg : `url("${bg}") no-repeat center center` + document.body.style.backgroundSize = "cover"; + + const height = Number(this.game.settings.display.boardHeight) + 10; + this.divBoard.style.transform = `scale(${height}%) translate(-50%, -50%)`; + this.canvasHold.style.outline = `0.2vh solid #dbeaf3`; + + // board opacity + const background = `rgba(0, 0, 0, ${Number(this.game.settings.display.boardOpacity) / 100})`; + this.divBackboard.style.backgroundColor = background; + document.body.style.setProperty('--background', background); + + // skins + let skin = this.game.settings.display.skin; + if (defaultSkins.includes(skin)) skin = `./assets/skins/${skin}.png`; + this.game.boardrender.loadImage(skin); + + // sidebar constants + this.sidebarStats = this.game.settings.game.sidebar; + this.sidebarFixed = this.sidebarStats.map(stat => this.createReverseLookup(statDecimals)[stat]); + this.sidebarSecondary = this.sidebarStats.map(stat => statsSecondaries[stat] ?? "None"); + + this.sidebarStats.forEach((stat, index) => { + if (stat == "None") stat = "" + this[`elementStatname${index + 1}`].textContent = stat; + }) + } + + renderSidebar() { + this.sidebarStats.forEach((stat, index) => { + if (stat == "None") { // no stat + this[`elementStats${index + 1}`].textContent = ""; + return; + }; + const displayStat = this.game.stats[stat].toFixed(this.sidebarFixed[index]); + this[`elementStats${index + 1}`].textContent = displayStat; + + if (this.sidebarSecondary[index]) { + const displaySecond = this.game.stats[this.sidebarSecondary[index]] + this[`elementSmallStat${index + 1}`].textContent = displaySecond; + } + }) + } + + renderTimeLeft(text){ + const e = document.getElementById("timeLeftText") + if (this.texttimeouts["timeLeft"] != 0){ + this.stopTimeout("timeLeft"); + //e.classList.remove("warn"); + } + e.textContent = text + e.classList.add("warn") + this.texttimeouts["timeLeft"] = setTimeout(() => { + e.classList.remove("warn"); + }, 3000); + } + + createReverseLookup(obj) { + const reverseLookup = {} + for (const [key, array] of Object.entries(obj)) { + array.forEach(item => { + reverseLookup[item] = key; + }); + } + return reverseLookup + } + + updateAlpha() { + if (this.game.settings.game.gamemode != 'lookahead') return; + const update = (type, amount) => { + if (this.game.stats.checkInvis()) { + if (this.game.boardrender[type] <= 0) { + this.game.boardrender[type] = 1; + this.updateNext(); + this.updateHold(); + } + } else { + if (this.game.boardrender[type] > 0) { + this.game.boardrender[type] += -amount / this.game.tickrate; + this.updateNext(); + this.updateHold(); + } else { + this.game.boardrender[type] = 0; + } + } + } + update("boardAlpha", 3) + update("queueAlpha", 3) + update("justPlacedAlpha", 6) + + } + + setEditPieceColours() { + const elPieces = [...this.elementEditPieces.children]; + elPieces.forEach(elpiece => { + const pieceid = elpiece.id.split("_")[0]; + elpiece.style.backgroundColor = this.getPiece(pieceid).colour + }) + } + + bounceBoard(direction) { + const force = Number(this.game.settings.display.boardBounce); + const forces = { "LEFT": [-force, 0], "RIGHT": [force, 0], "DOWN": [0, force], }; + this.game.boardeffects.move(...forces[direction]); + } + + rotateBoard(type) { + const force = Number(this.game.settings.display.boardBounce) * 0.5; + const forces = { "CW": force, "CCW": -force } + this.game.boardeffects.rotate(forces[type]); + } + + dangerParticles() { + if (!this.inDanger) return; + this.game.particles.spawnParticles(0, 0, "dangerboard"); + this.game.particles.spawnParticles(0, 20, "dangersides"); + } + + resetAnimation() { + if (this.resetAnimCurrent >= this.resetAnimLength * 2) return; + this.resetAnimCurrent++; + if (this.game.boardrender.boardAlpha < 0.99) this.game.boardrender.boardAlpha += 2 / this.resetAnimLength; + if (this.resetAnimCurrent > this.resetAnimLength) return; + + + const progress = this.resetAnimCurrent / this.resetAnimLength; + const startY = this.boardHeight / 2; + const dx = this.boardWidth; + const dy = dx + this.boardHeight / 2; + + const fillTriangle = (p, colour) => { + this.ctx.globalAlpha = 1; + this.ctx.fillStyle = colour; + this.ctx.beginPath(); + this.ctx.moveTo(0, startY - dy * p); + this.ctx.lineTo(dy * p, startY); + this.ctx.lineTo(0, startY + dy * p); + this.ctx.lineTo(0, 0); + this.ctx.fill(); + } + // fill triangle + const progress1 = this.easeInOutCubic(progress); + fillTriangle(progress1, 'white'); + + // clear smaller triangle + const progress2 = this.easeInOutCubic(progress - 0.1); + fillTriangle(progress2, 'black'); + + + if (this.resetAnimCurrent == this.resetAnimLength) { // finished + this.game.startGame(); + this.game.controls.resetting = false; + this.game.boardrender.boardAlpha = 0; + } + } + + easeInOutCubic(x) { + return -(Math.cos(Math.PI * x) - 1) / 2; + } +} diff --git a/src/features/editboard.js b/src/features/editboard.js index 194d45e..ebff330 100644 --- a/src/features/editboard.js +++ b/src/features/editboard.js @@ -1,119 +1,119 @@ -import { Game } from "../game.js"; - -export class BoardEditor { - clickareasdiv = document.getElementById("clickareas"); - mousedown = false; - currentMode = "fill"; - fillPiece = 'G'; - fillRow = false; - override = false; - - elementEditButton = document.getElementById("editButton"); - - /** - * @param {Game} game - */ - constructor(game) { - this.game = game; - this.board = game.board; - } - - addListeners() { - // delegate listeners for efficiency - document.body.addEventListener("mousedown", (e) => { - if (e.target.classList.contains('clickmino')) { - const j = Number(e.target.dataset.x) - const i = Number(e.target.dataset.y) - if (this.game.settings.game.gamemode != 'custom') return; - if (this.fillRow) { this.fillWholeRow([j, 19 - i]) } - else { this.fillCell([j, 19 - i]); } - } - }); - - document.body.addEventListener("mouseenter", (e) => { - if (e.target.classList.contains('clickmino')) { - const j = Number(e.target.dataset.x) - const i = Number(e.target.dataset.y) - if (this.game.settings.game.gamemode != 'custom') return; - e.target.classList.add('highlighting') - if (this.mousedown) { - if (this.fillRow) { this.fillWholeRow([j, 19 - i]) } - else { this.fillCell([j, 19 - i]); } - } - } - }, true); - - document.body.addEventListener("mouseleave", (e) => { - if (e.target.classList.contains('clickmino')) { - e.target.classList.remove('highlighting') - } - }, true); - - document.body.addEventListener("mouseup", () => { - if (this.mousedown) this.game.history.save(); - this.mousedown = false; - }); - - for (let i = 0; i < 20; i++) { - for (let j = 0; j < 10; j++) { - const clickarea = document.createElement("div"); - clickarea.classList.add("clickmino"); - clickarea.dataset.x = j.toString(); - clickarea.dataset.y = i.toString(); - this.clickareasdiv.appendChild(clickarea); - } - } - } - - fillCell([x, y]) { - if (!this.mousedown) this.currentMode = this.board.checkMino([x, y], "S") ? "remove" : "fill"; - this.mousedown = true; - if (this.board.checkMino([x, y], "A")) return; - if (!this.override && this.currentMode == "fill" && this.board.checkMino([x, y], "S")) return; - this.board.setValue([x, y], this.currentMode == "fill" ? "S " + this.fillPiece : ""); - this.game.mechanics.setShadow(); - } - - fillWholeRow([x, y]) { - for (let i = 0; i < 10; i++) { - this.board.setValue([i, y], x == i ? "" : "S G"); - this.mousedown = true; - } - } - - convertToMap() { - const board = this.game.board.boardState; - const next = this.game.bag.nextPieces; - const hold = this.game.hold.piece == null ? "" : this.game.hold.piece.name; - const currPiece = this.game.falling.piece.name; - - let boardstring = board.toReversed().flatMap(row => { - return row.map(col => { - col = col.replace("Sh", "").replace("NP", "").replace("G", "#"); - if (col.length == 1) col = "" - if (col[0] == "A") col = ""; - if (col[0] == "S") col = col[2]; - if (col.trim() == "") col = "_"; - return col; - }) - }).join("") - return `${boardstring}?${currPiece},${next.flat()}?${hold}` - - } - - convertFromMap(string) { - let [board, next, hold] = string.split("?"); - board = board.match(/.{1,10}/g).toReversed().map(row => { - return row.split("").map(col => { - col = col.replace("#", "G").replace("_", "") - if (col != "") col = `S ${col}` - return col - }); - }) - return { board, next, hold } - } - - setEditButton(bool) { - this.elementEditButton.style.display = bool ? "flex" : "none"; - } +import { Game } from "../game.js"; + +export class BoardEditor { + clickareasdiv = document.getElementById("clickareas"); + mousedown = false; + currentMode = "fill"; + fillPiece = 'G'; + fillRow = false; + override = false; + + elementEditButton = document.getElementById("editButton"); + + /** + * @param {Game} game + */ + constructor(game) { + this.game = game; + this.board = game.board; + } + + addListeners() { + // delegate listeners for efficiency + document.body.addEventListener("mousedown", (e) => { + if (e.target.classList.contains('clickmino')) { + const j = Number(e.target.dataset.x) + const i = Number(e.target.dataset.y) + if (this.game.settings.game.gamemode != 'custom') return; + if (this.fillRow) { this.fillWholeRow([j, 19 - i]) } + else { this.fillCell([j, 19 - i]); } + } + }); + + document.body.addEventListener("mouseenter", (e) => { + if (e.target.classList.contains('clickmino')) { + const j = Number(e.target.dataset.x) + const i = Number(e.target.dataset.y) + if (this.game.settings.game.gamemode != 'custom') return; + e.target.classList.add('highlighting') + if (this.mousedown) { + if (this.fillRow) { this.fillWholeRow([j, 19 - i]) } + else { this.fillCell([j, 19 - i]); } + } + } + }, true); + + document.body.addEventListener("mouseleave", (e) => { + if (e.target.classList.contains('clickmino')) { + e.target.classList.remove('highlighting') + } + }, true); + + document.body.addEventListener("mouseup", () => { + if (this.mousedown) this.game.history.save(); + this.mousedown = false; + }); + + for (let i = 0; i < 20; i++) { + for (let j = 0; j < 10; j++) { + const clickarea = document.createElement("div"); + clickarea.classList.add("clickmino"); + clickarea.dataset.x = j.toString(); + clickarea.dataset.y = i.toString(); + this.clickareasdiv.appendChild(clickarea); + } + } + } + + fillCell([x, y]) { + if (!this.mousedown) this.currentMode = this.board.checkMino([x, y], "S") ? "remove" : "fill"; + this.mousedown = true; + if (this.board.checkMino([x, y], "A")) return; + if (!this.override && this.currentMode == "fill" && this.board.checkMino([x, y], "S")) return; + this.board.setValue([x, y], this.currentMode == "fill" ? "S " + this.fillPiece : ""); + this.game.mechanics.setShadow(); + } + + fillWholeRow([x, y]) { + for (let i = 0; i < 10; i++) { + this.board.setValue([i, y], x == i ? "" : "S G"); + this.mousedown = true; + } + } + + convertToMap() { + const board = this.game.board.boardState; + const next = this.game.bag.nextPieces; + const hold = this.game.hold.piece == null ? "" : this.game.hold.piece.name; + const currPiece = this.game.falling.piece.name; + + let boardstring = board.toReversed().flatMap(row => { + return row.map(col => { + col = col.replace("Sh", "").replace("NP", "").replace("G", "#"); + if (col.length == 1) col = "" + if (col[0] == "A") col = ""; + if (col[0] == "S") col = col[2]; + if (col.trim() == "") col = "_"; + return col; + }) + }).join("") + return `${boardstring}?${currPiece},${next.flat()}?${hold}` + + } + + convertFromMap(string) { + let [board, next, hold] = string.split("?"); + board = board.match(/.{1,10}/g).toReversed().map(row => { + return row.split("").map(col => { + col = col.replace("#", "G").replace("_", "") + if (col != "") col = `S ${col}` + return col + }); + }) + return { board, next, hold } + } + + setEditButton(bool) { + this.elementEditButton.style.display = bool ? "flex" : "none"; + } } \ No newline at end of file diff --git a/src/features/history.js b/src/features/history.js index 2df0386..e758536 100644 --- a/src/features/history.js +++ b/src/features/history.js @@ -1,191 +1,191 @@ -import { Game } from "../game.js"; - -export class History { - /** - * stores every game state indexed - * @type {string[]} - */ - historyStates = []; - /** - * stores connections by assigning index as the node, and the value as an array of every connection to other nodes - * @type {Number[][]} - */ - historyConnections = []; - currentState = 0; - selectedbranch = 0; - - historyelement = document.getElementById("history"); - choiceselement = document.getElementById("redochoices"); - - /** - * @param {Game} game - */ - constructor(game) { - this.game = game; - } - - save() { - if (!this.game.settings.game.history) return; - const map = this.convertToMapCompressed(); - this.pushHistory(map); - this.updateUI(); - } - - pushHistory(map) { - this.historyStates.push(map); - const prevState = this.currentState; - this.currentState = this.historyStates.length - 1; - if (this.currentState == 0) return; - - if (this.historyConnections[prevState] == null) this.historyConnections[prevState] = []; - this.historyConnections[prevState].push(this.currentState); - } - - load() { - const state = this.historyStates[this.currentState] - this.convertFromMapCompressed(state); - this.updateUI() - } - - undo() { - if (this.currentState == 0 || !this.game.settings.game.history) return; - this.historyConnections.forEach((next, ind) => { - if (next.includes(this.currentState)) { - this.currentState = ind; - } - }) - this.game.sounds.playSound("undo"); - this.game.boardeffects.move(-1, 0); - this.load() - } - - redo() { - if (!this.game.settings.game.history) return; - const connection = this.historyConnections[this.currentState]; - if (connection == undefined) return; - this.currentState = this.selectedbranch || Math.max(...connection); - this.game.sounds.playSound("redo"); - this.game.boardeffects.move(1, 0); - this.load() - } - - goto(num) { - this.currentState = num; - this.load(); - } - - clearFuture(node) { - const futureNodes = this.historyConnections[node]; - if (futureNodes != undefined) { - futureNodes.forEach(node => this.clearFuture(node)) - } - this.historyStates[node] = undefined; - this.historyConnections[node] = undefined; - } - - updateUI() { - const branches = this.historyConnections[this.currentState] ?? []; - this.selectedbranch = Math.max(...branches); - this.historyelement.textContent = `history: ${this.currentState}`; - if (branches.length <= 1) { - this.choiceselement.style.opacity = "0"; - this.choiceselement.style.pointerEvents = "none"; - this.selectedbranch = 0; - return; - } - - this.choiceselement.style.opacity = "1"; - this.choiceselement.style.pointerEvents = "all"; - [...this.choiceselement.children].forEach(button => button.remove()) - branches.forEach(state => { - const button = document.createElement("button"); - button.classList.add("redochoice"); - if (state == this.selectedbranch) button.classList.add("selected"); - button.textContent = state.toString(); - button.onclick = () => this.setSelected(state, button); - this.choiceselement.appendChild(button) - }) - } - - setSelected(state, button) { - this.selectedbranch = state; - [...this.choiceselement.children].forEach(el => { - el.classList.remove("selected"); - }) - button.classList.add("selected"); - } - - compress(s) { - // saves anywhere from 50% worst case to 90% on average - // 250 blocks is 30kB ~~ 5 min of 3.3pps play is 120kB - let cs = ""; - let count = 1; - for (let i = 1; i < s.length; i++) { - if (s[i] == s[i - 1]) { - count++; - } else { - cs += `${count}${s[i - 1]}`; - count = 1; - } - } - cs += `${count}${s[s.length - 1]}`; - return cs; - } - - decompress(s) { - let ds = ""; - let int = ''; - for (let i = 0; i < s.length; i++) { - if (!isNaN(parseInt(s[i]))) { - int += s[i]; - continue; - } - const count = parseInt(int); - const char = s[i]; - ds += char.repeat(count); - int = ''; - } - return ds; - } - - convertToMapCompressed() { - const board = this.game.board.boardState; - const next = this.game.bag.nextPieces; - const hold = this.game.hold.piece == null ? "" : this.game.hold.piece.name; - const currPiece = this.game.falling.piece == null ? "" : this.game.falling.piece.name; - let boardstring = board.toReversed().flatMap(row => { - return row.map(col => { - col = col.replace("Sh", "").replace("NP", "").replace("G", "#"); - col = col.trim(); - if (col.length == 1) col = "" - if (col[0] == "A") col = ""; - if (col[0] == "S") col = col[2]; - if (col == "") col = "_"; - return col; - }) - }).join("") - return `${this.compress(boardstring)}?${currPiece},${next.flat()}?${hold}` - - } - - convertFromMapCompressed(string) { - let [board, next, hold] = string.split("?"); - board = this.decompress(board); - board = board.match(/.{1,10}/g).toReversed().map(row => { - return row.split("").map(col => { - col = col.replace("#", "G").replace("_", "") - if (col != "") col = `S ${col}` - return col - }); - }) - this.game.board.boardState = board; - this.game.bag.nextPieces = [next.split(","), []]; - this.game.hold.piece = this.game.renderer.getPiece(hold); - this.game.mechanics.spawnPiece(this.game.bag.randomiser()); - } - - setHistoryDiv(bool) { - this.historyelement.style.display = bool ? "block" : "none"; - } - +import { Game } from "../game.js"; + +export class History { + /** + * stores every game state indexed + * @type {string[]} + */ + historyStates = []; + /** + * stores connections by assigning index as the node, and the value as an array of every connection to other nodes + * @type {Number[][]} + */ + historyConnections = []; + currentState = 0; + selectedbranch = 0; + + historyelement = document.getElementById("history"); + choiceselement = document.getElementById("redochoices"); + + /** + * @param {Game} game + */ + constructor(game) { + this.game = game; + } + + save() { + if (!this.game.settings.game.history) return; + const map = this.convertToMapCompressed(); + this.pushHistory(map); + this.updateUI(); + } + + pushHistory(map) { + this.historyStates.push(map); + const prevState = this.currentState; + this.currentState = this.historyStates.length - 1; + if (this.currentState == 0) return; + + if (this.historyConnections[prevState] == null) this.historyConnections[prevState] = []; + this.historyConnections[prevState].push(this.currentState); + } + + load() { + const state = this.historyStates[this.currentState] + this.convertFromMapCompressed(state); + this.updateUI() + } + + undo() { + if (this.currentState == 0 || !this.game.settings.game.history) return; + this.historyConnections.forEach((next, ind) => { + if (next.includes(this.currentState)) { + this.currentState = ind; + } + }) + this.game.sounds.playSound("undo"); + this.game.boardeffects.move(-1, 0); + this.load() + } + + redo() { + if (!this.game.settings.game.history) return; + const connection = this.historyConnections[this.currentState]; + if (connection == undefined) return; + this.currentState = this.selectedbranch || Math.max(...connection); + this.game.sounds.playSound("redo"); + this.game.boardeffects.move(1, 0); + this.load() + } + + goto(num) { + this.currentState = num; + this.load(); + } + + clearFuture(node) { + const futureNodes = this.historyConnections[node]; + if (futureNodes != undefined) { + futureNodes.forEach(node => this.clearFuture(node)) + } + this.historyStates[node] = undefined; + this.historyConnections[node] = undefined; + } + + updateUI() { + const branches = this.historyConnections[this.currentState] ?? []; + this.selectedbranch = Math.max(...branches); + this.historyelement.textContent = `history: ${this.currentState}`; + if (branches.length <= 1) { + this.choiceselement.style.opacity = "0"; + this.choiceselement.style.pointerEvents = "none"; + this.selectedbranch = 0; + return; + } + + this.choiceselement.style.opacity = "1"; + this.choiceselement.style.pointerEvents = "all"; + [...this.choiceselement.children].forEach(button => button.remove()) + branches.forEach(state => { + const button = document.createElement("button"); + button.classList.add("redochoice"); + if (state == this.selectedbranch) button.classList.add("selected"); + button.textContent = state.toString(); + button.onclick = () => this.setSelected(state, button); + this.choiceselement.appendChild(button) + }) + } + + setSelected(state, button) { + this.selectedbranch = state; + [...this.choiceselement.children].forEach(el => { + el.classList.remove("selected"); + }) + button.classList.add("selected"); + } + + compress(s) { + // saves anywhere from 50% worst case to 90% on average + // 250 blocks is 30kB ~~ 5 min of 3.3pps play is 120kB + let cs = ""; + let count = 1; + for (let i = 1; i < s.length; i++) { + if (s[i] == s[i - 1]) { + count++; + } else { + cs += `${count}${s[i - 1]}`; + count = 1; + } + } + cs += `${count}${s[s.length - 1]}`; + return cs; + } + + decompress(s) { + let ds = ""; + let int = ''; + for (let i = 0; i < s.length; i++) { + if (!isNaN(parseInt(s[i]))) { + int += s[i]; + continue; + } + const count = parseInt(int); + const char = s[i]; + ds += char.repeat(count); + int = ''; + } + return ds; + } + + convertToMapCompressed() { + const board = this.game.board.boardState; + const next = this.game.bag.nextPieces; + const hold = this.game.hold.piece == null ? "" : this.game.hold.piece.name; + const currPiece = this.game.falling.piece == null ? "" : this.game.falling.piece.name; + let boardstring = board.toReversed().flatMap(row => { + return row.map(col => { + col = col.replace("Sh", "").replace("NP", "").replace("G", "#"); + col = col.trim(); + if (col.length == 1) col = "" + if (col[0] == "A") col = ""; + if (col[0] == "S") col = col[2]; + if (col == "") col = "_"; + return col; + }) + }).join("") + return `${this.compress(boardstring)}?${currPiece},${next.flat()}?${hold}` + + } + + convertFromMapCompressed(string) { + let [board, next, hold] = string.split("?"); + board = this.decompress(board); + board = board.match(/.{1,10}/g).toReversed().map(row => { + return row.split("").map(col => { + col = col.replace("#", "G").replace("_", "") + if (col != "") col = `S ${col}` + return col + }); + }) + this.game.board.boardState = board; + this.game.bag.nextPieces = [next.split(","), []]; + this.game.hold.piece = this.game.renderer.getPiece(hold); + this.game.mechanics.spawnPiece(this.game.bag.randomiser()); + } + + setHistoryDiv(bool) { + this.historyelement.style.display = bool ? "block" : "none"; + } + } \ No newline at end of file diff --git a/src/features/modes.js b/src/features/modes.js index e89226a..51d0c57 100644 --- a/src/features/modes.js +++ b/src/features/modes.js @@ -1,141 +1,141 @@ -import { Game } from "../game.js"; -import gamemodeJSON from "../data/gamemodes.json" with { type: "json" }; -import { gameoverResultText, gameoverText, resultSuffix } from "../data/data.js"; - -export class Modes { - elementobjectives = document.getElementById("objective"); - divObjectiveText = document.getElementById("objectiveText"); - modeJSON; - customSettings; - - /** - * @param {Game} game - */ - constructor(game) { - this.game = game; - } - - checkFinished() { - const goals = this.game.settings.game; - const stats = this.game.stats; - - // hardcoded objectives - let combobreak = this.game.stats.combo == -1 && stats.clearlines >= 1 && this.modeJSON.target == 'combobreak'; - let gameend = this.game.ended && this.modeJSON.target == 'gameEnd'; - - let stat = stats[this.modeJSON.goalStat] - let goal = goals[this.modeJSON.target] - let result = stats[this.modeJSON.result] - - if (stat >= goal || combobreak || gameend) { - result = Math.round(result * 1000) / 1000 - stat = Math.round(stat * 1000) / 1000 - this.game.profilestats.setPB(result); - const text = this.statText(this.modeJSON.goalStat, stat, this.modeJSON.result, result) - const suffix = resultSuffix[this.modeJSON.result] - this.game.endGame(result + suffix, text); - } - - if (this.game.settings.game.gamemode == 'ultra') { // changes ultra sidebar - stat = stats.score; - goal = undefined - } - this.setObjectiveText(stat, goal); - } - - statText(stat, value, result, resultvalue) { - const front = gameoverText[stat].replace("_", value); - const back = gameoverResultText[result].replace("_", resultvalue); - return front + back; - } - - setObjectiveText(statValue, resultValue) { - if (statValue != undefined) statValue = Math.round(statValue * 1000) / 1000 - let modetext = (statValue == undefined ? '' : statValue) - + (resultValue == undefined ? '' : `/${resultValue}`) - this.elementobjectives.textContent = modetext; - } - - loadModes() { - let currentGamemode = this.game.settings.game.gamemode; - if (typeof currentGamemode == 'number') { // backwards compatibility - this.game.settings.game.gamemode = 'sprint' - currentGamemode = 'sprint' - } - this.setGamemode(currentGamemode); - this.divObjectiveText.textContent = this.modeJSON.objectiveText; - } - - setGamemode(mode) { - this.game.settings.game.gamemode = mode; - const competitive = this.game.settings.game.competitiveMode; - const custom = JSON.parse(localStorage.getItem('customGame')); - - if (competitive) { - if (custom == null) { - localStorage.setItem('customGame', JSON.stringify(this.game.settings.game)); - } - this.modeJSON = this.getGamemodeJSON(mode); - this.game.settings.game = { ...this.game.settings.game, ...this.modeJSON.settings }; - } else { - if (custom != null) { - this.game.settings.game = custom; - this.game.settings.game.competitiveMode = false; - localStorage.removeItem('customGame'); - } - this.modeJSON = this.getGamemodeJSON(mode); - } - this.toggleDialogState(competitive); - this.game.menuactions.saveSettings(); - } - - toggleDialogState(enabled) { - document.getElementById('game').disabled = enabled; - document.getElementById('goals').disabled = enabled; - } - - getGamemodeJSON(mode) { - const modeinfo = gamemodeJSON[mode]; - const allinfo = gamemodeJSON["*"]; - - let info = {} - Object.keys(allinfo).forEach(key => info[key] = modeinfo[key] ?? allinfo[key]); - info.settings = { ...allinfo.settings, ...modeinfo.settings } - - return info; - } - - getGamemodeNames() { - return Object.keys(gamemodeJSON).filter(key => key != "*"); - } - - getSuffix(mode) { - const modeinfo = gamemodeJSON[mode] ?? {}; - return resultSuffix[modeinfo.result] ?? " (legacy)"; - } - - diggerAddGarbage(removed) { - if (this.game.stats.getRemainingGarbage() > 10 && this.game.settings.game.gamemode == "digger") - this.game.mechanics.addGarbage(removed); - } - - set4WCols(start) { - if (this.game.settings.game.gamemode == 'combo') this.game.board.setComboBoard(start); - - } - - startSurvival() { - const time = (60 * 1000) / this.game.settings.game.survivalRate; - if (this.game.settings.game.gamemode == 'survival') - this.game.survivalTimer = setInterval(() => this.game.mechanics.addGarbage(1), time); - } - - diggerGarbageSet(start) { - const rows = - this.game.settings.game.requiredGarbage < 10 - ? this.game.settings.game.requiredGarbage - : 10; - if (this.game.stats.getRemainingGarbage() > 0 && start && this.game.settings.game.gamemode == 'digger') - this.game.mechanics.addGarbage(rows); - } +import { Game } from "../game.js"; +import gamemodeJSON from "../data/gamemodes.json" with { type: "json" }; +import { gameoverResultText, gameoverText, resultSuffix } from "../data/data.js"; + +export class Modes { + elementobjectives = document.getElementById("objective"); + divObjectiveText = document.getElementById("objectiveText"); + modeJSON; + customSettings; + + /** + * @param {Game} game + */ + constructor(game) { + this.game = game; + } + + checkFinished() { + const goals = this.game.settings.game; + const stats = this.game.stats; + + // hardcoded objectives + let combobreak = this.game.stats.combo == -1 && stats.clearlines >= 1 && this.modeJSON.target == 'combobreak'; + let gameend = this.game.ended && this.modeJSON.target == 'gameEnd'; + + let stat = stats[this.modeJSON.goalStat] + let goal = goals[this.modeJSON.target] + let result = stats[this.modeJSON.result] + + if (stat >= goal || combobreak || gameend) { + if(this.game.settings.game.gamemode != "race" ) result = Math.round(result * 1000) / 1000 + stat = Math.round(stat * 1000) / 1000 + this.game.profilestats.setPB(result); + const text = this.statText(this.modeJSON.goalStat, stat, this.modeJSON.result, result) + const suffix = resultSuffix[this.modeJSON.result] + this.game.endGame(result + suffix, text); + } + + if (this.game.settings.game.gamemode == 'ultra') { // changes ultra sidebar + stat = stats.score; + goal = undefined + } + this.setObjectiveText(stat, goal); + } + + statText(stat, value, result, resultvalue) { + const front = gameoverText[stat].replace("_", value); + const back = gameoverResultText[result].replace("_", resultvalue); + return front + back; + } + + setObjectiveText(statValue, resultValue) { + if (statValue != undefined) statValue = Math.round(statValue * 1000) / 1000 + let modetext = (statValue == undefined ? '' : statValue) + + (resultValue == undefined ? '' : `/${resultValue}`) + this.elementobjectives.textContent = modetext; + } + + loadModes() { + let currentGamemode = this.game.settings.game.gamemode; + if (typeof currentGamemode == 'number') { // backwards compatibility + this.game.settings.game.gamemode = 'sprint' + currentGamemode = 'sprint' + } + this.setGamemode(currentGamemode); + this.divObjectiveText.textContent = this.modeJSON.objectiveText; + } + + setGamemode(mode) { + this.game.settings.game.gamemode = mode; + const competitive = this.game.settings.game.competitiveMode; + const custom = JSON.parse(localStorage.getItem('customGame')); + + if (competitive) { + if (custom == null) { + localStorage.setItem('customGame', JSON.stringify(this.game.settings.game)); + } + this.modeJSON = this.getGamemodeJSON(mode); + this.game.settings.game = { ...this.game.settings.game, ...this.modeJSON.settings }; + } else { + if (custom != null) { + this.game.settings.game = custom; + this.game.settings.game.competitiveMode = false; + localStorage.removeItem('customGame'); + } + this.modeJSON = this.getGamemodeJSON(mode); + } + this.toggleDialogState(competitive); + this.game.menuactions.saveSettings(); + } + + toggleDialogState(enabled) { + document.getElementById('game').disabled = enabled; + document.getElementById('goals').disabled = enabled; + } + + getGamemodeJSON(mode) { + const modeinfo = gamemodeJSON[mode]; + const allinfo = gamemodeJSON["*"]; + + let info = {} + Object.keys(allinfo).forEach(key => info[key] = modeinfo[key] ?? allinfo[key]); + info.settings = { ...allinfo.settings, ...modeinfo.settings } + + return info; + } + + getGamemodeNames() { + return Object.keys(gamemodeJSON).filter(key => key != "*"); + } + + getSuffix(mode) { + const modeinfo = gamemodeJSON[mode] ?? {}; + return resultSuffix[modeinfo.result] ?? " (legacy)"; + } + + diggerAddGarbage(removed) { + if (this.game.stats.getRemainingGarbage() > 10 && this.game.settings.game.gamemode == "digger") + this.game.mechanics.addGarbage(removed); + } + + set4WCols(start) { + if (this.game.settings.game.gamemode == 'combo') this.game.board.setComboBoard(start); + + } + + startSurvival() { + const time = (60 * 1000) / this.game.settings.game.survivalRate; + if (this.game.settings.game.gamemode == 'survival') + this.game.survivalTimer = setInterval(() => this.game.mechanics.addGarbage(1), time); + } + + diggerGarbageSet(start) { + const rows = + this.game.settings.game.requiredGarbage < 10 + ? this.game.settings.game.requiredGarbage + : 10; + if (this.game.stats.getRemainingGarbage() > 0 && start && this.game.settings.game.gamemode == 'digger') + this.game.mechanics.addGarbage(rows); + } } \ No newline at end of file diff --git a/src/features/profileStats.js b/src/features/profileStats.js index 4427eb4..9baf6b9 100644 --- a/src/features/profileStats.js +++ b/src/features/profileStats.js @@ -1,86 +1,86 @@ -import { lowerIsBetter } from "../data/data.js"; -import { Game } from "../game.js"; - -export class ProfileStats { - personalBests = {}; - notSaved = ['game', 'level', 'combo'] - - /** - * @param {Game} game - */ - constructor(game) { - this.game = game; - } - - setPB(score) { - this.game.elementGameEndTitle.textContent = 'GAME ENDED'; - const gamemode = this.game.settings.game.gamemode - const gamemodeStats = this.personalBests[gamemode] ?? {}; - const currentScore = Number(gamemodeStats.score); - const lower = lowerIsBetter[this.game.modes.modeJSON.result]; - - if (!this.game.settings.game.competitiveMode) return; - - if (isNaN(currentScore) || (lower && score < currentScore) || (!lower && score > currentScore)) { - let gameStatsKeys = Object.getOwnPropertyNames(this.game.stats) - gameStatsKeys = gameStatsKeys.filter(key => key != 'game') - const gameStats = {}; - gameStatsKeys.forEach(key => gameStats[key] = this.game.stats[key]) - const ts = new Date().toJSON(); - this.personalBests[gamemode] = { score, pbstats: gameStats, version: this.game.version, ts }; - this.game.elementGameEndTitle.textContent = 'NEW PB!'; - setTimeout(() => this.game.sounds.playSound("personalbest"), 1000); - - this.game.modals.generate.notif("PB Saved", `PB on ${gamemode} saved`, "success"); - } - } - - loadPBs() { - const stats = JSON.parse(localStorage.getItem("stats")) ?? {}; - this.personalBests = stats.pbs ?? {}; - } - - removePB(mode) { - delete this.personalBests[mode]; - this.saveSession(); - } - - saveSession() { - const prevstats = JSON.parse(localStorage.getItem("stats")) ?? {}; - const statsData = prevstats.lifetime ?? {}; - - Object.getOwnPropertyNames(this.game.stats).forEach(key => { - if (this.notSaved.includes(key)) return; - - if (key == 'clearCols' || key == 'tspins') { - if (statsData[key] == undefined || typeof statsData[key] != 'object') statsData[key] = [] - this.game.stats[key].forEach((_, index) => { - if (statsData[key][index] == undefined) statsData[key][index] = 0 - statsData[key][index] += this.game.stats[key][index]; - }); - return; - } - if (key == 'clearPieces') { - if (statsData[key] == undefined) { - statsData[key] = this.game.stats[key]; - return; - } - Object.keys(this.game.stats[key]).forEach(piece => { - this.game.stats[key][piece].forEach((_, index) => { - if (statsData[key][piece] == undefined) statsData[key][piece] = [0, 0, 0, 0]; - statsData[key][piece][index] += this.game.stats[key][piece][index]; - }); - }); - return; - } - if (key == "maxBTB" || key == "maxCombo") { - if (statsData[key] < this.game.stats[key] ) statsData[key] = this.game.stats[key]; - return; - } - if (this.game.stats[key] < 0) this.game.stats[key] = 0 - statsData[key] += this.game.stats[key]; - }) - const stats = { pbs: this.personalBests, lifetime: statsData } - localStorage.setItem("stats", JSON.stringify(stats)); - } +import { lowerIsBetter } from "../data/data.js"; +import { Game } from "../game.js"; + +export class ProfileStats { + personalBests = {}; + notSaved = ['game', 'level', 'combo'] + + /** + * @param {Game} game + */ + constructor(game) { + this.game = game; + } + + setPB(score) { + this.game.elementGameEndTitle.textContent = 'GAME ENDED'; + const gamemode = this.game.settings.game.gamemode + const gamemodeStats = this.personalBests[gamemode] ?? {}; + const currentScore = Number(gamemodeStats.score); + const lower = lowerIsBetter[this.game.modes.modeJSON.result]; + + if (!this.game.settings.game.competitiveMode) return; + + if (isNaN(currentScore) || (lower && score < currentScore) || (!lower && score > currentScore)) { + let gameStatsKeys = Object.getOwnPropertyNames(this.game.stats) + gameStatsKeys = gameStatsKeys.filter(key => key != 'game') + const gameStats = {}; + gameStatsKeys.forEach(key => gameStats[key] = this.game.stats[key]) + const ts = new Date().toJSON(); + this.personalBests[gamemode] = { score, pbstats: gameStats, version: this.game.version, ts }; + this.game.elementGameEndTitle.textContent = 'NEW PB!'; + setTimeout(() => this.game.sounds.playSound("personalbest"), 1000); + + this.game.modals.generate.notif("PB Saved", `PB on ${gamemode} saved`, "success"); + } + } + + loadPBs() { + const stats = JSON.parse(localStorage.getItem("stats")) ?? {}; + this.personalBests = stats.pbs ?? {}; + } + + removePB(mode) { + delete this.personalBests[mode]; + this.saveSession(); + } + + saveSession() { + const prevstats = JSON.parse(localStorage.getItem("stats")) ?? {}; + const statsData = prevstats.lifetime ?? {}; + + Object.getOwnPropertyNames(this.game.stats).forEach(key => { + if (this.notSaved.includes(key)) return; + + if (key == 'clearCols' || key == 'tspins') { + if (statsData[key] == undefined || typeof statsData[key] != 'object') statsData[key] = [] + this.game.stats[key].forEach((_, index) => { + if (statsData[key][index] == undefined) statsData[key][index] = 0 + statsData[key][index] += this.game.stats[key][index]; + }); + return; + } + if (key == 'clearPieces') { + if (statsData[key] == undefined) { + statsData[key] = this.game.stats[key]; + return; + } + Object.keys(this.game.stats[key]).forEach(piece => { + this.game.stats[key][piece].forEach((_, index) => { + if (statsData[key][piece] == undefined) statsData[key][piece] = [0, 0, 0, 0]; + statsData[key][piece][index] += this.game.stats[key][piece][index]; + }); + }); + return; + } + if (key == "maxBTB" || key == "maxCombo") { + if (statsData[key] < this.game.stats[key] ) statsData[key] = this.game.stats[key]; + return; + } + if (this.game.stats[key] < 0) this.game.stats[key] = 0 + statsData[key] += this.game.stats[key]; + }) + const stats = { pbs: this.personalBests, lifetime: statsData } + localStorage.setItem("stats", JSON.stringify(stats)); + } } \ No newline at end of file diff --git a/src/features/settings.js b/src/features/settings.js index 59abc67..fec2775 100644 --- a/src/features/settings.js +++ b/src/features/settings.js @@ -1,74 +1,74 @@ -import { Game } from "../game.js"; -import defaultSettings from "../data/defaultSettings.json" with { type: "json" }; - -export class Settings { - /** - * @param {Game} game - */ - constructor(game) { - this.gameObject = game; - this.loadDefault(); - } - - loadDefault() { - Object.keys(defaultSettings).forEach(type => { - this[type] = defaultSettings[type]; - }) - - // this is for type checking lmao - return; - this.game = defaultSettings.game; - this.display = defaultSettings.display; - this.control = defaultSettings.control; - this.handling = defaultSettings.handling; - this.volume = defaultSettings.volume; - } - - load(data) { - if (data instanceof Array) data = this.convert(data); - Object.keys(data).forEach(type => { - Object.keys(data[type]).forEach(setting => { - if (data[type][setting] === undefined || data[type][setting] === "") return; - this[type][setting] = data[type][setting]; - }) - }) - } - - save() { - const data = {}; - Object.getOwnPropertyNames(this).forEach(key => { - if (key == 'gameObject') return; - data[key] = this[key]; - }) - return data; - } - - // for backwards compatibility - convert(arr) { - const display = arr[0] - const game = arr[1] - const control = arr[2] - const handling = { - das: game.das, - arr: game.arr, - sdarr: game.sdarr - } - const volume = { - audioLevel: display.audioLevel, - sfxLevel: display.sfxLevel - } - game.das = undefined - game.arr = undefined - game.sdarr = undefined - display.audioLevel = undefined - display.sfxLevel = undefined - - return { display, game, control, handling, volume }; - } - - reset(group) { - for (let setting in this[group]) { - this[group][setting] = ""; - } - } +import { Game } from "../game.js"; +import defaultSettings from "../data/defaultSettings.json" with { type: "json" }; + +export class Settings { + /** + * @param {Game} game + */ + constructor(game) { + this.gameObject = game; + this.loadDefault(); + } + + loadDefault() { + Object.keys(defaultSettings).forEach(type => { + this[type] = defaultSettings[type]; + }) + + // this is for type checking lmao + return; + this.game = defaultSettings.game; + this.display = defaultSettings.display; + this.control = defaultSettings.control; + this.handling = defaultSettings.handling; + this.volume = defaultSettings.volume; + } + + load(data) { + if (data instanceof Array) data = this.convert(data); + Object.keys(data).forEach(type => { + Object.keys(data[type]).forEach(setting => { + if (data[type][setting] === undefined || data[type][setting] === "") return; + this[type][setting] = data[type][setting]; + }) + }) + } + + save() { + const data = {}; + Object.getOwnPropertyNames(this).forEach(key => { + if (key == 'gameObject') return; + data[key] = this[key]; + }) + return data; + } + + // for backwards compatibility + convert(arr) { + const display = arr[0] + const game = arr[1] + const control = arr[2] + const handling = { + das: game.das, + arr: game.arr, + sdarr: game.sdarr + } + const volume = { + audioLevel: display.audioLevel, + sfxLevel: display.sfxLevel + } + game.das = undefined + game.arr = undefined + game.sdarr = undefined + display.audioLevel = undefined + display.sfxLevel = undefined + + return { display, game, control, handling, volume }; + } + + reset(group) { + for (let setting in this[group]) { + this[group][setting] = ""; + } + } } \ No newline at end of file diff --git a/src/features/sounds.js b/src/features/sounds.js index 2a1d360..8ac415b 100644 --- a/src/features/sounds.js +++ b/src/features/sounds.js @@ -1,127 +1,127 @@ -import sfxobj from "../data/sfxlist.json" with { type: "json" }; -import { songsobj } from "../data/data.js"; -import { Game } from "../game.js"; - -export class Sounds { - sfx = {}; - songs = []; - songNames = []; - curSongIdx = 0; - elSongProgress = document.getElementById("songProgress"); - elSongText = document.getElementById("songText"); - - lowpassfilter; - audioContext; - - - /** - * @param {Game} game - */ - constructor(game) { - this.game = game; - } - - /** - * - * @param {string} audioName - * Name of audio as specified in sfxlist.json - * @param {Boolean} replace - * If true, stops currently playing audio and starts new one - * If false, skips if audio is already playing - */ - playSound(audioName, replace = true, silent = false) { - if (this.sfx[audioName] == undefined) return; - this.sfx[audioName].muted = silent; - this.sfx[audioName].volume = this.game.settings.volume.sfxLevel / 1000; - if (!replace && !this.sfx[audioName].ended && this.sfx[audioName].currentTime != 0) return; - this.sfx[audioName].currentTime = 0; - this.sfx[audioName].play(); - } - - startSong() { - this.elSongText.textContent = `Now Playing ${this.songNames[this.curSongIdx]}`; - this.songs[this.curSongIdx].onended = () => { - this.endSong(); - this.startSong(); - }; - this.songs[this.curSongIdx].volume = this.game.settings.volume.audioLevel / 1000; - this.songs[this.curSongIdx].play(); - } - - endSong() { - this.songs[this.curSongIdx].pause(); - this.songs[this.curSongIdx].currentTime = 0; - this.songs[this.curSongIdx].onended = () => { }; - this.curSongIdx = (this.curSongIdx + 1) % this.songs.length; - } - - pauseSong() { - if (this.songs[this.curSongIdx].paused) { - this.songs[this.curSongIdx].play(); - this.elSongText.textContent = `Playing ${this.songNames[this.curSongIdx]}`; - } else { - this.songs[this.curSongIdx].pause(); - this.elSongText.textContent = `Not Playing`; - - } - } - - addMenuSFX() { - let hoverSFX = (e) => { - document.querySelectorAll(e).forEach(el => (el.addEventListener("mouseenter", () => this.game.sounds.playSound("menutap")))); - }; - let clickSFX = (e) => { - document.querySelectorAll(e).forEach(el => (el.addEventListener("click", () => this.game.sounds.playSound("menuclick")))); - }; - hoverSFX(".settingRow"); - hoverSFX(".closeDialogButton"); - hoverSFX(".gamemodeSelect"); - hoverSFX(".settingPanelButton"); - clickSFX(".settingPanelButton"); - clickSFX(".closeDialogButton"); - } - - initSounds() { - setInterval(() => { - if (this.songs[this.curSongIdx].currentTime == 0) return; - this.elSongProgress.value = - (this.songs[this.curSongIdx].currentTime * 100) / this.songs[this.curSongIdx].duration; - }, 2000); - - // preload all sfx - sfxobj.forEach(file => { - const name = file.name.split(".")[0]; - const a = new Audio(file.path); - this.sfx[name] = a; - this.playSound(name, false, true); - }) - - - this.audioContext = new window.AudioContext(); - this.lowpassfilter = this.audioContext.createBiquadFilter(); - this.lowpassfilter.type = "lowpass"; - this.lowpassfilter.frequency.value = 20000; - - songsobj.forEach(file => { - const songaudio = new Audio(file.path); - this.songs.push(songaudio); - this.songNames.push(file.name.split(".")[0]); - - const track = this.audioContext.createMediaElementSource(songaudio); - track.connect(this.lowpassfilter); - this.lowpassfilter.connect(this.audioContext.destination); - }) - // this.playSound("allclear") // wait literally how is this fine by chrome - } - - setAudioLevel() { - this.songs[this.curSongIdx].volume = Number(this.game.settings.volume.audioLevel) / 1000; - } - - toggleSongMuffle(muffled) { - const currentTime = this.audioContext.currentTime; - this.lowpassfilter.frequency.cancelScheduledValues(currentTime); - this.lowpassfilter.frequency.setValueAtTime(this.lowpassfilter.frequency.value, currentTime); - this.lowpassfilter.frequency.exponentialRampToValueAtTime(muffled ? 300 : 20000, currentTime + 1); - } -} +import sfxobj from "../data/sfxlist.json" with { type: "json" }; +import { songsobj } from "../data/data.js"; +import { Game } from "../game.js"; + +export class Sounds { + sfx = {}; + songs = []; + songNames = []; + curSongIdx = 0; + elSongProgress = document.getElementById("songProgress"); + elSongText = document.getElementById("songText"); + + lowpassfilter; + audioContext; + + + /** + * @param {Game} game + */ + constructor(game) { + this.game = game; + } + + /** + * + * @param {string} audioName + * Name of audio as specified in sfxlist.json + * @param {Boolean} replace + * If true, stops currently playing audio and starts new one + * If false, skips if audio is already playing + */ + playSound(audioName, replace = true, silent = false) { + if (this.sfx[audioName] == undefined) return; + this.sfx[audioName].muted = silent; + this.sfx[audioName].volume = this.game.settings.volume.sfxLevel / 1000; + if (!replace && !this.sfx[audioName].ended && this.sfx[audioName].currentTime != 0) return; + this.sfx[audioName].currentTime = 0; + this.sfx[audioName].play(); + } + + startSong() { + this.elSongText.textContent = `Now Playing ${this.songNames[this.curSongIdx]}`; + this.songs[this.curSongIdx].onended = () => { + this.endSong(); + this.startSong(); + }; + this.songs[this.curSongIdx].volume = this.game.settings.volume.audioLevel / 1000; + this.songs[this.curSongIdx].play(); + } + + endSong() { + this.songs[this.curSongIdx].pause(); + this.songs[this.curSongIdx].currentTime = 0; + this.songs[this.curSongIdx].onended = () => { }; + this.curSongIdx = (this.curSongIdx + 1) % this.songs.length; + } + + pauseSong() { + if (this.songs[this.curSongIdx].paused) { + this.songs[this.curSongIdx].play(); + this.elSongText.textContent = `Playing ${this.songNames[this.curSongIdx]}`; + } else { + this.songs[this.curSongIdx].pause(); + this.elSongText.textContent = `Not Playing`; + + } + } + + addMenuSFX() { + let hoverSFX = (e) => { + document.querySelectorAll(e).forEach(el => (el.addEventListener("mouseenter", () => this.game.sounds.playSound("menutap")))); + }; + let clickSFX = (e) => { + document.querySelectorAll(e).forEach(el => (el.addEventListener("click", () => this.game.sounds.playSound("menuclick")))); + }; + hoverSFX(".settingRow"); + hoverSFX(".closeDialogButton"); + hoverSFX(".gamemodeSelect"); + hoverSFX(".settingPanelButton"); + clickSFX(".settingPanelButton"); + clickSFX(".closeDialogButton"); + } + + initSounds() { + setInterval(() => { + if (this.songs[this.curSongIdx].currentTime == 0) return; + this.elSongProgress.value = + (this.songs[this.curSongIdx].currentTime * 100) / this.songs[this.curSongIdx].duration; + }, 2000); + + // preload all sfx + sfxobj.forEach(file => { + const name = file.name.split(".")[0]; + const a = new Audio(file.path); + this.sfx[name] = a; + this.playSound(name, false, true); + }) + + + this.audioContext = new window.AudioContext(); + this.lowpassfilter = this.audioContext.createBiquadFilter(); + this.lowpassfilter.type = "lowpass"; + this.lowpassfilter.frequency.value = 20000; + + songsobj.forEach(file => { + const songaudio = new Audio(file.path); + this.songs.push(songaudio); + this.songNames.push(file.name.split(".")[0]); + + const track = this.audioContext.createMediaElementSource(songaudio); + track.connect(this.lowpassfilter); + this.lowpassfilter.connect(this.audioContext.destination); + }) + // this.playSound("allclear") // wait literally how is this fine by chrome + } + + setAudioLevel() { + this.songs[this.curSongIdx].volume = Number(this.game.settings.volume.audioLevel) / 1000; + } + + toggleSongMuffle(muffled) { + const currentTime = this.audioContext.currentTime; + this.lowpassfilter.frequency.cancelScheduledValues(currentTime); + this.lowpassfilter.frequency.setValueAtTime(this.lowpassfilter.frequency.value, currentTime); + this.lowpassfilter.frequency.exponentialRampToValueAtTime(muffled ? 300 : 20000, currentTime + 1); + } +} diff --git a/src/features/stats.js b/src/features/stats.js index 3563979..1ffbd8b 100644 --- a/src/features/stats.js +++ b/src/features/stats.js @@ -1,134 +1,137 @@ -import { levellingTable } from "../data/data.js"; -import { Game } from "../game.js"; - -export class GameStats { - // game stats - time = 0; - clearlines = 0; - pieceCount = 0; - score = 0; - pcs = 0; - quads = 0; - tspins = [0, 0, 0, 0]; - allspins = 0; - level = 0; - altitude = 0; - floor = 1; - climbSpeed = 1; - - // garbage stats - attack = 0; - cleargarbage = 0; - sent = 0; - recieved = 0; - - // modifier stats - combo = -1; - maxCombo = -1; - btbCount = -1; - maxBTB = -1; - - // calculated stats - pps = 0; - apm = 0; - vs = 0; // tetrio versus score - lpm = 0; // lines per minute - app = 0; - apl = 0; // attack per line - appw = 0; // weighted attack per piece - ppb = 0; - dss = 0; // garbage per second - dsp = 0; // garbage per piece - chzind = 0; // cheese index - garbeff = 0; // garbage efficiency - vsOnApm = 0; // vs / apm - - // x piece efficiency - tpE = 0; - ipE = 0; - - // input stats - inputs = 0; - holds = 0; - rotates = 0; - kps = 0; - kpp = 0; - - clearCols = [0, 0, 0, 0, 0, 0, 0, 0, 0, 0]; // amount cleared in a col = clearCols[col - 1] - clearPieces = { // lineclears by piece = clearPieces[piece][line_count - 1] - i: [0, 0, 0, 0], - j: [0, 0, 0, 0], - l: [0, 0, 0, 0], - o: [0, 0, 0, 0], - t: [0, 0, 0, 0], - s: [0, 0, 0, 0], - z: [0, 0, 0, 0], - }; - - /** - * @param {Game} game - */ - constructor(game) { - this.game = game; - } - - updateStats() { - this.time += 1 / this.game.tickrate; - - this.pps = this.pieceCount / this.time; - this.apm = this.attack * 60 / this.time; - this.vs = (this.attack + this.cleargarbage) * 100 / this.time; - - this.lpm = this.clearlines * 60 / this.time; - this.app = this.attack / this.pieceCount || 0; - this.apl = this.attack / this.clearlines || 0; - this.ppb = this.score / this.pieceCount || 0; - this.dss = this.cleargarbage / this.time - this.dsp = this.cleargarbage / this.pieceCount || 0; - this.vsOnApm = this.vs / this.apm || 0; - this.chzind = 25 * (this.dsp * 6 + this.vsOnApm * 2 - this.app * 5 - 1); - this.garbeff = this.app * this.dsp * 2; - this.appw = this.app - 5 * Math.tan(1 - this.chzind / 30) || 0; - this.kps = this.inputs / this.time; - this.kpp = this.inputs / this.pieceCount || 0; - - this.tpE = this.tspins.reduce((a, b) => a + b, 0) * 700 / this.pieceCount || 0; - this.ipE = this.quads * 700 / this.pieceCount || 0; - // use of || 0 to not show NaN - } - - checkInvis() { - return this.pieceCount % this.game.settings.game.lookAheadPieces == 0 && !this.game.falling.moved - } - - getRemainingGarbage() { - return this.game.settings.game.requiredGarbage - this.cleargarbage - } - - updateBTB(isBTB, count) { - this.btbCount = isBTB ? - this.btbCount + 1 : - count == 0 ? this.btbCount : -1; - if (this.btbCount > this.maxBTB) this.maxBTB = this.btbCount; - } - - updateCombo(count) { - this.combo = count == 0 ? -1 : this.combo + 1; - if (this.combo > this.maxCombo) this.maxCombo = this.combo; - } - - incrementStats(score, count, damage, isPC, isTspin, isAllspin, garb) { - this.score += score; - this.clearlines += count; - this.attack += damage; - this.quads += count >= 4 ? 1 : 0; - this.pcs += isPC ? 1 : 0; - this.cleargarbage += garb; - - if (isTspin) this.tspins[count]++; - this.allspins += isAllspin ? 1 : 0; - this.level += levellingTable[count]; - if (count > 0) this.clearPieces[this.game.falling.piece.name][count - 1]++; - } - +import { levellingTable } from "../data/data.js"; +import { Game } from "../game.js"; + +export class GameStats { + // game stats + time = 0; + clearlines = 0; + pieceCount = 0; + score = 0; + pcs = 0; + quads = 0; + tspins = [0, 0, 0, 0]; + allspins = 0; + tgm_level = 0; + altitude = 0; + floor = 1; + grade = "9"; + climbSpeed = 1; + + // garbage stats + attack = 0; + cleargarbage = 0; + sent = 0; + recieved = 0; + + // modifier stats + combo = -1; + maxCombo = -1; + btbCount = -1; + maxBTB = -1; + + // calculated stats + pps = 0; + apm = 0; + vs = 0; // tetrio versus score + lpm = 0; // lines per minute + app = 0; + apl = 0; // attack per line + appw = 0; // weighted attack per piece + ppb = 0; + dss = 0; // garbage per second + dsp = 0; // garbage per piece + chzind = 0; // cheese index + garbeff = 0; // garbage efficiency + vsOnApm = 0; // vs / apm + + // x piece efficiency + tpE = 0; + ipE = 0; + + // input stats + inputs = 0; + holds = 0; + rotates = 0; + kps = 0; + kpp = 0; + + clearCols = [0, 0, 0, 0, 0, 0, 0, 0, 0, 0]; // amount cleared in a col = clearCols[col - 1] + clearPieces = { // lineclears by piece = clearPieces[piece][line_count - 1] + i: [0, 0, 0, 0], + j: [0, 0, 0, 0], + l: [0, 0, 0, 0], + o: [0, 0, 0, 0], + t: [0, 0, 0, 0], + s: [0, 0, 0, 0], + z: [0, 0, 0, 0], + }; + + /** + * @param {Game} game + */ + constructor(game) { + this.game = game; + } + + updateStats() { + this.time += 1 / this.game.tickrate; + this.game.grandmaster.sectionTime += 1 / this.game.tickrate; + + this.pps = this.pieceCount / this.time; + this.apm = this.attack * 60 / this.time; + this.vs = (this.attack + this.cleargarbage) * 100 / this.time; + + this.lpm = this.clearlines * 60 / this.time; + this.app = this.attack / this.pieceCount || 0; + this.apl = this.attack / this.clearlines || 0; + this.ppb = this.score / this.pieceCount || 0; + this.dss = this.cleargarbage / this.time + this.dsp = this.cleargarbage / this.pieceCount || 0; + this.vsOnApm = this.vs / this.apm || 0; + this.chzind = 25 * (this.dsp * 6 + this.vsOnApm * 2 - this.app * 5 - 1); + this.garbeff = this.app * this.dsp * 2; + this.appw = this.app - 5 * Math.tan(1 - this.chzind / 30) || 0; + this.kps = this.inputs / this.time; + this.kpp = this.inputs / this.pieceCount || 0; + + this.tpE = this.tspins.reduce((a, b) => a + b, 0) * 700 / this.pieceCount || 0; + this.ipE = this.quads * 700 / this.pieceCount || 0; + // use of || 0 to not show NaN + } + + checkInvis() { + return this.pieceCount % this.game.settings.game.lookAheadPieces == 0 && !this.game.falling.moved + } + + getRemainingGarbage() { + return this.game.settings.game.requiredGarbage - this.cleargarbage + } + + updateBTB(isBTB, count) { + this.btbCount = isBTB ? + this.btbCount + 1 : + count == 0 ? this.btbCount : -1; + if (this.btbCount > this.maxBTB) this.maxBTB = this.btbCount; + } + + updateCombo(count) { + this.combo = count == 0 ? -1 : this.combo + 1; + if (this.combo > this.maxCombo) this.maxCombo = this.combo; + } + + incrementStats(score, count, damage, isPC, isTspin, isAllspin, garb) { + this.score += score; + this.clearlines += count; + this.attack += damage; + this.quads += count >= 4 ? 1 : 0; + this.pcs += isPC ? 1 : 0; + this.cleargarbage += garb; + + if (isTspin) this.tspins[count]++; + this.allspins += isAllspin ? 1 : 0; + this.tgm_level += levellingTable[count]; + this.game.grandmaster.addGrade(count, this.combo, this.tgm_level) + if (count > 0) this.clearPieces[this.game.falling.piece.name][count - 1]++; + } + } \ No newline at end of file diff --git a/src/game.js b/src/game.js index 5da791e..3d8776f 100644 --- a/src/game.js +++ b/src/game.js @@ -1,164 +1,170 @@ -import { Bag } from "./mechanics/bag.js"; -import { Board } from "./mechanics/board.js"; -import { Controls } from "./movement/controls.js"; -import { Hold } from "./mechanics/hold.js"; -import { Mechanics } from "./mechanics/mechanics.js"; -import { MenuActions } from "./menus/menuactions.js"; -import { ModalActions } from "./menus/modals.js"; -import { Movement } from "./movement/movement.js"; -import { Renderer } from "./display/renderer.js"; -import { Settings } from "./features/settings.js"; -import { Sounds } from "./features/sounds.js"; -import { Falling } from "./mechanics/fallingpiece.js"; -import { GameStats } from "./features/stats.js"; -import { BoardEditor } from "./features/editboard.js"; -import { History } from "./features/history.js"; -import { BoardEffects } from "./display/boardEffects.js"; -import { ProfileStats } from "./features/profileStats.js"; -import { Modes } from "./features/modes.js"; -import { BoardRenderer } from "./display/renderBoard.js"; -import { Particles } from "./display/particles.js"; -import { Zenith } from "./mechanics/zenith.js"; - -export class Game { - started; - ended; - gameTimer = 0; // id of timeout - survivalTimer = 0; // id of timeout - gravityTimer = 0; - version = '1.3.2'; - tickrate = 60; - - elementReason = document.getElementById("reason"); - elementResult = document.getElementById("result"); - elementGameEndTitle = document.getElementById("gameEndTitle"); - - - constructor() { - this.boardeffects = new BoardEffects(this); - this.profilestats = new ProfileStats(this); - this.stats = new GameStats(this); - this.falling = new Falling(this); - this.settings = new Settings(this); - this.hold = new Hold(this); - this.sounds = new Sounds(this); - this.board = new Board(this); - this.bag = new Bag(this); - this.mechanics = new Mechanics(this); - this.menuactions = new MenuActions(this); - this.modals = new ModalActions(this); - this.movement = new Movement(this); - this.renderer = new Renderer(this); - this.boardrender = new BoardRenderer(this); - this.particles = new Particles(this); - this.boardeditor = new BoardEditor(this); - this.controls = new Controls(this); - this.history = new History(this); - this.modes = new Modes(this); - this.zenith = new Zenith(this) - - this.renderer.sizeCanvas(); - this.particles.initBoard(); - this.renderer.setEditPieceColours(); - this.sounds.initSounds(); - this.startGame(); - this.renderer.renderingLoop(); - this.boardeditor.addListeners(); - this.menuactions.addRangeListener(); - this.modals.generate.addMenuListeners(); - this.modals.generate.generateGamemodeMenu(); - this.modals.generate.generateStatList(); - this.modals.generate.generateSkinList(); - this.sounds.addMenuSFX(); - this.profilestats.loadPBs(); - this.versionChecker(); - } - - startGame() { - this.menuactions.loadSettings(); - this.resetState(); - this.renderer.renderStyles(); - this.mechanics.spawnPiece(this.bag.randomiser(true), true); - this.history.save(); - } - - stopGameTimers() { //stop all the game's timers - clearInterval(this.gravityTimer); - clearInterval(this.gameTimer); - clearInterval(this.survivalTimer); - clearInterval(this.mechanics.zenithTimer) - this.mechanics.locking.lockingPause(); - } - - endGame(top, bottom = "Better luck next time") { - const dead = ["Lockout", "Topout", "Blockout"].includes(top); // survival mode end instead of lose - if (this.settings.game.gamemode == 'survival' && dead) { - this.ended = true; - return; - } - - if (top == "Topout" || top == "Blockout" || top == "Lockout") { - this.sounds.playSound("topout"); - this.sounds.playSound("failure"); - } else if (top == undefined) { - return; - } else { - this.sounds.playSound("finish"); - } - - this.ended = true; - this.modals.openModal("gameEnd"); - this.stopGameTimers() - this.elementReason.textContent = top; - this.elementResult.textContent = bottom; - this.profilestats.saveSession(); - } - - resetState() { - this.boardeffects.hasPace = true; - this.boardeffects.paceCooldown = 0; - this.boardrender.boardAlpha = 1; - this.boardrender.queueAlpha = 1; - this.renderer.inDanger = false; - this.started = false; - this.ended = false; - - this.board.resetBoard(); - this.mechanics.locking.clearLockDelay(); - this.boardeffects.toggleRainbow(false); - this.renderer.resetActionText(); - this.renderer.renderDanger(); - this.particles.clearParticles(); - this.renderer.clearHold(); - this.stopGameTimers(); - - this.bag = new Bag(this); - this.mechanics = new Mechanics(this); - this.falling = new Falling(this); - this.hold = new Hold(this); - this.stats = new GameStats(this); - this.history = new History(this); - this.zenith = new Zenith(this); - - this.renderer.renderSidebar(); - this.modes.checkFinished(); - this.stats.updateStats(); - this.renderer.updateAlpha(); - this.boardeffects.rainbowBoard(); - } - - gameClock() { - this.renderer.renderSidebar(); - this.modes.checkFinished(); - this.stats.updateStats(); - this.renderer.updateAlpha(); - this.boardeffects.rainbowBoard(); - } - - versionChecker() { - const userver = window.localStorage.getItem('version'); - document.getElementById('updatetext').style.display = this.version == userver ? "none" : "block"; - window.localStorage.setItem('version', this.version); - } - -} +import { Bag } from "./mechanics/bag.js"; +import { Board } from "./mechanics/board.js"; +import { Controls } from "./movement/controls.js"; +import { Hold } from "./mechanics/hold.js"; +import { Mechanics } from "./mechanics/mechanics.js"; +import { MenuActions } from "./menus/menuactions.js"; +import { ModalActions } from "./menus/modals.js"; +import { Movement } from "./movement/movement.js"; +import { Renderer } from "./display/renderer.js"; +import { Settings } from "./features/settings.js"; +import { Sounds } from "./features/sounds.js"; +import { Falling } from "./mechanics/fallingpiece.js"; +import { GameStats } from "./features/stats.js"; +import { BoardEditor } from "./features/editboard.js"; +import { History } from "./features/history.js"; +import { BoardEffects } from "./display/boardEffects.js"; +import { ProfileStats } from "./features/profileStats.js"; +import { Modes } from "./features/modes.js"; +import { BoardRenderer } from "./display/renderBoard.js"; +import { Particles } from "./display/particles.js"; +import { Zenith, Grandmaster } from "./mechanics/gamemode_extended.js"; + +export class Game { + started; + ended; + gameTimer = 0; // id of timeout + survivalTimer = 0; // id of timeout + gravityTimer = 0; + zenithTimer = 0; + grandmasterTimer = 0; + version = '1.3.2'; + tickrate = 60; + + elementReason = document.getElementById("reason"); + elementResult = document.getElementById("result"); + elementGameEndTitle = document.getElementById("gameEndTitle"); + + + constructor() { + this.boardeffects = new BoardEffects(this); + this.profilestats = new ProfileStats(this); + this.stats = new GameStats(this); + this.falling = new Falling(this); + this.settings = new Settings(this); + this.hold = new Hold(this); + this.sounds = new Sounds(this); + this.board = new Board(this); + this.bag = new Bag(this); + this.mechanics = new Mechanics(this); + this.menuactions = new MenuActions(this); + this.modals = new ModalActions(this); + this.movement = new Movement(this); + this.renderer = new Renderer(this); + this.boardrender = new BoardRenderer(this); + this.particles = new Particles(this); + this.boardeditor = new BoardEditor(this); + this.controls = new Controls(this); + this.history = new History(this); + this.modes = new Modes(this); + this.zenith = new Zenith(this); + this.grandmaster = new Grandmaster(this); + + this.renderer.sizeCanvas(); + this.particles.initBoard(); + this.renderer.setEditPieceColours(); + this.sounds.initSounds(); + this.startGame(); + this.renderer.renderingLoop(); + this.boardeditor.addListeners(); + this.menuactions.addRangeListener(); + this.modals.generate.addMenuListeners(); + this.modals.generate.generateGamemodeMenu(); + this.modals.generate.generateStatList(); + this.modals.generate.generateSkinList(); + this.sounds.addMenuSFX(); + this.profilestats.loadPBs(); + this.versionChecker(); + } + + startGame() { + this.menuactions.loadSettings(); + this.resetState(); + this.renderer.renderStyles(); + this.mechanics.spawnPiece(this.bag.randomiser(true), true); + this.history.save(); + } + + stopGameTimers() { //stop all the game's timers + clearInterval(this.gravityTimer); + clearInterval(this.gameTimer); + clearInterval(this.survivalTimer); + clearInterval(this.zenithTimer); + clearInterval(this.grandmasterTimer); + this.mechanics.locking.lockingPause(); + } + + endGame(top, bottom = "Better luck next time") { + const dead = ["Lockout", "Topout", "Blockout"].includes(top); // survival mode end instead of lose + if (this.settings.game.gamemode == 'survival' && dead) { + this.ended = true; + return; + } + + if (top == "Topout" || top == "Blockout" || top == "Lockout") { + this.sounds.playSound("topout"); + this.sounds.playSound("failure"); + } else if (top == undefined) { + return; + } else { + this.sounds.playSound("finish"); + } + + this.ended = true; + this.modals.openModal("gameEnd"); + this.stopGameTimers() + this.elementReason.textContent = top; + this.elementResult.textContent = bottom; + this.profilestats.saveSession(); + } + + resetState() { + this.boardeffects.hasPace = true; + this.boardeffects.paceCooldown = 0; + this.boardrender.boardAlpha = 1; + this.boardrender.queueAlpha = 1; + this.renderer.inDanger = false; + this.started = false; + this.ended = false; + + this.board.resetBoard(); + this.mechanics.locking.clearLockDelay(); + this.boardeffects.toggleRainbow(false); + this.renderer.resetActionText(); + this.renderer.renderDanger(); + this.particles.clearParticles(); + this.renderer.clearHold(); + this.stopGameTimers(); + + this.bag = new Bag(this); + this.mechanics = new Mechanics(this); + this.falling = new Falling(this); + this.hold = new Hold(this); + this.stats = new GameStats(this); + this.history = new History(this); + this.zenith = new Zenith(this); + this.grandmaster = new Grandmaster(this); + + + this.renderer.renderSidebar(); + this.modes.checkFinished(); + this.stats.updateStats(); + this.renderer.updateAlpha(); + this.boardeffects.rainbowBoard(); + } + + gameClock() { + this.renderer.renderSidebar(); + this.modes.checkFinished(); + this.stats.updateStats(); + this.renderer.updateAlpha(); + this.boardeffects.rainbowBoard(); + } + + versionChecker() { + const userver = window.localStorage.getItem('version'); + document.getElementById('updatetext').style.display = this.version == userver ? "none" : "block"; + window.localStorage.setItem('version', this.version); + } + +} diff --git a/src/main.js b/src/main.js index d36e77d..fa50ce4 100644 --- a/src/main.js +++ b/src/main.js @@ -1,60 +1,63 @@ -import { Game } from "./game.js"; - -const game = new Game(); - -// allow html to access functions -window["menu"] = game.menuactions; -window["modal"] = game.modals; -window["songs"] = game.sounds; - -const elementSplashScreen = document.getElementById("splashScreen"); -const elementSplashText = document.getElementById("splashText"); - -window.addEventListener("keydown", event => { - if (event.key == undefined) return; - let key = event.key.length > 1 ? event.key : event.key.toLowerCase(); // 1 letter words are lowercase - if (event.altKey) key = "Alt+" + key; - if (event.ctrlKey) key = "Ctrl+" + key; - - game.controls.onKeyDownRepeat(event, key); - if (event.repeat) return; - game.controls.onKeyDown(event, key); -}) - -window.addEventListener("keyup", event => { - if (event.key == undefined) return; - let key = event.key.length > 1 ? event.key : event.key.toLowerCase(); - game.controls.onKeyUp(event, key); -}); - -window.addEventListener('mousemove', () => { - game.controls.toggleCursor(true); -}) - -document.onresize = () => { - game.renderer.sizeCanvas(); - game.renderer.updateNext(); - game.renderer.updateHold(); -} - -// splash menu -window.addEventListener("DOMContentLoaded", () => { - elementSplashText.textContent = "Ready"; - elementSplashScreen.style.opacity = 0; - elementSplashScreen.style.scale = 1.2; - elementSplashScreen.style.display = "none"; - document.getElementById("ignoreText").style.opacity = 0.5; -}) - -window.addEventListener("focus", function () { - document.getElementById("nofocus").style.display = "none"; -}); - -window.addEventListener("blur", function () { - if (!game.settings.display.outoffocus) return - document.getElementById("nofocus").style.display = "block"; -}); - -window.onerror = (msg, url, lineNo, columnNo, error) => { - game.modals.generate.notif(error, msg + ". ln "+ lineNo, "error"); -} \ No newline at end of file +import { Game } from "./game.js"; + +const game = new Game(); + +console.log("%cTETI", "color: #DFC0F3;\n\t\t\t\t\t display: block;\n\t\t\t\t\t font-size: 5em;\n\t\t\t\t\t font-weight: 900;\n\t\t\t\t\t text-shadow: 0px 0px 2px #9150BA;\n\t\t\t\t\t background-color: #45345088;\n\t\t\t\t\t padding: 0 0.25em;\n\t\t\t\t\t border-radius: 3px;"), + + +// allow html to access functions +window["menu"] = game.menuactions; +window["modal"] = game.modals; +window["songs"] = game.sounds; + +const elementSplashScreen = document.getElementById("splashScreen"); +const elementSplashText = document.getElementById("splashText"); + +window.addEventListener("keydown", event => { + if (event.key == undefined) return; + let key = event.key.length > 1 ? event.key : event.key.toLowerCase(); // 1 letter words are lowercase + if (event.altKey) key = "Alt+" + key; + if (event.ctrlKey) key = "Ctrl+" + key; + + game.controls.onKeyDownRepeat(event, key); + if (event.repeat) return; + game.controls.onKeyDown(event, key); +}) + +window.addEventListener("keyup", event => { + if (event.key == undefined) return; + let key = event.key.length > 1 ? event.key : event.key.toLowerCase(); + game.controls.onKeyUp(event, key); +}); + +window.addEventListener('mousemove', () => { + game.controls.toggleCursor(true); +}) + +document.onresize = () => { + game.renderer.sizeCanvas(); + game.renderer.updateNext(); + game.renderer.updateHold(); +} + +// splash menu +window.addEventListener("DOMContentLoaded", () => { + elementSplashText.textContent = "Ready"; + elementSplashScreen.style.opacity = 0; + elementSplashScreen.style.scale = 1.2; + elementSplashScreen.style.display = "none"; + document.getElementById("ignoreText").style.opacity = 0.5; +}) + +window.addEventListener("focus", function () { + document.getElementById("nofocus").style.display = "none"; +}); + +window.addEventListener("blur", function () { + if (!game.settings.display.outoffocus) return + document.getElementById("nofocus").style.display = "block"; +}); + +window.onerror = (msg, url, lineNo, columnNo, error) => { + game.modals.generate.notif(error, msg + ". ln "+ lineNo, "error"); +} diff --git a/src/mechanics/bag.js b/src/mechanics/bag.js index 5a022c8..ac4b6a3 100644 --- a/src/mechanics/bag.js +++ b/src/mechanics/bag.js @@ -1,79 +1,79 @@ -import { Game } from "../game.js"; -import pieces from "../data/pieces.json" with { type: "json" }; - -export class Bag { - /** - * @type {Array>} - */ - nextPieces = [[], []]; - - - /** - * @param {Game} game - */ - constructor(game) { - this.game = game; - } - - randomiser(start = false) { - if (this.nextPieces[1].length == 0) this.shuffleRemainingPieces(); - if (this.nextPieces[0].length == 0) { - this.nextPieces = [this.nextPieces[1], []]; - this.shuffleRemainingPieces(); - } - const piece = this.nextPieces[0].splice(0, 1)[0]; - - if (["o", "s", "z"].includes(piece) && this.game.settings.game.stride && start) { // stride mode - this.nextPieces = [[], []]; - return this.randomiser(start); - } - - return pieces.filter(element => { - return element.name == piece; - })[0]; - } - - shuffleRemainingPieces() { - pieces.forEach(piece => this.nextPieces[1].push(piece.name)); - this.nextPieces[1] = this.nextPieces[1] - .map(value => ({ value, sort: Math.random() })) - .sort((a, b) => a.sort - b.sort) - .map(({ value }) => value); - } - - getFirstFive() { - return this.nextPieces[0] - .concat(this.nextPieces[1]) - .slice(0, this.game.settings.game.nextPieces); - } - - getQueue() { - return this.game.falling.piece.name - + this.nextPieces[0] - .concat(this.nextPieces[1]) - .splice(0, 6) - .join(""); - } - - setQueue(value, names) { - this.nextPieces[0] = value - .split("") - .filter(p => names.includes(p)); - this.shuffleRemainingPieces(); - this.game.renderer.updateNext(); - - this.game.mechanics.locking.clearLockDelay(); - this.game.board.MinoToNone("A"); - this.game.mechanics.isTspin = false; - this.game.mechanics.isAllspin = false; - this.game.mechanics.isMini = false; - this.game.mechanics.spawnPiece(this.game.bag.randomiser()); - this.game.history.save(); - } - - nextPiece() { - return pieces.filter( - p => p.name == this.nextPieces[0].concat(this.nextPieces[1])[0] - )[0]; - } +import { Game } from "../game.js"; +import pieces from "../data/pieces.json" with { type: "json" }; + +export class Bag { + /** + * @type {Array>} + */ + nextPieces = [[], []]; + + + /** + * @param {Game} game + */ + constructor(game) { + this.game = game; + } + + randomiser(start = false) { + if (this.nextPieces[1].length == 0) this.shuffleRemainingPieces(); + if (this.nextPieces[0].length == 0) { + this.nextPieces = [this.nextPieces[1], []]; + this.shuffleRemainingPieces(); + } + const piece = this.nextPieces[0].splice(0, 1)[0]; + + if (["o", "s", "z"].includes(piece) && this.game.settings.game.stride && start) { // stride mode + this.nextPieces = [[], []]; + return this.randomiser(start); + } + + return pieces.filter(element => { + return element.name == piece; + })[0]; + } + + shuffleRemainingPieces() { + pieces.forEach(piece => this.nextPieces[1].push(piece.name)); + this.nextPieces[1] = this.nextPieces[1] + .map(value => ({ value, sort: Math.random() })) + .sort((a, b) => a.sort - b.sort) + .map(({ value }) => value); + } + + getFirstFive() { + return this.nextPieces[0] + .concat(this.nextPieces[1]) + .slice(0, this.game.settings.game.nextPieces); + } + + getQueue() { + return this.game.falling.piece.name + + this.nextPieces[0] + .concat(this.nextPieces[1]) + .splice(0, 6) + .join(""); + } + + setQueue(value, names) { + this.nextPieces[0] = value + .split("") + .filter(p => names.includes(p)); + this.shuffleRemainingPieces(); + this.game.renderer.updateNext(); + + this.game.mechanics.locking.clearLockDelay(); + this.game.board.MinoToNone("A"); + this.game.mechanics.isTspin = false; + this.game.mechanics.isAllspin = false; + this.game.mechanics.isMini = false; + this.game.mechanics.spawnPiece(this.game.bag.randomiser()); + this.game.history.save(); + } + + nextPiece() { + return pieces.filter( + p => p.name == this.nextPieces[0].concat(this.nextPieces[1])[0] + )[0]; + } } \ No newline at end of file diff --git a/src/mechanics/board.js b/src/mechanics/board.js index cf1532d..23016e3 100644 --- a/src/mechanics/board.js +++ b/src/mechanics/board.js @@ -1,127 +1,127 @@ -import { Game } from "../game.js"; - -export class Board { - /** - * @type {string[][]} - */ - boardState = []; - - /** - * @param {Game} game - */ - constructor(game) { - this.game = game; - } - // modify board - checkMino([x, y], val) { - return this.boardState[y][x].split(" ").includes(val); - } - - MinoToNone(val) { - this.getMinos(val).forEach(([x, y]) => this.rmValue([x, y], val)); - } - - EradicateMinoCells(val) { - this.getCoords(this.boardState, c => c.includes(val), [0, 0]).forEach(([x, y]) => this.rmValue([x, y], val)); - } - - addMinos(val, c, [dx, dy]) { - c.forEach(([x, y]) => this.setValue([x + dx, y + dy], val)); - } - - addValFront([x, y], val) { - this.boardState[y][x] = `${val} ${this.boardState[y][x]}`; - } - - addValue([x, y], val) { - this.boardState[y][x] = (this.boardState[y][x] + " " + val).trim(); - } - - setValue([x, y], val) { - this.boardState[y][x] = val; - } - - rmValue([x, y], val) { - this.boardState[y][x] = this.boardState[y][x].replace(val, "").trim(); - } - - getMinos(name) { - return this.getCoords(this.boardState, c => c.split(" ").includes(name), [0, 0]); - } - - pieceToCoords(arr, [dx, dy] = [0, 0]) { - return this.getCoords(arr.toReversed(), c => c == 1, [dx, dy]); - } - - setCoordEmpty([x, y]) { - this.boardState[y][x] = ""; - } - - resetBoard() { - this.boardState = [...Array(40)].map(() => [...Array(10)].map(() => "")); - } - - getFullRows() { - const rows = this.getMinos("S") - .map(coord => coord[1]) - .reduce((prev, curr) => ((prev[curr] = ++prev[curr] || 1), prev), {}); - return Object.keys(rows) - .filter(key => rows[key] >= 10) - .map(row => +row) - .toReversed(); - } - - getCoords(array, filter, [dx, dy]) { - const coords = []; - array.forEach((row, y) => - row.forEach((col, x) => { - if (filter(col)) coords.push([x + dx, y + dy]); - }) - ); - return coords; - } - - moveMinos(coords, dir, size, value = "") { - const getChange = ([x, y], a) => { - return { RIGHT: [x + a, y], LEFT: [x - a, y], DOWN: [x, y - a], UP: [x, y + a] }; - }; - const newcoords = coords.map(c => getChange(c, size)[dir]); - - if (newcoords.some(([x, y]) => y > 39)) { - this.game.endGame("Topout"); - return; - } - - const valTable = coords.map(([x, y]) => (value ? value : this.boardState[y][x])); - coords.forEach((c, idx) => this.rmValue(c, valTable[idx])); - - newcoords.forEach((c, idx) => - value ? this.addValue(c, valTable[idx]) : this.setValue(c, valTable[idx]) - ); - this.game.mechanics.spawnOverlay(); - } - - setComboBoard(start) { - // 4w sides - this.boardState.forEach((row, y) => - row.forEach((col, x) => { - if (x < 3 || x > 6) this.addMinos("S G", [[x, y]], [0, 0]); - }) - ); - - if (!start) return; - // garbage pattern - const validCoords = [[[0, 0], [1, 0], [2, 0], [3, 0]], [[0, 1], [1, 1], [2, 1], [3, 1]]]; - const garbAmount = Math.random() > 0.5 ? 3 : 6; - const garbCoords = []; - for (let i = 0; i < garbAmount; i++) { - const y = Math.random() > 0.5 ? 0 : 1; - if (validCoords[y].length == 1) { i--; continue; } - const coord = validCoords[y].splice(Math.floor(Math.random() * validCoords[y].length), 1); - garbCoords.push(coord[0]); - } - - this.addMinos("S G", garbCoords.map(([x, y]) => [x + 3, y]), [0, 0]); - this.game.mechanics.setShadow(); - } -} +import { Game } from "../game.js"; + +export class Board { + /** + * @type {string[][]} + */ + boardState = []; + + /** + * @param {Game} game + */ + constructor(game) { + this.game = game; + } + // modify board + checkMino([x, y], val) { + return this.boardState[y][x].split(" ").includes(val); + } + + MinoToNone(val) { + this.getMinos(val).forEach(([x, y]) => this.rmValue([x, y], val)); + } + + EradicateMinoCells(val) { + this.getCoords(this.boardState, c => c.includes(val), [0, 0]).forEach(([x, y]) => this.rmValue([x, y], val)); + } + + addMinos(val, c, [dx, dy]) { + c.forEach(([x, y]) => this.setValue([x + dx, y + dy], val)); + } + + addValFront([x, y], val) { + this.boardState[y][x] = `${val} ${this.boardState[y][x]}`; + } + + addValue([x, y], val) { + this.boardState[y][x] = (this.boardState[y][x] + " " + val).trim(); + } + + setValue([x, y], val) { + this.boardState[y][x] = val; + } + + rmValue([x, y], val) { + this.boardState[y][x] = this.boardState[y][x].replace(val, "").trim(); + } + + getMinos(name) { + return this.getCoords(this.boardState, c => c.split(" ").includes(name), [0, 0]); + } + + pieceToCoords(arr, [dx, dy] = [0, 0]) { + return this.getCoords(arr.toReversed(), c => c == 1, [dx, dy]); + } + + setCoordEmpty([x, y]) { + this.boardState[y][x] = ""; + } + + resetBoard() { + this.boardState = [...Array(40)].map(() => [...Array(10)].map(() => "")); + } + + getFullRows() { + const rows = this.getMinos("S") + .map(coord => coord[1]) + .reduce((prev, curr) => ((prev[curr] = ++prev[curr] || 1), prev), {}); + return Object.keys(rows) + .filter(key => rows[key] >= 10) + .map(row => +row) + .toReversed(); + } + + getCoords(array, filter, [dx, dy]) { + const coords = []; + array.forEach((row, y) => + row.forEach((col, x) => { + if (filter(col)) coords.push([x + dx, y + dy]); + }) + ); + return coords; + } + + moveMinos(coords, dir, size, value = "") { + const getChange = ([x, y], a) => { + return { RIGHT: [x + a, y], LEFT: [x - a, y], DOWN: [x, y - a], UP: [x, y + a] }; + }; + const newcoords = coords.map(c => getChange(c, size)[dir]); + + if (newcoords.some(([x, y]) => y > 39)) { + this.game.endGame("Topout"); + return; + } + + const valTable = coords.map(([x, y]) => (value ? value : this.boardState[y][x])); + coords.forEach((c, idx) => this.rmValue(c, valTable[idx])); + + newcoords.forEach((c, idx) => + value ? this.addValue(c, valTable[idx]) : this.setValue(c, valTable[idx]) + ); + this.game.mechanics.spawnOverlay(); + } + + setComboBoard(start) { + // 4w sides + this.boardState.forEach((row, y) => + row.forEach((col, x) => { + if (x < 3 || x > 6) this.addMinos("S G", [[x, y]], [0, 0]); + }) + ); + + if (!start) return; + // garbage pattern + const validCoords = [[[0, 0], [1, 0], [2, 0], [3, 0]], [[0, 1], [1, 1], [2, 1], [3, 1]]]; + const garbAmount = Math.random() > 0.5 ? 3 : 6; + const garbCoords = []; + for (let i = 0; i < garbAmount; i++) { + const y = Math.random() > 0.5 ? 0 : 1; + if (validCoords[y].length == 1) { i--; continue; } + const coord = validCoords[y].splice(Math.floor(Math.random() * validCoords[y].length), 1); + garbCoords.push(coord[0]); + } + + this.addMinos("S G", garbCoords.map(([x, y]) => [x + 3, y]), [0, 0]); + this.game.mechanics.setShadow(); + } +} diff --git a/src/mechanics/clearlines.js b/src/mechanics/clearlines.js index 15eeab1..3964865 100644 --- a/src/mechanics/clearlines.js +++ b/src/mechanics/clearlines.js @@ -37,6 +37,7 @@ export class ClearLines { if (clearRows.length > 0) this.game.renderer.bounceBoard("DOWN"); this.game.particles.spawnParticles(0, Math.min(...clearRows), "clear") this.processLineClear(removedGarbage, clearRows); + return clearRows.length; } clearRow(rowNumber) { @@ -66,6 +67,7 @@ export class ClearLines { this.manageGarbageSent(damage); this.game.zenith.AwardLines(damage); + if (linecount == 1) this.game.zenith.AwardLines(1); // render action text if (mech.isAllspin) damagetype = damagetype.replace("Tspin ", this.game.falling.piece.name + " spin "); this.game.renderer.renderActionText(damagetype, isBTB, isPC, damage, linecount); diff --git a/src/mechanics/fallingpiece.js b/src/mechanics/fallingpiece.js index b145676..f578a9e 100644 --- a/src/mechanics/fallingpiece.js +++ b/src/mechanics/fallingpiece.js @@ -1,64 +1,64 @@ -import { KickData, KickData180 } from "../data/kicks.js"; -import { Game } from "../game.js"; - -export class Falling { - piece = null; - location = []; - moved = false; - rotation = 1; - - /** - * @param {Game} game - */ - constructor(game) { - this.game = game; - } - - spawn(piece) { - const dx = piece.name == "o" ? 4 : 3; - const dy = piece.name == "o" ? 21 : piece.name == "i" ? 19 : 20; - const coords = this.game.mechanics.board.pieceToCoords(piece.shape1); - this.game.mechanics.board.addMinos("A " + piece.name, coords, [dx, dy]); - this.location = [dx, dy]; - this.piece = piece; - this.rotation = 1; - } - - getKickData(rotationType, shapeNo) { - const isI = this.piece.name == "i" ? 1 : 0; - const direction = rotationType == "CCW" ? (shapeNo > 3 ? 0 : shapeNo) : shapeNo - 1; - return { - 180: KickData180[isI][direction], - CW: KickData[isI][direction], - CCW: KickData[isI][direction].map(row => row.map(x => x * -1)), - }[rotationType]; - } - - getRotateState(type) { - const newState = (this.rotation + { CW: 1, CCW: -1, 180: 2 }[type]) % 4; - return newState == 0 ? 4 : newState; - } - - getNewCoords(rotation) { - return this.game.board.pieceToCoords( - this.piece[`shape${rotation}`], - this.location - ); - } - - newName() { - return "A " + this.piece.name; - } - - updateLocation([dx, dy]) { - this.location = [ - this.location[0] + dx, - this.location[1] + dy, - ]; - if (dx != 0 || dy != 0) { - this.moved = true; - } - } - - +import { KickData, KickData180 } from "../data/kicks.js"; +import { Game } from "../game.js"; + +export class Falling { + piece = null; + location = []; + moved = false; + rotation = 1; + + /** + * @param {Game} game + */ + constructor(game) { + this.game = game; + } + + spawn(piece) { + const dx = piece.name == "o" ? 4 : 3; + const dy = piece.name == "o" ? 21 : piece.name == "i" ? 19 : 20; + const coords = this.game.mechanics.board.pieceToCoords(piece.shape1); + this.game.mechanics.board.addMinos("A " + piece.name, coords, [dx, dy]); + this.location = [dx, dy]; + this.piece = piece; + this.rotation = 1; + } + + getKickData(rotationType, shapeNo) { + const isI = this.piece.name == "i" ? 1 : 0; + const direction = rotationType == "CCW" ? (shapeNo > 3 ? 0 : shapeNo) : shapeNo - 1; + return { + 180: KickData180[isI][direction], + CW: KickData[isI][direction], + CCW: KickData[isI][direction].map(row => row.map(x => x * -1)), + }[rotationType]; + } + + getRotateState(type) { + const newState = (this.rotation + { CW: 1, CCW: -1, 180: 2 }[type]) % 4; + return newState == 0 ? 4 : newState; + } + + getNewCoords(rotation) { + return this.game.board.pieceToCoords( + this.piece[`shape${rotation}`], + this.location + ); + } + + newName() { + return "A " + this.piece.name; + } + + updateLocation([dx, dy]) { + this.location = [ + this.location[0] + dx, + this.location[1] + dy, + ]; + if (dx != 0 || dy != 0) { + this.moved = true; + } + } + + } \ No newline at end of file diff --git a/src/mechanics/gamemode_extended.js b/src/mechanics/gamemode_extended.js new file mode 100644 index 0000000..251c268 --- /dev/null +++ b/src/mechanics/gamemode_extended.js @@ -0,0 +1,277 @@ +import { Game } from "../game.js"; + +export class Zenith { + + /** + * @param {Game} game + */ + + constructor(game) { + this.game = game + } + + climbPoints = 0; + isLastRankChangePromote = !0; + isHyperspeed = true; + rankLock = 0; + promotionFatigue = 0; + rankLock = 0; + tickPass = 0; + tempAltitude = 0 + + FloorDistance = [0, 50, 150, 300, 450, 650, 850, 1100, 1350, 1650, 1 / 0]; + SpeedrunReq = [7, 8, 8, 9, 9, 10, 0, 0, 0, 0, 0]; + + + GetSpeedCap(e) { + const t = this.FloorDistance.find((t => e < t)) - e; + return Math.max(0, Math.min(1, t / 5 - .2)) + } + + GetFloorLevel(e) { + return this.FloorDistance.filter((t => e >= t)).length || 1 + } + + AwardLines(e, t=!0, n=!0) { + const s = .25 * Math.floor(this.game.stats.climbSpeed); + this.GiveBonus(s * e * (t ? 1 : 0)); + if (e <= 0 ) return + this.GiveClimbPts((e + .05) * (n ? 1 : 0)) + } + + GiveBonus(e) { + this.tempAltitude += e + } + + GiveClimbPts(e) { + this.climbPoints += e + } + + startZenithMode() { + clearInterval(this.game.zenithTimer); + document.getElementById("climbSpeedBar").style.display = "none" + if(this.game.settings.game.gamemode != "zenith") return + document.getElementById("climbSpeedBar").style.display = "block" + this.game.zenithTimer = setInterval( + () => { + let t = Math.floor(this.game.stats.climbSpeed), + o = .25 * t, + a = this.GetSpeedCap(this.tempAltitude); + + if (this.tickPass >= this.rankLock) { + let e = 3; + this.climbPoints -= e * (t ** 2 + t) / 3600 + } + const s = 4 * t, + i = 4 * (t - 1) + + if (this.climbPoints < 0){ + if (t <= 1){ + this.climbPoints = 0; + } + else { + this.climbPoints += i, + this.game.sounds.playSound("speed_down") + this.isLastRankChangePromote = !1, + t-- + } + } + else if (this.climbPoints >= s) { + this.climbPoints -= s, + this.game.sounds.playSound("speed_up") + this.isLastRankChangePromote = !0, + t++; + this.rankLock = this.tickPass + Math.max(60, 60 * (5 - this.promotionFatigue)); + this.promotionFatigue++; + } + + this.game.stats.climbSpeed = t + this.climbPoints / (4 * t); + + this.tempAltitude += o / 60 * a + + if(this.game.stats.floor != this.GetFloorLevel(this.tempAltitude)){ + this.startZenithMode() + this.game.stats.floor = this.GetFloorLevel(this.tempAltitude) + this.game.sounds.playSound("zenith_levelup") + this.game.renderer.renderTimeLeft("FLOOR " + this.game.stats.floor) + } + const g = this.tempAltitude + this.game.stats.altitude = this.tempAltitude + this.tickPass++ + this.drawClimbSpeedBar(Math.floor(this.game.stats.climbSpeed), this.climbPoints, s) + } + , 1000 / this.game.tickrate); + } + + drawClimbSpeedBar(speed, point, require){ // todo: drawing polygons (parallelogram) cus idk + const color = ["var(--invis)", "red", "orange", "green", "blue", "#FF1493", "tan", "lightgreen", "lightblue", "pink", "white", "white", "white", "white", "white", "white", "white", "white", "white", "white", "white", "white", "white", "white", "white", "white"] + const climbSpeedBar = document.getElementById("climbSpeedBar") + + climbSpeedBar.value = point + climbSpeedBar.max = require + document.styleSheets[1].cssRules[24].style.backgroundColor = color[speed - 1] + document.styleSheets[1].cssRules[23].style.backgroundColor = color[speed] + } + +} + +export class Grandmaster { + + /** + * @param {Game} game + */ + + constructor(game) { + this.game = game + } + + gradeBoost = 0; // the one used to determine which grade to shown in the array + gradePoint = 0; + internalGrade = 0; // the one to determine how many grade to boost + isCoolCheck = false; + coolsCount = 0; + regretsCount = 0; + sectionTarget = 100; + sectionTime = 0; + + grades = [ + "9","8","7","6","5","4","3","2","1", + "S1","S2","S3","S4","S5","S6","S7","S8","S9", + "m1","m2","m3","m4","m5","m6","m7","m8","m9", + "M","MK","MV","MO","MM-","MM","MM+","GM-","GM","GM+","TM-","TM","TM+" + ]; + gradePointDecay = [ + 125, 80, 80, 50, 45, 45, 45, + 40, 40, 40, 40, 40, 30, 30, 30, + 20, 20, 20, 20, 20, + 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, + 10, 10 + ]; + mult = [ + [1.0, 1.0, 1.0, 1.0], + [1.0, 1.2, 1.4, 1.5], + [1.0, 1.2, 1.5, 1.8], + [1.0, 1.4, 1.6, 2.0], + [1.0, 1.4, 1.7, 2.2], + [1.0, 1.4, 1.8, 2.3], + [1.0, 1.4, 1.9, 2.4], + [1.0, 1.5, 2.0, 2.5], + [1.0, 1.5, 2.1, 2.6], + [1.0, 2.0, 2.5, 3.0], + ]; + gradePointBonus = [ + [10, 20, 40, 50], + [10, 20, 30, 40], + [10, 20, 30, 40], + [10, 15, 30, 40], + [10, 15, 20, 40], + [5, 15, 20, 30], + [5, 10, 20, 30], + [5, 10, 15, 30], + [5, 10, 15, 30], + [5, 10, 15, 30], + [2, 12, 13, 30], + [2, 12, 13, 30], + [2, 12, 13, 30], + [2, 12, 13, 30], + [2, 12, 13, 30], + [2, 12, 13, 30], + [2, 12, 13, 30], + [2, 12, 13, 30], + [2, 12, 13, 30], + [2, 12, 13, 30], + [2, 12, 13, 30], + [2, 12, 13, 30], + [2, 12, 13, 30], + [2, 12, 13, 30], + [2, 12, 13, 30], + [2, 12, 13, 30], + [2, 12, 13, 30], + [2, 12, 13, 30], + [2, 12, 13, 30], + [2, 12, 13, 30], + [2, 12, 13, 30], + [2, 12, 13, 30], + [2, 12, 13, 30], + [2, 12, 13, 30], + [2, 12, 13, 30], + [2, 12, 13, 30], + [2, 12, 13, 30], + [2, 12, 13, 30], + [2, 12, 13, 30], + [2, 12, 13, 30], + [2, 12, 13, 30], + [2, 12, 13, 30], + [2, 12, 13, 30], + [2, 12, 13, 30], + [2, 12, 13, 30], + [2, 12, 13, 30], + [2, 12, 13, 30], + [2, 12, 13, 30], + [2, 12, 13, 30], + [2, 12, 13, 30], + [2, 12, 13, 30], + [2, 12, 13, 30], + [2, 12, 13, 30], + [2, 12, 13, 30], // is this long enough? + ]; + gradeBoostTable = [ + 0,1,2,3,4,5,5,6,6,7,7,7,8,8,8,9,9,9,10,11,12,12,12,13,13,14,14,15,15,16,16,17,17,18,18,19,19,20,20,21,21,22,22,23,23,24,24,25,25,26,26,26,27,27,27,27,28,28,28,28,28,29,29,29,29,29,30,30,30,30,30 + ]; + + coolsTable = [52, 52, 49, 45, 45, 42, 42, 38, 38, 0]; + regretsTable = [90, 75, 75, 68, 60, 60, 50, 50, 50, 50]; + + addGrade(row, cmb, lvl){ + if(this.game.settings.game.gamemode != "race") return + this.checkSectionCleared(); + this.checkCool(); + this.game.stats.grade = this.grades[this.gradeBoost + this.coolsCount - this.regretsCount]; + if (row<1) return; + + const pts = this.gradePointBonus[this.internalGrade][row - 1]; + const cmb_mult = this.mult[Math.min(9, cmb)][row - 1]; + const lvl_mult = Math.floor(lvl / 250) + 1; + + this.gradePoint += pts*cmb_mult*lvl_mult; + + if (this.gradePoint >= 100) { + this.gradePoint = 0; + this.internalGrade++; + this.gradeBoost = this.gradeBoostTable[this.internalGrade]; + this.startGrandmasterTimer(this.gradePointDecay[this.internalGrade]); + }; + } + + startGrandmasterTimer(){ + clearInterval(this.game.grandmasterTimer); + if(this.game.settings.game.gamemode != "race") return + this.game.grandmasterTimer = setInterval(() => { + this.gradePoint = Math.max(0, this.gradePoint - 1); + }, (1000 / 60 * this.gradePointDecay[this.internalGrade]) ) + } + + checkSectionCleared(){ + if(this.game.stats.tgm_level >= this.sectionTarget){ + this.game.renderer.renderTimeLeft("SECTION " + this.sectionTarget / 100 + " CLEAR"); + this.game.sounds.playSound("levelup"); + if(this.sectionTime >= this.regretsTable[(this.sectionTarget / 100) - 1]){ + this.game.renderer.renderTimeLeft("REGRET"); + this.regretsCount++; + } + this.sectionTime = 0; + this.isCoolCheck = false; + this.sectionTarget = Math.min(this.sectionTarget + 100, this.game.settings.game[this.game.modes.modeJSON.target]) + } + } + + checkCool(){ + if(this.game.stats.tgm_level % 100 >= 70 && !this.isCoolCheck){ + this.isCoolCheck = true; + if(this.sectionTime <= this.coolsTable[(this.sectionTarget / 100) - 1]){ + this.game.renderer.renderTimeLeft("COOL!"); + this.coolsCount++; + } + } + } +} diff --git a/src/mechanics/hold.js b/src/mechanics/hold.js index b64b07b..bf1e05f 100644 --- a/src/mechanics/hold.js +++ b/src/mechanics/hold.js @@ -1,43 +1,43 @@ -import { Game } from "../game.js"; -import pieces from "../data/pieces.json" with { type: "json" }; - - -export class Hold { - piece; - occured = false; - pieceNames = ["s", "z", "i", "j", "l", "o", "t"]; - - /** - * @param {Game} game - */ - constructor(game) { - this.game = game; - this.curr = this.game.falling; - } - - setHold() { - this.piece = this.curr.piece; - } - - swapHold() { - [this.game.hold.piece, this.curr.piece] - = [this.curr.piece, this.game.hold.piece,]; - } - - getHold() { - return this.game.hold.piece ? this.game.hold.piece.name : "" - } - - setNewHold(val) { - const validPiece = [val].filter(p => this.pieceNames.includes(p)); - this.piece = this.getPiece(validPiece); - this.occured = false; - this.game.renderer.updateHold(); - this.game.history.save(); - } - - getPiece(name) { - return pieces.filter(p => p.name == name)[0]; - } - +import { Game } from "../game.js"; +import pieces from "../data/pieces.json" with { type: "json" }; + + +export class Hold { + piece; + occured = false; + pieceNames = ["s", "z", "i", "j", "l", "o", "t"]; + + /** + * @param {Game} game + */ + constructor(game) { + this.game = game; + this.curr = this.game.falling; + } + + setHold() { + this.piece = this.curr.piece; + } + + swapHold() { + [this.game.hold.piece, this.curr.piece] + = [this.curr.piece, this.game.hold.piece,]; + } + + getHold() { + return this.game.hold.piece ? this.game.hold.piece.name : "" + } + + setNewHold(val) { + const validPiece = [val].filter(p => this.pieceNames.includes(p)); + this.piece = this.getPiece(validPiece); + this.occured = false; + this.game.renderer.updateHold(); + this.game.history.save(); + } + + getPiece(name) { + return pieces.filter(p => p.name == name)[0]; + } + } \ No newline at end of file diff --git a/src/mechanics/locking.js b/src/mechanics/locking.js index c6fe641..add4963 100644 --- a/src/mechanics/locking.js +++ b/src/mechanics/locking.js @@ -1,127 +1,135 @@ -import { Game } from "../game.js"; - -export class LockPiece { - divLockTimer = document.getElementById("lockTimer"); - divLockCounter = document.getElementById("lockCounter"); - lockCount; - timings = { lockdelay: 0, lockingTimer: 0 } - - startTime = 0; - remaining = 0; - - /** - * @param {Game} game - */ - constructor(game) { - this.game = game; - } - - incrementLock() { - if (this.timings.lockdelay != 0) { - this.lockCount++; - this.game.mechanics.locking.clearLockDelay(false); - if (this.game.settings.game.maxLockMovements != 0 && this.game.settings.display.lockBar) { - const amountToAdd = 100 / this.game.settings.game.maxLockMovements; - this.divLockCounter.value += amountToAdd; - } - } - if (this.game.movement.checkCollision(this.game.mechanics.board.getMinos("A"), "DOWN")) { - this.game.mechanics.locking.scheduleLock(); - } - } - - scheduleLock() { - const LockMoves = - this.game.settings.game.maxLockMovements == 0 - ? Infinity - : this.game.settings.game.maxLockMovements; - if (this.lockCount >= LockMoves) { - this.game.mechanics.locking.lockPiece(); - return; - } - if (this.game.settings.game.lockDelay == 0) return; - - this.lockDelayStart(this.game.settings.game.lockDelay); - } - - lockDelayStart(delay) { - clearTimeout(this.timings.lockdelay); - clearInterval(this.timings.lockingTimer); - this.startTime = Date.now(); - this.timings.lockdelay = setTimeout( - () => this.game.mechanics.locking.lockPiece(), - delay); - this.timings.lockingTimer = setInterval(() => { - const amountToAdd = 1000 / this.game.settings.game.lockDelay; - if (this.game.settings.display.lockBar) this.divLockTimer.value += amountToAdd; - }, 10); - } - - lockingPause() { - if (this.timings.lockdelay == 0) return; - this.remaining = this.game.settings.game.lockDelay - (Date.now() - this.startTime); - clearTimeout(this.timings.lockdelay); - clearInterval(this.timings.lockingTimer); - } - - lockingResume() { - if (this.timings.lockdelay == 0) return; - this.lockDelayStart(this.remaining); - } - - lockPiece() { - const lockCoords = this.game.mechanics.board.getMinos("A"); - this.game.boardrender.justPlacedCoords = lockCoords; - this.game.boardrender.justPlacedAlpha = 1; - - lockCoords.forEach(([x, y]) => { - this.game.boardrender.flashTimes.push({ c:[x, y], t: 15 }) - this.game.mechanics.board.rmValue([x, y], "A"); - this.game.mechanics.board.addValFront([x, y], "S"); - }); - this.game.mechanics.locking.clearLockDelay(); - clearInterval(this.game.gravityTimer); - this.game.mechanics.clear.clearLines(lockCoords); - this.game.endGame( // stopped overlap next - this.game.mechanics.checkDeath( - this.game.mechanics.board.getMinos("S"), - this.game.mechanics.board.getMinos("NP") - ) - ); - this.game.endGame( // check lockout - this.game.mechanics.checkDeath( - lockCoords, - this.game.mechanics.board.getMinos("NP") - ) - ); - this.game.stats.pieceCount++; - this.game.hold.occured = false; - this.game.mechanics.isTspin = false; - this.game.mechanics.isAllspin = false; - this.game.mechanics.isMini = false; - this.game.falling.moved = false; - if (this.game.stats.level % 100 != 99 && this.game.stats.level != this.game.settings.game.raceTarget - 1) this.game.stats.level++; - const xvals = [...new Set(lockCoords.map(([x, y]) => x))]; - const yval = Math.min(...lockCoords.map(([x, y]) => y)); - this.game.particles.spawnParticles(Math.min(...xvals) + 1, yval, "lock", xvals.length); - this.game.mechanics.spawnPiece(this.game.bag.randomiser()); - this.game.history.save(); - this.game.renderer.renderDanger(); - } - - clearLockDelay(clearCount = true) { - clearInterval(this.timings.lockingTimer); - this.stopTimeout("lockdelay"); - this.divLockTimer.value = 0; - if (!clearCount) return; - this.divLockCounter.value = 0; - this.lockCount = 0; - if (this.game.settings.game.preserveARR) return; - this.game.controls.resetMovements(); - } - - stopTimeout(name) { - clearTimeout(this.timings[name]); - this.timings[name] = 0; - } -} +import { Game } from "../game.js"; + +export class LockPiece { + divLockTimer = document.getElementById("lockTimer"); + divLockCounter = document.getElementById("lockCounter"); + lockCount; + timings = { lockdelay: 0, lockingTimer: 0, clearDelay: 0 } + + startTime = 0; + remaining = 0; + + /** + * @param {Game} game + */ + constructor(game) { + this.game = game; + } + + incrementLock() { + if (this.timings.lockdelay != 0) { + this.lockCount++; + this.game.mechanics.locking.clearLockDelay(false); + if (this.game.settings.game.maxLockMovements != 0 && this.game.settings.display.lockBar) { + const amountToAdd = 100 / this.game.settings.game.maxLockMovements; + this.divLockCounter.value += amountToAdd; + } + } + if (this.game.movement.checkCollision(this.game.mechanics.board.getMinos("A"), "DOWN")) { + this.game.mechanics.locking.scheduleLock(); + } + } + + scheduleLock() { + const LockMoves = + this.game.settings.game.maxLockMovements == 0 + ? Infinity + : this.game.settings.game.maxLockMovements; + if (this.lockCount >= LockMoves) { + this.game.mechanics.locking.lockPiece(); + return; + } + if (this.game.settings.game.lockDelay == 0) return; + + this.lockDelayStart(this.game.settings.game.lockDelay); + } + + lockDelayStart(delay) { + clearTimeout(this.timings.lockdelay); + clearInterval(this.timings.lockingTimer); + this.startTime = Date.now(); + this.timings.lockdelay = setTimeout( + () => this.game.mechanics.locking.lockPiece(), + delay); + this.timings.lockingTimer = setInterval(() => { + const amountToAdd = 1000 / this.game.settings.game.lockDelay; + if (this.game.settings.display.lockBar) this.divLockTimer.value += amountToAdd; + }, 10); + } + + lockingPause() { + if (this.timings.lockdelay == 0) return; + this.remaining = this.game.settings.game.lockDelay - (Date.now() - this.startTime); + clearTimeout(this.timings.lockdelay); + clearInterval(this.timings.lockingTimer); + } + + lockingResume() { + if (this.timings.lockdelay == 0) return; + this.lockDelayStart(this.remaining); + } + + lockPiece() { + const lockCoords = this.game.mechanics.board.getMinos("A"); + this.game.boardrender.justPlacedCoords = lockCoords; + this.game.boardrender.justPlacedAlpha = 1; + + lockCoords.forEach(([x, y]) => { + this.game.boardrender.flashTimes.push({ c: [x, y], t: 15 }) + this.game.mechanics.board.rmValue([x, y], "A"); + this.game.mechanics.board.addValFront([x, y], "S"); + }); + + this.game.mechanics.locking.clearLockDelay(); + clearInterval(this.game.gravityTimer); + const cleared = this.game.mechanics.clear.clearLines(lockCoords); + + this.game.endGame( // check stopped overlap next + this.game.mechanics.checkDeath( + this.game.mechanics.board.getMinos("S"), + this.game.mechanics.board.getMinos("NP") + ) + ); + this.game.endGame( // check lockout + this.game.mechanics.checkDeath( + lockCoords, + this.game.mechanics.board.getMinos("NP") + ) + ); + this.game.stats.pieceCount++; + this.game.hold.occured = false; + this.game.mechanics.isTspin = false; + this.game.mechanics.isAllspin = false; + this.game.mechanics.isMini = false; + this.game.falling.moved = false; + if (this.game.stats.tgm_level % 100 != 99 && this.game.stats.tgm_level != this.game.settings.game.raceTarget - 1) this.game.stats.tgm_level++; + + const xvals = [...new Set(lockCoords.map(([x, y]) => x))]; + const yval = Math.min(...lockCoords.map(([x, y]) => y)); + this.game.particles.spawnParticles(Math.min(...xvals) + 1, yval, "lock", xvals.length); + this.game.renderer.renderDanger(); + + const delay = (cleared > 0) ? this.game.settings.game.clearDelay : 0; + this.timings.clearDelay = setTimeout(() => { + this.game.mechanics.spawnPiece(this.game.bag.randomiser()); + this.game.history.save(); + this.timings.clearDelay = 0; + }, delay); + } + + clearLockDelay(clearCount = true) { + clearInterval(this.timings.lockingTimer); + this.stopTimeout("lockdelay"); + this.divLockTimer.value = 0; + if (!clearCount) return; + this.divLockCounter.value = 0; + this.lockCount = 0; + if (this.game.settings.game.preserveARR) return; + this.game.controls.resetMovements(); + } + + stopTimeout(name) { + clearTimeout(this.timings[name]); + this.timings[name] = 0; + } +} diff --git a/src/mechanics/mechanics.js b/src/mechanics/mechanics.js index a468c9a..403409b 100644 --- a/src/mechanics/mechanics.js +++ b/src/mechanics/mechanics.js @@ -1,141 +1,140 @@ -import { Game } from "../game.js"; -import { ClearLines } from "./clearlines.js"; -import { LockPiece } from "./locking.js"; - -export class Mechanics { - board; - isTspin = false; - isAllspin = false; - isMini = false; - garbageQueue = 0; - spikeCounter = 0; - toppingOut = false; - zenithTimer = 0; - - /** - * @param {Game} game - */ - constructor(game) { - this.game = game; - this.board = game.board; - this.clear = new ClearLines(game); - this.locking = new LockPiece(game); - } - - checkDeath(coords, collider) { - if (coords.length == 0) return; - const collision = coords.every(c => this.game.movement.checkCollision([c], "PLACE", [])); - const collision2 = this.game.movement.checkCollision(coords, "SPAWN", collider); - const isGarbage = collider.some(c => this.board.checkMino(c, "G")); - if (collision && this.game.settings.game.allowLockout) return "Lockout"; - if (collision2 && isGarbage) return "Topout"; - if (collision2) return "Blockout"; - } - - deathAlert() { - const check = this.checkDeath(this.board.getMinos('Sh'), this.board.getMinos('NP')); - const check2 = this.checkDeath(this.board.getMinos('G'), this.board.getMinos('NP')); - const warn = document.getElementById('warningText'); - if (!!(check || check2)) { - if (this.toppingOut) return; - this.game.sounds.playSound('hyperalert'); - this.toppingOut = true; - warn.classList.toggle('warn', true); - } else { - this.toppingOut = false; - warn.classList.toggle('warn', false); - } - } - - spawnPiece(piece, start = false) { - if (this.game.ended) return; - this.game.falling.spawn(piece); - this.spawnOverlay(); - this.game.renderer.updateNext(); - this.game.renderer.updateHold(); - this.setShadow(); - this.locking.incrementLock(); - this.game.modes.diggerGarbageSet(start); - this.game.modes.set4WCols(start); - if (this.game.settings.game.preserveARR) this.game.controls.startArr("current"); - if (this.game.started) this.startGravity(); - } - - spawnOverlay() { - this.board.MinoToNone("NP"); - const next = this.game.bag.nextPiece(); - const x = next.name == "o" ? 4 : 3; - const y = next.name == "o" ? 21 : next.name == "i" ? 19 : 20; - this.board.pieceToCoords(next.shape1, [x, y]).forEach(([x, y]) => this.board.addValue([x, y], "NP")); - } - - setShadow() { - this.board.MinoToNone("Sh"); - const coords = this.board.getMinos("A"); - if (coords.length == 0) return; - coords.forEach(([x, y]) => this.board.addValue([x, y], "Sh")); - let count = 0; - const shadow = this.board.getMinos("Sh"); - while (!this.game.movement.checkCollision(shadow.map(c => [c[0], c[1] - count]), "DOWN")) - count++; - this.board.moveMinos(shadow, "DOWN", count, "Sh"); - this.deathAlert(); - } - - startGravity() { - clearInterval(this.game.gravityTimer); - if (this.game.settings.game.gravitySpeed > 1000) return; - if (this.game.settings.game.gravitySpeed == 0) { - this.game.movement.movePieceDown(true); - return; - } - this.game.movement.movePieceDown(false); - this.game.gravityTimer = setInterval( - () => this.game.movement.movePieceDown(false), - this.game.settings.game.gravitySpeed - ); - } - - addGarbage(lines, messiness = 100) { - let randCol = Math.floor(Math.random() * 10); - for (let i = 0; i < lines; i++) { - if (this.game.movement.checkCollision(this.board.getMinos("A"), "DOWN")) { - if (this.locking.timings.lockdelay == 0) this.locking.scheduleLock(); - this.board.moveMinos(this.board.getMinos("A"), "UP", 1); - } - this.board.moveMinos(this.board.getMinos("S"), "UP", 1); - const mustchange = Math.floor(Math.random() * 100); - if (mustchange < messiness) randCol = Math.floor(Math.random() * 10); - for (let col = 0; col < 10; col++) { - if (col != randCol) this.board.addMinos("S G", [[col, 0]], [0, 0]); - } - } - this.setShadow(); - } - - switchHold() { - if (this.game.hold.occured || !this.game.settings.game.allowHold) return; - this.locking.clearLockDelay(); - this.board.MinoToNone("A"); - this.isTspin = false; - this.isAllspin = false; - this.isMini = false; - this.game.stats.holds++; - if (this.game.hold.piece == null) { - this.game.hold.setHold(); - this.spawnPiece(this.game.bag.randomiser()); - } else { - this.game.hold.swapHold(); - this.spawnPiece(this.game.falling.piece); - } - if (this.checkDeath(this.board.getMinos("A"), this.board.getMinos("S")) == "Blockout") { - this.game.endGame("Blockout"); - return; - } - if (!this.game.settings.game.infiniteHold) this.game.hold.occured = true; - this.game.sounds.playSound("hold"); - this.game.renderer.renderDanger(); - this.startGravity(); - this.game.renderer.updateHold(); - } -} +import { Game } from "../game.js"; +import { ClearLines } from "./clearlines.js"; +import { LockPiece } from "./locking.js"; + +export class Mechanics { + board; + isTspin = false; + isAllspin = false; + isMini = false; + garbageQueue = 0; + spikeCounter = 0; + toppingOut = false; + + /** + * @param {Game} game + */ + constructor(game) { + this.game = game; + this.board = game.board; + this.clear = new ClearLines(game); + this.locking = new LockPiece(game); + } + + checkDeath(coords, collider) { + if (coords.length == 0) return; + const collision = coords.every(c => this.game.movement.checkCollision([c], "PLACE", [])); + const collision2 = this.game.movement.checkCollision(coords, "SPAWN", collider); + const isGarbage = collider.some(c => this.board.checkMino(c, "G")); + if (collision && this.game.settings.game.allowLockout) return "Lockout"; + if (collision2 && isGarbage) return "Topout"; + if (collision2) return "Blockout"; + } + + deathAlert() { + const check = this.checkDeath(this.board.getMinos('Sh'), this.board.getMinos('NP')); + const check2 = this.checkDeath(this.board.getMinos('G'), this.board.getMinos('NP')); + const warn = document.getElementById('warningText'); + if (!!(check || check2)) { + if (this.toppingOut) return; + this.game.sounds.playSound('hyperalert'); + this.toppingOut = true; + warn.classList.toggle('warn', true); + } else { + this.toppingOut = false; + warn.classList.toggle('warn', false); + } + } + + spawnPiece(piece, start = false) { + if (this.game.ended) return; + this.game.falling.spawn(piece); + this.spawnOverlay(); + this.game.renderer.updateNext(); + this.game.renderer.updateHold(); + this.setShadow(); + this.locking.incrementLock(); + this.game.modes.diggerGarbageSet(start); + this.game.modes.set4WCols(start); + if (this.game.settings.game.preserveARR) this.game.controls.startArr("current"); + if (this.game.started) this.startGravity(); + } + + spawnOverlay() { + this.board.MinoToNone("NP"); + const next = this.game.bag.nextPiece(); + const x = next.name == "o" ? 4 : 3; + const y = next.name == "o" ? 21 : next.name == "i" ? 19 : 20; + this.board.pieceToCoords(next.shape1, [x, y]).forEach(([x, y]) => this.board.addValue([x, y], "NP")); + } + + setShadow() { + this.board.MinoToNone("Sh"); + const coords = this.board.getMinos("A"); + if (coords.length == 0) return; + coords.forEach(([x, y]) => this.board.addValue([x, y], "Sh")); + let count = 0; + const shadow = this.board.getMinos("Sh"); + while (!this.game.movement.checkCollision(shadow.map(c => [c[0], c[1] - count]), "DOWN")) + count++; + this.board.moveMinos(shadow, "DOWN", count, "Sh"); + this.deathAlert(); + } + + startGravity() { + clearInterval(this.game.gravityTimer); + if (this.game.settings.game.gravitySpeed > 1000) return; + if (this.game.settings.game.gravitySpeed == 0) { + this.game.movement.movePieceDown(true); + return; + } + this.game.movement.movePieceDown(false); + this.game.gravityTimer = setInterval( + () => this.game.movement.movePieceDown(false), + this.game.settings.game.gravitySpeed + ); + } + + addGarbage(lines, messiness = 100) { + let randCol = Math.floor(Math.random() * 10); + for (let i = 0; i < lines; i++) { + if (this.game.movement.checkCollision(this.board.getMinos("A"), "DOWN")) { + if (this.locking.timings.lockdelay == 0) this.locking.scheduleLock(); + this.board.moveMinos(this.board.getMinos("A"), "UP", 1); + } + this.board.moveMinos(this.board.getMinos("S"), "UP", 1); + const mustchange = Math.floor(Math.random() * 100); + if (mustchange < messiness) randCol = Math.floor(Math.random() * 10); + for (let col = 0; col < 10; col++) { + if (col != randCol) this.board.addMinos("S G", [[col, 0]], [0, 0]); + } + } + this.setShadow(); + } + + switchHold() { + if (this.game.hold.occured || !this.game.settings.game.allowHold) return; + this.locking.clearLockDelay(); + this.board.MinoToNone("A"); + this.isTspin = false; + this.isAllspin = false; + this.isMini = false; + this.game.stats.holds++; + if (this.game.hold.piece == null) { + this.game.hold.setHold(); + this.spawnPiece(this.game.bag.randomiser()); + } else { + this.game.hold.swapHold(); + this.spawnPiece(this.game.falling.piece); + } + if (this.checkDeath(this.board.getMinos("A"), this.board.getMinos("S")) == "Blockout") { + this.game.endGame("Blockout"); + return; + } + if (!this.game.settings.game.infiniteHold) this.game.hold.occured = true; + this.game.sounds.playSound("hold"); + this.game.renderer.renderDanger(); + this.startGravity(); + this.game.renderer.updateHold(); + } +} diff --git a/src/mechanics/zenith.js b/src/mechanics/zenith.js deleted file mode 100644 index b4cb54f..0000000 --- a/src/mechanics/zenith.js +++ /dev/null @@ -1,116 +0,0 @@ -import { Game } from "../game.js"; - -export class Zenith { - - /** - * @param {Game} game - */ - - constructor(game) { - this.game = game - } - - climbPoints = 0; - isLastRankChangePromote = !0; - isHyperspeed = true; - rankLock = 0; - promotionFatigue = 0; - rankLock = 0; - tickPass = 0; - tempAltitude = 0 - - FloorDistance = [0, 50, 150, 300, 450, 650, 850, 1100, 1350, 1650, 1 / 0]; - SpeedrunReq = [7, 8, 8, 9, 9, 10, 0, 0, 0, 0, 0]; - - - GetSpeedCap(e) { - const t = this.FloorDistance.find((t => e < t)) - e; - return Math.max(0, Math.min(1, t / 5 - .2)) - } - - GetFloorLevel(e) { - return this.FloorDistance.filter((t => e >= t)).length || 1 - } - - AwardLines(e, t=!0, n=!0) { - const s = .25 * Math.floor(this.game.stats.climbSpeed); - this.GiveBonus(s * e * (t ? 1 : 0)); - if (e <= 0 ) return - this.GiveClimbPts((e + .05) * (n ? 1 : 0)) - } - - GiveBonus(e) { - this.tempAltitude += e - } - - GiveClimbPts(e) { - this.climbPoints += e - } - - startZenithMode() { - clearInterval(this.game.mechanics.zenithTimer); - document.getElementById("climbSpeedBar").style.display = "none" - if(this.game.settings.game.gamemode != "zenith") return - document.getElementById("climbSpeedBar").style.display = "block" - this.game.mechanics.zenithTimer = setInterval( - () => { - let t = Math.floor(this.game.stats.climbSpeed), - o = .25 * t, - a = this.GetSpeedCap(this.tempAltitude); - - if (this.tickPass >= this.rankLock) { - let e = 3; - this.climbPoints -= e * (t ** 2 + t) / 3600 - } - const s = 4 * t, - i = 4 * (t - 1) - - if (this.climbPoints < 0){ - if (t <= 1){ - this.climbPoints = 0; - } - else { - this.climbPoints += i, - this.game.sounds.playSound("speed_down") - this.isLastRankChangePromote = !1, - t-- - } - } - else if (this.climbPoints >= s) { - this.climbPoints -= s, - this.game.sounds.playSound("speed_up") - this.isLastRankChangePromote = !0, - t++; - this.rankLock = this.tickPass + Math.max(60, 60 * (5 - this.promotionFatigue)); - this.promotionFatigue++; - } - - this.game.stats.climbSpeed = t + this.climbPoints / (4 * t); - - this.tempAltitude += o / 60 * a - - if(this.game.stats.floor != this.GetFloorLevel(this.tempAltitude)){ - this.startZenithMode() - this.game.stats.floor = this.GetFloorLevel(this.tempAltitude) - this.game.sounds.playSound("zenith_levelup") - this.game.renderer.renderTimeLeft("FLOOR " + this.game.stats.floor) - } - - this.game.stats.altitude = Math.floor(this.tempAltitude) - this.tickPass++ - this.drawClimbSpeedBar(Math.floor(this.game.stats.climbSpeed), this.climbPoints, s) - } - , 1000 / this.game.tickrate); - } - - drawClimbSpeedBar(speed, point, require){ - const color = ["var(--invis)", "red", "orange", "green", "blue", "#FF1493", "tan", "lightgreen", "lightblue", "pink", "white", "white", "white", "white", "white", "white", "white", "white", "white", "white", "white", "white", "white", "white", "white", "white"] - const climbSpeedBar = document.getElementById("climbSpeedBar") - - climbSpeedBar.value = point - climbSpeedBar.max = require - document.styleSheets[1].cssRules[24].style.backgroundColor = color[speed - 1] - document.styleSheets[1].cssRules[23].style.backgroundColor = color[speed] - } - -} \ No newline at end of file diff --git a/src/menus/generate.js b/src/menus/generate.js index 51dac74..fdb2ebe 100644 --- a/src/menus/generate.js +++ b/src/menus/generate.js @@ -1,217 +1,217 @@ -import { defaultSkins } from "../data/data.js"; -import { Game } from "../game.js"; - - -export class GenerateMenus { - gamemodeStart = document.getElementById("startGamemodeList"); - pblistStart = document.getElementById("PBlist"); - statsStart = document.getElementById("startStatsList"); - notifStack = document.getElementById("notifications"); - - settingDialogs = [...document.getElementsByClassName("settingsBox")]; - settings = [...document.getElementsByClassName("settingRow")]; - - /** - * @param {Game} game - */ - constructor(game) { - this.game = game; - } - - generateGamemodeMenu() { - this.game.modes.getGamemodeNames().forEach(name => { - const setting = this.game.modes.getGamemodeJSON(name); - const button = document.createElement("button"); - button.id = name; - button.classList.add("gamemodeSelect"); - button.textContent = setting.displayName; - button.addEventListener("click", () => { - menu.setGamemode(name); - modal.closeModal("gamemodeDialog"); - }); - this.gamemodeStart.parentNode.insertBefore(button, this.gamemodeStart); - }) - this.gamemodeStart.remove(); - } - - highlightGamemodeInMenu() { - const gamemodeSelect = [...document.getElementsByClassName("gamemodeSelect")]; - gamemodeSelect.forEach(setting => { - setting.classList.remove("selected"); - if (setting.id == this.game.settings.game.gamemode) - setting.classList.add("selected"); - }); - } - - generateSkinList() { - const el = document.getElementById("skin").parentElement; - const list = document.createElement("datalist"); - list.id = "options"; - defaultSkins.forEach(skin => { - const option = document.createElement("option"); - option.value = skin; - list.appendChild(option); - }) - el.appendChild(list); - } - - generateStatList() { - const statoptions = [...document.getElementsByClassName("statoption")]; - const options = Object.getOwnPropertyNames(this.game.stats); - options.sort(); - options.unshift("None"); - - statoptions.forEach(setting => { - options.forEach(stat => { - const skip = ['clearCols', 'clearPieces', 'game', 'tspins']; - if (skip.includes(stat)) return; - const option = document.createElement("option"); - option.textContent = stat; - setting.appendChild(option); - }); - }); - } - - renderPBs() { - const previous = [...document.getElementsByClassName("pbbox")]; - previous.forEach(el => el.remove()); - - const pbs = this.game.profilestats.personalBests; - Object.keys(pbs).forEach(mode => { - const score = pbs[mode].score - const pbbox = document.createElement("div"); - - const text1 = document.createElement("h2") - text1.textContent = mode[0].toUpperCase() + mode.slice(1) + ': '; - const text2 = document.createElement("h2") - text2.textContent = score + this.game.modes.getSuffix(mode); - const clearbutton = document.createElement("button"); - clearbutton.textContent = "X"; - clearbutton.addEventListener("click", (event) => { - event.stopPropagation(); - this.game.profilestats.removePB(mode); - pbbox.remove() - }); - pbbox.appendChild(text1); - pbbox.appendChild(text2); - pbbox.appendChild(clearbutton); - pbbox.addEventListener("click", () => { - let el = document.createElement("a"); - el.setAttribute("href", "data:text/plain;charset=utf-8," + encodeURIComponent(JSON.stringify(pbs[mode]))); - el.setAttribute("download", `${mode}_pb.json`); - document.body.appendChild(el); - el.click(); - document.body.removeChild(el); - }) - - pbbox.classList.add("pbbox"); - - this.pblistStart.parentNode.insertBefore(pbbox, this.pblistStart); - }) - } - - displayStats() { - const previous = [...document.getElementsByClassName("statText")]; - previous.forEach(el => el.remove()); - - const stats = Object.getOwnPropertyNames(this.game.stats); - const skip = ['clearCols', 'clearPieces', 'game'] - stats.forEach(stat => { - if (skip.includes(stat)) return; - let score = this.game.stats[stat] - if (stat == "tspins") score = score.reduce((a, b) => a + b, 0) - const statItem = document.createElement("p"); - statItem.classList = "statText"; - - const text1 = document.createElement("span") - text1.classList = "spanleft" - text1.textContent = stat + ":" - const text2 = document.createElement("span") - text2.classList = "spanright" - text2.textContent = Math.round(score * 1000) / 1000 - statItem.appendChild(text1); - statItem.appendChild(text2); - this.statsStart.parentNode.insertBefore(statItem, this.statsStart); - }) - } - - updateSizes(box, settings) { - const totalHeight = box.scrollHeight; - settings.forEach(setting => { - const position = setting.getBoundingClientRect().top - let newpos = 1 - (position - window.innerHeight * 0.46) / totalHeight - if (newpos > 1) newpos = newpos - 2 * (newpos - 1); - - setting.style.scale = newpos < 0.7 ? 0.9 : 1 - setting.style.opacity = newpos < 0.7 ? 0.3 : 1 - }) - } - - addMenuListeners() { - this.settingDialogs.forEach(box => { - const boxsettings = this.settings.filter(item => item.parentElement.parentElement.id == box.parentElement.id); - box.addEventListener("scroll", () => { - this.updateSizes(box, boxsettings); - }) - }) - - const selectKeys = [...document.getElementsByClassName("keybind")]; - selectKeys.forEach(key => { - key.parentElement.addEventListener("click", () => { - menu.buttonInput(key); - }) - }) - - const sliders = [...document.getElementsByClassName("range")]; - sliders.forEach(slider => { - slider.addEventListener("input", () => { - menu.sliderChange(slider); - }) - }) - - const limiter = document.getElementById("limiter"); - const limiter2 = document.getElementById("limiter2"); - const numberInput = [...document.getElementsByClassName("number")]; - - numberInput.forEach(input => { - input.addEventListener("input", () => { - if (input.id == "backfireMulti") { menu.checkValue(input, limiter2) } - else if (input.id == "rangeValue") { menu.checkValue(input) } - else { menu.checkValue(input, limiter); } - }) - }) - - const gridType = document.getElementById("gridType"); - const types = ["round", "square", "dot"]; - types.forEach(type => { - const option = document.createElement("option"); - option.textContent = type; - gridType.appendChild(option); - }) - } - - notif(heading, message, type) { - const types = { "message": "white", "success": "lightgreen", "error": "red", } - const notif = document.createElement("div"); // notif box - notif.classList.add("notif"); - notif.style.setProperty("--color", types[type]); - const title = document.createElement("p"); // heading - title.classList.add("notif_title"); - title.textContent = heading; - notif.appendChild(title); - const text = document.createElement("p"); - text.classList.add("notif_text"); - text.textContent = message; - notif.appendChild(text); // text - this.notifStack.appendChild(notif); - - const remove = () => { - notif.style.animation = "fadeout 0.5s forwards"; - setTimeout(() => notif.remove(), 1000) - } - - setTimeout(() => remove(), 10 * 1000) - notif.addEventListener("click", () => remove()) - - } +import { defaultSkins } from "../data/data.js"; +import { Game } from "../game.js"; + + +export class GenerateMenus { + gamemodeStart = document.getElementById("startGamemodeList"); + pblistStart = document.getElementById("PBlist"); + statsStart = document.getElementById("startStatsList"); + notifStack = document.getElementById("notifications"); + + settingDialogs = [...document.getElementsByClassName("settingsBox")]; + settings = [...document.getElementsByClassName("settingRow")]; + + /** + * @param {Game} game + */ + constructor(game) { + this.game = game; + } + + generateGamemodeMenu() { + this.game.modes.getGamemodeNames().forEach(name => { + const setting = this.game.modes.getGamemodeJSON(name); + const button = document.createElement("button"); + button.id = name; + button.classList.add("gamemodeSelect"); + button.textContent = setting.displayName; + button.addEventListener("click", () => { + menu.setGamemode(name); + modal.closeModal("gamemodeDialog"); + }); + this.gamemodeStart.parentNode.insertBefore(button, this.gamemodeStart); + }) + this.gamemodeStart.remove(); + } + + highlightGamemodeInMenu() { + const gamemodeSelect = [...document.getElementsByClassName("gamemodeSelect")]; + gamemodeSelect.forEach(setting => { + setting.classList.remove("selected"); + if (setting.id == this.game.settings.game.gamemode) + setting.classList.add("selected"); + }); + } + + generateSkinList() { + const el = document.getElementById("skin").parentElement; + const list = document.createElement("datalist"); + list.id = "options"; + defaultSkins.forEach(skin => { + const option = document.createElement("option"); + option.value = skin; + list.appendChild(option); + }) + el.appendChild(list); + } + + generateStatList() { + const statoptions = [...document.getElementsByClassName("statoption")]; + const options = Object.getOwnPropertyNames(this.game.stats); + options.sort(); + options.unshift("None"); + + statoptions.forEach(setting => { + options.forEach(stat => { + const skip = ['clearCols', 'clearPieces', 'game', 'tspins']; + if (skip.includes(stat)) return; + const option = document.createElement("option"); + option.textContent = stat; + setting.appendChild(option); + }); + }); + } + + renderPBs() { + const previous = [...document.getElementsByClassName("pbbox")]; + previous.forEach(el => el.remove()); + + const pbs = this.game.profilestats.personalBests; + Object.keys(pbs).forEach(mode => { + const score = pbs[mode].score + const pbbox = document.createElement("div"); + + const text1 = document.createElement("h2") + text1.textContent = mode[0].toUpperCase() + mode.slice(1) + ': '; + const text2 = document.createElement("h2") + text2.textContent = score + this.game.modes.getSuffix(mode); + const clearbutton = document.createElement("button"); + clearbutton.textContent = "X"; + clearbutton.addEventListener("click", (event) => { + event.stopPropagation(); + this.game.profilestats.removePB(mode); + pbbox.remove() + }); + pbbox.appendChild(text1); + pbbox.appendChild(text2); + pbbox.appendChild(clearbutton); + pbbox.addEventListener("click", () => { + let el = document.createElement("a"); + el.setAttribute("href", "data:text/plain;charset=utf-8," + encodeURIComponent(JSON.stringify(pbs[mode]))); + el.setAttribute("download", `${mode}_pb.json`); + document.body.appendChild(el); + el.click(); + document.body.removeChild(el); + }) + + pbbox.classList.add("pbbox"); + + this.pblistStart.parentNode.insertBefore(pbbox, this.pblistStart); + }) + } + + displayStats() { + const previous = [...document.getElementsByClassName("statText")]; + previous.forEach(el => el.remove()); + + const stats = Object.getOwnPropertyNames(this.game.stats); + const skip = ['clearCols', 'clearPieces', 'game'] + stats.forEach(stat => { + if (skip.includes(stat)) return; + let score = this.game.stats[stat] + if (stat == "tspins") score = score.reduce((a, b) => a + b, 0) + const statItem = document.createElement("p"); + statItem.classList = "statText"; + + const text1 = document.createElement("span") + text1.classList = "spanleft" + text1.textContent = stat + ":" + const text2 = document.createElement("span") + text2.classList = "spanright" + text2.textContent = Math.round(score * 1000) / 1000 + statItem.appendChild(text1); + statItem.appendChild(text2); + this.statsStart.parentNode.insertBefore(statItem, this.statsStart); + }) + } + + updateSizes(box, settings) { + const totalHeight = box.scrollHeight; + settings.forEach(setting => { + const position = setting.getBoundingClientRect().top + let newpos = 1 - (position - window.innerHeight * 0.46) / totalHeight + if (newpos > 1) newpos = newpos - 2 * (newpos - 1); + + setting.style.scale = newpos < 0.7 ? 0.9 : 1 + setting.style.opacity = newpos < 0.7 ? 0.3 : 1 + }) + } + + addMenuListeners() { + this.settingDialogs.forEach(box => { + const boxsettings = this.settings.filter(item => item.parentElement.parentElement.id == box.parentElement.id); + box.addEventListener("scroll", () => { + this.updateSizes(box, boxsettings); + }) + }) + + const selectKeys = [...document.getElementsByClassName("keybind")]; + selectKeys.forEach(key => { + key.parentElement.addEventListener("click", () => { + menu.buttonInput(key); + }) + }) + + const sliders = [...document.getElementsByClassName("range")]; + sliders.forEach(slider => { + slider.addEventListener("input", () => { + menu.sliderChange(slider); + }) + }) + + const limiter = document.getElementById("limiter"); + const limiter2 = document.getElementById("limiter2"); + const numberInput = [...document.getElementsByClassName("number")]; + + numberInput.forEach(input => { + input.addEventListener("input", () => { + if (input.id == "backfireMulti") { menu.checkValue(input, limiter2) } + else if (input.id == "rangeValue") { menu.checkValue(input) } + else { menu.checkValue(input, limiter); } + }) + }) + + const gridType = document.getElementById("gridType"); + const types = ["round", "square", "dot"]; + types.forEach(type => { + const option = document.createElement("option"); + option.textContent = type; + gridType.appendChild(option); + }) + } + + notif(heading, message, type) { + const types = { "message": "white", "success": "lightgreen", "error": "red", } + const notif = document.createElement("div"); // notif box + notif.classList.add("notif"); + notif.style.setProperty("--color", types[type]); + const title = document.createElement("p"); // heading + title.classList.add("notif_title"); + title.textContent = heading; + notif.appendChild(title); + const text = document.createElement("p"); + text.classList.add("notif_text"); + text.textContent = message; + notif.appendChild(text); // text + this.notifStack.appendChild(notif); + + const remove = () => { + notif.style.animation = "fadeout 0.5s forwards"; + setTimeout(() => notif.remove(), 1000) + } + + setTimeout(() => remove(), 10 * 1000) + notif.addEventListener("click", () => remove()) + + } } \ No newline at end of file diff --git a/src/menus/menuactions.js b/src/menus/menuactions.js index 3120e4b..96f5e5c 100644 --- a/src/menus/menuactions.js +++ b/src/menus/menuactions.js @@ -1,240 +1,240 @@ -import { Game } from "../game.js"; -import { toExpValue } from "./modals.js"; - -export class MenuActions { - bindingKey; - menus; - elementSelectKeyText = document.getElementById("selectkeytext"); - controlUsed = false; - altUsed = false; - - /** - * @param {Game} game - */ - constructor(game) { - this.game = game; - } - - // sliders - sliderChange(el) { - const text = el.parentElement.children[0].textContent.split(":")[0]; - let value = el.value; - if (el.classList[2] == "exp") value = toExpValue(value); - if (el.classList[2] == "exp" && value > 1000) value = "None"; - el.parentElement.children[0].textContent = `${text}: ${value}`; - } - - addRangeListener() { - document.body.addEventListener("click", (event) => { - if (event.target.dataset.isRange == "true") { - const el = event.target.children[1] - this.game.modals.selectedRangeElement = el; - this.menus.openModal("changeRangeValue"); - document.getElementById("rangeValue").value = el.value; - } - }) - } - - rangeClickInit(el) { - el.parentElement.dataset.isRange = true; - } - - buttonInput(el) { - document.getElementById("frontdrop").showModal(); - this.bindingKey = el.id; - } - - checkValue(el, el2 = this.game.modals.selectedRangeElement) { - this.game.modals.selectedRangeElement = el2; - if (el.value == "") return; - if (el.value < Number(el2.min)) el.value = Number(el2.min); - if (el.value > Number(el2.max)) el.value = Number(el2.max); - } - - // keybinds - checkKeybind(event) { - if (!event.ctrlKey) this.controlUsed = false; - if (!event.altKey) this.altUsed = false; - this.elementSelectKeyText.textContent = "Click to remove keybind"; - - } - - setKeybind(event) { - if (!this.controlUsed && event.ctrlKey) { - this.controlUsed = true; - this.elementSelectKeyText.textContent += ". Control modifier used"; - return; - } - if (!this.altUsed && event.altKey) { - this.altUsed = true; - this.elementSelectKeyText.textContent += ". Alt modifier used"; - return; - } - - const key = (event.ctrlKey ? "Ctrl+" : "") + (event.altKey ? "Alt+" : "") + event.key; - document.getElementById(this.bindingKey).textContent = key; - for (let i in this.game.settings.control) { - if (i == this.bindingKey) continue; - const otherKeys = document.getElementById(i); - if (otherKeys.textContent == key) otherKeys.textContent = "None"; - } - this.menus.closeDialog(document.getElementById("frontdrop")); - this.game.modals.open = true; - this.bindingKey = undefined; - this.controlUsed = false; - this.altUsed = false; - this.elementSelectKeyText.textContent = "Click to remove keybind"; - } - - // settings - saveSettings() { - const data = this.game.settings.save(); - localStorage.setItem("settings", JSON.stringify(data)); - } - - loadSettings() { - const data = localStorage.getItem("settings") ?? "{}"; - this.game.settings.load(JSON.parse(data)) - this.game.modes.loadModes(); - this.game.history.setHistoryDiv(this.game.settings.game.gamemode == 'custom'); - this.game.boardeditor.setEditButton(this.game.settings.game.gamemode == 'custom'); - } - - setGamemode(mode) { - this.game.modes.setGamemode(mode); - this.game.modes.loadModes(); - this.game.history.setHistoryDiv(this.game.settings.game.gamemode == 'custom'); - this.game.boardeditor.setEditButton(this.game.settings.game.gamemode == 'custom'); - } - - downloadSettings() { - this.saveSettings(); - let el = document.createElement("a"); - const text = localStorage.getItem("settings"); - el.setAttribute("href", "data:text/plain;charset=utf-8," + encodeURIComponent(text)); - el.setAttribute("download", "settings.teti"); - document.body.appendChild(el); - el.click(); - document.body.removeChild(el); - } - - uploadSettings(el) { - const reader = new FileReader(); - reader.readAsText(el.files[0]); - reader.onload = () => { - localStorage.setItem("settings", reader.result.toString()); - this.loadSettings(); - this.game.modals.generate.notif("Settings Loaded", "User settings have successfully loaded", "message"); - }; - } - - resetSettings(group) { - this.game.settings.reset(group); - this.saveSettings(); - location.reload(); - this.game.modals.generate.notif("Settings Reset", `${group} settings have been reset to default`, "message"); - } - - // menu - toggleDialog() { - if (this.game.menuactions.bindingKey != undefined) return; - if (!this.game.modals.open) { - this.menus.openModal("settingsPanel"); - return; - } - document.querySelectorAll("dialog[open]").forEach(e => this.menus.closeDialog(e)); - document.querySelectorAll("scrollSettings[open]").forEach(e => this.menus.closeDialog(e)); - if (this.game.started && !this.game.ended) this.game.movement.firstMovement(); - } - - newGame(key, modal) { - if (key == this.game.settings.control.resetKey) { - this.game.modals.closeModal(modal); - } - } - - openPage(url) { - window.open("https://" + url, "blank_") - } - - // edit menu - openEditMenu() { - if (this.game.modals.open) { - if (document.querySelectorAll("#editMenu[open]").length == 0) return; - this.toggleDialog(); - return; - } - if (this.game.settings.game.gamemode != 'custom') return - this.menus.openModal("editMenu"); - } - - changeEditPiece(pieceName) { this.game.boardeditor.fillPiece = pieceName; } - - addGarbageRow() { - this.game.mechanics.addGarbage(1); - this.game.mechanics.setShadow(); - } - - removeLastRow() { - this.game.mechanics.clear.clearRow(0); - this.game.mechanics.setShadow(); - } - - clearGarbage() { - this.game.mechanics.board.EradicateMinoCells("S G"); - this.game.mechanics.setShadow(); - } - - setBoard() { - const input = prompt("Enter Map String Here:") - const { board, next, hold } = this.game.boardeditor.convertFromMap(input); - this.game.board.boardState = board; - this.game.bag.nextPieces = [next.split(","), []]; - this.game.hold.piece = this.game.renderer.getPiece(hold); - this.game.mechanics.spawnPiece(this.game.bag.randomiser()); - this.game.modals.generate.notif("Map Loaded", "Custom map has successfully loaded", "message"); - } - - getBoardString() { - const exportstring = this.game.boardeditor.convertToMap(); - navigator.clipboard.writeText(exportstring) - this.game.modals.generate.notif("Map Exported", "Custom map has been copied to your clipboard", "message"); - alert("TETR.IO Map String:\n" + exportstring) - } - - // stats - exportStats() { - let stats = {} - Object.getOwnPropertyNames(this.game.stats).forEach(key => { - if (key == "game") return; - stats[key] = this.game.stats[key]; - }) - - let el = document.createElement("a"); - el.setAttribute("href", "data:text/plain;charset=utf-8," + encodeURIComponent(JSON.stringify(stats))); - el.setAttribute("download", "stats.json"); - document.body.appendChild(el); - el.click(); - document.body.removeChild(el); - this.game.modals.generate.notif("Stats Exported", "The current game's stats have been exported.", "message"); - } - - closeStats() { - this.menus.closeDialog(document.getElementById("gameStatsDialog")); - this.game.modals.open = true; - } - - exportLifetime() { - this.game.profilestats.saveSession(); - const data = localStorage.getItem("stats"); - const day = (new Date()).toLocaleDateString().replace("/", "-"); - - let el = document.createElement("a"); - el.setAttribute("href", "data:text/plain;charset=utf-8," + encodeURIComponent(JSON.stringify(data))); - el.setAttribute("download", `teti_stats_${day}.json`); - document.body.appendChild(el); - el.click(); - document.body.removeChild(el); - this.game.modals.generate.notif("Lifetime Stats Exported", "All your lifetime stats and PBs have been exported. Enjoy the many stats you can analyse!", "success"); - } -} +import { Game } from "../game.js"; +import { toExpValue } from "./modals.js"; + +export class MenuActions { + bindingKey; + menus; + elementSelectKeyText = document.getElementById("selectkeytext"); + controlUsed = false; + altUsed = false; + + /** + * @param {Game} game + */ + constructor(game) { + this.game = game; + } + + // sliders + sliderChange(el) { + const text = el.parentElement.children[0].textContent.split(":")[0]; + let value = el.value; + if (el.classList[2] == "exp") value = toExpValue(value); + if (el.classList[2] == "exp" && value > 1000) value = "None"; + el.parentElement.children[0].textContent = `${text}: ${value}`; + } + + addRangeListener() { + document.body.addEventListener("click", (event) => { + if (event.target.dataset.isRange == "true") { + const el = event.target.children[1] + this.game.modals.selectedRangeElement = el; + this.menus.openModal("changeRangeValue"); + document.getElementById("rangeValue").value = el.value; + } + }) + } + + rangeClickInit(el) { + el.parentElement.dataset.isRange = true; + } + + buttonInput(el) { + document.getElementById("frontdrop").showModal(); + this.bindingKey = el.id; + } + + checkValue(el, el2 = this.game.modals.selectedRangeElement) { + this.game.modals.selectedRangeElement = el2; + if (el.value == "") return; + if (el.value < Number(el2.min)) el.value = Number(el2.min); + if (el.value > Number(el2.max)) el.value = Number(el2.max); + } + + // keybinds + checkKeybind(event) { + if (!event.ctrlKey) this.controlUsed = false; + if (!event.altKey) this.altUsed = false; + this.elementSelectKeyText.textContent = "Click to remove keybind"; + + } + + setKeybind(event) { + if (!this.controlUsed && event.ctrlKey) { + this.controlUsed = true; + this.elementSelectKeyText.textContent += ". Control modifier used"; + return; + } + if (!this.altUsed && event.altKey) { + this.altUsed = true; + this.elementSelectKeyText.textContent += ". Alt modifier used"; + return; + } + + const key = (event.ctrlKey ? "Ctrl+" : "") + (event.altKey ? "Alt+" : "") + event.key; + document.getElementById(this.bindingKey).textContent = key; + for (let i in this.game.settings.control) { + if (i == this.bindingKey) continue; + const otherKeys = document.getElementById(i); + if (otherKeys.textContent == key) otherKeys.textContent = "None"; + } + this.menus.closeDialog(document.getElementById("frontdrop")); + this.game.modals.open = true; + this.bindingKey = undefined; + this.controlUsed = false; + this.altUsed = false; + this.elementSelectKeyText.textContent = "Click to remove keybind"; + } + + // settings + saveSettings() { + const data = this.game.settings.save(); + localStorage.setItem("settings", JSON.stringify(data)); + } + + loadSettings() { + const data = localStorage.getItem("settings") ?? "{}"; + this.game.settings.load(JSON.parse(data)) + this.game.modes.loadModes(); + this.game.history.setHistoryDiv(this.game.settings.game.gamemode == 'custom'); + this.game.boardeditor.setEditButton(this.game.settings.game.gamemode == 'custom'); + } + + setGamemode(mode) { + this.game.modes.setGamemode(mode); + this.game.modes.loadModes(); + this.game.history.setHistoryDiv(this.game.settings.game.gamemode == 'custom'); + this.game.boardeditor.setEditButton(this.game.settings.game.gamemode == 'custom'); + } + + downloadSettings() { + this.saveSettings(); + let el = document.createElement("a"); + const text = localStorage.getItem("settings"); + el.setAttribute("href", "data:text/plain;charset=utf-8," + encodeURIComponent(text)); + el.setAttribute("download", "settings.teti"); + document.body.appendChild(el); + el.click(); + document.body.removeChild(el); + } + + uploadSettings(el) { + const reader = new FileReader(); + reader.readAsText(el.files[0]); + reader.onload = () => { + localStorage.setItem("settings", reader.result.toString()); + this.loadSettings(); + this.game.modals.generate.notif("Settings Loaded", "User settings have successfully loaded", "message"); + }; + } + + resetSettings(group) { + this.game.settings.reset(group); + this.saveSettings(); + location.reload(); + this.game.modals.generate.notif("Settings Reset", `${group} settings have been reset to default`, "message"); + } + + // menu + toggleDialog() { + if (this.game.menuactions.bindingKey != undefined) return; + if (!this.game.modals.open) { + this.menus.openModal("settingsPanel"); + return; + } + document.querySelectorAll("dialog[open]").forEach(e => this.menus.closeDialog(e)); + document.querySelectorAll("scrollSettings[open]").forEach(e => this.menus.closeDialog(e)); + if (this.game.started && !this.game.ended) this.game.movement.firstMovement(); + } + + newGame(key, modal) { + if (key == this.game.settings.control.resetKey) { + this.game.modals.closeModal(modal); + } + } + + openPage(url) { + window.open("https://" + url, "blank_") + } + + // edit menu + openEditMenu() { + if (this.game.modals.open) { + if (document.querySelectorAll("#editMenu[open]").length == 0) return; + this.toggleDialog(); + return; + } + if (this.game.settings.game.gamemode != 'custom') return + this.menus.openModal("editMenu"); + } + + changeEditPiece(pieceName) { this.game.boardeditor.fillPiece = pieceName; } + + addGarbageRow() { + this.game.mechanics.addGarbage(1); + this.game.mechanics.setShadow(); + } + + removeLastRow() { + this.game.mechanics.clear.clearRow(0); + this.game.mechanics.setShadow(); + } + + clearGarbage() { + this.game.mechanics.board.EradicateMinoCells("S G"); + this.game.mechanics.setShadow(); + } + + setBoard() { + const input = prompt("Enter Map String Here:") + const { board, next, hold } = this.game.boardeditor.convertFromMap(input); + this.game.board.boardState = board; + this.game.bag.nextPieces = [next.split(","), []]; + this.game.hold.piece = this.game.renderer.getPiece(hold); + this.game.mechanics.spawnPiece(this.game.bag.randomiser()); + this.game.modals.generate.notif("Map Loaded", "Custom map has successfully loaded", "message"); + } + + getBoardString() { + const exportstring = this.game.boardeditor.convertToMap(); + navigator.clipboard.writeText(exportstring) + this.game.modals.generate.notif("Map Exported", "Custom map has been copied to your clipboard", "message"); + alert("TETR.IO Map String:\n" + exportstring) + } + + // stats + exportStats() { + let stats = {} + Object.getOwnPropertyNames(this.game.stats).forEach(key => { + if (key == "game") return; + stats[key] = this.game.stats[key]; + }) + + let el = document.createElement("a"); + el.setAttribute("href", "data:text/plain;charset=utf-8," + encodeURIComponent(JSON.stringify(stats))); + el.setAttribute("download", "stats.json"); + document.body.appendChild(el); + el.click(); + document.body.removeChild(el); + this.game.modals.generate.notif("Stats Exported", "The current game's stats have been exported.", "message"); + } + + closeStats() { + this.menus.closeDialog(document.getElementById("gameStatsDialog")); + this.game.modals.open = true; + } + + exportLifetime() { + this.game.profilestats.saveSession(); + const data = localStorage.getItem("stats"); + const day = (new Date()).toLocaleDateString().replace("/", "-"); + + let el = document.createElement("a"); + el.setAttribute("href", "data:text/plain;charset=utf-8," + encodeURIComponent(JSON.stringify(data))); + el.setAttribute("download", `teti_stats_${day}.json`); + document.body.appendChild(el); + el.click(); + document.body.removeChild(el); + this.game.modals.generate.notif("Lifetime Stats Exported", "All your lifetime stats and PBs have been exported. Enjoy the many stats you can analyse!", "success"); + } +} diff --git a/src/menus/modals.js b/src/menus/modals.js index a3f1f1b..223a7fb 100644 --- a/src/menus/modals.js +++ b/src/menus/modals.js @@ -1,128 +1,128 @@ -import { Game } from "../game.js"; -import { GenerateMenus } from "./generate.js"; - -export class ModalActions { - open; - closing; - selectedRangeElement; - pieceNames = ["s", "z", "i", "j", "l", "o", "t"]; - - settingPanel = document.getElementById("settingsPanel"); - options = [...document.getElementsByClassName("option")]; - - - /** - * @param {Game} game - */ - constructor(game) { - this.game = game; - this.actions = this.game.menuactions; - game.menuactions.menus = this; - this.generate = new GenerateMenus(game); - } - - openModal(id) { - //ensure that everything has been closed before trying to open the settings panel - if (id == "settingsPanel" && this.closing) return; - if (id == "queueModify" && !this.game.settings.game.allowQueueModify) return; - this.game.stopGameTimers() - this.getOptions(id).forEach(setting => { - let settingType = this.getSettingType(id); - let newval; - if (this.game.settings.hasOwnProperty(settingType)) newval = this.game.settings[settingType][setting.id] - if (setting.classList[2] == "exp") newval = toLogValue(newval); - if (setting.classList[2] == "statoption") newval = this.game.settings.game.sidebar[setting.id[10]-1]; - if (setting.id == "nextQueue") newval = this.game.bag.getQueue(); - if (setting.id == "holdQueue") newval = this.game.hold.getHold(); - if (setting.id == "rowfillmode") newval = this.game.boardeditor.fillRow; - setting.value = newval; - if (setting.classList[1] == "keybind") setting.textContent = newval; - if (setting.classList[1] == "check") setting.checked = newval; - if (setting.classList[1] == "range") { - this.actions.sliderChange(setting); - this.actions.rangeClickInit(setting); - } - }); - - document.getElementById(id).showModal(); - - if (id == "gameStatsDialog") this.generate.displayStats(); - if (id == "gamemodeDialog") this.generate.highlightGamemodeInMenu(); - if (id == "competitiveDialog") this.generate.renderPBs(); - if (id != "settingsPanel" && this.settingPanel.open) this.closeDialog(this.settingPanel); - this.open = true; - this.game.sounds.toggleSongMuffle(this.open); - } - - getOptions(id) { - const settings = [...this.options].filter(item => item.parentElement.parentElement.parentElement.id == id) - return settings; - } - - getSettingType(id) { - let type = id.replace("Dialog", ""); - if (id == "gamemodeDialog" || id == "goalsDialog" || id == "competitiveDialog") type = "game"; - return type; - } - - closeModal(id) { - this.getOptions(id).forEach(setting => { - let settingType = this.getSettingType(id); - let val = setting.value; - if (setting.classList[1] == "number" && val == "") val = this.selectedRangeElement.min; - if (setting.classList[1] == "check") val = setting.checked; - if (setting.classList[1] == "keybind") { - val = setting.textContent.length > 1 - ? setting.textContent - : setting.textContent.toLowerCase(); - } - if (setting.classList[2] == "exp") val = toExpValue(val); - if (setting.classList[2] == "statoption") this.game.settings.game.sidebar[setting.id[10]-1] = val; - if (setting.id == "nextQueue") this.game.bag.setQueue(val, this.pieceNames); - if (setting.id == "holdQueue") this.game.hold.setNewHold(val); - if (setting.id == "rowfillmode") this.game.boardeditor.fillRow = val; - if (setting.id == "override") this.game.boardeditor.override = val; - - if (id == "changeRangeValue") { - this.selectedRangeElement.value = document.getElementById("rangeValue").value; - this.actions.sliderChange(this.selectedRangeElement); - } - if (setting.id == "audioLevel") this.game.sounds.setAudioLevel(); - - if (!this.game.settings.hasOwnProperty(settingType)) return; - this.game.settings[settingType][setting.id] = val; - }); - - this.closeDialog(document.getElementById(id)); - if (id != 'changeRangeValue' && id != "frontdrop" && this.game.started && !this.game.ended) - this.game.movement.firstMovement(); - this.actions.saveSettings(); - if (id == "displayDialog") this.game.renderer.renderStyles(); - - const restartMenus = ["gameDialog", "gamemodeDialog", "gameEnd", "goalsDialog", "competitiveDialog"]; - if (restartMenus.includes(id)) this.game.controls.retry(false); - if (id == "changeRangeValue") this.open = true; - } - - closeDialog(element) { - this.closing = true //track if the closing animation is still ongoing - const closingAnimation = () => { - element.removeEventListener("animationend", closingAnimation); - element.classList.remove("closingAnimation"); - element.close(); - this.closing = false - }; - this.open = false; - this.game.sounds.toggleSongMuffle(this.open); - element.classList.add("closingAnimation"); - element.addEventListener("animationend", closingAnimation); - } -} - -function toLogValue(y) { - return Math.round(Math.log2(y + 1) * 10); -} - -export function toExpValue(x) { - return Math.round(Math.pow(2, 0.1 * x) - 1); +import { Game } from "../game.js"; +import { GenerateMenus } from "./generate.js"; + +export class ModalActions { + open; + closing; + selectedRangeElement; + pieceNames = ["s", "z", "i", "j", "l", "o", "t"]; + + settingPanel = document.getElementById("settingsPanel"); + options = [...document.getElementsByClassName("option")]; + + + /** + * @param {Game} game + */ + constructor(game) { + this.game = game; + this.actions = this.game.menuactions; + game.menuactions.menus = this; + this.generate = new GenerateMenus(game); + } + + openModal(id) { + //ensure that everything has been closed before trying to open the settings panel + if (id == "settingsPanel" && this.closing) return; + if (id == "queueModify" && !this.game.settings.game.allowQueueModify) return; + this.game.stopGameTimers() + this.getOptions(id).forEach(setting => { + let settingType = this.getSettingType(id); + let newval; + if (this.game.settings.hasOwnProperty(settingType)) newval = this.game.settings[settingType][setting.id] + if (setting.classList[2] == "exp") newval = toLogValue(newval); + if (setting.classList[2] == "statoption") newval = this.game.settings.game.sidebar[setting.id[10]-1]; + if (setting.id == "nextQueue") newval = this.game.bag.getQueue(); + if (setting.id == "holdQueue") newval = this.game.hold.getHold(); + if (setting.id == "rowfillmode") newval = this.game.boardeditor.fillRow; + setting.value = newval; + if (setting.classList[1] == "keybind") setting.textContent = newval; + if (setting.classList[1] == "check") setting.checked = newval; + if (setting.classList[1] == "range") { + this.actions.sliderChange(setting); + this.actions.rangeClickInit(setting); + } + }); + + document.getElementById(id).showModal(); + + if (id == "gameStatsDialog") this.generate.displayStats(); + if (id == "gamemodeDialog") this.generate.highlightGamemodeInMenu(); + if (id == "competitiveDialog") this.generate.renderPBs(); + if (id != "settingsPanel" && this.settingPanel.open) this.closeDialog(this.settingPanel); + this.open = true; + this.game.sounds.toggleSongMuffle(this.open); + } + + getOptions(id) { + const settings = [...this.options].filter(item => item.parentElement.parentElement.parentElement.id == id) + return settings; + } + + getSettingType(id) { + let type = id.replace("Dialog", ""); + if (id == "gamemodeDialog" || id == "goalsDialog" || id == "competitiveDialog") type = "game"; + return type; + } + + closeModal(id) { + this.getOptions(id).forEach(setting => { + let settingType = this.getSettingType(id); + let val = setting.value; + if (setting.classList[1] == "number" && val == "") val = this.selectedRangeElement.min; + if (setting.classList[1] == "check") val = setting.checked; + if (setting.classList[1] == "keybind") { + val = setting.textContent.length > 1 + ? setting.textContent + : setting.textContent.toLowerCase(); + } + if (setting.classList[2] == "exp") val = toExpValue(val); + if (setting.classList[2] == "statoption") this.game.settings.game.sidebar[setting.id[10]-1] = val; + if (setting.id == "nextQueue") this.game.bag.setQueue(val, this.pieceNames); + if (setting.id == "holdQueue") this.game.hold.setNewHold(val); + if (setting.id == "rowfillmode") this.game.boardeditor.fillRow = val; + if (setting.id == "override") this.game.boardeditor.override = val; + + if (id == "changeRangeValue") { + this.selectedRangeElement.value = document.getElementById("rangeValue").value; + this.actions.sliderChange(this.selectedRangeElement); + } + if (setting.id == "audioLevel") this.game.sounds.setAudioLevel(); + + if (!this.game.settings.hasOwnProperty(settingType)) return; + this.game.settings[settingType][setting.id] = val; + }); + + this.closeDialog(document.getElementById(id)); + if (id != 'changeRangeValue' && id != "frontdrop" && this.game.started && !this.game.ended) + this.game.movement.firstMovement(); + this.actions.saveSettings(); + if (id == "displayDialog") this.game.renderer.renderStyles(); + + const restartMenus = ["gameDialog", "gamemodeDialog", "gameEnd", "goalsDialog", "competitiveDialog"]; + if (restartMenus.includes(id)) this.game.controls.retry(false); + if (id == "changeRangeValue") this.open = true; + } + + closeDialog(element) { + this.closing = true //track if the closing animation is still ongoing + const closingAnimation = () => { + element.removeEventListener("animationend", closingAnimation); + element.classList.remove("closingAnimation"); + element.close(); + this.closing = false + }; + this.open = false; + this.game.sounds.toggleSongMuffle(this.open); + element.classList.add("closingAnimation"); + element.addEventListener("animationend", closingAnimation); + } +} + +function toLogValue(y) { + return Math.round(Math.log2(y + 1) * 10); +} + +export function toExpValue(x) { + return Math.round(Math.pow(2, 0.1 * x) - 1); } \ No newline at end of file diff --git a/src/movement/controls.js b/src/movement/controls.js index 0d4346e..fc44288 100644 --- a/src/movement/controls.js +++ b/src/movement/controls.js @@ -25,7 +25,7 @@ export class Controls { if (key == this.menuKey) this.game.menuactions.toggleDialog(); else if (key == keys.editMenuKey) this.game.menuactions.openEditMenu(); - if (this.game.modals.open || this.game.modals.closing) return; + if (this.game.modals.open || this.game.modals.closing || this.game.mechanics.locking.timings.clearDelay != 0) return; if (event.key != this.menuKey && !this.game.started) this.moves.firstMovement(); if (key == keys.resetKey) this.retry(true); if (this.game.ended) return; diff --git a/src/movement/movement.js b/src/movement/movement.js index a43fe55..2b29bcc 100644 --- a/src/movement/movement.js +++ b/src/movement/movement.js @@ -1,161 +1,162 @@ -import { spinChecks } from "../data/data.js"; -import { Game } from "../game.js"; - -export class Movement { - /** - * @param {Game} game - */ - constructor(game) { - this.game = game; - } - - firstMovement() { - this.game.zenith.startZenithMode(); - if(this.game.zenith.tickPass == 0 && this.game.settings.game.gamemode == "zenith") this.game.renderer.renderTimeLeft("FLOOR 1") - this.game.started = true; - this.game.mechanics.startGravity(); - this.game.modes.startSurvival(); - this.game.mechanics.locking.lockingResume(); - this.game.gameTimer = setInterval(() => - this.game.gameClock(), (1000 / this.game.tickrate) - ); - } - - checkCollision(coords, action, collider) { - collider = collider ?? this.game.board.getMinos("S"); - for (let [x, y] of coords) { - if ( - (action == "RIGHT" && x > 8) || - (action == "LEFT" && x < 1) || - (action == "DOWN" && y < 1) || - (action == "ROTATE" && x < 0) || - x > 9 || - y < 0 || - (action == "PLACE" && y > 19) - ) - return true; - for (let [x2, y2] of collider) { - const col = (dx, dy) => x + dx == x2 && y + dy == y2; - if ( - (action == "RIGHT" && col(1, 0)) || - (action == "LEFT" && col(-1, 0)) || - (action == "DOWN" && col(0, -1)) || - ((action == "ROTATE" || action == "SPAWN") && col(0, 0)) - ) - return true; - } - } - } - - checkTspin(rotation, [x, y], [dx, dy]) { - if (this.game.falling.piece.name != "t") return false; - this.game.mechanics.isMini = false; - const minos = spinChecks[(rotation + 1) % 4] - .concat(spinChecks[rotation - 1]) - .map(([ddx, ddy]) => this.checkCollision([[ddx + x, ddy + y]], "ROTATE")); - if (minos[2] && minos[3] && (minos[0] || minos[1])) return true; - if ((minos[2] || minos[3]) && minos[0] && minos[1]) { - if ((dx == 1 || dx == -1) && dy == -2) return true; - this.game.mechanics.isMini = true; - return true; - } - } - - checkAllspin(pieceCoords) { - if (this.game.falling.piece.name == "t") return false; - const directions = [[1, 0], [0, 1], [-1, 0], [0, -1]]; - const validSpin = directions.every(([dx, dy]) => - this.checkCollision(pieceCoords.map(([x, y]) => [x + dx, y + dy]), "ROTATE")); - if (validSpin && this.game.settings.game.allspinminis) this.game.mechanics.isMini = true; - return validSpin; - } - - rotate(type) { - if (this.game.falling.piece.name == "o") return; - const newRotation = this.game.falling.getRotateState(type); - const kickdata = this.game.falling.getKickData(type, newRotation); - const rotatingCoords = this.game.falling.getNewCoords(newRotation); - const change = kickdata.find(([dx, dy]) => - !this.checkCollision(rotatingCoords.map(c => [c[0] + dx, c[1] + dy]), "ROTATE")); - if (!change) return; - this.game.board.MinoToNone("A"); - this.game.board.addMinos(this.game.falling.newName(), rotatingCoords, change); - this.game.falling.updateLocation(change); - this.game.mechanics.isTspin = this.checkTspin(newRotation, this.game.falling.location, change); - this.game.mechanics.isAllspin = this.checkAllspin(this.game.board.getMinos("A")); - this.game.falling.rotation = newRotation; - this.game.mechanics.locking.incrementLock(); - this.game.stats.rotates++; - this.game.sounds.playSound("rotate"); - this.game.mechanics.setShadow(); - if (this.game.settings.game.gravitySpeed == 0) this.game.mechanics.startGravity(); - this.game.controls.startArr("current"); - this.game.controls.checkSD(); - if (this.game.mechanics.isTspin || (this.game.mechanics.isAllspin && this.game.settings.game.allspin)) { - this.game.renderer.rotateBoard(type); - this.game.particles.spawnParticles(this.game.falling.location[0], this.game.falling.location[1] + 2, - "spin", 5, type == "CW", this.game.falling.piece.colour); - this.game.sounds.playSound("spin"); - } - } - - movePieceSide(direction, max = 1) { - this.game.controls.checkSD(); - const minos = this.game.board.getMinos("A"); - let amount = 0; - const check = dx => !this.checkCollision(minos.map(([x, y]) => [x + dx, y]), direction); - while (check(amount) && Math.abs(amount) < max) direction == "RIGHT" ? amount++ : amount--; - if (!check(amount)) this.game.renderer.bounceBoard(direction); - if (amount == 0) { - this.game.controls.stopInterval("arr"); - return; - } - this.game.board.moveMinos(minos, "RIGHT", amount); - this.game.falling.updateLocation([amount, 0]); - this.game.mechanics.isTspin = false; - this.game.mechanics.isAllspin = false; - this.game.mechanics.isMini = false; - this.game.mechanics.locking.incrementLock(); - this.game.sounds.playSound("move"); - this.game.mechanics.setShadow(); - this.game.controls.checkSD(); - if (this.game.settings.game.gravitySpeed == 0) this.game.mechanics.startGravity(); - } - - movePieceDown(sonic, scoring = false) { - const minos = this.game.board.getMinos("A"); - if (this.checkCollision(minos, "DOWN")) return; - this.game.board.moveMinos(minos, "DOWN", 1); - - this.game.mechanics.isTspin = false; - this.game.mechanics.isAllspin = false; - this.game.mechanics.isMini = false; - this.game.falling.updateLocation([0, -1]); - if (this.checkCollision(this.game.board.getMinos("A"), "DOWN")) { - this.game.mechanics.locking.scheduleLock(); - this.game.renderer.bounceBoard("DOWN"); - this.game.controls.startArr("current"); - } - if (scoring && sonic) this.game.stats.score += 1; - if (sonic) this.movePieceDown(true, scoring); - } - - harddrop() { - const minos = this.game.board.getMinos("A"); - let amount = 0; - while (!this.checkCollision(minos.map(([x, y]) => [x, y - amount]), "DOWN")) amount++; - if (amount > 0) { - this.game.mechanics.isTspin = false; - this.game.mechanics.isAllspin = false; - this.game.mechanics.isMini = false; - } - this.game.board.moveMinos(minos, "DOWN", amount); - this.game.falling.updateLocation([0, -amount]); - this.game.stats.score += 2 * amount; - this.game.sounds.playSound("harddrop"); - this.game.renderer.bounceBoard('DOWN'); - const xvals = [...new Set(minos.map(([x, y]) => x))]; - this.game.particles.spawnParticles(Math.min(...xvals)+1, this.game.falling.location[1], "drop", xvals.length); - this.game.mechanics.locking.lockPiece(); - } -} +import { spinChecks } from "../data/data.js"; +import { Game } from "../game.js"; + +export class Movement { + /** + * @param {Game} game + */ + constructor(game) { + this.game = game; + } + + firstMovement() { + this.game.zenith.startZenithMode(); + this.game.grandmaster.startGrandmasterTimer(); + if(this.game.zenith.tickPass == 0 && this.game.settings.game.gamemode == "zenith") this.game.renderer.renderTimeLeft("FLOOR 1") + this.game.started = true; + this.game.mechanics.startGravity(); + this.game.modes.startSurvival(); + this.game.mechanics.locking.lockingResume(); + this.game.gameTimer = setInterval(() => + this.game.gameClock(), (1000 / this.game.tickrate) + ); + } + + checkCollision(coords, action, collider) { + collider = collider ?? this.game.board.getMinos("S"); + for (let [x, y] of coords) { + if ( + (action == "RIGHT" && x > 8) || + (action == "LEFT" && x < 1) || + (action == "DOWN" && y < 1) || + (action == "ROTATE" && x < 0) || + x > 9 || + y < 0 || + (action == "PLACE" && y > 19) + ) + return true; + for (let [x2, y2] of collider) { + const col = (dx, dy) => x + dx == x2 && y + dy == y2; + if ( + (action == "RIGHT" && col(1, 0)) || + (action == "LEFT" && col(-1, 0)) || + (action == "DOWN" && col(0, -1)) || + ((action == "ROTATE" || action == "SPAWN") && col(0, 0)) + ) + return true; + } + } + } + + checkTspin(rotation, [x, y], [dx, dy]) { + if (this.game.falling.piece.name != "t") return false; + this.game.mechanics.isMini = false; + const minos = spinChecks[(rotation + 1) % 4] + .concat(spinChecks[rotation - 1]) + .map(([ddx, ddy]) => this.checkCollision([[ddx + x, ddy + y]], "ROTATE")); + if (minos[2] && minos[3] && (minos[0] || minos[1])) return true; + if ((minos[2] || minos[3]) && minos[0] && minos[1]) { + if ((dx == 1 || dx == -1) && dy == -2) return true; + this.game.mechanics.isMini = true; + return true; + } + } + + checkAllspin(pieceCoords) { + if (this.game.falling.piece.name == "t") return false; + const directions = [[1, 0], [0, 1], [-1, 0], [0, -1]]; + const validSpin = directions.every(([dx, dy]) => + this.checkCollision(pieceCoords.map(([x, y]) => [x + dx, y + dy]), "ROTATE")); + if (validSpin && this.game.settings.game.allspinminis) this.game.mechanics.isMini = true; + return validSpin; + } + + rotate(type) { + if (this.game.falling.piece.name == "o") return; + const newRotation = this.game.falling.getRotateState(type); + const kickdata = this.game.falling.getKickData(type, newRotation); + const rotatingCoords = this.game.falling.getNewCoords(newRotation); + const change = kickdata.find(([dx, dy]) => + !this.checkCollision(rotatingCoords.map(c => [c[0] + dx, c[1] + dy]), "ROTATE")); + if (!change) return; + this.game.board.MinoToNone("A"); + this.game.board.addMinos(this.game.falling.newName(), rotatingCoords, change); + this.game.falling.updateLocation(change); + this.game.mechanics.isTspin = this.checkTspin(newRotation, this.game.falling.location, change); + this.game.mechanics.isAllspin = this.checkAllspin(this.game.board.getMinos("A")); + this.game.falling.rotation = newRotation; + this.game.mechanics.locking.incrementLock(); + this.game.stats.rotates++; + this.game.sounds.playSound("rotate"); + this.game.mechanics.setShadow(); + if (this.game.settings.game.gravitySpeed == 0) this.game.mechanics.startGravity(); + this.game.controls.startArr("current"); + this.game.controls.checkSD(); + if (this.game.mechanics.isTspin || (this.game.mechanics.isAllspin && this.game.settings.game.allspin)) { + this.game.renderer.rotateBoard(type); + this.game.particles.spawnParticles(this.game.falling.location[0], this.game.falling.location[1] + 2, + "spin", 5, type == "CW", this.game.falling.piece.colour); + this.game.sounds.playSound("spin"); + } + } + + movePieceSide(direction, max = 1) { + this.game.controls.checkSD(); + const minos = this.game.board.getMinos("A"); + let amount = 0; + const check = dx => !this.checkCollision(minos.map(([x, y]) => [x + dx, y]), direction); + while (check(amount) && Math.abs(amount) < max) direction == "RIGHT" ? amount++ : amount--; + if (!check(amount)) this.game.renderer.bounceBoard(direction); + if (amount == 0) { + this.game.controls.stopInterval("arr"); + return; + } + this.game.board.moveMinos(minos, "RIGHT", amount); + this.game.falling.updateLocation([amount, 0]); + this.game.mechanics.isTspin = false; + this.game.mechanics.isAllspin = false; + this.game.mechanics.isMini = false; + this.game.mechanics.locking.incrementLock(); + this.game.sounds.playSound("move"); + this.game.mechanics.setShadow(); + this.game.controls.checkSD(); + if (this.game.settings.game.gravitySpeed == 0) this.game.mechanics.startGravity(); + } + + movePieceDown(sonic, scoring = false) { + const minos = this.game.board.getMinos("A"); + if (this.checkCollision(minos, "DOWN")) return; + this.game.board.moveMinos(minos, "DOWN", 1); + + this.game.mechanics.isTspin = false; + this.game.mechanics.isAllspin = false; + this.game.mechanics.isMini = false; + this.game.falling.updateLocation([0, -1]); + if (this.checkCollision(this.game.board.getMinos("A"), "DOWN")) { + this.game.mechanics.locking.scheduleLock(); + this.game.renderer.bounceBoard("DOWN"); + this.game.controls.startArr("current"); + } + if (scoring && sonic) this.game.stats.score += 1; + if (sonic) this.movePieceDown(true, scoring); + } + + harddrop() { + const minos = this.game.board.getMinos("A"); + let amount = 0; + while (!this.checkCollision(minos.map(([x, y]) => [x, y - amount]), "DOWN")) amount++; + if (amount > 0) { + this.game.mechanics.isTspin = false; + this.game.mechanics.isAllspin = false; + this.game.mechanics.isMini = false; + } + this.game.board.moveMinos(minos, "DOWN", amount); + this.game.falling.updateLocation([0, -amount]); + this.game.stats.score += 2 * amount; + this.game.sounds.playSound("harddrop"); + this.game.renderer.bounceBoard('DOWN'); + const xvals = [...new Set(minos.map(([x, y]) => x))]; + this.game.particles.spawnParticles(Math.min(...xvals)+1, this.game.falling.location[1], "drop", xvals.length); + this.game.mechanics.locking.lockPiece(); + } +} diff --git a/styles/boards.css b/styles/boards.css index e6f70ca..5ac38c4 100644 --- a/styles/boards.css +++ b/styles/boards.css @@ -1,437 +1,437 @@ -#board { - position: fixed; - top: 50%; - left: 50%; - transform-origin: 0 0; - transform: scale(1.1) translate(-50%, -50%); - height: 60vh; - aspect-ratio: 0.5; -} - -#backboard, -#backborder { - position: absolute; - height: 100%; - width: 100%; - background-color: var(--background); - outline: 0.2vh solid var(--cl-blue); - box-shadow: 0 0 5vh var(--l-gray); -} - -@property --angle { - syntax: ""; - initial-value: 0deg; - inherits: false; -} - -#backboard { - --blur-size: 7vmin; - --spin-speed: 6s; - --blur-radius: 20vmin; - --blur-strength: 0; - --colours: #ff4545, #00ff99, #006aff, #ff0095, #ff4545; -} - -#backborder { - --blur-size: 0.3vmin; - --spin-speed: 2s; - --blur-radius: 0.3vmin; - --blur-strength: 0; - --colours: #ff4545, #00ff99, #006aff, #ff0095, #ff4545; -} - -#backboard::before, -#backborder::before { - content: " "; - position: absolute; - width: 100%; - height: 100%; - background-image: conic-gradient(from var(--angle), var(--colours)); - padding: var(--blur-size); - top: 50%; - left: 50%; - transform: translate(-50%, -50%); - border-radius: 1vmin; - animation: var(--spin-speed) spin linear infinite; - filter: blur(var(--blur-radius)); - opacity: var(--blur-strength); - transition: all 1s ease; -} - -#backboard { - outline: none; -} - -@keyframes spin { - from { - --angle: 0deg; - } - - to { - --angle: 360deg; - } -} - -#backboard::after, -#backborder::after { - content: " "; - position: absolute; - width: 100%; - height: 100%; - background-color: var(--background); -} - -#backborder.boardDanger { - outline: 0.2vh solid red !important; - box-shadow: 0 0 3vh red !important; - transition: all 0.5s ease; -} - -#dangerOverlay { - position: absolute; - background-color: red; - transition: all 0.2s ease; - width: 100%; - height: 100%; - opacity: 0; -} - -#playingfield { - position: fixed; - width: 100%; - height: 200%; - top: -100%; -} - -#clickareas { - position: absolute; - width: 100%; - height: 100%; - display: grid; - grid-template-columns: repeat(10, 1fr); - grid-template-rows: repeat(20, 1fr); -} - -.clickmino { - user-select: none; - z-index: 10; -} - -.highlighting { - background-color: #7d7d7d4e; -} - -#grid { - position: fixed; - width: 100%; - height: 100%; -} - -#next { - position: fixed; - left: 101%; - display: grid; - grid-template-columns: repeat(4, 1fr); - grid-template-rows: repeat(15, 1fr); - height: 79%; - width: 40%; - border-radius: 0 1.5vh 1.5vh 0; - outline: 0.2vh solid var(--cl-blue); - background-color: black; - transition: all 0.3s ease; -} - -#hold { - position: fixed; - left: -40%; - display: grid; - grid-template-columns: repeat(4, 1fr); - grid-template-rows: repeat(3, 1fr); - height: 15%; - width: 39.5%; - border-radius: 1.5vh 0 0 1.5vh; - background-color: black; -} - -#lockTimer, -#lockCounter { - position: absolute; - bottom: -0.8vh; - width: 100%; - height: 0.8vh; - border-radius: 0.5vh; -} - -#lockCounter { - bottom: -1.4vh; - height: 0.6vh; -} - -#lockTimer::-webkit-progress-bar, -#lockCounter::-webkit-progress-bar, -#garbageQueue::-webkit-progress-bar { - background-color: var(--invis); -} - -#lockTimer::-moz-progress-bar, -#lockCounter::-moz-progress-bar, -#garbageQueue::-moz-progress-bar { - background-color: var(--invis); -} - -#lockTimer::-webkit-progress-value, -#lockCounter::-webkit-progress-value { - background-color: var(--cl-blue); - border-radius: 0.7vh; -} - -#lockTimer::-moz-progress-value, -#lockCounter::-moz-progress-value { - background-color: var(--cl-blue); - border-radius: 0.7vh; -} - -#climbSpeedBar { - position: absolute; - width: 32vh; - height: 1vh; - border-radius: 0.5vh; - transform-origin: 0 0; - left: -2%; - bottom: -5%; -} - -#climbSpeedBar::-webkit-progress-value { - background-color: red; - border-radius: 0.7vh; -} - -#climbSpeedBar::-webkit-progress-bar { - background-color: var(--invis); -} - -#timeLeftText { - font-family: "Montserrat", sans-serif; - font-weight: bold; - color: gold; - font-size: 1em; - width: 100%; - text-align: center; - bottom: 75%; - opacity: 0; - transition: all 0.3s ease; -} - -#timeLeftText.warn { - animation: timeLeft 3s; -} - -@keyframes timeLeft { - 0% { - opacity: 1; - letter-spacing: 0px; - color: gold; - } - - 5%{ - color: red; - } - - 10%{ - color: gold; - - } - - 15%{ - color: red; - } - - 20%{ - color: gold; - - } - - 25%{ - color: red; - } - - 30%{ - color: gold; - - } - - 35%{ - color: red; - } - - 40%{ - color: gold; - - } - - 45%{ - color: red; - } - - 50%{ - color: gold; - - } - - 55%{ - color: red; - } - - 60%{ - color: gold; - - } - - 65%{ - color: red; - } - - 70% { - opacity: 1; - color:gold - } - - 75%{ - color: red; - } - - 80%{ - color: gold; - - } - - 85%{ - color: red; - } - - 90%{ - color: gold; - - } - - 95%{ - color: red; - } - - - 100% { - opacity: 0; - letter-spacing: 5px; - color: gold; - } -} - - -#garbageQueue { - position: absolute; - width: 122vh; - height: 0.5vh; - border-radius: 0.5vh; - transform-origin: 0 0; - transform: rotate(-90deg); - left: -2%; - bottom: -1%; -} - -#garbageQueue::-webkit-progress-value { - background-color: red; - border-radius: 0.7vh; -} - -#garbageQueue::-moz-progress-value { - background-color: red; - border-radius: 0.7vh; -} - -.text { - position: fixed; - font-size: 1.6em; - margin: 0; - user-select: none; -} - -#warningText { - font-family: "Montserrat", sans-serif; - font-weight: bold; - color: red; - font-size: 1em; - width: 100%; - text-align: center; - bottom: 100%; - opacity: 0; - transition: all 0.3s ease; -} - -#warningText.warn { - opacity: 0.5; - animation: warn 0.3s infinite; -} - -@keyframes warn { - 0% { - opacity: 0.5; - } - - 50% { - opacity: 0; - } -} - -.nextText { - left: 110%; - bottom: 100%; -} - -.holdText { - right: 110%; - bottom: 100%; -} - -.objectiveText { - left: 110%; - bottom: 10%; -} - -#redochoices { - position: absolute; - top: 100%; - left: 110%; - width: 40vw; - opacity: 0; - pointer-events: none; - transition: all 0.3s ease; -} - -.redochoice { - height: 3.5vh; - aspect-ratio: 1/1; - border: 0.3vh solid var(--l-gray); - border-radius: 10vmin; - transition: all 0.3s ease-out; - background-color: var(--invis); - outline: none; - color: var(--l-gray); - margin: 0.3vmin; -} - -.redochoice:hover { - border-color: var(--cl-blue); - color: var(--cl-blue); - transition: all 0.05s ease; -} - -.redochoice.selected { - border-color: var(--p-green); - color: var(--p-green); -} - -.redochoice.selected:hover { - border-color: var(--green); - color: var(--green); - transition: all 0.05s ease; +#board { + position: fixed; + top: 50%; + left: 50%; + transform-origin: 0 0; + transform: scale(1.1) translate(-50%, -50%); + height: 60vh; + aspect-ratio: 0.5; +} + +#backboard, +#backborder { + position: absolute; + height: 100%; + width: 100%; + background-color: var(--background); + outline: 0.2vh solid var(--cl-blue); + box-shadow: 0 0 5vh var(--l-gray); +} + +@property --angle { + syntax: ""; + initial-value: 0deg; + inherits: false; +} + +#backboard { + --blur-size: 7vmin; + --spin-speed: 6s; + --blur-radius: 20vmin; + --blur-strength: 0; + --colours: #ff4545, #00ff99, #006aff, #ff0095, #ff4545; +} + +#backborder { + --blur-size: 0.3vmin; + --spin-speed: 2s; + --blur-radius: 0.3vmin; + --blur-strength: 0; + --colours: #ff4545, #00ff99, #006aff, #ff0095, #ff4545; +} + +#backboard::before, +#backborder::before { + content: " "; + position: absolute; + width: 100%; + height: 100%; + background-image: conic-gradient(from var(--angle), var(--colours)); + padding: var(--blur-size); + top: 50%; + left: 50%; + transform: translate(-50%, -50%); + border-radius: 1vmin; + animation: var(--spin-speed) spin linear infinite; + filter: blur(var(--blur-radius)); + opacity: var(--blur-strength); + transition: all 1s ease; +} + +#backboard { + outline: none; +} + +@keyframes spin { + from { + --angle: 0deg; + } + + to { + --angle: 360deg; + } +} + +#backboard::after, +#backborder::after { + content: " "; + position: absolute; + width: 100%; + height: 100%; + background-color: var(--background); +} + +#backborder.boardDanger { + outline: 0.2vh solid red !important; + box-shadow: 0 0 3vh red !important; + transition: all 0.5s ease; +} + +#dangerOverlay { + position: absolute; + background-color: red; + transition: all 0.2s ease; + width: 100%; + height: 100%; + opacity: 0; +} + +#playingfield { + position: fixed; + width: 100%; + height: 200%; + top: -100%; +} + +#clickareas { + position: absolute; + width: 100%; + height: 100%; + display: grid; + grid-template-columns: repeat(10, 1fr); + grid-template-rows: repeat(20, 1fr); +} + +.clickmino { + user-select: none; + z-index: 10; +} + +.highlighting { + background-color: #7d7d7d4e; +} + +#grid { + position: fixed; + width: 100%; + height: 100%; +} + +#next { + position: fixed; + left: 101%; + display: grid; + grid-template-columns: repeat(4, 1fr); + grid-template-rows: repeat(15, 1fr); + height: 79%; + width: 40%; + border-radius: 0 1.5vh 1.5vh 0; + outline: 0.2vh solid var(--cl-blue); + background-color: black; + transition: all 0.3s ease; +} + +#hold { + position: fixed; + left: -40%; + display: grid; + grid-template-columns: repeat(4, 1fr); + grid-template-rows: repeat(3, 1fr); + height: 15%; + width: 39.5%; + border-radius: 1.5vh 0 0 1.5vh; + background-color: black; +} + +#lockTimer, +#lockCounter { + position: absolute; + bottom: -0.8vh; + width: 100%; + height: 0.8vh; + border-radius: 0.5vh; +} + +#lockCounter { + bottom: -1.4vh; + height: 0.6vh; +} + +#lockTimer::-webkit-progress-bar, +#lockCounter::-webkit-progress-bar, +#garbageQueue::-webkit-progress-bar { + background-color: var(--invis); +} + +#lockTimer::-moz-progress-bar, +#lockCounter::-moz-progress-bar, +#garbageQueue::-moz-progress-bar { + background-color: var(--invis); +} + +#lockTimer::-webkit-progress-value, +#lockCounter::-webkit-progress-value { + background-color: var(--cl-blue); + border-radius: 0.7vh; +} + +#lockTimer::-moz-progress-value, +#lockCounter::-moz-progress-value { + background-color: var(--cl-blue); + border-radius: 0.7vh; +} + +#climbSpeedBar { + position: absolute; + width: 32vh; + height: 1vh; + border-radius: 0.5vh; + transform-origin: 0 0; + left: -2%; + bottom: -5%; +} + +#climbSpeedBar::-webkit-progress-value { + background-color: red; + border-radius: 0.7vh; +} + +#climbSpeedBar::-webkit-progress-bar { + background-color: var(--invis); +} + +#timeLeftText { + font-family: "Montserrat", sans-serif; + font-weight: bold; + color: gold; + font-size: 1em; + width: 100%; + text-align: center; + bottom: 75%; + opacity: 0; + transition: all 0.3s ease; +} + +#timeLeftText.warn { + animation: timeLeft 3s; +} + +@keyframes timeLeft { + 0% { + opacity: 1; + letter-spacing: 0px; + color: gold; + } + + 5%{ + color: red; + } + + 10%{ + color: gold; + + } + + 15%{ + color: red; + } + + 20%{ + color: gold; + + } + + 25%{ + color: red; + } + + 30%{ + color: gold; + + } + + 35%{ + color: red; + } + + 40%{ + color: gold; + + } + + 45%{ + color: red; + } + + 50%{ + color: gold; + + } + + 55%{ + color: red; + } + + 60%{ + color: gold; + + } + + 65%{ + color: red; + } + + 70% { + opacity: 1; + color:gold + } + + 75%{ + color: red; + } + + 80%{ + color: gold; + + } + + 85%{ + color: red; + } + + 90%{ + color: gold; + + } + + 95%{ + color: red; + } + + + 100% { + opacity: 0; + letter-spacing: 5px; + color: gold; + } +} + + +#garbageQueue { + position: absolute; + width: 122vh; + height: 0.5vh; + border-radius: 0.5vh; + transform-origin: 0 0; + transform: rotate(-90deg); + left: -2%; + bottom: -1%; +} + +#garbageQueue::-webkit-progress-value { + background-color: red; + border-radius: 0.7vh; +} + +#garbageQueue::-moz-progress-value { + background-color: red; + border-radius: 0.7vh; +} + +.text { + position: fixed; + font-size: 1.6em; + margin: 0; + user-select: none; +} + +#warningText { + font-family: "Montserrat", sans-serif; + font-weight: bold; + color: red; + font-size: 1em; + width: 100%; + text-align: center; + bottom: 100%; + opacity: 0; + transition: all 0.3s ease; +} + +#warningText.warn { + opacity: 0.5; + animation: warn 0.3s infinite; +} + +@keyframes warn { + 0% { + opacity: 0.5; + } + + 50% { + opacity: 0; + } +} + +.nextText { + left: 110%; + bottom: 100%; +} + +.holdText { + right: 110%; + bottom: 100%; +} + +.objectiveText { + left: 110%; + bottom: 10%; +} + +#redochoices { + position: absolute; + top: 100%; + left: 110%; + width: 40vw; + opacity: 0; + pointer-events: none; + transition: all 0.3s ease; +} + +.redochoice { + height: 3.5vh; + aspect-ratio: 1/1; + border: 0.3vh solid var(--l-gray); + border-radius: 10vmin; + transition: all 0.3s ease-out; + background-color: var(--invis); + outline: none; + color: var(--l-gray); + margin: 0.3vmin; +} + +.redochoice:hover { + border-color: var(--cl-blue); + color: var(--cl-blue); + transition: all 0.05s ease; +} + +.redochoice.selected { + border-color: var(--p-green); + color: var(--p-green); +} + +.redochoice.selected:hover { + border-color: var(--green); + color: var(--green); + transition: all 0.05s ease; } \ No newline at end of file diff --git a/styles/menus.css b/styles/menus.css index 3b3a563..f4cd5bb 100644 --- a/styles/menus.css +++ b/styles/menus.css @@ -1,349 +1,349 @@ -/* main menu */ -#settingsPanel[open] { - display: flex; - align-items: center; - justify-content: center; - flex-wrap: wrap; - flex-direction: row; - border: none; - width: 100vw; - height: 50vh; - background-color: var(--invis); - overflow: visible; -} - -#settingsPanel>br { - width: 100%; - content: ' '; -} - -button>img { - -webkit-user-drag: none; -} - -.settingPanelButton { - display: flex; - align-items: center; - justify-content: center; - flex-direction: column; - margin: 1%; - height: 18vh; - aspect-ratio: 1/1; - border: 0.3vh solid var(--l-gray); - border-radius: 1.5vh; - transition: all 0.5s ease-out; - background-color: var(--invis); - padding: 0.5vh; - outline: none; -} - - -.settingPanelButton>p { - color: white; - margin: 0; - padding: 0; - opacity: 0.3; - transform: translateY(1vh); - letter-spacing: 0.1em; -} - -.settingPanelButton>img { - height: 75%; -} - -.settingPanelButton:hover { - border: 0.3vh solid var(--cl-blue); - transition: all 0.1s ease; -} - -.settingPanelButton:disabled, -.smallPanelButton:disabled { - opacity: 0.5; - cursor: not-allowed; -} - -.smallPanelButton, -.smallerPanelButton { - height: 6vh; - width: 6vh; - border-radius: 1vmin; - display: flex; - align-items: center; - justify-content: center; - flex-wrap: wrap; - flex-direction: row; -} - -.smallerPanelButton { - height: 4vh; - width: 4vh; - border-radius: 0.7vh; - border-width: 0.2vh; -} - -.smallerPanelButton:hover { - border-width: 0.2vh; -} - -.smallerPanelButton>img { - height: 100% -} - -.spanleft { - flex: 1; - text-align: right; - padding-right: 1vw; - width: 9vw; -} - -.spanright { - flex: 1; - padding-left: 1vw; -} - -/* songs */ -#songSelector { - display: flex; - align-items: center; - justify-content: center; - width: 40vw; - height: 15vh; - overflow: visible; - white-space: nowrap; - opacity: 0.4; - transition: all 0.5s ease; -} - -#songSelector:hover { - opacity: 0.8; - transition: all 0.1s ease; -} - -#songText { - position: absolute; - transform: translateY(-4vh); - align-self: center; - padding: 0; - margin: 0; -} - -#songProgress { - height: 0.8vh; - border-radius: 5vh; -} - -#songProgress::-webkit-progress-bar { - background-color: var(--e-black); - border-radius: 5vh; -} - -#songProgress::-moz-progress-bar { - background-color: var(--e-black); - border-radius: 5vh; -} - - -#songProgress::-webkit-progress-value { - background-color: var(--cl-blue); - border-radius: 5vh; -} - -#songProgress::-moz-progress-value { - background-color: var(--cl-blue); - border-radius: 5vh; -} - -/* option styles */ -.range { - appearance: none; - -webkit-appearance: none; - height: 1vh; - width: 40%; - right: 3%; - border: 0.2vmin solid white; - background: var(--e-black); - outline: none; - -webkit-transition: 0.2s; - transition: opacity 0.2s; -} - -.range:hover { - border-color: var(--p-green); -} - -.check { - width: 3.5vh; - height: 3.5vh; - appearance: none; - -webkit-appearance: none; - background-color: var(--almost-invis); - border: 0.2vmin solid white; - border-radius: 3vh; - transition: all 0.1s ease; -} - -.check:hover { - border-color: var(--p-green); -} - -.check:checked { - background-color: var(--p-green); - border: none; -} - -.textInput, -.dropdown, -.number { - padding: 0.6vh; - background-color: var(--almost-invis); - border: 0.1vmin solid white; - color: rgb(225, 225, 225); - font-size: 1em; -} - -.number { - width: 20%; - appearance: initial; - -moz-appearance: textfield; -} - -.textInput:hover, -.textInput:focus, -.dropdown:hover, -.dropdown:focus, -.number:hover, -.number:focus { - border-radius: 0; - outline: none; - border-color: var(--p-green); -} - -.keybind { - padding: 0.5vh; - background-color: var(--invis); - border: none; - font-size: 1em; - color: var(--c-blue); -} - -.settingRow:hover>.keybind { - color: var(--p-green); -} - -/* change keybinds */ -#frontdrop { - width: 100%; - height: 100%; - position: fixed; - background-color: var(--slight-invis); - overflow: hidden; - color: var(--cl-blue); - outline: none; - user-select: none; - border: none; - animation: fadein 0.4s forwards ease; - font-family: "Montserrat", sans-serif; -} - -#frontdrop.closingAnimation { - animation: fadeout 0.2s forwards ease; -} - -#selectkeydiv { - display: flex; - width: 100%; - height: 100%; - justify-content: center; - align-items: center; - font-size: 5em; - text-shadow: 0 0 3vh white; -} - -#selectkeybigtext { - transform: translateY(-8vh); -} - -#selectkeytext { - position: absolute; - top: 46vh; - font-size: 0.3em; - text-shadow: 0 0 3vh white; -} - -/* stats */ - -.actiontext, -.statstext { - position: fixed; - font-size: 3vh; - text-shadow: 0 0 3vh var(--vl-gray); - margin: 0; - text-align: right; - width: 200%; - user-select: none; - transition: all 0.2s ease; - right: 105%; -} - -.actiontext { - opacity: 0; -} - -.smalltext { - position: absolute; - font-size: 0.9em; - color: var(--l-gray); - user-select: none; -} - -#smallStat1, -#smallStat2, -#smallStat3 { - right: 150%; - font-size: 2.1vh; - text-align: right; -} - -#statName1, -#statName2, -#statName3 { - text-align: right; - right: 105%; - transform: translateY(0.5vh); -} - -.statText, -#explanationText, -#updatetext { - display: flex; - margin: 0; - opacity: 0.4; -} - -.statText { - opacity: 1; -} - -/* focus */ -#nofocus { - font-family: "Montserrat", sans-serif; - display: none; - opacity: 1; - text-align: center; - position: fixed; - left: 50%; - top: 50%; - transform: translate(-50%, -50%); - border: 3px solid red; - color: red; - font-size: 2.5em; - padding: .2em; - background-color: #000A; - transition: 0.2s; -} - -#nofocus span { - display: block; - color: #fff; - font-size: .6em; +/* main menu */ +#settingsPanel[open] { + display: flex; + align-items: center; + justify-content: center; + flex-wrap: wrap; + flex-direction: row; + border: none; + width: 100vw; + height: 50vh; + background-color: var(--invis); + overflow: visible; +} + +#settingsPanel>br { + width: 100%; + content: ' '; +} + +button>img { + -webkit-user-drag: none; +} + +.settingPanelButton { + display: flex; + align-items: center; + justify-content: center; + flex-direction: column; + margin: 1%; + height: 18vh; + aspect-ratio: 1/1; + border: 0.3vh solid var(--l-gray); + border-radius: 1.5vh; + transition: all 0.5s ease-out; + background-color: var(--invis); + padding: 0.5vh; + outline: none; +} + + +.settingPanelButton>p { + color: white; + margin: 0; + padding: 0; + opacity: 0.3; + transform: translateY(1vh); + letter-spacing: 0.1em; +} + +.settingPanelButton>img { + height: 75%; +} + +.settingPanelButton:hover { + border: 0.3vh solid var(--cl-blue); + transition: all 0.1s ease; +} + +.settingPanelButton:disabled, +.smallPanelButton:disabled { + opacity: 0.5; + cursor: not-allowed; +} + +.smallPanelButton, +.smallerPanelButton { + height: 6vh; + width: 6vh; + border-radius: 1vmin; + display: flex; + align-items: center; + justify-content: center; + flex-wrap: wrap; + flex-direction: row; +} + +.smallerPanelButton { + height: 4vh; + width: 4vh; + border-radius: 0.7vh; + border-width: 0.2vh; +} + +.smallerPanelButton:hover { + border-width: 0.2vh; +} + +.smallerPanelButton>img { + height: 100% +} + +.spanleft { + flex: 1; + text-align: right; + padding-right: 1vw; + width: 9vw; +} + +.spanright { + flex: 1; + padding-left: 1vw; +} + +/* songs */ +#songSelector { + display: flex; + align-items: center; + justify-content: center; + width: 40vw; + height: 15vh; + overflow: visible; + white-space: nowrap; + opacity: 0.4; + transition: all 0.5s ease; +} + +#songSelector:hover { + opacity: 0.8; + transition: all 0.1s ease; +} + +#songText { + position: absolute; + transform: translateY(-4vh); + align-self: center; + padding: 0; + margin: 0; +} + +#songProgress { + height: 0.8vh; + border-radius: 5vh; +} + +#songProgress::-webkit-progress-bar { + background-color: var(--e-black); + border-radius: 5vh; +} + +#songProgress::-moz-progress-bar { + background-color: var(--e-black); + border-radius: 5vh; +} + + +#songProgress::-webkit-progress-value { + background-color: var(--cl-blue); + border-radius: 5vh; +} + +#songProgress::-moz-progress-value { + background-color: var(--cl-blue); + border-radius: 5vh; +} + +/* option styles */ +.range { + appearance: none; + -webkit-appearance: none; + height: 1vh; + width: 40%; + right: 3%; + border: 0.2vmin solid white; + background: var(--e-black); + outline: none; + -webkit-transition: 0.2s; + transition: opacity 0.2s; +} + +.range:hover { + border-color: var(--p-green); +} + +.check { + width: 3.5vh; + height: 3.5vh; + appearance: none; + -webkit-appearance: none; + background-color: var(--almost-invis); + border: 0.2vmin solid white; + border-radius: 3vh; + transition: all 0.1s ease; +} + +.check:hover { + border-color: var(--p-green); +} + +.check:checked { + background-color: var(--p-green); + border: none; +} + +.textInput, +.dropdown, +.number { + padding: 0.6vh; + background-color: var(--almost-invis); + border: 0.1vmin solid white; + color: rgb(225, 225, 225); + font-size: 1em; +} + +.number { + width: 20%; + appearance: initial; + -moz-appearance: textfield; +} + +.textInput:hover, +.textInput:focus, +.dropdown:hover, +.dropdown:focus, +.number:hover, +.number:focus { + border-radius: 0; + outline: none; + border-color: var(--p-green); +} + +.keybind { + padding: 0.5vh; + background-color: var(--invis); + border: none; + font-size: 1em; + color: var(--c-blue); +} + +.settingRow:hover>.keybind { + color: var(--p-green); +} + +/* change keybinds */ +#frontdrop { + width: 100%; + height: 100%; + position: fixed; + background-color: var(--slight-invis); + overflow: hidden; + color: var(--cl-blue); + outline: none; + user-select: none; + border: none; + animation: fadein 0.4s forwards ease; + font-family: "Montserrat", sans-serif; +} + +#frontdrop.closingAnimation { + animation: fadeout 0.2s forwards ease; +} + +#selectkeydiv { + display: flex; + width: 100%; + height: 100%; + justify-content: center; + align-items: center; + font-size: 5em; + text-shadow: 0 0 3vh white; +} + +#selectkeybigtext { + transform: translateY(-8vh); +} + +#selectkeytext { + position: absolute; + top: 46vh; + font-size: 0.3em; + text-shadow: 0 0 3vh white; +} + +/* stats */ + +.actiontext, +.statstext { + position: fixed; + font-size: 3vh; + text-shadow: 0 0 3vh var(--vl-gray); + margin: 0; + text-align: right; + width: 200%; + user-select: none; + transition: all 0.2s ease; + right: 105%; +} + +.actiontext { + opacity: 0; +} + +.smalltext { + position: absolute; + font-size: 0.9em; + color: var(--l-gray); + user-select: none; +} + +#smallStat1, +#smallStat2, +#smallStat3 { + right: 150%; + font-size: 2.1vh; + text-align: right; +} + +#statName1, +#statName2, +#statName3 { + text-align: right; + right: 105%; + transform: translateY(0.5vh); +} + +.statText, +#explanationText, +#updatetext { + display: flex; + margin: 0; + opacity: 0.4; +} + +.statText { + opacity: 1; +} + +/* focus */ +#nofocus { + font-family: "Montserrat", sans-serif; + display: none; + opacity: 1; + text-align: center; + position: fixed; + left: 50%; + top: 50%; + transform: translate(-50%, -50%); + border: 3px solid red; + color: red; + font-size: 2.5em; + padding: .2em; + background-color: #000A; + transition: 0.2s; +} + +#nofocus span { + display: block; + color: #fff; + font-size: .6em; } \ No newline at end of file diff --git a/styles/miscmenus.css b/styles/miscmenus.css index ec152f2..647882b 100644 --- a/styles/miscmenus.css +++ b/styles/miscmenus.css @@ -1,146 +1,146 @@ -.dialog { - font-family: "Montserrat", sans-serif; - text-align: center; - height: 35vh; - aspect-ratio: 1.3/1; - background-color: var(--almost-invis); - color: white; - user-select: none; - animation: zoomin 0.3s forwards ease; - border-radius: 2vh; - border: 0.3vh solid var(--vl-gray); - padding: 0; - outline: none; -} - -.dialog.closingAnimation { - animation: zoomout 0.3s forwards ease; -} - -.dialog::backdrop { - background-color: #000000b5; - backdrop-filter: blur(5px); - animation: fadein 0.4s forwards ease; -} - -.dialog.closingAnimation::backdrop { - animation: fadeout 0.4s forwards ease; -} - -/* pbs */ -.pbbox { - display: grid; - margin: 1vmin; - grid-template-columns: 2fr 2fr 1fr; - gap: 2vw; - padding: 1vmin; - border: 0.1vh solid white; -} - -.pbbox:hover { - border: 0.1vh solid var(--p-green); -} - -.pbbox>h2 { - margin: 0; - pointer-events: none; -} - -.pbbox>button { - height: 4vh; - aspect-ratio: 1/1; - border: 0.3vh solid var(--l-gray); - color: var(--cl-blue); - border-radius: 1vmin; - transition: all 0.1s ease-out; - background-color: var(--invis); - padding: 0.5vh; - outline: none; -} - -.pbbox>button:hover { - border: 0.3vh solid var(--cl-blue); -} - - -/* edit menu */ -.pieceselection { - height: 5vmin; - aspect-ratio: 1/1; - margin: 0.3vw; - border: none; - outline: none; - transition: all 0.3s ease; - margin-bottom: 2vh; -} - -.pieceselection:hover { - transform: scale(1.1); -} - -.pieceselection:active { - transform: scale(0.9); -} - -#editbuttons { - display: flex; - gap: 1vw; - width: 100%; - align-items: center; - justify-content: center; - flex-wrap: wrap; - margin-top: 3vh; -} - -#editbuttons>br { - width: 100%; - content: ' '; -} - -/* notifications */ -#notifications { - position: absolute; - right: 0; - bottom: 0; -} - -.notif { - font-family: "Montserrat", sans-serif; - animation: error 0.6s forwards; - background-color: black; - color: white; - border-left-color: var(--color); - border-left-width: 0.3vw; - border-left-style: solid; - padding: 1vmin; - border-radius: 1vh; - margin: 2vmin; - width: 30vw; - cursor: pointer; - transition: opacity 1s ease; - z-index: 10; -} - -@keyframes error { - from { - opacity: 0; - transform: translate(10vw); - } - - to { - opacity: 1; - transform: translate(0); - } -} - -.notif_title { - margin: 0.5vmin; - font-size: 1em; - font-weight: bold; -} - -.notif_text { - margin: 0.3vmin; - font-size: 0.8em; - opacity: 0.8; +.dialog { + font-family: "Montserrat", sans-serif; + text-align: center; + height: 35vh; + aspect-ratio: 1.3/1; + background-color: var(--almost-invis); + color: white; + user-select: none; + animation: zoomin 0.3s forwards ease; + border-radius: 2vh; + border: 0.3vh solid var(--vl-gray); + padding: 0; + outline: none; +} + +.dialog.closingAnimation { + animation: zoomout 0.3s forwards ease; +} + +.dialog::backdrop { + background-color: #000000b5; + backdrop-filter: blur(5px); + animation: fadein 0.4s forwards ease; +} + +.dialog.closingAnimation::backdrop { + animation: fadeout 0.4s forwards ease; +} + +/* pbs */ +.pbbox { + display: grid; + margin: 1vmin; + grid-template-columns: 2fr 2fr 1fr; + gap: 2vw; + padding: 1vmin; + border: 0.1vh solid white; +} + +.pbbox:hover { + border: 0.1vh solid var(--p-green); +} + +.pbbox>h2 { + margin: 0; + pointer-events: none; +} + +.pbbox>button { + height: 4vh; + aspect-ratio: 1/1; + border: 0.3vh solid var(--l-gray); + color: var(--cl-blue); + border-radius: 1vmin; + transition: all 0.1s ease-out; + background-color: var(--invis); + padding: 0.5vh; + outline: none; +} + +.pbbox>button:hover { + border: 0.3vh solid var(--cl-blue); +} + + +/* edit menu */ +.pieceselection { + height: 5vmin; + aspect-ratio: 1/1; + margin: 0.3vw; + border: none; + outline: none; + transition: all 0.3s ease; + margin-bottom: 2vh; +} + +.pieceselection:hover { + transform: scale(1.1); +} + +.pieceselection:active { + transform: scale(0.9); +} + +#editbuttons { + display: flex; + gap: 1vw; + width: 100%; + align-items: center; + justify-content: center; + flex-wrap: wrap; + margin-top: 3vh; +} + +#editbuttons>br { + width: 100%; + content: ' '; +} + +/* notifications */ +#notifications { + position: absolute; + right: 0; + bottom: 0; +} + +.notif { + font-family: "Montserrat", sans-serif; + animation: error 0.6s forwards; + background-color: black; + color: white; + border-left-color: var(--color); + border-left-width: 0.3vw; + border-left-style: solid; + padding: 1vmin; + border-radius: 1vh; + margin: 2vmin; + width: 30vw; + cursor: pointer; + transition: opacity 1s ease; + z-index: 10; +} + +@keyframes error { + from { + opacity: 0; + transform: translate(10vw); + } + + to { + opacity: 1; + transform: translate(0); + } +} + +.notif_title { + margin: 0.5vmin; + font-size: 1em; + font-weight: bold; +} + +.notif_text { + margin: 0.3vmin; + font-size: 0.8em; + opacity: 0.8; } \ No newline at end of file diff --git a/styles/settings.css b/styles/settings.css index 3937a09..57aa1b0 100644 --- a/styles/settings.css +++ b/styles/settings.css @@ -1,226 +1,226 @@ -/* dialog */ -.scrollSettings { - font-family: "Montserrat", sans-serif; - height: 90vh; - aspect-ratio: 2/3; - background-color: var(--almost-invis); - padding: 0.5vmin; - color: white; - user-select: none; - outline: none; - text-shadow: 0 0 5vh var(--vl-gray); - border: 0.5vmin solid transparent; - animation: zoomin 0.3s forwards ease; - background-clip: padding-box; - overflow: hidden; -} - -.scrollSettings.closingAnimation { - animation: zoomout 0.3s forwards ease; -} - -.scrollSettings::backdrop { - background-color: #000000b5; - backdrop-filter: blur(5px); - animation: fadein 0.4s forwards ease; -} - -.scrollSettings.closingAnimation::backdrop { - animation: fadeout 0.4s forwards ease; -} - -/* border */ -.scrollSettings::before, -.scrollSettings::after { - content: ''; - position: absolute; - top: 0; - bottom: 0; - width: 0.3vmin; - background: linear-gradient(to bottom, transparent, white 35%, white 65%, transparent); - box-sizing: border-box; -} - -.scrollSettings::before { - left: 0; -} - -.scrollSettings::after { - right: 0; -} - -/* top */ -.settingsTop { - font-size: 1.5em; - text-align: center; - height: 10%; - width: 100%; -} - -/* settings list */ -.settingsBox { - height: 70%; - width: 100%; - overflow-x: hidden; - overflow-y: scroll; - font-size: 1.1em; - padding: 0px; - margin: 0px; - display: flex; - flex-direction: column; - box-sizing: border-box; - padding-top: 8vh; - padding-bottom: 10vh; -} - -.settingRow, -.closeDialogButton, -.settingsReset, -.gamemodeSelect { - display: flex; - align-items: center; - height: 4.2vh; - --s: 2vmin; - --t: 0.3vmin; - --g: 1vmin; - - padding: calc(var(--g) + var(--t)); - outline: var(--t) solid #ffffff; - outline-offset: calc(var(--g)/-1); - mask: conic-gradient(at var(--s) var(--s), #0000 75%, #000 0) 0 0/calc(100% - var(--s)) calc(100% - var(--s)), - linear-gradient(#000 0 0) content-box; - transition: scale 0.6s, opacity 0.6s, outline-offset 0.3s; -} - -.settingRow:hover, -.closeDialogButton:hover, -.settingsReset:hover, -.gamemodeSelect:hover { - outline-offset: calc(-1*var(--t)); - animation: jump 1.5s infinite ease 0.4s; - transition: outline-offset 0.1s, scale 0.6s; -} - -.settingRow>p { - width: 50%; - display: flex; - justify-content: flex-end; - text-align: right; - padding-right: 1.5vw; - pointer-events: none; -} - -.settingRow>.option { - position: unset; - display: flex; - justify-content: flex-start; - transform: none; -} - -/* footer */ -.settingsFooter { - height: 20%; - width: 100%; - display: flex; - flex-direction: column; - align-items: center; - justify-content: center; - gap: 1vh; -} - -.settingsFooter>p { - margin: 0; - font-size: 0.8em; - opacity: 0.4; - width: 100%; - text-align: center; - -} - -.closeDialogButton, -.settingsReset { - height: auto; - background-color: var(--invis); - border: none; - padding: 2vh; - font-size: 1.4em; - color: white; - transition: all 0.4s ease; -} - -/* buttons and misc */ -.settingsReset { - position: absolute; - right: 1vw; - bottom: 1vh; - padding: 1vh; - padding-top: 1.5vh; - display: flex; - justify-content: center; - align-items: center; - height: 6vh; - aspect-ratio: 1/1; -} - -#gamemodeDialog>.settingsBox { - align-items: center; - padding-top: 0; - padding-bottom: 0; -} - -.gamemodeSelect { - height: 8vh; - width: 80%; - background-color: var(--invis); - border: none; - padding: 1.3vh; - font-size: 1.3em; - color: white; - transition: all 0.4s ease; - justify-content: center; -} - -.gamemodeSelect:hover { - outline-color: var(--p-green); - color: var(--p-green); -} - -.gamemodeSelect.selected { - outline-color: var(--green); - color: var(--green); -} - -#editMenu>.settingsBox { - padding-top: 0; -} - -#gameStatsDialog>.settingsFooter { - flex-direction: row; - height: 15%; -} - -#gameEnd>.settingsFooter { - position: absolute; - bottom: 10%; - width: 100%; - flex-direction: row; - height: auto; -} - -#changeRangeValue>.settingsBox { - padding-top: 0; - padding-bottom: 0; - height: auto; - align-items: center; -} - -.settingRow:has(.dropdown) { - animation: none; -} - -.dropdown { - width: 20%; - margin: 1%; - background: black; - color: white; -} +/* dialog */ +.scrollSettings { + font-family: "Montserrat", sans-serif; + height: 90vh; + aspect-ratio: 2/3; + background-color: var(--almost-invis); + padding: 0.5vmin; + color: white; + user-select: none; + outline: none; + text-shadow: 0 0 5vh var(--vl-gray); + border: 0.5vmin solid transparent; + animation: zoomin 0.3s forwards ease; + background-clip: padding-box; + overflow: hidden; +} + +.scrollSettings.closingAnimation { + animation: zoomout 0.3s forwards ease; +} + +.scrollSettings::backdrop { + background-color: #000000b5; + backdrop-filter: blur(5px); + animation: fadein 0.4s forwards ease; +} + +.scrollSettings.closingAnimation::backdrop { + animation: fadeout 0.4s forwards ease; +} + +/* border */ +.scrollSettings::before, +.scrollSettings::after { + content: ''; + position: absolute; + top: 0; + bottom: 0; + width: 0.3vmin; + background: linear-gradient(to bottom, transparent, white 35%, white 65%, transparent); + box-sizing: border-box; +} + +.scrollSettings::before { + left: 0; +} + +.scrollSettings::after { + right: 0; +} + +/* top */ +.settingsTop { + font-size: 1.5em; + text-align: center; + height: 10%; + width: 100%; +} + +/* settings list */ +.settingsBox { + height: 70%; + width: 100%; + overflow-x: hidden; + overflow-y: scroll; + font-size: 1.1em; + padding: 0px; + margin: 0px; + display: flex; + flex-direction: column; + box-sizing: border-box; + padding-top: 8vh; + padding-bottom: 10vh; +} + +.settingRow, +.closeDialogButton, +.settingsReset, +.gamemodeSelect { + display: flex; + align-items: center; + height: 4.2vh; + --s: 2vmin; + --t: 0.3vmin; + --g: 1vmin; + + padding: calc(var(--g) + var(--t)); + outline: var(--t) solid #ffffff; + outline-offset: calc(var(--g)/-1); + mask: conic-gradient(at var(--s) var(--s), #0000 75%, #000 0) 0 0/calc(100% - var(--s)) calc(100% - var(--s)), + linear-gradient(#000 0 0) content-box; + transition: scale 0.6s, opacity 0.6s, outline-offset 0.3s; +} + +.settingRow:hover, +.closeDialogButton:hover, +.settingsReset:hover, +.gamemodeSelect:hover { + outline-offset: calc(-1*var(--t)); + animation: jump 1.5s infinite ease 0.4s; + transition: outline-offset 0.1s, scale 0.6s; +} + +.settingRow>p { + width: 50%; + display: flex; + justify-content: flex-end; + text-align: right; + padding-right: 1.5vw; + pointer-events: none; +} + +.settingRow>.option { + position: unset; + display: flex; + justify-content: flex-start; + transform: none; +} + +/* footer */ +.settingsFooter { + height: 20%; + width: 100%; + display: flex; + flex-direction: column; + align-items: center; + justify-content: center; + gap: 1vh; +} + +.settingsFooter>p { + margin: 0; + font-size: 0.8em; + opacity: 0.4; + width: 100%; + text-align: center; + +} + +.closeDialogButton, +.settingsReset { + height: auto; + background-color: var(--invis); + border: none; + padding: 2vh; + font-size: 1.4em; + color: white; + transition: all 0.4s ease; +} + +/* buttons and misc */ +.settingsReset { + position: absolute; + right: 1vw; + bottom: 1vh; + padding: 1vh; + padding-top: 1.5vh; + display: flex; + justify-content: center; + align-items: center; + height: 6vh; + aspect-ratio: 1/1; +} + +#gamemodeDialog>.settingsBox { + align-items: center; + padding-top: 0; + padding-bottom: 0; +} + +.gamemodeSelect { + height: 8vh; + width: 80%; + background-color: var(--invis); + border: none; + padding: 1.3vh; + font-size: 1.3em; + color: white; + transition: all 0.4s ease; + justify-content: center; +} + +.gamemodeSelect:hover { + outline-color: var(--p-green); + color: var(--p-green); +} + +.gamemodeSelect.selected { + outline-color: var(--green); + color: var(--green); +} + +#editMenu>.settingsBox { + padding-top: 0; +} + +#gameStatsDialog>.settingsFooter { + flex-direction: row; + height: 15%; +} + +#gameEnd>.settingsFooter { + position: absolute; + bottom: 10%; + width: 100%; + flex-direction: row; + height: auto; +} + +#changeRangeValue>.settingsBox { + padding-top: 0; + padding-bottom: 0; + height: auto; + align-items: center; +} + +.settingRow:has(.dropdown) { + animation: none; +} + +.dropdown { + width: 20%; + margin: 1%; + background: black; + color: white; +} diff --git a/styles/style.css b/styles/style.css index b3b09af..ff7a80c 100644 --- a/styles/style.css +++ b/styles/style.css @@ -1,241 +1,241 @@ -@import url("https://fonts.googleapis.com/css2?family=Montserrat&display=swap"); -@import url("https://fonts.googleapis.com/css2?family=Major+Mono+Display&display=swap"); - -:root { - --night: #080b0c; - --e-black: #1b1e22; - --c-blue: #accbe1; - --cl-blue: #dbeaf3; - --l-gray: #ffffff50; - --vl-gray: #ffffffc4; - --invis: #00000000; - --almost-invis: #00000024; - --slight-invis: #000000cf; - --p-green: #53b565; - --green: #30f24d; - --gray: #808080; -} - -body { - color: var(--cl-blue); - font-family: "Major Mono Display", monospace; - margin: 0; - height: 100vh; - width: 100vw; - overflow: hidden; - background: var(--night); - --background: black; -} - -#splashScreen { - position: fixed; - width: 100vw; - height: 100vh; - top: 0; - left: 0; - display: flex; - flex-direction: column; - justify-content: center; - align-items: center; - background-color: var(--slight-invis); - backdrop-filter: blur(10px); - z-index: 1; - text-shadow: 0 0 8vh rgb(255, 255, 255); - transition: all 0.5s ease, display 1.5s allow-discrete; - scale: 1; -} - -#splashScreen>h1 { - font-size: 7em; - animation: pulse 2s infinite ease; - opacity: 1; -} - -#ignoreText { - opacity: 0; - transition: all 1s ease; -} - -#openSettingsButton, -#editButton { - top: 5%; - position: fixed; - left: 127%; - display: flex; - justify-content: right; - padding-right: 3%; - align-items: center; - height: 8%; - width: 24%; - border: 0.2vh solid white; - border-radius: 0 1.5vh 1.5vh 0; - transition: all 0.5s ease-out; - background-color: var(--invis); - outline: none; - opacity: 0.6; -} - -#editButton { - top: 15%; -} - -#openSettingsButton:hover, -#editButton:hover { - left: 133%; - opacity: 1; - transition: all 0.2s ease; -} - -#openSettingsButton>img { - height: 70%; - user-select: none; -} - -#editButton>img { - height: 100%; - user-select: none; -} - -.dialog img, -.scrollSettings img { - opacity: 0.6; - transition: all 0.3s ease; -} - -.dialog>*:hover img, -.scrollSettings img:hover { - opacity: 1; -} - -/* scrollbars */ -::-webkit-scrollbar { - width: 0.4vw; -} - -::-webkit-scrollbar-track { - border-radius: 1vh; - background: var(--invis); - margin-top: 1vh; - margin-bottom: 1vh; -} - -::-webkit-scrollbar-thumb { - border-radius: 1vh; - background: var(--vl-gray); -} - -::-webkit-scrollbar-thumb:hover { - background: white; - transition: all 0.2s ease; -} - -@-moz-document url-prefix() { - * { - scrollbar-width: thin; - scrollbar-color: var(--vl-gray) var(--invis); - } -} - -/* sliders */ -.range::-webkit-slider-thumb { - -webkit-appearance: none; - appearance: none; - width: 1.5vh; - height: 2.5vh; - background: white; - cursor: pointer; -} - -.range::-moz-range-thumb { - -moz-appearance: none; - appearance: none; - width: 1.5vh; - height: 2.5vh; - background: white; - cursor: pointer; -} - -.range::-webkit-slider-thumb:hover { - background: var(--p-green); -} - -/* remove number arrows */ -.number::-webkit-outer-spin-button, -.number::-webkit-inner-spin-button { - -webkit-appearance: none; - margin: 0; -} - -/* animations */ -@keyframes jump { - 0% { - transform: translateY(0vmin); - } - - 50% { - transform: translateY(0.2vmin); - } - - 100% { - transform: translateY(0vmin); - } -} - -@keyframes fadein { - from { - opacity: 0; - } - - to { - opacity: 1; - } -} - - -@keyframes zoomin { - from { - opacity: 0; - transform: scale(0.9); - } - - to { - opacity: 1; - transform: scale(1); - } -} - -@keyframes fadeout { - from { - opacity: 1; - } - - to { - opacity: 0; - } -} - -@keyframes zoomout { - from { - opacity: 1; - transform: scale(1); - } - - to { - opacity: 0; - transform: scale(0.9); - } -} - -@keyframes pulse { - 0% { - opacity: 1; - } - - 50% { - opacity: 0.75; - } - - 100% { - opacity: 1; - } +@import url("https://fonts.googleapis.com/css2?family=Montserrat&display=swap"); +@import url("https://fonts.googleapis.com/css2?family=Major+Mono+Display&display=swap"); + +:root { + --night: #080b0c; + --e-black: #1b1e22; + --c-blue: #accbe1; + --cl-blue: #dbeaf3; + --l-gray: #ffffff50; + --vl-gray: #ffffffc4; + --invis: #00000000; + --almost-invis: #00000024; + --slight-invis: #000000cf; + --p-green: #53b565; + --green: #30f24d; + --gray: #808080; +} + +body { + color: var(--cl-blue); + font-family: "Major Mono Display", monospace; + margin: 0; + height: 100vh; + width: 100vw; + overflow: hidden; + background: var(--night); + --background: black; +} + +#splashScreen { + position: fixed; + width: 100vw; + height: 100vh; + top: 0; + left: 0; + display: flex; + flex-direction: column; + justify-content: center; + align-items: center; + background-color: var(--slight-invis); + backdrop-filter: blur(10px); + z-index: 1; + text-shadow: 0 0 8vh rgb(255, 255, 255); + transition: all 0.5s ease, display 1.5s allow-discrete; + scale: 1; +} + +#splashScreen>h1 { + font-size: 7em; + animation: pulse 2s infinite ease; + opacity: 1; +} + +#ignoreText { + opacity: 0; + transition: all 1s ease; +} + +#openSettingsButton, +#editButton { + top: 5%; + position: fixed; + left: 127%; + display: flex; + justify-content: right; + padding-right: 3%; + align-items: center; + height: 8%; + width: 24%; + border: 0.2vh solid white; + border-radius: 0 1.5vh 1.5vh 0; + transition: all 0.5s ease-out; + background-color: var(--invis); + outline: none; + opacity: 0.6; +} + +#editButton { + top: 15%; +} + +#openSettingsButton:hover, +#editButton:hover { + left: 133%; + opacity: 1; + transition: all 0.2s ease; +} + +#openSettingsButton>img { + height: 70%; + user-select: none; +} + +#editButton>img { + height: 100%; + user-select: none; +} + +.dialog img, +.scrollSettings img { + opacity: 0.6; + transition: all 0.3s ease; +} + +.dialog>*:hover img, +.scrollSettings img:hover { + opacity: 1; +} + +/* scrollbars */ +::-webkit-scrollbar { + width: 0.4vw; +} + +::-webkit-scrollbar-track { + border-radius: 1vh; + background: var(--invis); + margin-top: 1vh; + margin-bottom: 1vh; +} + +::-webkit-scrollbar-thumb { + border-radius: 1vh; + background: var(--vl-gray); +} + +::-webkit-scrollbar-thumb:hover { + background: white; + transition: all 0.2s ease; +} + +@-moz-document url-prefix() { + * { + scrollbar-width: thin; + scrollbar-color: var(--vl-gray) var(--invis); + } +} + +/* sliders */ +.range::-webkit-slider-thumb { + -webkit-appearance: none; + appearance: none; + width: 1.5vh; + height: 2.5vh; + background: white; + cursor: pointer; +} + +.range::-moz-range-thumb { + -moz-appearance: none; + appearance: none; + width: 1.5vh; + height: 2.5vh; + background: white; + cursor: pointer; +} + +.range::-webkit-slider-thumb:hover { + background: var(--p-green); +} + +/* remove number arrows */ +.number::-webkit-outer-spin-button, +.number::-webkit-inner-spin-button { + -webkit-appearance: none; + margin: 0; +} + +/* animations */ +@keyframes jump { + 0% { + transform: translateY(0vmin); + } + + 50% { + transform: translateY(0.2vmin); + } + + 100% { + transform: translateY(0vmin); + } +} + +@keyframes fadein { + from { + opacity: 0; + } + + to { + opacity: 1; + } +} + + +@keyframes zoomin { + from { + opacity: 0; + transform: scale(0.9); + } + + to { + opacity: 1; + transform: scale(1); + } +} + +@keyframes fadeout { + from { + opacity: 1; + } + + to { + opacity: 0; + } +} + +@keyframes zoomout { + from { + opacity: 1; + transform: scale(1); + } + + to { + opacity: 0; + transform: scale(0.9); + } +} + +@keyframes pulse { + 0% { + opacity: 1; + } + + 50% { + opacity: 0.75; + } + + 100% { + opacity: 1; + } } \ No newline at end of file diff --git a/tauri/package.json b/tauri/package.json index 59c0c41..c1d80b7 100644 --- a/tauri/package.json +++ b/tauri/package.json @@ -1,14 +1,14 @@ -{ - "name": "teti", - "private": true, - "version": "1.0.0", - "type": "module", - "scripts": { - "tauri": "tauri", - "build": "build.bat", - "dev": "dev.bat" - }, - "devDependencies": { - "@tauri-apps/cli": "^1" - } -} +{ + "name": "teti", + "private": true, + "version": "1.0.0", + "type": "module", + "scripts": { + "tauri": "tauri", + "build": "build.bat", + "dev": "dev.bat" + }, + "devDependencies": { + "@tauri-apps/cli": "^1" + } +} diff --git a/tauri/src-tauri/Cargo.toml b/tauri/src-tauri/Cargo.toml index dc17b90..a8cb52b 100644 --- a/tauri/src-tauri/Cargo.toml +++ b/tauri/src-tauri/Cargo.toml @@ -1,20 +1,20 @@ -[package] -name = "Teti" -version = "1.0.0" -description = "Teti - A tetris client by Titan" -authors = ["TitanPlayz"] -edition = "2021" - -# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html - -[build-dependencies] -tauri-build = { version = "1", features = [] } - -[dependencies] -tauri = { version = "1", features = ["shell-open"] } -serde = { version = "1", features = ["derive"] } -serde_json = "1" - -[features] -# This feature is used for production builds or when a dev server is not specified, DO NOT REMOVE!! -custom-protocol = ["tauri/custom-protocol"] +[package] +name = "Teti" +version = "1.0.0" +description = "Teti - A tetris client by Titan" +authors = ["TitanPlayz"] +edition = "2021" + +# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html + +[build-dependencies] +tauri-build = { version = "1", features = [] } + +[dependencies] +tauri = { version = "1", features = ["shell-open"] } +serde = { version = "1", features = ["derive"] } +serde_json = "1" + +[features] +# This feature is used for production builds or when a dev server is not specified, DO NOT REMOVE!! +custom-protocol = ["tauri/custom-protocol"] diff --git a/tauri/src-tauri/build.rs b/tauri/src-tauri/build.rs index d860e1e..2ba80a8 100644 --- a/tauri/src-tauri/build.rs +++ b/tauri/src-tauri/build.rs @@ -1,3 +1,3 @@ -fn main() { - tauri_build::build() -} +fn main() { + tauri_build::build() +} diff --git a/tauri/src-tauri/src/main.rs b/tauri/src-tauri/src/main.rs index e6ad770..db1310c 100644 --- a/tauri/src-tauri/src/main.rs +++ b/tauri/src-tauri/src/main.rs @@ -1,8 +1,8 @@ -// Prevents additional console window on Windows in release, DO NOT REMOVE!! -#![cfg_attr(not(debug_assertions), windows_subsystem = "windows")] - -fn main() { - tauri::Builder::default() - .run(tauri::generate_context!()) - .expect("error while running tauri application"); -} +// Prevents additional console window on Windows in release, DO NOT REMOVE!! +#![cfg_attr(not(debug_assertions), windows_subsystem = "windows")] + +fn main() { + tauri::Builder::default() + .run(tauri::generate_context!()) + .expect("error while running tauri application"); +} diff --git a/tauri/src-tauri/tauri.conf.json b/tauri/src-tauri/tauri.conf.json index 56eccfa..fb90467 100644 --- a/tauri/src-tauri/tauri.conf.json +++ b/tauri/src-tauri/tauri.conf.json @@ -1,44 +1,44 @@ -{ - "build": { - "devPath": "../src", - "distDir": "../src", - "withGlobalTauri": true - }, - "package": { - "productName": "Teti", - "version": "1.0.0" - }, - "tauri": { - "allowlist": { - "all": false, - "shell": { - "all": false, - "open": true - } - }, - "windows": [ - { - "title": "TETI", - "width": 800, - "height": 600, - "resizable": true, - "maximized": true - } - ], - "security": { - "csp": null - }, - "bundle": { - "active": true, - "targets": "all", - "identifier": "com.titanplayz", - "icon": [ - "icons/32x32.png", - "icons/128x128.png", - "icons/128x128@2x.png", - "icons/icon.icns", - "icons/icon.ico" - ] - } - } +{ + "build": { + "devPath": "../src", + "distDir": "../src", + "withGlobalTauri": true + }, + "package": { + "productName": "Teti", + "version": "1.0.0" + }, + "tauri": { + "allowlist": { + "all": false, + "shell": { + "all": false, + "open": true + } + }, + "windows": [ + { + "title": "TETI", + "width": 800, + "height": 600, + "resizable": true, + "maximized": true + } + ], + "security": { + "csp": null + }, + "bundle": { + "active": true, + "targets": "all", + "identifier": "com.titanplayz", + "icon": [ + "icons/32x32.png", + "icons/128x128.png", + "icons/128x128@2x.png", + "icons/icon.icns", + "icons/icon.ico" + ] + } + } } \ No newline at end of file