285 Commits

Author SHA1 Message Date
adrien 36112f1ec8 less drop-shadows 2026-03-04 00:30:50 +01:00
adrien b28b44507b white border 2026-03-03 14:52:31 +01:00
adrien 24b0e72eab more bounce 2026-03-03 14:51:59 +01:00
adrien ab105bf485 settings reorder 2026-03-01 21:29:21 +01:00
adrien ea8dbff564 pseudo 3d 2026-03-01 21:29:10 +01:00
adrien 9a2989616a top side 2026-03-01 15:32:02 +01:00
adrien d6006e657f ghost 2026-03-01 11:48:18 +01:00
adrien ce758c1e92 update cleared line animation 2026-02-28 10:31:24 +01:00
adrien 8a0590f1b3 hsl 2026-02-23 23:21:11 +01:00
adrien 70caed8fb8 ridge 2026-02-23 18:15:31 +01:00
adrien b6eeae15b9 pop is the new classic 2026-02-23 17:11:34 +01:00
adrien 45c0e090e5 less brilliant 2026-02-21 11:52:26 +01:00
adrien 73ec2015ba more more brilliant 2026-02-21 02:23:48 +01:00
adrien 91e1ea9d3a more brilliant 2026-02-20 21:02:52 +01:00
adrien 798ac21372 pause on fullscreen exit 2026-02-20 09:05:23 +01:00
adrien 2820c42ba8 cover 2026-02-16 00:20:54 +01:00
adrien 8e043b9e8c background position 2026-02-16 00:18:31 +01:00
adrien 136ea4156f fix hold glich; prefered theme pop 2026-02-16 00:07:32 +01:00
adrien c01c6dcf58 new theme 2026-02-01 16:30:01 +01:00
adrien 25826565af bootstrap checkbox 2026-01-27 11:04:09 +01:00
adrien 56b906723c fullscreen checkbox 2026-01-27 08:56:28 +01:00
adrien 20b96da34b fix border 2026-01-23 08:22:58 +01:00
adrien bd195f7da6 softer blink 2026-01-23 08:21:15 +01:00
adrien 06b444c37e more transparency 2026-01-23 00:56:55 +01:00
adrien 71945a7ade clearPiece 2026-01-23 00:54:17 +01:00
adrien 0956e3d6e0 don't close game over modal 2026-01-21 20:45:44 +01:00
adrien cc4c477a10 classic locked-animation box-shadow 2026-01-21 20:36:47 +01:00
adrien e196d931ca speed up css 2026-01-18 05:29:07 +01:00
adrien a4117b782a locked-animation 2026-01-16 21:16:05 +01:00
adrien 3e3fd6d7f4 tweaks 2026-01-16 03:38:58 +01:00
adrien 50a4536994 100px 2026-01-16 03:27:09 +01:00
adrien a4e210526f 3D cleared line animation 2026-01-16 03:23:26 +01:00
adrien cb49d42266 3D cleared line animation 2026-01-16 02:31:13 +01:00
adrien 90b1251ebf restart hard dropped animation 2026-01-16 01:31:15 +01:00
adrien 26a4d113b5 no cleared line animation 2026-01-15 17:55:24 +01:00
adrien b3d012f489 3d trail 2026-01-15 16:51:02 +01:00
adrien 021d67b877 less grab 2026-01-15 16:15:51 +01:00
adrien 3af40de841 scroll backward 2026-01-15 13:39:47 +01:00
adrien 2d700f1927 fix border translation 2026-01-12 08:45:19 +01:00
adrien 280ff0ef9f more light 2026-01-12 01:21:44 +01:00
adrien 8e5b45bf6f fix border translation 2026-01-12 01:19:21 +01:00
adrien 18977dfd1b more light 2026-01-12 00:23:34 +01:00
adrien 334e0e0178 tweaks 2026-01-11 22:38:06 +01:00
adrien 02a24ec1f2 border 2026-01-11 22:26:58 +01:00
adrien 0cb9fd4c27 more shadows 2026-01-11 18:07:00 +01:00
adrien e21f9c7dfa text-shadow 2026-01-11 17:56:25 +01:00
adrien baf5672de8 hold and next light 2026-01-11 00:55:54 +01:00
adrien 9ed62d1e79 hold and next light 2026-01-11 00:51:01 +01:00
adrien 2a5ce8faab more text-shadow 2026-01-10 03:49:30 +01:00
adrien 4f7d44de4b text-shadow 2026-01-10 03:28:11 +01:00
adrien 44e048624a top light 2026-01-10 02:30:12 +01:00
adrien d7fa6c4fe5 Merge branch 'master' of https://git.malingrey.fr/adrien/quatuor 2026-01-10 02:18:05 +01:00
adrien b5ece6f892 stats 2026-01-10 02:15:57 +01:00
adrien 181b6d28f6 stats 2026-01-10 02:01:10 +01:00
adrien 616b364d40 light! 2026-01-09 21:38:04 +01:00
adrien e17517e2eb left always bright, right always dark 2026-01-09 08:15:50 +01:00
adrien e4c0ba5719 floating ghost 2026-01-09 02:32:03 +01:00
adrien a1028cb054 ghost desaturate 2026-01-09 02:28:41 +01:00
adrien 1babf41efe beautifuller 2026-01-09 02:17:45 +01:00
adrien 8d371a52ec 3D light 2026-01-09 01:45:22 +01:00
adrien 743e23d729 nearer 2026-01-08 22:43:50 +01:00
adrien 46ca9f92d3 change side color from orientation 2026-01-08 18:49:41 +01:00
adrien 6d0b93dfdb left face from right neighbour 2026-01-08 18:35:58 +01:00
adrien 97ca770641 left face on first then right face 2026-01-08 18:34:44 +01:00
adrien 3093d880aa border-radius: 2px; 2026-01-07 02:31:33 +01:00
adrien fe77be0381 border-radius 2026-01-07 02:29:09 +01:00
adrien a7fae60a1c 4 faced cubes 2026-01-07 02:18:31 +01:00
adrien 7927038fb2 universal? double backdrop-filters 2026-01-06 21:02:03 +01:00
adrien bb74fbef45 Revert "universal? double backdrop-filters"
This reverts commit 994a7a7f04.
2026-01-06 17:26:45 +01:00
adrien 994a7a7f04 universal? double backdrop-filters 2026-01-06 11:57:25 +01:00
adrien 0c186ccd59 backdrop-filter bug 2026-01-06 09:14:37 +01:00
adrien 44d68d79cc grab cursor on all sceneDiv 2026-01-06 02:16:56 +01:00
adrien 52a7f37eb0 replace synthwave by new wave style 2026-01-06 02:12:32 +01:00
adrien 1f78b2dc10 bout à bout 2026-01-06 01:53:51 +01:00
adrien 491f5c021a corrections 2026-01-03 13:06:28 +01:00
adrien c4d9621551 corrections 2026-01-03 05:17:20 +01:00
adrien 3d8bc0ce11 Merge branch '3729a3762aa6b1aa08a682aea48fb37ab5fe9bd5' 2026-01-03 05:06:08 +01:00
adrien 3f665d115a rename styles 2026-01-03 04:52:13 +01:00
adrien d2d4a8e737 classic ghost 2026-01-03 04:42:32 +01:00
adrien fb4755c870 Merge remote-tracking branch 'refs/remotes/malingrey.fr/master' 2026-01-03 03:05:04 +01:00
adrien 3729a3762a pop2 style 2026-01-03 02:55:03 +01:00
adrien 4b2cd0e5d2 change minimal ghost piece 2025-08-28 10:40:19 +02:00
adrien 08763c501e change classic ghost piece 2025-08-28 10:32:48 +02:00
adrien bc5de2448d less blink 2025-08-28 02:32:22 +02:00
adrien 67a0f06d03 trail on soft drop 2025-08-28 02:30:17 +02:00
adrien 2f1ec2a6f7 white border 2025-08-28 02:17:10 +02:00
adrien 26aeb8f9a2 white border 2025-08-28 02:15:48 +02:00
adrien 051fc223e9 Add README 2025-05-21 10:22:42 +02:00
adrien 9feaa3c098 meta 2025-05-20 17:01:49 +02:00
adrien 3175e7b7ad mouse event only on sceneDiv 2025-04-24 20:41:33 +02:00
adrien 4281bf1735 grab 2025-04-21 05:03:50 +02:00
adrien ff37cb7823 jpg background 2025-04-14 08:40:52 +02:00
adrien 2a8df78d22 background 2025-04-14 08:30:40 +02:00
adrien 344575bdc4 center background 2025-04-13 23:28:06 +02:00
adrien 2453b2f6b6 background 2025-04-13 17:19:29 +02:00
adrien 25a98bf42d little tweaks 2025-04-10 00:52:43 +02:00
adrien 022512a5e6 perspective 2025-04-09 08:29:19 +02:00
adrien d56af40362 clear animation 2025-04-09 01:14:39 +02:00
adrien f33723a786 ghost border 2025-04-09 00:57:53 +02:00
adrien df95139650 ghost 2025-04-09 00:51:34 +02:00
adrien d3b527570c radial gradient 2025-04-08 03:42:07 +02:00
adrien a1722a700d floatting text 2025-04-08 03:30:01 +02:00
adrien 6cc8a9e645 fix zoom 2025-04-08 00:46:47 +02:00
adrien 9023252822 remove log 2025-04-08 00:29:00 +02:00
adrien 9bf3c0de0c zoom on wheel 2025-04-07 23:51:13 +02:00
adrien 09f4785ef4 3D 2025-04-07 23:37:24 +02:00
adrien bf9554d917 no need block 2025-04-07 09:36:51 +02:00
adrien c33d80facb add binaural (3D) style 2025-04-07 09:30:07 +02:00
adrien 9282f94956 add binaural (3D) style 2025-04-07 09:01:14 +02:00
adrien fda289dc9c opéra 2024-10-25 00:10:47 +02:00
adrien b6e58b41aa opera border 2024-10-21 09:05:13 +02:00
adrien bd5c7dad3b remove wall sound 2024-10-21 08:57:13 +02:00
adrien 7faae294dc new theme 2024-10-21 01:09:18 +02:00
adrien 3b59534f90 colored ghost 2024-09-28 17:32:07 +02:00
adrien ec08747066 more glowing 2024-08-18 02:49:01 +02:00
adrien db8a9a3f74 pop colors 2024-08-17 13:01:48 +02:00
adrien 0ac36444c4 disabled pop 2024-08-08 01:35:57 +02:00
adrien 5d451db8f9 disable first held piece 2024-08-03 00:07:23 +02:00
adrien ab023ec982 show disabled held piece 2024-08-02 23:24:39 +02:00
adrien d75696fbc3 update pop theme 2024-05-28 21:26:40 +02:00
adrien f97b5f0cf9 fix pop buffer zone 2024-05-18 04:54:47 +02:00
adrien ffefb77f3f serviceWorker in html 2024-05-12 11:31:31 +02:00
adrien 21929261e4 shadow 2024-04-29 17:51:17 +02:00
adrien 58389623cc move service worker 2024-04-29 17:51:04 +02:00
adrien 04f6eaf5dc fix action timout on key up 2023-12-23 03:02:47 +01:00
adrien d56a8a6b06 stripe line clear animation 2023-12-23 03:02:17 +01:00
adrien e1992f4c1e rgba 2023-12-23 03:02:01 +01:00
adrien 27d7a0689d rgba 2023-12-23 03:01:43 +01:00
adrien df62a40c2a update themes 2023-12-19 00:43:27 +01:00
adrien 2105296238 not mino border 2023-12-18 01:32:42 +01:00
adrien 05862b8587 remove double lines 2023-12-18 01:29:01 +01:00
adrien 6c1291833d new Pop theme 2023-12-18 01:24:57 +01:00
adrien 095e3f8dbe modified: css/pop.css 2023-12-18 00:52:15 +01:00
adrien 82929b6f3b fix I color 2023-12-18 00:26:11 +01:00
adrien 29661421a5 fix input onblur 2023-12-14 00:39:15 +01:00
adrien d3a6f5d6d6 check for key duplicates 2023-12-13 23:14:28 +01:00
adrien 8af1801527 sometime wall sound reset 2023-12-11 23:43:56 +01:00
adrien 6d95acddd2 fix autorepeat and volume save 2023-12-11 23:14:26 +01:00
adrien 6062f6895b aliasing 2023-12-08 22:00:18 +01:00
adrien b726e95bed center pieces in newt and hold tables 2023-12-08 21:21:20 +01:00
adrien 228fc56916 fix settings load 2023-12-08 12:12:15 +01:00
adrien f39d677b62 align hold table and stats 2023-12-08 08:52:39 +01:00
adrien fc21a1b12b change stylesheet for chrome & firefox 2023-12-08 02:13:33 +01:00
adrien 072661ff5a ghost 2023-12-08 00:54:46 +01:00
adrien f93f3c71af format 2023-12-08 00:50:56 +01:00
adrien ed4d9b82c9 improve electro style 2023-12-08 00:05:17 +01:00
adrien b17db2ffd4 prevent capslock 2023-12-07 23:36:06 +01:00
adrien 9f8c38e6bf update electro style 2023-12-07 22:41:28 +01:00
adrien 262a26940c this.form.querySelectorAll("[name]") 2023-12-07 22:36:00 +01:00
adrien 7996e4a7ae fix hard drop animation 2023-12-07 21:44:30 +01:00
adrien a0af16d0dd add transparency 2023-12-07 21:44:21 +01:00
adrien 9547e585ca format 2023-12-07 21:44:03 +01:00
adrien 4d0d51e20d move KEY_NAMES to interface.js 2023-12-07 21:43:50 +01:00
adrien e7246c0a5d fix styleSheet change on chrome 2023-12-07 21:29:48 +01:00
adrien 50502be1d1 little changes 2023-12-07 21:29:29 +01:00
adrien a89a60691c no wall nound on hard drop 2023-12-07 20:02:43 +01:00
adrien a04e118ff1 reduce blink frequency 2023-12-07 20:00:33 +01:00
adrien ef8ddca950 wall sound on lock 2023-12-07 00:48:54 +01:00
adrien 8773e65885 play wall sound only if player action succeded previously 2023-12-07 00:32:27 +01:00
adrien ebb9f4810b change tSpin sound 2023-12-07 00:29:53 +01:00
adrien baf6e66244 change tSpin sound 2023-12-06 08:50:27 +01:00
adrien 490566e49f tSpin & lineClear sound together 2023-12-06 02:30:26 +01:00
adrien e3d4520af7 format 2023-12-06 02:07:26 +01:00
adrien bf37ca8135 more drums! 2023-12-06 02:06:50 +01:00
adrien 048d17e041 change note on combo 2023-12-06 01:07:15 +01:00
adrien 365fb17694 small changes 2023-12-05 08:51:08 +01:00
adrien caca970a53 splt js 2023-12-03 22:00:43 +01:00
adrien f42a14d75f split js 2023-12-03 21:57:20 +01:00
adrien 46a0500c66 change sounds 2023-12-03 21:47:03 +01:00
adrien 227a26880d wall sound on soft drop 2023-12-03 06:50:48 +01:00
adrien ee4945bb27 wall sound 2023-12-03 03:15:33 +01:00
adrien c010ccba49 try to reduce audio latency 2023-12-03 02:45:47 +01:00
adrien 5e4d729803 Sounds! 2023-12-01 02:28:01 +01:00
adrien 8940c1b79b blur clear line animation 2023-12-01 01:30:13 +01:00
adrien a9e3557b56 retro stats width 2023-11-30 01:43:02 +01:00
adrien 4f1904acaa brief 2023-11-30 01:27:26 +01:00
adrien 895635b601 improve style 2023-11-29 02:05:55 +01:00
adrien 71a36fdc4d input as button, abbr 2023-11-28 23:42:20 +01:00
adrien 1b18b00619 improve styles 2023-11-28 23:25:15 +01:00
adrien 682b0c61a1 cleared line shadow 2023-11-13 16:05:42 +01:00
adrien 230f20befb less brightness 2023-11-13 09:04:44 +01:00
adrien a13d97b550 minimal cleared line 2023-11-13 08:53:28 +01:00
adrien f8466cb27f cleared-line-animation 2023-11-13 08:33:26 +01:00
adrien ea2463157f faster bllinking 2023-11-13 08:17:38 +01:00
adrien c1d6fb87eb glowing title 2023-11-12 22:43:13 +01:00
adrien 5e600bec56 bout en bout 2023-11-12 22:43:07 +01:00
adrien 31123fc332 rename QUATUOR 2023-11-07 23:53:46 +01:00
adrien cbf86e0bf5 less z-index 2023-11-07 22:10:07 +01:00
adrien 875fc21cac add minimal style 2023-11-07 21:42:37 +01:00
adrien 4f4447e889 small fixes 2023-07-01 22:10:56 +02:00
adrien b9bb673e7e hard-dropped-table-animation 2023-06-24 01:32:43 +02:00
adrien fd8b46a212 quicker blink 2023-06-18 19:01:42 +02:00
adrien 36d58eb526 preload favicons 2023-06-16 02:10:41 +02:00
adrien 57639b0c2a use piece for glint 2023-06-16 02:05:42 +02:00
adrien 2d687d5fcb electro ghost mino 2023-06-16 01:44:56 +02:00
adrien 6e089315bf update moving piece in electro style 2023-06-16 00:51:28 +02:00
adrien e6f238629a update style on moving and locked 2023-06-16 00:23:08 +02:00
adrien d615b6b72b locked animation 2023-06-16 00:06:49 +02:00
adrien 70d8e6e0aa locking class & moving on matrix 2023-06-15 23:56:44 +02:00
adrien ff20114a92 ghost glint on electro style 2023-06-15 23:48:55 +02:00
adrien 3318a716f1 fix locked held piece 2023-06-15 22:52:42 +02:00
adrien 1b82e05bb5 b2b & combo properties 2023-06-15 08:56:46 +02:00
adrien 4cee98ff49 update styles 2023-06-15 01:20:23 +02:00
adrien 36cba9b6ef update electro style 2023-06-14 23:05:40 +02:00
adrien 5bacff51fc change electro bg 2023-06-14 22:56:15 +02:00
adrien 0a491b89be bug fixes 2023-06-14 22:54:32 +02:00
adrien b4a241b3a9 update electro style 2023-06-14 20:34:46 +02:00
adrien ec5ed9425e update electro style 2023-06-14 20:34:21 +02:00
adrien fa5c5fb33b blink locked piece 2023-06-14 19:00:10 +02:00
adrien fd09458ee9 formatting 2023-06-14 18:15:28 +02:00
adrien ba313cbbd7 Backspace translation 2023-06-14 18:14:45 +02:00
adrien b358611c28 onblur = pausesettings after gameOver 2023-05-16 23:14:34 +02:00
adrien af35a5d6b2 remove duplicate pauseSettings() 2023-05-14 22:01:40 +02:00
adrien 3e13e77d26 restart 2023-05-14 21:22:23 +02:00
adrien 47bbc83788 fix T.tSpin 2023-05-10 09:05:24 +02:00
adrien dcaf1389eb refactoring 2023-05-09 15:13:46 +02:00
adrien bff9225bb0 refactoring 2023-05-09 09:21:16 +02:00
adrien e92a6cf5de refactoring 2023-05-09 09:16:18 +02:00
adrien 13539af376 css 2023-05-03 14:39:46 +02:00
adrien 7388f13e29 doctype 2023-05-02 21:12:38 +02:00
adrien 82b05a902d use bootstrap shadow 2023-05-02 20:36:25 +02:00
adrien e4a162728f smaller ghost glow 2023-05-02 20:35:32 +02:00
adrien e539a8e24d new classic ghost 2023-05-01 22:46:22 +02:00
adrien 06ac4650d5 fix S favicon 2023-04-30 02:09:02 +02:00
adrien f2c11ccbf2 mino colors 2023-04-30 02:04:21 +02:00
adrien fdbe8c5a73 S color 2023-04-30 01:52:45 +02:00
adrien ad0f57db33 S color 2023-04-30 01:51:45 +02:00
adrien 6fc00d50ab highscore 2023-04-29 22:14:38 +02:00
adrien 8f96053440 fix restart livel 2023-04-29 20:55:37 +02:00
adrien 77d68ece87 change combo name 2023-04-29 20:43:13 +02:00
adrien 9cf55ed479 drop animation more transparent 2023-04-29 18:23:49 +02:00
adrien 58915a7d69 dynamic favicon! 2023-04-29 16:00:42 +02:00
adrien a0a7075b6e tune common style 2023-04-29 12:30:19 +02:00
adrien 889351dfa7 settingsForm 2023-04-27 02:51:49 +02:00
adrien 44276b8d6a no plural 2023-04-27 02:35:33 +02:00
adrien bac13f1eca oninput 2023-04-26 13:43:04 +02:00
adrien d7d32e38d1 hour 2023-04-26 08:43:36 +02:00
adrien de9c72f760 settings arrangement 2023-04-26 00:06:07 +02:00
adrien c774cc25fc start center 2023-04-25 22:56:03 +02:00
adrien 6520a25cb9 save stylesheet 2023-04-25 16:51:09 +02:00
adrien 4587f5a147 col gap 2023-04-25 14:52:38 +02:00
adrien d535bd9c66 border small 2023-04-25 14:50:05 +02:00
adrien 3390df79f2 letter spacing 2023-04-25 14:22:05 +02:00
adrien 34c4ad95e1 high res retro 2023-04-25 14:20:57 +02:00
adrien b01f0a7c06 refactoring 2023-04-25 12:16:34 +02:00
adrien 40a5b59b8a stylesheet select 2023-04-25 11:27:30 +02:00
adrien f9e743f7ae screen 2023-04-25 11:27:15 +02:00
adrien 7b3db0b58d fix lines per minutes 2023-04-25 09:08:47 +02:00
adrien cc44b69b3b game over stats 2023-04-25 04:16:26 +02:00
adrien 649fdbc064 #statsTable border 0 2023-04-25 02:52:07 +02:00
adrien d1a9532842 meilleur 2023-04-25 02:49:03 +02:00
adrien 5ae2b7c4ed color 2023-04-25 02:42:22 +02:00
adrien d6ae8f7475 shadow 2023-04-25 02:41:26 +02:00
adrien cb86833e14 retro style 2023-04-25 02:34:54 +02:00
adrien 9baf03fa7e relative col 2023-04-25 00:50:30 +02:00
adrien a720f36de1 nbClearedLines++ 2023-04-24 23:53:11 +02:00
adrien 7f07d71f0b facing 2023-04-24 22:31:28 +02:00
adrien fb34ef72dc softDrop 2023-04-24 22:24:51 +02:00
adrien 0d5bbc77b6 pop! 2023-04-24 21:58:58 +02:00
adrien 063ccba7d7 fix line clear !? 2023-04-24 21:55:57 +02:00
adrien 44684fe459 refactoring 2023-04-24 21:26:37 +02:00
adrien a431a88d2f css rename 2023-04-24 21:26:31 +02:00
adrien e924ad101a simplify css 2023-04-24 20:45:41 +02:00
adrien 9b45c106cc add neon style 2023-04-24 20:37:39 +02:00
adrien 0f7e63d1c7 @supports (backdrop-filter: blur()) 2023-04-24 18:06:47 +02:00
adrien 4a377b6a7a controls input type text 2023-04-24 17:10:20 +02:00
adrien e0e46c4fa7 resume & prevent close 2023-04-24 14:11:20 +02:00
adrien 6498571656 validation 2023-04-24 12:35:15 +02:00
adrien ebb060e5a4 settingsForm.reportValidity 2023-04-24 09:22:34 +02:00
adrien 7407fd1138 reportValidity 2023-04-24 09:19:45 +02:00
adrien de3e54b9cd arr and das default values 2023-04-24 08:56:14 +02:00
adrien 2be279fa10 document.write(highScore) 2023-04-24 05:59:11 +02:00
adrien 9a3fb5bbcc terminology 2023-04-24 04:25:55 +02:00
adrien 82fb27036b marketing colors 2023-04-24 02:39:21 +02:00
adrien 40576ad1a0 buffer zone 2023-04-24 02:24:30 +02:00
adrien d73c0948d0 combo & b2b x1 2023-04-24 02:11:44 +02:00
adrien 1e8d6fb170 pause on one key press 2023-04-24 01:21:51 +02:00
adrien 89d0ef98f9 ticktack 2023-04-24 01:00:02 +02:00
adrien c4e269481d Fransösich 2023-04-24 00:34:17 +02:00
adrien 80a9b6f7a2 heldPiece 2023-04-23 22:24:59 +02:00
adrien e588d966c8 arrInput and dasInput as input type number 2023-04-23 21:13:04 +02:00
adrien 71a52925f4 B2B END message 2023-04-23 20:58:31 +02:00
adrien 33ba4ad0b9 hold 2023-04-23 03:40:11 +02:00
adrien 63df324e38 classier background 2023-04-23 01:45:09 +02:00
adrien c0dd5bdc66 css 2023-04-22 22:04:27 +02:00
63 changed files with 2552 additions and 1087 deletions
+5
View File
@@ -0,0 +1,5 @@
# Quatuor
Falling blocks
![screenshot](https://git.malingrey.fr/adrien/quatuor/raw/branch/master/thumbnail.png)
-789
View File
@@ -1,789 +0,0 @@
/* Contants */
const TRANSLATION = {
NONE: [ 0, 0],
LEFT: [-1, 0],
RIGHT: [ 1, 0],
DOWN: [ 0, 1],
}
const ROTATION = {
CW: 1, // ClockWise
CCW: -1, // CounterClockWise
}
const T_SPIN = {
NONE: "",
MINI: "MINI T-SPIN",
T_SPIN: "T-SPIN"
}
// score = SCORES[tSpin][nbClearedLines]
const SCORES = {
[T_SPIN.NONE]: [0, 100, 300, 500, 800],
[T_SPIN.MINI]: [100, 200],
[T_SPIN.T_SPIN]: [400, 800, 1200, 1600]
}
const CLEARED_LINES_NAMES = [
"",
"SINGLE",
"DOUBLE",
"TRIPLE",
"QUATRIS",
]
const DELAY = {
LOCK: 500,
FALL: 1000,
}
const ORIENTATION = {
NORTH: 0,
EAST: 1,
SOUTH: 2,
WEST: 3,
}
const KEY_NAMES = {
["ArrowLeft"]: "←",
["ArrowRight"]: "→",
["ArrowUp"]: "↑",
["ArrowDown"]: "↓",
[" "]: "Space",
["←"]: "ArrowLeft",
["→"]: "ArrowRight",
["↑"]: "ArrowUp",
["↓"]: "ArrowDown",
["Space"]: " ",
}
/* Customize Array to be use as coord */
Object.defineProperties(Array.prototype, {
"x": {
get: function() { return this[0] },
set: function(x) { this[0] = x }
},
"y": {
get: function() { return this[1] },
set: function(y) { this[1] = y }
}
})
Array.prototype.add = function(other) { return this.map((x, i) => x + other[i]) }
Array.prototype.mul = function(k) { return this.map(x => k * x) }
Array.prototype.translate = function(vector) { return this.map(pos => pos.add(vector)) }
Array.prototype.rotate = function(rotation) { return [-rotation*this.y, rotation*this.x] }
Array.prototype.pick = function() { return this.splice(Math.floor(Math.random()*this.length), 1)[0] }
HTMLElement.prototype.addNewChild = function(tag, properties) {
let child = document.createElement(tag)
for (key in properties) {
child[key] = properties[key]
}
this.appendChild(child)
}
/* Classes */
class Scheduler {
constructor() {
this.intervalTasks = new Map()
this.timeoutTasks = new Map()
}
setInterval(func, delay, ...args) {
this.intervalTasks.set(func, window.setInterval(func, delay, ...args))
}
setTimeout(func, delay, ...args) {
this.timeoutTasks.set(func, window.setTimeout(func, delay, ...args))
}
clearInterval(func) {
if (this.intervalTasks.has(func))
window.clearInterval(this.intervalTasks.get(func))
this.intervalTasks.delete(func)
}
clearTimeout(func) {
if (this.timeoutTasks.has(func))
window.clearTimeout(this.timeoutTasks.get(func))
this.timeoutTasks.delete(func)
}
}
class MinoesTable {
constructor(id) {
this.table = document.getElementById(id)
this.rows = this.table.rows.length
this.columns = this.table.rows[0].childElementCount
this._piece = null
}
get piece() {
return this._piece
}
set piece(piece) {
this._piece = piece
this._piece.center = Array.from(this.init_center)
this.redraw()
this.drawPiece()
}
drawMino(coord, className) {
this.table.rows[coord.y].cells[coord.x].className = className
}
drawPiece(piece=this.piece, className=piece.className + (piece.locked? " locked" : "")) {
piece.minoesCoord[piece.orientation]
.translate(piece.center)
.forEach(minoCoord => {
this.drawMino(minoCoord, className)
})
}
redraw() {
for (let y=0; y<this.rows; y++) {
for (let x=0; x<this.columns; x++) {
this.drawMino([x, y], "")
}
}
}
}
MinoesTable.prototype.init_center = [2, 2]
class NextQueue extends MinoesTable {
constructor(id) {
super(id)
this.pieces = this.init_centers.map(center => {
let piece = new Tetromino.pick()
piece.center = Array.from(center)
return piece
})
}
shift() {
let fistPiece = this.pieces.shift()
this.pieces.push(new Tetromino.pick())
this.pieces.forEach((piece, i) => {
piece.center = Array.from(this.init_centers[i])
})
this.redraw()
return fistPiece
}
redraw() {
super.redraw()
this.pieces.forEach((piece) => {
this.drawPiece(piece)
})
}
}
NextQueue.prototype.init_centers = [[2, 2], [2, 5], [2, 8], [2, 11], [2, 14]]
class PlayfieldMatrix extends MinoesTable {
constructor(id, piece_init_position) {
super(id, piece_init_position)
this.lockedMinoes = Array(this.rows).fill().map(() => Array(this.columns))
}
cellIsEmpty(coord) {
return 0 <= coord.x && coord.x < this.columns && 0 <= coord.y && coord.y < this.rows && !this.lockedMinoes[coord.y][coord.x]
}
get piece() {
return this._piece
}
set piece(piece) {
this._piece = piece
this._piece.center = Array.from(this.init_center)
this.ghost = piece.ghost
this.redraw()
this.drawPiece()
}
drawPiece(piece=this.piece, className=piece.className + (piece.locked? " locked" : "")) {
super.drawPiece(this.ghost, "")
this.ghost = piece.ghost
while (this.ghost.canMove(TRANSLATION.DOWN)) this.ghost.center.y++
super.drawPiece(this.ghost)
super.drawPiece(piece, className)
}
redraw() {
for (let y=0; y<this.rows; y++) {
for (let x=0; x<this.columns; x++) {
if (this.table.rows[y].cells[x].classList != "hard-drop-animation")
this.drawMino([x, y], this.lockedMinoes[y][x] || "")
}
}
}
}
PlayfieldMatrix.prototype.init_center = [5, 4]
class Tetromino {
static randomBag = []
static get pick() {
if (!this.randomBag.length) this.randomBag = [I, J, L, O, S, T, Z]
return this.randomBag.pick()
}
constructor(center, orientation=0, className=this.constructor.name + " mino") {
this.center = center
this.className = className
this.orientation = orientation
this.lastRotation = false
this.rotationPoint4Used = false
this.holdEnabled = true
this.locked = false
}
canMove(translation, rotation=ROTATION.NONE) {
let testCenter = this.center.add(translation)
let testOrientation = rotation? (this.orientation + rotation + 4) % 4: this.orientation
let testMinoesCoord = this.minoesCoord[testOrientation]
if (testMinoesCoord
.translate(testCenter)
.every(minoCoord => matrix.cellIsEmpty(minoCoord)))
return {center: testCenter, orientation: testOrientation}
else
return false
}
move(translation, rotation=ROTATION.NONE, clearClassName="") {
let success = this.canMove(translation, rotation)
if (success) {
scheduler.clearTimeout(lockDown)
matrix.drawPiece(this, clearClassName)
this.center = success.center
if (rotation) this.orientation = success.orientation
this.lastRotation = rotation
if (this.canMove(TRANSLATION.DOWN)) {
this.locked = false
} else {
this.locked = true
scheduler.setTimeout(lockDown, stats.lockDelay)
}
matrix.drawPiece()
return true
} else if (translation == TRANSLATION.DOWN) {
this.locked = true
if (!scheduler.timeoutTasks.has(lockDown))
scheduler.setTimeout(lockDown, stats.lockDelay)
matrix.drawPiece()
}
}
rotate(rotation) {
return this.srs[this.orientation][rotation].some((translation, rotationPoint) => {
if (this.move(translation, rotation)) {
if (rotationPoint == 4) this.rotationPoint4Used = true
return true
}
})
}
get ghost() {
return new this.constructor(Array.from(this.center), this.orientation, "ghost " + this.className)
}
}
// Super Rotation System
// freedom of movement = srs[piece.orientation][rotation]
Tetromino.prototype.srs = [
{ [ROTATION.CW]: [[0, 0], [-1, 0], [-1, -1], [0, 2], [-1, 2]], [ROTATION.CCW]: [[0, 0], [ 1, 0], [ 1, -1], [0, 2], [ 1, 2]] },
{ [ROTATION.CW]: [[0, 0], [ 1, 0], [ 1, 1], [0, -2], [ 1, -2]], [ROTATION.CCW]: [[0, 0], [ 1, 0], [ 1, 1], [0, -2], [ 1, -2]] },
{ [ROTATION.CW]: [[0, 0], [ 1, 0], [ 1, -1], [0, 2], [ 1, 2]], [ROTATION.CCW]: [[0, 0], [-1, 0], [-1, -1], [0, 2], [-1, 2]] },
{ [ROTATION.CW]: [[0, 0], [-1, 0], [-1, 1], [0, -2], [-1, -2]], [ROTATION.CCW]: [[0, 0], [-1, 0], [-1, 1], [0, -2], [-1, -2]] },
]
class I extends Tetromino {}
I.prototype.minoesCoord = [
[[-1, 0], [0, 0], [1, 0], [2, 0]],
[[1, -1], [1, 0], [1, 1], [1, 2]],
[[-1, 1], [0, 1], [1, 1], [2, 1]],
[[0, -1], [0, 0], [0, 1], [0, 2]],
]
I.prototype.srs = [
{ [ROTATION.CW]: [[0, 0], [-2, 0], [ 1, 0], [-2, 1], [ 1, -2]], [ROTATION.CCW]: [[0, 0], [-1, 0], [ 2, 0], [-1, -2], [ 2, 1]] },
{ [ROTATION.CW]: [[0, 0], [-1, 0], [ 2, 0], [-1, -2], [ 2, 1]], [ROTATION.CCW]: [[0, 0], [ 2, 0], [-1, 0], [ 2, -1], [-1, 2]] },
{ [ROTATION.CW]: [[0, 0], [ 2, 0], [-1, 0], [ 2, -1], [-1, 2]], [ROTATION.CCW]: [[0, 0], [ 1, 0], [-2, 0], [ 1, 2], [-2, -1]] },
{ [ROTATION.CW]: [[0, 0], [ 1, 0], [-2, 0], [ 1, 2], [-2, -1]], [ROTATION.CCW]: [[0, 0], [-2, 0], [ 1, 0], [-2, 1], [ 1, -2]] },
]
class J extends Tetromino {}
J.prototype.minoesCoord = [
[[-1, -1], [-1, 0], [0, 0], [1, 0]],
[[ 0, -1], [1, -1], [0, 0], [0, 1]],
[[ 1, 1], [-1, 0], [0, 0], [1, 0]],
[[ 0, -1], [-1, 1], [0, 0], [0, 1]],
]
class L extends Tetromino {}
L.prototype.minoesCoord = [
[[-1, 0], [0, 0], [1, 0], [ 1, -1]],
[[0, -1], [0, 0], [0, 1], [ 1, 1]],
[[-1, 0], [0, 0], [1, 0], [-1, 1]],
[[0, -1], [0, 0], [0, 1], [-1, -1]],
]
class O extends Tetromino {}
O.prototype.minoesCoord = [
[[0, 0], [1, 0], [0, -1], [1, -1]]
]
O.prototype.srs = [
{[ROTATION.CW]: [], [ROTATION.CCW]: []}
]
class S extends Tetromino {}
S.prototype.minoesCoord = [
[[-1, 0], [0, 0], [0, -1], [1, -1]],
[[ 0, -1], [0, 0], [1, 0], [1, 1]],
[[-1, 1], [0, 0], [1, 0], [0, 1]],
[[-1, -1], [0, 0], [-1, 0], [0, 1]],
]
class T extends Tetromino {}
T.prototype.minoesCoord = [
[[-1, 0], [0, 0], [1, 0], [0, -1]],
[[0, -1], [0, 0], [1, 0], [0, 1]],
[[-1, 0], [0, 0], [1, 0], [0, 1]],
[[0, -1], [0, 0], [0, 1], [-1, 0]],
]
T.prototype.tSlots = [
[[-1, -1], [ 1, -1], [ 1, 1], [-1, 1]],
[[ 1, -1], [ 1, 1], [-1, 1], [-1, -1]],
[[ 1, 1], [-1, 1], [-1, -1], [ 1, -1]],
[[-1, 1], [-1, -1], [ 1, -1], [ 1, 1]],
]
class Z extends Tetromino {}
Z.prototype.minoesCoord = [
[[-1, -1], [0, -1], [0, 0], [ 1, 0]],
[[ 1, -1], [1, 0], [0, 0], [ 0, 1]],
[[-1, 0], [0, 0], [0, 1], [ 1, 1]],
[[ 0, -1], [-1, 0], [0, 0], [-1, 1]]
]
class Settings {
constructor() {
for (let input of settingsForm.getElementsByTagName("input")) {
if (localStorage[input.name]) input.value = localStorage[input.name]
}
arrOutput.value = arrInput.value + " ms"
dasOutput.value = dasInput.value + " ms"
settingsForm.onsubmit = newGame
this.modal = new bootstrap.Modal('#settingsModal')
document.getElementById('settingsModal').addEventListener('shown.bs.modal', () => {
resumeButton.focus()
})
}
load() {
for (let input of keyBindFielset.getElementsByTagName("input")) {
this[input.name] = KEY_NAMES[input.value] || input.value
}
for (let input of autorepearFieldset.getElementsByTagName("input")) {
this[input.name] = input.valueAsNumber
}
this.keyBind = {}
for (let actionName in playerActions) {
this.keyBind[settings[actionName]] = playerActions[actionName]
}
}
}
function changeKey(input) {
prevValue = input.value
input.value = "Touche ?"
input.onkeydown = function (event) {
event.preventDefault()
input.value = KEY_NAMES[event.key] || event.key
input.blur()
}
input.onblur = function (event) {
if (input.value == "Touche ?") input.value = prevValue
input.onkeydown = null
input.onblur = null
}
}
class Stats {
constructor() {
this.highScore = Number(localStorage["highScore"]) || 0
this.combo = -1
this.b2b = -1
this._time = 0
}
set score(score) {
this._score = score
scoreCell.innerText = score.toLocaleString()
if (score > this.highScore) {
this.highScore = score
}
}
get score() {
return this._score
}
set highScore(highScore) {
this._highScore = highScore
highScoreCell.innerText = highScore.toLocaleString()
}
get highScore() {
return this._highScore
}
set level(level) {
this._level = level
this.goal += level * 5
if (level <= 20){
this.fallPeriod = 1000 * Math.pow(0.8 - ((level - 1) * 0.007), level - 1)
}
if (level > 15)
this.lockDelay = 500 * Math.pow(0.9, level - 15)
levelInput.value = level
levelCell.innerText = level
messagesSpan.addNewChild("div", { className: "show-level-animation", innerHTML: `<h1>LEVEL<br/>${this.level}</h1>` })
}
get level() {
return this._level
}
set goal(goal) {
this._goal = goal
goalCell.innerText = goal
}
get goal() {
return this._goal
}
set time(time) {
this._time = time - this._time
}
get time() {
return this.timeFormat.format(new Date() - this._time)
}
lockDown(nbClearedLines, tSpin) {
// Cleared lines & T-Spin
let patternScore = SCORES[tSpin][nbClearedLines] * this.level
if (tSpin) messagesSpan.addNewChild("div", {
className: "rotate-in-animation",
innerHTML: tSpin
})
if (nbClearedLines) messagesSpan.addNewChild("div", {
className: "zoom-in-animation",
innerHTML: CLEARED_LINES_NAMES[nbClearedLines]
})
if (patternScore) {
messagesSpan.addNewChild("div", {
className: "zoom-in-animation",
style: "animation-delay: .2s",
innerHTML: patternScore
})
this.score += patternScore
}
// Combo
if (nbClearedLines) {
this.combo++
if (this.combo >= 1) {
let comboScore = (nbClearedLines == 1 ? 20 : 50) * this.combo * this.level
messagesSpan.addNewChild("div", {
className: "zoom-in-animation",
style: "animation-delay: .4s",
innerHTML: `COMBO x${this.combo}<br/>${comboScore}`
})
this.score += comboScore
}
} else {
this.combo = -1
}
// Back to back sequence
if ((nbClearedLines == 4) || (tSpin && nbClearedLines)) {
this.b2b++
if (this.b2b >= 1) {
let b2bScore = patternScore / 2
messagesSpan.addNewChild("div", {
className: "zoom-in-animation",
style: "animation-delay: .4s",
innerHTML: `BACK TO BACK x${this.b2b}<br/>${b2bScore}`
})
this.score += b2bScore
}
} else if (nbClearedLines && !tSpin ) {
this.b2b = -1
}
this.goal -= nbClearedLines
if (this.goal <= 0) this.level++
}
}
Stats.prototype.timeFormat = new Intl.DateTimeFormat("fr-FR", {
minute: "2-digit",
second: "2-digit",
timeZone: "UTC"
})
/* Game */
onanimationend = function (event) {
event.target.classList.remove(event.animationName)
}
messagesSpan.onanimationend = function(event) {
event.target.remove()
}
let scheduler = new Scheduler()
let settings = new Settings()
let stats = new Stats()
let holdQueue = new MinoesTable("holdTable")
let matrix = new PlayfieldMatrix("matrixTable")
let nextQueue = new NextQueue("nextTable")
function pause() {
document.onkeydown = null
document.onkeyup = null
scheduler.clearInterval(fall)
scheduler.clearTimeout(lockDown)
scheduler.clearTimeout(repeat)
scheduler.clearInterval(autorepeat)
scheduler.clearInterval(clock)
stats.time = new Date()
resumeButton.disabled = false
settings.modal.show()
}
onblur = pause
pause()
function newGame(event) {
stats.lockDelay = DELAY.LOCK
resume(event)
levelInput.name = "level"
levelInput.disabled = true
resumeButton.innerHTML = "Reprendre"
event.target.onsubmit = resume
stats.score = 0
stats.goal = 0
stats.level = levelInput.valueAsNumber
localStorage["startLevel"] = levelInput.value
generate()
}
function resume(event) {
event.preventDefault()
settings.load()
document.onkeydown = onkeydown
document.onkeyup = onkeyup
stats.time = new Date()
scheduler.setInterval(clock, 1000)
if (stats.fallPeriod) scheduler.setInterval(fall, stats.fallPeriod)
}
function clock() {
timeCell.innerText = stats.time
}
function generate(piece=nextQueue.shift()) {
matrix.piece = piece
if (matrix.piece.canMove(TRANSLATION.NONE)) {
scheduler.setInterval(fall, stats.fallPeriod)
} else {
gameOver()
}
}
let playerActions = {
moveLeft: () => matrix.piece.move(TRANSLATION.LEFT),
moveRight: () => matrix.piece.move(TRANSLATION.RIGHT),
rotateClockwise: () => matrix.piece.rotate(ROTATION.CW),
rotateCounterclockwise: () => matrix.piece.rotate(ROTATION.CCW),
softDrop: function() {
if (matrix.piece.move(TRANSLATION.DOWN)) stats.score++
},
hardDrop: function() {
scheduler.clearTimeout(lockDown)
//matrix.table.classList.add("hard-dropped-table-animation")
while (matrix.piece.move(TRANSLATION.DOWN, ROTATION.NONE, "hard-drop-animation")) stats.score +=2
lockDown()
},
hold: function() {
if (matrix.piece.holdEnabled) {
scheduler.clearInterval(fall)
scheduler.clearTimeout(lockDown)
matrix.piece.holdEnabled = false
matrix.piece.locked = false
matrix.piece.orientation = ORIENTATION.NORTH
if (holdQueue.piece) {
let piece = holdQueue.piece
holdQueue.piece = matrix.piece
generate(piece)
} else {
holdQueue.piece = matrix.piece
generate()
}
}
},
pause: pause,
}
// Handle player inputs
const REPEATABLE_ACTIONS = [
playerActions.moveLeft,
playerActions.moveRight,
playerActions.softDrop
]
pressedKeys = new Set()
actionsQueue = []
function onkeydown(event) {
if (event.key in settings.keyBind) {
event.preventDefault()
if (!pressedKeys.has(event.key)) {
pressedKeys.add(event.key)
action = settings.keyBind[event.key]
action()
if (REPEATABLE_ACTIONS.includes(action)) {
actionsQueue.unshift(action)
scheduler.clearTimeout(repeat)
scheduler.clearInterval(autorepeat)
scheduler.setTimeout(repeat, settings.das)
}
}
}
}
function repeat() {
if (actionsQueue.length) {
actionsQueue[0]()
scheduler.setInterval(autorepeat, settings.arr)
}
}
function autorepeat() {
if (actionsQueue.length) {
actionsQueue[0]()
} else {
scheduler.clearInterval(autorepeat)
}
}
function onkeyup(event) {
if (event.key in settings.keyBind) {
event.preventDefault()
pressedKeys.delete(event.key)
action = settings.keyBind[event.key]
if (actionsQueue.includes(action)) {
actionsQueue.splice(actionsQueue.indexOf(action), 1)
if (!actionsQueue.length) {
scheduler.clearTimeout(repeat)
scheduler.clearInterval(autorepeat)
}
}
}
}
function fall() {
matrix.piece.move(TRANSLATION.DOWN)
}
function lockDown() {
scheduler.clearTimeout(lockDown)
scheduler.clearInterval(fall)
lockedMinoesCoord = matrix.piece.minoesCoord[matrix.piece.orientation]
.translate(matrix.piece.center)
if (lockedMinoesCoord.some(minoCoord => minoCoord.y >= 4)) {
lockedMinoesCoord.forEach(minoCoord => {
matrix.lockedMinoes[minoCoord.y][minoCoord.x] = matrix.piece.className
matrix.drawMino(minoCoord, matrix.piece.className)
})
// T-Spin
let tSpin = T_SPIN.NONE
if (matrix.piece.lastRotation && matrix.piece.constructor == T) {
let [a, b, c, d] = matrix.piece.tSlots[matrix.piece.orientation]
.translate(matrix.piece.center)
.map(minoCoord => !matrix.cellIsEmpty(minoCoord))
if (a && b && (c || d))
tSpin = T_SPIN.T_SPIN
else if (c && d && (a || b))
tSpin = matrix.piece.rotationPoint5Used ? T_SPIN.T_SPIN : T_SPIN.MINI
}
// Cleared lines
let clearedLines = Array.from(new Set(lockedMinoesCoord.map(minoCoord => minoCoord.y)))
.filter(y => matrix.lockedMinoes[y].filter(lockedMino => lockedMino).length == matrix.columns)
.sort()
for (y of clearedLines) {
matrix.lockedMinoes.splice(y, 1)
matrix.lockedMinoes.unshift(Array(matrix.columns))
matrix.table.rows[y].classList.add("cleared-line-animation")
}
matrix.redraw()
stats.lockDown(clearedLines.length, tSpin)
generate()
} else {
gameOver()
}
}
function gameOver() {
matrix.piece.locked = false
matrix.drawPiece()
document.onkeydown = null
document.onkeyup = null
scheduler.clearInterval(clock)
messagesSpan.onanimationend = null
messagesSpan.addNewChild("div", {
className: "game-over-animation",
style: "opacity: 100%",
innerHTML: "<h1>GAME<br/>OVER</h1>"
})
}
window.onbeforeunload = function(event) {
localStorage["highScore"] = stats.highScore
for (let input of settingsForm.getElementsByTagName("input")) {
localStorage[input.name] = input.value
}
}
if ('serviceWorker' in navigator) {
navigator.serviceWorker.register('service-worker.js');
}
-91
View File
@@ -1,91 +0,0 @@
.mino {
background: radial-gradient(
ellipse 140% 66% at 122% 88%,
var(--background-color) 100%,
var(--frontier-color) 105%,
var(--light-color) 130%
);
border: 4px solid;
padding: 0;
opacity: 100%;
border-radius: 2px;
}
.I.mino {
--background-color: #00d6fb;
--frontier-color: #43e7fd;
--light-color: #afeff9;
border-top-color: #7cf2fd;
border-left-color: #2ed5e5;
border-right-color: #00b8ca;
border-bottom-color: #00a4b0;
}
.J.mino {
--background-color: #2d00fa;
--frontier-color: #7054fb;
--light-color: #b8b4ff;
border-top-color: #4985fd;
border-left-color: #2f36ea;
border-right-color: #0006ca;
border-bottom-color: #00009d;
}
.L.mino {
--background-color: #ff6c13;
--frontier-color: #fe9551;
--light-color: #fdd0b7;
border-top-color: #fd9f6b;
border-left-color: #e76d28;
border-right-color: #e74f00;
border-bottom-color: #c54800;
}
.O.mino {
--background-color: #ffce12;
--frontier-color: #fce15c;
--light-color: #ffedac;;
border-top-color: #ffe364;
border-left-color: #e7ba23;
border-right-color: #e3a707;
border-bottom-color: #ca9501;
}
.T.mino {
--background-color: #ad00fa;
--frontier-color: #c541fc;
--light-color: #edb2ff;
border-top-color: #d380ff;
border-left-color: #b42deb;
border-right-color: #8000cd;
border-bottom-color: #6e019a;
}
.S.mino {
--background-color: #6e1;
--frontier-color: #93f85a;
--light-color: #93f85a;
border-top-color: #a4fc6d;
border-left-color: #5ee82b;
border-right-color: #35db00;
border-bottom-color: #1cbc02;
}
.Z.mino {
--background-color: #ff1945;
--frontier-color: #fe6483;
--light-color: #ffb8c5;
border-top-color: #fd718d;
border-left-color: #e62250;
border-right-color: #e20332;
border-bottom-color: #ad1936;
}
.locked.mino {
filter: saturate(50%) brightness(200%)
}
.ghost.mino {
opacity: 20%;
filter: brightness(200%)
}
+119
View File
@@ -0,0 +1,119 @@
.minoes-table {
filter:
drop-shadow(-1px -1px 0 white)
drop-shadow( 1px 1px 0 white);
}
.minoes-table tr {
z-index: calc(100 - var(--row));
position: sticky;
}
tr.matrix td:not(.mino) {
border-left: none;
border-bottom: none;
border-right: 1px solid #30303003;
border-top: 1px solid #30303003;
}
.mino {
--color: hsl(var(--hue), var(--saturation), 40%);
--light: hsl(var(--hue), calc(0.66 * var(--saturation)), 84%);
--top: hsl(var(--hue), calc(0.6 * var(--saturation)), 68%);
background-color: var(--color);
background-image:
radial-gradient(
ellipse 22% 8% at 25% 22%,
#ffffff66,
#ffffff33 40%,
transparent 70%
),
radial-gradient(
ellipse 140% 85% at 50% -15%,
var(--light) 0%,
#ffffff77 40%,
#00000005 55%
),
radial-gradient(
ellipse 120% 220% at 50% 140%,
var(--light) 0%,
var(--color) 55%,
#00000066 95%
);
border: 4px ridge var(--color);
border-top-color: var(--light);
border-radius: 3px;
box-shadow:
inset 2px 0 4px rgba(0,0,0,.06),
inset -2px 0 4px rgba(0,0,0,.12),
0 -4px 0 var(--top);
filter: saturate(1.1) contrast(1.05);
}
.I {
--hue: 193;
--saturation: 100%;
}
.J {
--hue: 215;
--saturation: 100%;
}
.L {
--hue: 25;
--saturation: 100%;
}
.O {
--hue: 42;
--saturation: 100%;
}
.S {
--hue: 95;
--saturation: 100%;
}
.T {
--hue: 300;
--saturation: 56%;
}
.Z {
--hue: 357;
--saturation: 84%;
}
.ghost {
border: 3px solid #fff1;
padding: 2px;
background-color: #fff1;
background-clip: content-box;
background-image: none;
box-shadow: none;
}
@keyframes trail-animation {
from {
background-color: #ceffff10;
}
to {
background-color: transparent;
}
}
@keyframes cleared-line-animation {
from {
background-color: #fff6;
filter: saturate(50%) brightness(300%);
box-shadow: 0 0 0 #adb5bd66, 0 0 0 #adb5bd66;
}
60% {
box-shadow: -60px 0 2px #adb5bd33, 60px 0 2px #adb5bd33;
}
to {
background-color: transparent;
box-shadow: -100px 0 5px transparent, 100px 0 5px transparent;
}
}
+125 -36
View File
@@ -1,18 +1,60 @@
:root {
--cell-side: 20px;
--cell-side: 25px;
--rX: -15;
--rY: 0;
--tZ: 25px;
}
.modal-content {
background-color: rgba(33, 37, 41, 30%);
backdrop-filter: blur(15px);
body {
background: linear-gradient(#212529, #14171a) fixed;
}
@supports (backdrop-filter: blur()) {
.modal::before {
content: "";
position: absolute;
top: 0;
bottom: 0;
left: 0;
right: 0;
backdrop-filter: blur(2px);
}
.modal-content {
background-color: #212529b3;
backdrop-filter: blur(10px);
}
}
.card {
background-color: rgb(37, 41, 45);
box-shadow: 5px 5px 20px #202020;
background-color: #25292d;
}
#matrixCard {
background-image: radial-gradient(#222, #25292d)
}
.card-header {
text-shadow: 0 0 2px black;
}
.modal-title {
text-shadow: 0 0 8px var(--bs-light);
font-weight: 600;
}
#statsTable td,
#statsModal td {
text-align: right;
}
td#timeCell {
min-width: 10ch;
}
.minoes-table {
--piece-column: 0;
--piece-row : 0;
table-layout: fixed;
border-collapse: separate;
border-spacing: 0;
@@ -20,77 +62,99 @@
}
#matrixTable {
margin-top: calc(-1 * var(--no-bordered-rows) * var(--cell-side));
margin-top: calc(-1 * var(--buffer-zone-rows) * var(--cell-side));
}
@keyframes hard-dropped-table-animation {
from {
transform: translateY(0);
}
25% {
transform: translateY(5px);
}
to {
transform: translateY(0);
transform: translateY(3px);
}
}
#matrixTable.hard-dropped-table-animation {
.hard-dropped-table-animation {
animation: hard-dropped-table-animation .2s;
}
tr.no-border td:not(.mino) {
tr.buffer-zone td:not(.mino) {
border-width: 0;
padding: 4px;
}
tr.border td:not(.mino) {
border: 1px solid #333;
padding: 3px;
tr.matrix td:not(.mino) {
border-left : 1px solid #333;
border-right : 1px solid #333;
border-top : 1px solid #303030;
border-bottom: 1px solid #303030;
}
td {
overflow: hidden;
width: calc(var(--cell-side) + 4px);
height: calc(var(--cell-side) + 4px);
width: var(--cell-side);
height: var(--cell-side);
box-sizing: border-box;
}
@keyframes hard-drop-animation {
@keyframes trail-animation {
from {
background-color: rgb(206, 255, 255, 40%);
filter: saturate(50%) brightness(300%);
background-color: #ceffff40;
}
to {
background-color: transparent;
}
}
td.hard-drop-animation {
animation: hard-drop-animation ease-out .3s;
@keyframes locked-animation {
from {
filter: saturate(50%) brightness(400%);
}
}
.locked.mino {
animation: locked-animation;
animation-duration: 0.2s;
}
td.trail-animation {
animation: trail-animation ease-out .3s;
}
@keyframes cleared-line-animation {
from {
background-color: rgb(206, 255, 255, 40%);
background-color: white;
filter: saturate(50%) brightness(300%);
box-shadow: -100px 0 5px white, 100px 0 5px white;
box-shadow: 0 0 0 #adb5bd, 0 0 0 #adb5bd;
}
60% {
box-shadow: -60px 0 2px #adb5bd66, 60px 0 2px #adb5bd66;
}
to {
background-color: transparent;
box-shadow: -100px 0 5px transparent, 100px 0 5px transparent;
}
}
tr.cleared-line-animation{
tr.cleared-line-animation {
animation: cleared-line-animation ease-out .3s;
}
span {
#holdTable .J,
#holdTable .L,
#holdTable .S,
#holdTable .T,
#holdTable .Z,
#nextTable .J,
#nextTable .L,
#nextTable .S,
#nextTable .T,
#nextTable .Z {
transform: translateX(50%);
}
#messagesSpan {
position: absolute;
top: 20%;
top: 5%;
left: 50%;
transform: translate(-50%, 0);
color: rgba(255, 255, 255, 0.8);
text-shadow: 1px 1px rgba(0, 0, 0, 0.8);
color: #fffc;
text-shadow: 1px 1px #000c;
font-size: 3vmin;
text-align: center;
}
@@ -198,4 +262,29 @@ span {
animation: game-over-animation;
animation-timing-function: (0.4, 0, 0.6, 1);
animation-duration: 2s;
}
}
#statsModal table {
border-collapse: collapse;
}
#statsModal th {
padding-left: 0;
padding-right: 0.5rem;
padding-bottom: 0.2rem;
border-left: 0.5rem solid transparent;
border-right: 0;
}
#statsModal td {
padding-left: 0.5rem;
padding-right: 0;
padding-bottom: 0.2rem;
border-left: 0;
border-right: 0.5rem solid transparent;
}
#statsModal tr:last-child th,
#statsModal tr:last-child td {
border-bottom: none;
}
-20
View File
@@ -1,20 +0,0 @@
.mino {
background-image: radial-gradient(
farthest-corner at 4px 6px,
rgba(204, 238, 247, 0.3) 0%,
rgba(106, 197, 220, 0.3) 100%
);
border: 1px solid rgba(127, 229, 255, 0.7);
border-radius: 0.3vmin;
}
.ghost.mino {
background: rgba(255, 255, 255, 0.2);
border: 1px solid rgba(255, 255, 255, 0.4);
border-radius: 0.3vmin;
}
.locked.mino {
background: rgba(242, 255, 255, 0.5);
border-color: rgba(242, 255, 255, 0.7);
}
+95
View File
@@ -0,0 +1,95 @@
body {
background-image: url("electro/bg.jpg");
background-size: cover;
}
body[data-bs-theme="dark"] {
--bs-body-bg: #2125296b;
}
.btn-dark {
--bs-btn-bg: #2125296b;
}
.card {
background-color: rgba(37, 41, 45, 40%);
}
tr.matrix td:not(.mino) {
border-left : none;
border-bottom: none;
}
.mino:not(.ghost):not(.locking):not(.disabled) {
padding: 1px;
position: relative;
z-index: 0;
border-radius: 4px;
background-color: rgba(128, 128, 128, 25%);
box-shadow: 0px 0px 8px rgba(128, 128, 128, 75%);
}
.mino:not(.ghost):not(.locking):before {
content: "";
position: absolute;
z-index: -1;
inset: 0;
margin: 1px 1px 0px 0px;
padding: 2px;
border-radius: 4px;
--glint-x: calc(50% + 50% * (var(--piece-column) - var(--column))/10);
--glint-y: calc(50% + 50% * (var(--piece-row) - var(--row))/25);
background: radial-gradient(
at var(--glint-x) var(--glint-y),
rgba(204, 238, 247, 0.9) 0%,
rgba(10, 159, 218, 0.9) 150%);
mask:
linear-gradient(#666 0 0) content-box,
linear-gradient(#fff 0 0);
mask-mode: luminance;
mask-composite: intersect;
}
.ghost.mino {
background: rgba(242, 255, 255, 10%);
border : 2px solid rgba(255, 255, 255, 0.3);
border-radius: 3px;
box-shadow: 0px 0px 10px rgba(242, 255, 255, 75%);
}
.moving.mino {
box-shadow: 0px 0px 5px rgba(128, 128, 128, 75%);
}
.moving.mino:not(.locking) {
padding: 2px;
background: rgba(186, 211, 255, 30%);
border: none;
border-radius: 4px;
}
.locking.mino {
border-width: 3px;
background: rgba(186, 211, 255, 70%);
border-color: rgba(242, 255, 255, 0.7);
border-radius: 4px;
box-shadow: 0px 0px 10px rgba(242, 255, 255, 100%);
}
.disabled.mino {
opacity: 60%;
position: relative;
}
.disabled.mino:before {
opacity: 50%;
box-shadow: none;
}
@keyframes locked-animation {
from {
opacity: 1;
background: white;
border-color: white;
}
}
Binary file not shown.

After

Width:  |  Height:  |  Size: 456 KiB

+125
View File
@@ -0,0 +1,125 @@
.card {
background-color: #363941;
}
.minoes-table {
display: flex;
flex-direction: column;
filter:
drop-shadow(-2px 0 0 white)
drop-shadow(2px 0 0 white)
drop-shadow(0 -2px 0 white)
drop-shadow(0 2px 0 white)
drop-shadow(5px 8px 0 rgba(9, 9, 9, 22%));
}
.minoes-table tr {
display: flex;
position: relative;
flex-direction: row;
z-index: calc(100 - var(--row));
}
tr.matrix td:not(.mino) {
border: 0;
}
.minoes-table td {
display: inline-block;
width: var(--cell-side);
height: var(--cell-side);
padding: 0 !important;
z-index: calc(200 - var(--row));
}
.mino {
background: var(--background-color);
width: inherit;
height: inherit;
display: block;
box-shadow: 0 -6px 0 var(--box-shadow-color);
}
.I.mino {
--background-color: #42AFE1;
--box-shadow-color: #6CEAFF;
}
.J.mino {
--background-color: #1165B5;
--box-shadow-color: #339BFF;
}
.L.mino {
--background-color: #F38927;
--box-shadow-color: #FFBA59;
}
.O.mino {
--background-color: #F6D03C;
--box-shadow-color: #FFFF7F;
}
.S.mino {
--background-color: #51B84D;
--box-shadow-color: #84F880;
}
.T.mino {
--background-color: #9739A2;
--box-shadow-color: #D958E9;
}
.Z.mino {
--background-color: #EB4F65;
--box-shadow-color: #FF7F79;
}
.ghost.mino {
opacity: 5%;
box-shadow: none;
transform: translateY(-6px);
}
.moving.mino {
filter: saturate(80%) brightness(110%);
}
.locking.mino {
--background-color: white;
--box-shadow-color: #DDD;
}
.locked.mino {
animation: locked-animation;
animation-duration: 0.2s;
}
.disabled.mino {
filter: brightness(50%) contrast(50%);
}
@keyframes locked-animation {
from {
filter: saturate(50%) brightness(300%);
}
}
@keyframes cleared-line-animation {
from {
background-color: #eeeeee;
}
to {
background-color: transparent;
}
}
@keyframes trail-animation {
from {
background-color: #ceffff10;
filter: saturate(50%) brightness(110%);
}
to {
background-color: transparent;
}
}
+148
View File
@@ -0,0 +1,148 @@
body {
--bs-gutter-x: 0;
background: black !important;
}
#screenRow {
gap: 0 !important;
margin: 0;
text-transform: uppercase;
letter-spacing: .1em;
}
#screenRow {
--bs-gutter-x: 0;
}
.card {
background: black;
border: none;
border-radius: 0;
margin-bottom: 0.5em !important;
}
.card-header,
.card-header th{
background: transparent;
font-weight: 400 !important;
font-size: 1.3em;
border: none;
}
#screenRow .table {
--bs-border-width: 0;
}
#holdTable {
margin: 0 0 0 auto !important;
}
#holdTable,
#nextTable {
border-bottom: 2px solid white;
}
#statsTable {
margin-right: 1.5em;
}
#statsTable tr {
display: flex;
flex-flow: column;
align-items: center;
}
#statsTable td,
#statsTable th {
margin: 0;
padding: 0 !important;
width: auto;
height: auto;
text-transform: uppercase;
}
#statsTable th {
display: inline;
flex-flow: row;
font-size: 0.8em;
text-align: center;
width: 200%;
}
#statsTable td {
font-size: 1.3em;
font-weight: 600;
color: white;
}
td#timeCell {
text-align: center;
}
#matrixCard {
background: transparent;
border-top: none;
border-left: 4px solid white;
border-right: 4px solid white;
border-bottom: 4px solid white;
}
.mino {
padding: 0;
opacity: 100%;
border-width: 1px;
border-style: solid;
}
.I.mino {
background-color: #42AFE1;
border-color: #6CEAFF;
}
.J.mino {
background-color: #1165B5;
border-color: #339BFF;
}
.L.mino {
background-color: #F38927;
border-color: #FFBA59;
}
.O.mino {
background-color: #F6D03C;
border-color: #FFFF7F;
}
.S.mino {
background-color: #32ee3e;
border-color: #84F880;
}
.T.mino {
background-color: #9739A2;
border-color: #D958E9;
}
.Z.mino {
background-color: #EB4F65;
border-color: #FF7F79;
}
.ghost.mino {
background-color: #fff4;
border-color: #fff8;
}
.moving.mino {
filter: saturate(80%) brightness(150%);
}
.locking.mino {
filter: saturate(50%) brightness(200%);
}
.disabled.mino {
filter: brightness(50%) contrast(80%);
opacity: 70%;
}
+153
View File
@@ -0,0 +1,153 @@
:root {
--cell-side: 20px;
}
@font-face {
font-family: "Early GameBoy";
src: url("retro/Early GameBoy.ttf");
}
#screenRow {
background-image: url("retro/bg.png");
background-size: 10px;
padding: 40px 20px;
border: 3px inset black;
border-radius: 10px;
}
.col {
gap: 8px;
}
.card {
background: #8D8E04;
border-radius: 0;
border-image-source: url("retro/border-sm.png");
border-image-slice: 25;
border-image-width: 13px;
border-image-repeat: repeat;
border-image-outset: 12px;
box-shadow: unset;
width: 100%;
}
.card-header {
background: #8D8E04;
}
#matrixCard {
background: #808302;
border-image-source: url("retro/border-lg.png");
border-image-slice: 30;
border-image-width: 15px;
border-image-outset: 15px;
}
#statsTable,
.card,
.card-header,
#messagesSpan {
font-family: "Early GameBoy", monospace;
font-smooth: never;
-webkit-font-smoothing: none;
color: #254806;
text-shadow: -1px -1px 3px rgba(0, 0, 0, 40%), 1px 1px 1px rgba(0, 0, 0, 40%);
}
#statsTable {
font-size: .7em;
letter-spacing: -.1em;
}
#statsTable tr {
display: flex;
flex-flow: column;
}
#statsTable th,
#statsTable td {
border: 0;
padding: 0 .2rem;
}
#statsTable td {
width: auto;
text-align: end;
}
#messagesSpan {
text-shadow: -2px -2px #808302, -2px 2px #808302, 2px -2px #808302, 2px 2px #808302;
}
.minoes-table td {
border: 0 !important;
}
.mino {
box-shadow: -2px -2px 5px rgba(0, 0, 0, 40%), 1px 1px 2px rgba(0, 0, 0, 40%);
background-size: 100%;
}
.I.mino {
background-image: url("retro/I-mino.png")
}
.J.mino {
background-image: url("retro/J-mino.png")
}
.L.mino {
background-image: url("retro/L-mino.png")
}
.O.mino {
background-image: url("retro/O-mino.png")
}
.S.mino {
background-image: url("retro/S-mino.png")
}
.T.mino {
background-image: url("retro/T-mino.png")
}
.Z.mino {
background-image: url("retro/Z-mino.png")
}
@keyframes blinker {
35% {
opacity: 50%;
}
}
.locking.mino {
animation: blinker 0.08s step-start infinite;
}
.ghost.mino,
.disabled.mino {
opacity: 50%;
}
.locked.mino {
animation: none;
}
.hard-dropped-table-animation {
animation: hard-dropped-table-animation steps(1) .2s;
}
@keyframes cleared-line-animation {
10%, 30%, 50%, 70%, 90% {
opacity: 0;
}
20%, 40%, 60%, 80% {
opacity: 100%;
}
}
tr.cleared-line-animation {
animation: cleared-line-animation ease-out .4s;
}
Binary file not shown.
Binary file not shown.

After

Width:  |  Height:  |  Size: 2.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.9 KiB

BIN
View File
Binary file not shown.

After

Width:  |  Height:  |  Size: 870 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 14 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 10 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 298 KiB

+370
View File
@@ -0,0 +1,370 @@
body {
background-image: url(stereo/bg.jpg),
radial-gradient(circle at center,
#39444f 0%,
#2c323b 25%,
#293036 28%,
#252b32 34%,
#242930 38%,
#1a1d22 52%,
#191c22 53%,
#151519 63%,
#141418 65%,
#0f0f12 74%,
#0a0c0d 100%);
background-repeat: space;
background-position: center;
background-size: cover;
}
#sceneDiv {
perspective: 500px;
cursor: grab;
}
#sceneDiv:active {
cursor: grabbing;
}
#sceneDiv * {
transform-style: preserve-3d;
}
#screenRow {
display: block;
transform: translateZ(var(--tZ)) rotateX(calc((var(--rX)) * 1deg)) rotateY(calc((var(--rY)) * 1deg));
}
#screenRow * {
display: block;
}
#screenRow .col {
display: inline-block !important;
width: max-content;
height: 100%;
vertical-align: top;
}
.card {
background: #36394180;
}
#matrixCard {
background-image: none;
}
#screenRow .card>* {
transform: translateZ(var(--cell-side));
}
#screenRow .card-header {
background-color: transparent;
border: none;
}
.card,
.card-header {
text-shadow:
calc(-0.3px * var(--rY)) calc(0.4px * var(--rX)) 5px #0008;
}
#holdTable .mino {
--row: 7;
--column: -5;
}
#nextTable .mino {
--row: 15;
--column: 15;
}
.minoes-table th,
.minoes-table td {
display: inline-block !important;
width: max-content;
}
.minoes-table tr {
width: max-content;
height: var(--cell-side);
}
#statsTable tr {
display: table;
width: 100%;
}
#statsTable th,
#statsTable td {
display: table-cell;
border: 0;
}
tr.matrix td:not(.mino) {
border: 0;
}
.minoes-table td {
width: var(--cell-side) !important;
height: var(--cell-side) !important;
overflow: visible;
}
.minoes-table .mino,
.minoes-table .mino::before,
.minoes-table .mino + :not(.mino)::before,
.minoes-table .mino::after {
--light-x: calc(-0.5 - var(--rY) / 30 - var(--column) / 10 + 1);
--light-y: calc(-0.5 + var(--rX) / 20 - var(--row) / 6 + 4);
--center-color: hsla(var(--h), var(--s), calc(var(--l) * var(--light) * 1.1), var(--a));
--edge-color: hsla(var(--h), var(--s), calc(var(--l) * (var(--light) * 0.9)), var(--a));
background: var(--center-color);
border-radius: 2px;
border: 2px outset var(--center-color);
}
.minoes-table .mino::before,
.minoes-table .mino + :not(.mino)::before,
.minoes-table .mino::after,
td.trail-animation::before,
td.trail-animation::after,
tr.cleared-line-animation td::before,
tr.cleared-line-animation td::after {
content: '';
position: absolute;
top: 0;
left: 0;
display: block;
width: var(--cell-side);
height: var(--cell-side);
}
/* Front face */
.minoes-table .mino,
td.trail-animation {
--light: calc(
1
+ (var(--light-y) * 0.3)
+ (var(--light-x) * 0.2)
);
--center-x: calc(35% + var(--light-x) * 10%);
--center-y: calc(35% + var(--light-y) * 10%);
background: radial-gradient(
circle at var(--center-x) var(--center-y),
var(--center-color) 15%,
var(--edge-color) 100%
);
box-shadow: 0 0 7px hsla(var(--h), var(--s), calc(var(--l) * var(--light) * 1.3), 20%);
}
/* Left face */
.minoes-table .mino::before,
td.trail-animation::before,
tr.cleared-line-animation td::before,
.left .minoes-table .mino + .mino::before {
--light: calc(
1.1
+ (var(--light-x) * -0.2)
+ (var(--light-y) * 0.15)
);
transform: translate3d(-1.5px, -1.5px, calc(-1 * var(--cell-side))) rotateY(-90deg);
transform-origin: left;
}
/* Right face */
.right .minoes-table .mino + .mino::before,
.minoes-table .mino + :not(.mino)::before,
.right td.trail-animation::before,
.right tr.cleared-line-animation td::before {
--light: calc(
0.85
+ (var(--light-x) * -0.2)
+ (var(--light-y) * -0.15)
);
--center-x: calc(65% + var(--light-x) * 10%);
--center-y: calc(35% + var(--light-y) * 10%);
filter: saturate(0.95);
transform: translate3d(0, 0, calc(-1 * var(--cell-side))) rotateY(-90deg);
transform-origin: left;
}
.right .minoes-table .mino:last-child::before {
transform: translate3d(-1.5px, -1.5px, calc(-1 * var(--cell-side))) rotateY(90deg) !important;
transform-origin: right !important;
}
/* Top face */
.minoes-table .mino::after,
td.trail-animation::after,
tr.cleared-line-animation td::after {
--light: calc(
1.5
+ (var(--light-y) * 0.2)
);
transform: translate3d(-1.5px, -1.5px, calc(-1 * var(--cell-side))) rotateX(90deg);
transform-origin: top;
}
/* Bottom face */
.bottom .minoes-table .mino::after,
.bottom td.trail-animation::after,
.bottom tr.cleared-line-animation td::after {
--light: calc(
1.1
+ (var(--light-y) * -0.3)
);
--center-x: calc(65% + var(--light-x) * 10%);
--center-y: calc(65% + var(--light-y) * 10%);
filter: saturate(0.95);
transform: translate3d(-1.5px, -1.5px, calc(-1 * var(--cell-side))) rotateX(-90deg);
transform-origin: bottom;
}
.J.mino, .J.mino + :not(.mino) { --h: 210deg; --s: 78%; --l: 52%; --a: 0.75; }
.L.mino, .L.mino + :not(.mino) { --h: 28deg; --s: 85%; --l: 52%; --a: 0.75; }
.O.mino, .O.mino + :not(.mino) { --h: 48deg; --s: 88%; --l: 52%; --a: 0.75; }
.I.mino, .I.mino + :not(.mino) { --h: 200deg; --s: 70%; --l: 52%; --a: 0.75; }
.S.mino, .S.mino + :not(.mino) { --h: 118deg; --s: 45%; --l: 52%; --a: 0.75; }
.T.mino, .T.mino + :not(.mino) { --h: 293deg; --s: 48%; --l: 52%; --a: 0.75; }
.Z.mino, .Z.mino + :not(.mino) { --h: 352deg; --s: 75%; --l: 52%; --a: 0.75; }
.ghost.mino, .ghost.mino + :not(.mino) { --h: 0deg; --s: 0%; --l: 55%; --a: 0.40; }
.locking.mino, .locking.mino + :not(.mino) { --h: 0deg; --s: 0%; --l: 92%; --a: 0.72; }
.disabled.mino, .disabled.mino + :not(.mino) { --h: 0deg; --s: 0%; --l: 45%; --a: 0.72; }
#holdTable .J + :not(.mino),
#holdTable .L + :not(.mino),
#holdTable .S + :not(.mino),
#holdTable .T + :not(.mino),
#holdTable .Z + :not(.mino),
#nextTable .J + :not(.mino),
#nextTable .L + :not(.mino),
#nextTable .S + :not(.mino),
#nextTable .T + :not(.mino),
#nextTable .Z + :not(.mino) {
transform: translateX(50%);
}
@keyframes trail-animation {
from {
background-color: hsla(180, 100%, 100%, 0.1);
border-color: hsla(180, 100%, 100%, 0.1);
}
to {
background-color: transparent;
border-color: transparent;
}
}
td.trail-animation::before,
td.trail-animation::after {
animation: trail-animation ease-out .3s;
}
@keyframes locked-animation {
from {
--h: 0deg; --s: 0%; --l: 100%; --a: 1;
box-shadow: 0 0 10px hsla(180, 100%, 100%, 0.2);
}
}
.locked.mino::before,
.locked.mino::after {
animation: locked-animation;
animation-duration: .2s;
}
@keyframes cleared-line-animation {
from {
background-color: white !important;
box-shadow: 0 0 0 white;
}
to {
background-color: #fff0;
box-shadow: 0 0 100px transparent;
}
}
tr.cleared-line-animation td::before,
tr.cleared-line-animation td::after {
animation: cleared-line-animation ease-out .3s;
}
@keyframes show-level-animation {
from {
opacity: 1;
transform: translateY(200%);
}
50% {
transform: translateY(0) scaleY(1);
line-height: var(--bs-body-line-height);
}
to {
opacity: 1;
transform: translateY(-100%) scaleY(0);
line-height: 0;
}
}
@keyframes zoom-in-animation {
from {
opacity: 1;
transform: scale3d(0.3, 0.3, 0.3);
line-height: var(--bs-body-line-height);
}
30% {
transform: scale3d(1, 1, 1);
}
80% {
transform: scale3d(1, 1, 1);
line-height: var(--bs-body-line-height);
}
to {
opacity: 1;
transform: scale3d(1.5, 0, 1);
line-height: 0;
}
}
@keyframes rotate-in-animation {
0% {
opacity: 1;
transform: rotate(200deg);
line-height: var(--bs-body-line-height);
}
30% {
transform: translateZ(0);
transform: scale3d(1, 1, 1);
}
80% {
transform: scale3d(1, 1, 1);
line-height: var(--bs-body-line-height);
}
to {
opacity: 1;
transform: scale3d(1.5, 0, 1);
line-height: 0;
}
}
@keyframes game-over-animation {
from {
opacity: 1;
transform: translateY(200%);
}
to {
opacity: 1;
transform: translateY(0) scaleY(1);
line-height: var(--bs-body-line-height);
}
}
BIN
View File
Binary file not shown.

After

Width:  |  Height:  |  Size: 62 KiB

+76
View File
@@ -0,0 +1,76 @@
body {
background-image: url(synthwave/bg.png);
background-size: cover;
}
body[data-bs-theme="dark"] {
--bs-body-bg: #2125296b;
}
.btn-dark {
--bs-btn-bg: #2125296b;
}
.card {
background: #25292d66;
}
#matrixCard {
background-image: radial-gradient(#2226, #25292d66);
}
.card,
#matrixCard {
background: repeating-linear-gradient(transparent, #111 1px);
backdrop-filter: blur(3px);
}
.minoes-table {
background: transparent;
}
.mino {
background: var(--color);
border: 3px solid var(--border);
box-shadow: 0 0 8px var(--border);
}
.I {
--color: #00eaf866;
--border: #00eaf5;
}
.J {
--color: #00a9f766;
--border: #00a9f7;
}
.L {
--color: #f9b60066;
--border: #f9b600;
}
.O {
--color: #e3e04966;
--border: #e3e049;
}
.S {
--color: #7bd59e66;
--border: #7bd59e;
}
.T {
--color: #d136e266;
--border: #d136e2;
}
.Z {
--color: #E67D8666;
--border: #E67D86;
}
.ghost {
--color: #fff4;
--border: #fff5;
}
Binary file not shown.

After

Width:  |  Height:  |  Size: 444 KiB

Before

Width:  |  Height:  |  Size: 15 KiB

After

Width:  |  Height:  |  Size: 15 KiB

BIN
View File
Binary file not shown.

After

Width:  |  Height:  |  Size: 2.9 KiB

BIN
View File
Binary file not shown.

After

Width:  |  Height:  |  Size: 2.9 KiB

BIN
View File
Binary file not shown.

After

Width:  |  Height:  |  Size: 2.9 KiB

BIN
View File
Binary file not shown.

After

Width:  |  Height:  |  Size: 2.9 KiB

BIN
View File
Binary file not shown.

After

Width:  |  Height:  |  Size: 3.0 KiB

BIN
View File
Binary file not shown.

After

Width:  |  Height:  |  Size: 3.0 KiB

BIN
View File
Binary file not shown.

After

Width:  |  Height:  |  Size: 3.0 KiB

BIN
View File
Binary file not shown.

After

Width:  |  Height:  |  Size: 2.9 KiB

BIN
View File
Binary file not shown.

After

Width:  |  Height:  |  Size: 3.0 KiB

BIN
View File
Binary file not shown.

After

Width:  |  Height:  |  Size: 3.0 KiB

BIN
View File
Binary file not shown.

After

Width:  |  Height:  |  Size: 3.0 KiB

BIN
View File
Binary file not shown.

After

Width:  |  Height:  |  Size: 3.0 KiB

BIN
View File
Binary file not shown.

After

Width:  |  Height:  |  Size: 2.9 KiB

BIN
View File
Binary file not shown.

After

Width:  |  Height:  |  Size: 3.0 KiB

BIN
View File
Binary file not shown.

After

Width:  |  Height:  |  Size: 3.0 KiB

BIN
View File
Binary file not shown.

After

Width:  |  Height:  |  Size: 3.0 KiB

BIN
View File
Binary file not shown.

After

Width:  |  Height:  |  Size: 3.0 KiB

BIN
View File
Binary file not shown.

After

Width:  |  Height:  |  Size: 3.0 KiB

BIN
View File
Binary file not shown.

After

Width:  |  Height:  |  Size: 3.0 KiB

BIN
View File
Binary file not shown.

After

Width:  |  Height:  |  Size: 3.0 KiB

BIN
View File
Binary file not shown.

After

Width:  |  Height:  |  Size: 3.0 KiB

BIN
View File
Binary file not shown.

After

Width:  |  Height:  |  Size: 3.0 KiB

BIN
View File
Binary file not shown.

After

Width:  |  Height:  |  Size: 3.0 KiB

BIN
View File
Binary file not shown.

After

Width:  |  Height:  |  Size: 3.0 KiB

BIN
View File
Binary file not shown.

After

Width:  |  Height:  |  Size: 3.0 KiB

+196 -150
View File
@@ -1,100 +1,99 @@
<!doctype html>
<html lang="fr">
<head>
<meta charset="utf-8" />
<title>QUATRIS</title>
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>Quatuor</title>
<meta name="viewport" content="width=device-width, initial-scale=1">
<meta name="color-scheme" content="dark">
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0-alpha3/dist/css/bootstrap.min.css" rel="stylesheet" integrity="sha384-KK94CHFLLe+nY2dmCWGMq91rCGa5gtU4mk92HdvYe+M/SXH301p5ILy+dN9+nJOZ" crossorigin="anonymous">
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0-alpha3/dist/css/bootstrap.min.css" integrity="sha384-KK94CHFLLe+nY2dmCWGMq91rCGa5gtU4mk92HdvYe+M/SXH301p5ILy+dN9+nJOZ" crossorigin="anonymous">
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bootstrap-icons@1.10.4/font/bootstrap-icons.css">
<link href="css/common.css" rel="stylesheet">
<link href="css/99.css" rel="stylesheet" title="99">
<link href="css/effect.css" rel="alternate stylesheet" title="Effect">
<link rel="apple-touch-icon" sizes="180x180" href="favicons/apple-touch-icon.png">
<link rel="icon" type="image/png" sizes="32x32" href="favicons/favicon-32x32.png">
<link rel="icon" type="image/png" sizes="16x16" href="favicons/favicon-16x16.png">
<link rel="stylesheet" href="css/common.css">
<link rel="stylesheet" href="css/classic.css" title="Thème sélectionné" id="selectedStyleSheet">
<link rel="alternate stylesheet" href="css/classic.css" title="Classique">
<link rel="alternate stylesheet" href="css/minimal.css" title="Minimal">
<link rel="alternate stylesheet" href="css/electro.css" title="Électro">
<link rel="alternate stylesheet" href="css/synthwave.css" title="Synthwave">
<link rel="alternate stylesheet" href="css/retro.css" title="Rétro">
<link rel="alternate stylesheet" href="css/opera.css" title="Opéra">
<link rel="alternate stylesheet" href="css/stereo.css" title="Stéréo">
<link rel="apple-touch-icon" sizes="180x180" href="favicons/apple-touch-icon.png">
<link rel="icon" type="image/png" sizes="32x32" href="favicons/T-2.png">
<link rel="icon" type="image/png" sizes="16x16" href="favicons/favicon-16x16.png">
<link rel="manifest" href="manifest.json">
<meta property="og:title" content="Quatuor"/>
<meta property="og:type" content="game"/>
<meta property="og:url" content="https://adrien.malingrey.fr/jeux/quatuor/"/>
<meta property="og:image" content="https://adrien.malingrey.fr/jeux/quatuor/thumbnail.png"/>
<meta property="og:image:width" content="288"/>
<meta property="og:image:height" content="288"/>
<meta property="og:description" content="Un jeu avec un quatuor de blocs qui tombent."/>
<meta property="og:locale" content="fr_FR"/>
<meta property="og:site_name" content="adrien.malingrey.fr"/>
</head>
<body data-bs-theme="dark">
<div class="modal fade" id="settingsModal" data-bs-backdrop="static" data-bs-keyboard="false">
<div class="modal-dialog modal-dialog-centered">
<div class="modal-content">
<div class="modal-header">
<h1 class="modal-title w-100 text-center">QUATRIS</h1>
<h1 id="titleHeader" class="modal-title w-100 text-center">QUATUOR</h1>
</div>
<div class="modal-body">
<form name="settingsForm">
<fieldset id="keyBindFielset" class="row g-2 mb-3 align-items-center text-center">
<legend class="text-start">Commandes</legend>
<label for="moveLeftInput" title="Gauche" class="col-sm-3 col-form-label d-flex align-items-center justify-content-center">
<i class="bi bi-arrow-left"></i>
</label>
<div class="col-sm-3">
<input name="moveLeft" id="moveLeftInput" type="button" class="form-control text-center" value="" onclick="changeKey(this)">
</div>
<div class="col-sm-3">
<input name="moveRight" id="moveRightInput" type="button" class="form-control text-center" value="→" onclick="changeKey(this)">
</div>
<label for="moveRightInput" title="Droite" class="col-sm-3 col-form-label d-flex align-items-center justify-content-center">
<i class="bi bi-arrow-right"></i>
</label>
<label for="rotateCounterclockwiseInput" title="Rotation anti-horaire" class="col-sm-3 col-form-label d-flex align-items-center justify-content-center">
<i class="bi bi-arrow-counterclockwise"></i>
</label>
<div class="col-sm-3">
<input name="rotateCounterclockwise" id="rotateCounterclockwiseInput" type="button" class="form-control text-center" value="w" onclick="changeKey(this)">
</div>
<div class="col-sm-3">
<input name="rotateClockwise" id="rotateClockwiseInput" type="button" class="form-control text-center" value="↑" onclick="changeKey(this)">
</div>
<label for="rotateClockwiseInput" title="Rotation horaire" class="col-sm-3 col-form-label d-flex align-items-center justify-content-center">
<i class="bi bi-arrow-clockwise"></i>
</label>
<label for="softDropInput" title="Chute lente" class="col-sm-3 col-form-label d-flex align-items-center justify-content-center">
<i class="bi bi-arrow-down-short"></i>
</label>
<div class="col-sm-3">
<input name="softDrop" id="softDropInput" type="button" class="form-control text-center" value="↓" onclick="changeKey(this)">
</div>
<div class="col-sm-3">
<input name="hardDrop" id="hardDropInput" type="button" class="form-control text-center" value="Space" onclick="changeKey(this)">
</div>
<label for="hardDropInput" title="Chute rapide" class="col-sm-3 col-form-label d-flex align-items-center justify-content-center">
<i class="bi bi-download"></i>
</label>
<label for="holdInput" title="Échanger la pièce" class="col-sm-3 col-form-label d-flex align-items-center justify-content-center">
<i class="bi bi-arrow-left-right"></i>
</label>
<div class="col-sm-3">
<input name="hold" id="holdInput" type="button" class="form-control text-center" value="c" onclick="changeKey(this)">
</div>
<div class="col-sm-3">
<input name="pause" id="pauseInput" type="button" class="form-control text-center" value="Escape" onclick="changeKey(this)">
</div>
<label for="pauseInput" title="Pause" class="col-sm-3 col-form-label d-flex align-items-center justify-content-center">
<i class="bi bi-pause"></i>
</label>
<form name="settingsForm" class="needs-validation" novalidate>
<fieldset id="keyBindFielset" class="row g-2 mb-3 align-items-center text-center"><legend class="text-start">Commandes</legend>
<label for="moveLeftInput" title="Gauche" class="col-2 col-form-label d-flex align-items-center justify-content-center"><i class="bi bi-arrow-left"></i></label>
<div class="col-4"><input name="moveLeft" id="moveLeftInput" type="text" class="form-control text-center" value="←" onfocus="changeKey(this)" placeholder="Touche ?" title="Modifier la touche" required></div>
<div class="col-4"><input name="moveRight" id="moveRightInput" type="text" class="form-control text-center" value="→" onfocus="changeKey(this)" placeholder="Touche ?" title="Modifier la touche" required></div>
<label for="moveRightInput" title="Droite" class="col-2 col-form-label d-flex align-items-center justify-content-center"><i class="bi bi-arrow-right"></i></label>
<label for="rotateCounterclockwiseInput" title="Rotation anti-horaire" class="col-2 col-form-label d-flex align-items-center justify-content-center"><i class="bi bi-arrow-counterclockwise"></i></label>
<div class="col-4"><input name="rotateCounterclockwise" id="rotateCounterclockwiseInput" type="text" class="form-control text-center" value="w" onfocus="changeKey(this)" placeholder="Touche ?" title="Modifier la touche" required></div>
<div class="col-4"><input name="rotateClockwise" id="rotateClockwiseInput" type="text" class="form-control text-center" value="↑" onfocus="changeKey(this)" placeholder="Touche ?" title="Modifier la touche" required></div>
<label for="rotateClockwiseInput" title="Rotation horaire" class="col-2 col-form-label d-flex align-items-center justify-content-center"><i class="bi bi-arrow-clockwise"></i></label>
<label for="softDropInput" title="Chute lente" class="col-2 col-form-label d-flex align-items-center justify-content-center"><i class="bi bi-arrow-down-short"></i></label>
<div class="col-4"><input name="softDrop" id="softDropInput" type="text" class="form-control text-center" value="↓" onfocus="changeKey(this)" placeholder="Touche ?" title="Modifier la touche" required></div>
<div class="col-4"><input name="hardDrop" id="hardDropInput" type="text" class="form-control text-center" value="Espace" onfocus="changeKey(this)" placeholder="Touche ?" title="Modifier la touche" required></div>
<label for="hardDropInput" title="Chute rapide" class="col-2 col-form-label d-flex align-items-center justify-content-center"><i class="bi bi-download"></i></label>
<label for="holdInput" title="Échanger la pièce" class="col-2 col-form-label d-flex align-items-center justify-content-center"><i class="bi bi-arrow-left-right"></i></label>
<div class="col-4"><input name="hold" id="holdInput" type="text" class="form-control text-center" value="c" onfocus="changeKey(this)" placeholder="Touche ?" title="Modifier la touche" required></div>
<div class="col-4"><input name="pause" id="pauseInput" type="text" class="form-control text-center" value="Échap." onfocus="changeKey(this)" placeholder="Touche ?" title="Modifier la touche" required></div>
<label for="pauseInput" title="Pause" class="col-2 col-form-label d-flex align-items-center justify-content-center"><i class="bi bi-pause"></i></label>
</fieldset>
<fieldset id="autorepearFieldset" class="row g-2 mb-3 align-items-center text-center">
<legend class="text-start">Répétition automatique</legend>
<label for="arrInput" class="col-sm-3 col-form-label" title="Automatic Repeat Rate : période de répétition de l'action">ARR</label>
<div class="col-sm-3">
<output id="arrOutput" class="form-text form-text-sm w-100 m-auto text-center" for="arrInput">33 ms</output>
<input name="arr" id="arrInput" type="range" class="form-range h-100" value="33" min="2" max="200" step="1" oninput="arrOutput.value = value + ' ms'">
</div>
<div class="col-sm-3">
<output id="dasOutput" class="form-text form-text-sm w-100 m-auto text-center" for="dasInput">180 ms</output>
<input name="das" id="dasInput" type="range" class="form-range h-100" value="180" min="100" max="500" step="5" oninput="dasOutput.value = value + ' ms'">
</div>
<label for="dasInput" class="col-sm-3 col-form-label" title="Delayed AutoShift : délai initial avant répétition">DAS</label>
<fieldset id="autorepeatFieldset" class="row g-2 mb-3 align-items-center text-center"><!--<legend class="text-start">Répétition automatique</legend>-->
<label for="arrInput" class="col-2 col-form-label"><abbr title="Automatic Repeat Rate : période de répétition de l'action">ARR</abbr></label>
<div class="col-4"><div class="input-group"><input name="arr" id="arrInput" type="number" class="form-control text-center" value="50" min="2" max="200" step="1"><div class="input-group-text">ms</div></div></div>
<div class="col-4"><div class="input-group"><input name="das" id="dasInput" type="number" class="form-control text-center" value="300" min="100" max="500" step="5"><div class="input-group-text">ms</div></div></div>
<label for="dasInput" class="col-2 col-form-label"><abbr title="Delayed AutoShift : délai initial avant répétition">DAS</abbr></label>
</fieldset>
<fieldset class="row g-2 mb-3 align-items-center text-center">
<legend class="text-start">Partie</legend>
<label for="levelInput" class="col-sm-3 col-form-label text-center">Niveau</label>
<div class="col-sm-3">
<input name="startLevel" id="levelInput" type="number" class="form-control" value="1" min="1" max="15">
<fieldset class="row g-2 mb-3 align-items-center text-center"><legend class="text-start">Interface</legend>
<label for="stylesheetSelect" class="col-2 col-form-label">Thème</label>
<div class="col-4"><select name="stylesheet" id="stylesheetSelect" class="form-select" oninput="selectedStyleSheet.href = this.value">
<option value="css/classic.css" selected>Classique</option>
<option value="css/minimal.css">Minimal</option>
<option value="css/synthwave.css">Synthwave</option>
<option value="css/electro.css">Électro</option>
<option value="css/retro.css">Rétro</option>
<option value="css/opera.css">Opéra</option>
<option value="css/stereo.css">Stéréo</option>
</select></div>
<div class="col-4">
<div class="form-check form-switch text-start">
<input id="fullscreenCheckbox" type="checkbox" role="switch" class="form-check-input" tabindex="0">
<label for="fullscreenCheckbox" class="form-check-label">Plein écran</label>
</div>
</div>
<div class="col-sm-3">
<button id="resumeButton" type="submit" data-bs-dismiss="modal" class="btn btn-primary form-control" autofocus>Jouer</button>
<div class="col-2"></div>
<label for="sfxVolumeRange" class="col-2 col-form-label">Volume</label>
<div class="col-4 d-flex align-items-baseline"><input name="sfxVolumeRange" id="sfxVolumeRange" class="form-range" type="range" min="0" max="1" step="any" value="0.7"></div>
</fieldset>
<fieldset class="row g-2 mb-3 align-items-center text-center"><legend class="text-start">Partie</legend>
<label for="levelInput" class="col-2 col-form-label text-center">Niveau</label>
<div class="col-4">
<input name="startLevel" id="levelInput" type="number" class="form-control text-center" value="1" min="1" max="15">
</div>
<div class="col-4">
<button id="resumeButton" type="submit" class="btn btn-primary w-100" autofocus>Jouer</button>
</div>
</fieldset>
</form>
@@ -102,96 +101,143 @@
</div>
</div>
</div>
<div class="container-fluid d-flex h-100 justify-content-center d-flex align-items-center">
<div class="row row-cols-auto align-items-start gap-2">
<div id="sceneDiv" class="container-fluid d-flex vh-100 justify-content-center d-flex align-items-center">
<div id="screenRow" class="row row-cols-auto align-items-start gap-2">
<div class="col d-flex flex-column align-items-end">
<div class="card mb-4">
<div class="card-header text-center"><strong>HOLD</strong></div>
<div class="card shadow mb-4 w-100">
<div class="card-header fw-bold text-uppercase text-center">Hold</div>
<div class="card-body p-0">
<table id="holdTable" class="minoes-table">
<tr class="no-border"><td></td><td></td><td></td><td></td><td></td><td></td></tr>
<tr class="no-border"><td></td><td></td><td></td><td></td><td></td><td></td></tr>
<tr class="no-border"><td></td><td></td><td></td><td></td><td></td><td></td></tr>
<tr class="no-border"><td></td><td></td><td></td><td></td><td></td><td></td></tr>
<tr class="no-border"><td></td><td></td><td></td><td></td><td></td><td></td></tr>
<table id="holdTable" class="minoes-table m-auto">
<tr class="buffer-zone"><td></td><td></td><td></td><td></td><td></td><td></td></tr>
<tr class="buffer-zone"><td></td><td></td><td></td><td></td><td></td><td></td></tr>
<tr class="buffer-zone"><td></td><td></td><td></td><td></td><td></td><td></td></tr>
<tr class="buffer-zone"><td></td><td></td><td></td><td></td><td></td><td></td></tr>
<tr class="buffer-zone"><td></td><td></td><td></td><td></td><td></td><td></td></tr>
</table>
</div>
</div>
<div class="card w-100">
<table id="statsTable" class="table mb-0 align-middle">
<tr><td>Score</td><th id="scoreCell" class="text-end">0</th></tr>
<tr><td>Meilleur score</td><th id="highScoreCell" class="text-end">0</th></tr>
<tr><td>Niveau</td><th id="levelCell" class="text-end">0</th></tr>
<tr><td>But</td><th id="goalCell" class="text-end">0</th></tr>
<tr><td>Temps</td><th id="timeCell" class="text-end">00:00</th></tr>
<div class="card shadow">
<table id="statsTable" class="table mb-0">
<tr class="card-header fw-bold text-uppercase"><th>Score</th><td id="scoreCell">0</td> </tr>
<tr><th>Meilleur<br/>score</th><td id="highScoreCell"><script>document.write(Number(localStorage["highScore"]) || 0)</script></td></tr>
<tr><th>Niveau</th> <td id="levelCell">0</td> </tr>
<tr><th>But</th> <td id="goalCell">0</td> </tr>
<tr><th>Temps</th> <td id="timeCell">00:00:00</td> </tr>
</table>
</div>
</div>
<div class="col">
<div class="card">
<table id="matrixTable" class="minoes-table" style="--no-bordered-rows: 6">
<tr class="no-border"><td></td><td></td><td></td><td></td><td></td><td></td><td></td><td></td><td></td><td></td></tr>
<tr class="no-border"><td></td><td></td><td></td><td></td><td></td><td></td><td></td><td></td><td></td><td></td></tr>
<tr class="no-border"><td></td><td></td><td></td><td></td><td></td><td></td><td></td><td></td><td></td><td></td></tr>
<tr class="no-border"><td></td><td></td><td></td><td></td><td></td><td></td><td></td><td></td><td></td><td></td></tr>
<tr class="no-border"><td></td><td></td><td></td><td></td><td></td><td></td><td></td><td></td><td></td><td></td></tr>
<tr class="border"><td></td><td></td><td></td><td></td><td></td><td></td><td></td><td></td><td></td><td></td></tr>
<tr class="border"><td></td><td></td><td></td><td></td><td></td><td></td><td></td><td></td><td></td><td></td></tr>
<tr class="border"><td></td><td></td><td></td><td></td><td></td><td></td><td></td><td></td><td></td><td></td></tr>
<tr class="border"><td></td><td></td><td></td><td></td><td></td><td></td><td></td><td></td><td></td><td></td></tr>
<tr class="border"><td></td><td></td><td></td><td></td><td></td><td></td><td></td><td></td><td></td><td></td></tr>
<tr class="border"><td></td><td></td><td></td><td></td><td></td><td></td><td></td><td></td><td></td><td></td></tr>
<tr class="border"><td></td><td></td><td></td><td></td><td></td><td></td><td></td><td></td><td></td><td></td></tr>
<tr class="border"><td></td><td></td><td></td><td></td><td></td><td></td><td></td><td></td><td></td><td></td></tr>
<tr class="border"><td></td><td></td><td></td><td></td><td></td><td></td><td></td><td></td><td></td><td></td></tr>
<tr class="border"><td></td><td></td><td></td><td></td><td></td><td></td><td></td><td></td><td></td><td></td></tr>
<tr class="border"><td></td><td></td><td></td><td></td><td></td><td></td><td></td><td></td><td></td><td></td></tr>
<tr class="border"><td></td><td></td><td></td><td></td><td></td><td></td><td></td><td></td><td></td><td></td></tr>
<tr class="border"><td></td><td></td><td></td><td></td><td></td><td></td><td></td><td></td><td></td><td></td></tr>
<tr class="border"><td></td><td></td><td></td><td></td><td></td><td></td><td></td><td></td><td></td><td></td></tr>
<tr class="border"><td></td><td></td><td></td><td></td><td></td><td></td><td></td><td></td><td></td><td></td></tr>
<tr class="border"><td></td><td></td><td></td><td></td><td></td><td></td><td></td><td></td><td></td><td></td></tr>
<tr class="border"><td></td><td></td><td></td><td></td><td></td><td></td><td></td><td></td><td></td><td></td></tr>
<tr class="border"><td></td><td></td><td></td><td></td><td></td><td></td><td></td><td></td><td></td><td></td></tr>
<tr class="border"><td></td><td></td><td></td><td></td><td></td><td></td><td></td><td></td><td></td><td></td></tr>
<tr class="border"><td></td><td></td><td></td><td></td><td></td><td></td><td></td><td></td><td></td><td></td></tr>
<div class="col position-relative">
<div id="matrixCard" class="card shadow">
<table id="matrixTable" class="minoes-table" style="--buffer-zone-rows: 5">
<tr class="buffer-zone"><td></td><td></td><td></td><td></td><td></td><td></td><td></td><td></td><td></td><td></td></tr>
<tr class="buffer-zone"><td></td><td></td><td></td><td></td><td></td><td></td><td></td><td></td><td></td><td></td></tr>
<tr class="buffer-zone"><td></td><td></td><td></td><td></td><td></td><td></td><td></td><td></td><td></td><td></td></tr>
<tr class="buffer-zone"><td></td><td></td><td></td><td></td><td></td><td></td><td></td><td></td><td></td><td></td></tr>
<tr class="buffer-zone"><td></td><td></td><td></td><td></td><td></td><td></td><td></td><td></td><td></td><td></td></tr>
<tr class="matrix"><td></td><td></td><td></td><td></td><td></td><td></td><td></td><td></td><td></td><td></td></tr>
<tr class="matrix"><td></td><td></td><td></td><td></td><td></td><td></td><td></td><td></td><td></td><td></td></tr>
<tr class="matrix"><td></td><td></td><td></td><td></td><td></td><td></td><td></td><td></td><td></td><td></td></tr>
<tr class="matrix"><td></td><td></td><td></td><td></td><td></td><td></td><td></td><td></td><td></td><td></td></tr>
<tr class="matrix"><td></td><td></td><td></td><td></td><td></td><td></td><td></td><td></td><td></td><td></td></tr>
<tr class="matrix"><td></td><td></td><td></td><td></td><td></td><td></td><td></td><td></td><td></td><td></td></tr>
<tr class="matrix"><td></td><td></td><td></td><td></td><td></td><td></td><td></td><td></td><td></td><td></td></tr>
<tr class="matrix"><td></td><td></td><td></td><td></td><td></td><td></td><td></td><td></td><td></td><td></td></tr>
<tr class="matrix"><td></td><td></td><td></td><td></td><td></td><td></td><td></td><td></td><td></td><td></td></tr>
<tr class="matrix"><td></td><td></td><td></td><td></td><td></td><td></td><td></td><td></td><td></td><td></td></tr>
<tr class="matrix"><td></td><td></td><td></td><td></td><td></td><td></td><td></td><td></td><td></td><td></td></tr>
<tr class="matrix"><td></td><td></td><td></td><td></td><td></td><td></td><td></td><td></td><td></td><td></td></tr>
<tr class="matrix"><td></td><td></td><td></td><td></td><td></td><td></td><td></td><td></td><td></td><td></td></tr>
<tr class="matrix"><td></td><td></td><td></td><td></td><td></td><td></td><td></td><td></td><td></td><td></td></tr>
<tr class="matrix"><td></td><td></td><td></td><td></td><td></td><td></td><td></td><td></td><td></td><td></td></tr>
<tr class="matrix"><td></td><td></td><td></td><td></td><td></td><td></td><td></td><td></td><td></td><td></td></tr>
<tr class="matrix"><td></td><td></td><td></td><td></td><td></td><td></td><td></td><td></td><td></td><td></td></tr>
<tr class="matrix"><td></td><td></td><td></td><td></td><td></td><td></td><td></td><td></td><td></td><td></td></tr>
<tr class="matrix"><td></td><td></td><td></td><td></td><td></td><td></td><td></td><td></td><td></td><td></td></tr>
<tr class="matrix"><td></td><td></td><td></td><td></td><td></td><td></td><td></td><td></td><td></td><td></td></tr>
</table>
</div>
<span id="messagesSpan"></span>
<span id="messagesSpan">
<div class="show-level-animation">Chargement...</div>
</span>
</div>
<div class="col">
<div class="card">
<div class="card-header text-center"><strong>NEXT</strong></div>
<div class="card shadow">
<div class="card-header fw-bold text-uppercase text-center">Next</div>
<div class="card-body p-0">
<table id="nextTable" class="minoes-table caption-top">
<tr class="no-border"><td></td><td></td><td></td><td></td><td></td><td></td></tr>
<tr class="no-border"><td></td><td></td><td></td><td></td><td></td><td></td></tr>
<tr class="no-border"><td></td><td></td><td></td><td></td><td></td><td></td></tr>
<tr class="no-border"><td></td><td></td><td></td><td></td><td></td><td></td></tr>
<tr class="no-border"><td></td><td></td><td></td><td></td><td></td><td></td></tr>
<tr class="no-border"><td></td><td></td><td></td><td></td><td></td><td></td></tr>
<tr class="no-border"><td></td><td></td><td></td><td></td><td></td><td></td></tr>
<tr class="no-border"><td></td><td></td><td></td><td></td><td></td><td></td></tr>
<tr class="no-border"><td></td><td></td><td></td><td></td><td></td><td></td></tr>
<tr class="no-border"><td></td><td></td><td></td><td></td><td></td><td></td></tr>
<tr class="no-border"><td></td><td></td><td></td><td></td><td></td><td></td></tr>
<tr class="no-border"><td></td><td></td><td></td><td></td><td></td><td></td></tr>
<tr class="no-border"><td></td><td></td><td></td><td></td><td></td><td></td></tr>
<tr class="no-border"><td></td><td></td><td></td><td></td><td></td><td></td></tr>
<tr class="no-border"><td></td><td></td><td></td><td></td><td></td><td></td></tr>
<tr class="no-border"><td></td><td></td><td></td><td></td><td></td><td></td></tr>
<tr class="buffer-zone"><td></td><td></td><td></td><td></td><td></td><td></td></tr>
<tr class="buffer-zone"><td></td><td></td><td></td><td></td><td></td><td></td></tr>
<tr class="buffer-zone"><td></td><td></td><td></td><td></td><td></td><td></td></tr>
<tr class="buffer-zone"><td></td><td></td><td></td><td></td><td></td><td></td></tr>
<tr class="buffer-zone"><td></td><td></td><td></td><td></td><td></td><td></td></tr>
<tr class="buffer-zone"><td></td><td></td><td></td><td></td><td></td><td></td></tr>
<tr class="buffer-zone"><td></td><td></td><td></td><td></td><td></td><td></td></tr>
<tr class="buffer-zone"><td></td><td></td><td></td><td></td><td></td><td></td></tr>
<tr class="buffer-zone"><td></td><td></td><td></td><td></td><td></td><td></td></tr>
<tr class="buffer-zone"><td></td><td></td><td></td><td></td><td></td><td></td></tr>
<tr class="buffer-zone"><td></td><td></td><td></td><td></td><td></td><td></td></tr>
<tr class="buffer-zone"><td></td><td></td><td></td><td></td><td></td><td></td></tr>
<tr class="buffer-zone"><td></td><td></td><td></td><td></td><td></td><td></td></tr>
<tr class="buffer-zone"><td></td><td></td><td></td><td></td><td></td><td></td></tr>
<tr class="buffer-zone"><td></td><td></td><td></td><td></td><td></td><td></td></tr>
<tr class="buffer-zone"><td></td><td></td><td></td><td></td><td></td><td></td></tr>
</table>
</div>
</div>
</div>
</div>
</div>
<div class="modal fade" id="statsModal" data-bs-backdrop="static" tabindex="-1">
<div class="modal-dialog modal-dialog-centered">
<div class="modal-dialog">
<div class="modal-content">
<div class="modal-header">
<h2 class="modal-title w-100 text-center">Fin</h2>
<!-- <button type="button" class="btn-close" data-bs-dismiss="modal"></button> -->
</div>
<div class="modal-body p-0">
<table class="table mb-0">
<tr><th>Score</th> <td id="statsModalScoreCell"></td> <th>Quatuors</th> <td id="statsModalNbQuatuors"></td> </tr>
<tr><th>Meilleur score</th><td id="statsModalHighScoreCell"></td> <th>Pirouettes</th> <td id="statsModalNbTSpin"></td></td> </tr>
<tr><th>Temps</th> <td id="statsModalTimeCell"></td> <th>Plus long combo</th> <td id="statsModalMaxCombo"></td> </tr>
<tr><th>Niveau</th> <td id="statsModalLevelCell"></td> <th>Plus long bout à bout</th><td id="statsModalMaxB2B"></td> </tr>
<tr><th>Lignes</th> <td id="statsModaltotalClearedLines"></td><th>Lignes par minute</th> <td id="statsModaltotalClearedLinesPM"></td></tr>
</table>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-primary" onclick="restart()">Rejouer ?</button>
</div>
</div>
</div>
</div>
</div>
<span style="display: none;">
<img src="favicons/I-0.png"/><img src="favicons/I-1.png"/><img src="favicons/I-2.png"/><img src="favicons/I-3.png"/>
<img src="favicons/J-0.png"/><img src="favicons/J-1.png"/><img src="favicons/J-2.png"/><img src="favicons/J-3.png"/>
<img src="favicons/L-0.png"/><img src="favicons/L-1.png"/><img src="favicons/L-2.png"/><img src="favicons/L-3.png"/>
<img src="favicons/O-0.png"/>
<img src="favicons/S-0.png"/><img src="favicons/S-1.png"/><img src="favicons/S-2.png"/><img src="favicons/S-3.png"/>
<img src="favicons/T-0.png"/><img src="favicons/T-1.png"/><img src="favicons/T-2.png"/><img src="favicons/T-3.png"/>
<img src="favicons/Z-0.png"/><img src="favicons/Z-1.png"/><img src="favicons/Z-2.png"/><img src="favicons/Z-3.png"/>
<audio id="wallSound" src="sounds/808K_A.wav" preload="auto" type="audio/wav"></audio>
<audio id="hardDropSound" src="sounds/909S.wav" preload="auto" type="audio/wav"></audio>
<audio id="lineClearSound" src="sounds/808COW.wav" preload="auto" type="audio/wav"></audio>
<audio id="tSpinSound" src="sounds/78GUIR.wav" preload="auto" type="audio/wav"></audio>
<audio id="quatuorSound" src="sounds/BRRDC1.wav" preload="auto" type="audio/wav"></audio>
</span>
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0-alpha3/dist/js/bootstrap.bundle.min.js" integrity="sha384-ENjdO4Dr2bkBIFxQpeoTz1HIcje39Wm4jDKdf19U8gI4ddQ3GYNS7NTKfAdVQSZe" crossorigin="anonymous"></script>
<script src="app.js"></script>
<script>
</script>
<script src="js/game_logic.js" language="Javascript" type="text/javascript"></script>
<script src="js/interface.js" language="Javascript" type="text/javascript"></script>
<script src="js/app.js" language="Javascript" type="text/javascript"></script>
<script>navigator?.serviceWorker.register('js/service-worker.js')</script>
</body>
</html>
+336
View File
@@ -0,0 +1,336 @@
let scheduler = new Scheduler()
let settings = new Settings()
let stats = new Stats()
let holdQueue = new HoldQueue()
let matrix = new Matrix()
let nextQueue = new NextQueue()
let playing = false
//let lastActionSucceded = true
let favicon
window.onload = function(event) {
document.selectedStyleSheetSet = selectedStyleSheet.title
selectedStyleSheet.href = stylesheetSelect.value
favicon = document.querySelector("link[rel~='icon']")
fullscreenCheckbox.onchange = function() {
if (this.checked) {
document.documentElement.requestFullscreen()
} else {
document.exitFullscreen()
}
}
restart()
}
document.onfullscreenchange = function() {
if (document.fullscreenElement) {
fullscreenCheckbox.checked = true
} else {
fullscreenCheckbox.checked = false
if (playing) pauseSettings()
}
}
document.onfullscreenerror = function() {
fullscreenCheckbox.checked = false
}
function restart() {
stats.modal.hide()
holdQueue.init()
holdQueue.redraw()
stats.init()
matrix.init()
nextQueue.init()
settings.init()
pauseSettings()
}
function pauseSettings() {
scheduler.clearInterval(fall)
scheduler.clearTimeout(lockDown)
scheduler.clearTimeout(repeat)
scheduler.clearInterval(autorepeat)
scheduler.clearInterval(ticktack)
stats.pauseTime = stats.time
document.onkeydown = null
settings.show()
}
function newGame(event) {
if (!settings.form.checkValidity()) {
event.preventDefault()
event.stopPropagation()
settings.form.reportValidity()
settings.form.classList.add('was-validated')
} else {
const audioContext = new AudioContext()
for(const sound of document.getElementsByTagName("audio")) {
sound.preservesPitch = false
audioContext.createMediaElementSource(sound).connect(audioContext.destination)
}
levelInput.name = "level"
levelInput.disabled = true
titleHeader.innerHTML = "PAUSE"
resumeButton.innerHTML = "Reprendre"
event.target.onsubmit = resume
stats.level = levelInput.valueAsNumber
localStorage["startLevel"] = levelInput.value
playing = true
onblur = pauseSettings
resume(event)
}
}
function resume(event) {
event.preventDefault()
event.stopPropagation()
settings.form.reportValidity()
settings.form.classList.add('was-validated')
if (settings.form.checkValidity()) {
for(const sound of document.getElementsByTagName("audio"))
sound.volume = sfxVolumeRange.value
settings.modal.hide()
settings.getInputs()
document.onkeydown = onkeydown
document.onkeyup = onkeyup
stats.time = stats.pauseTime
scheduler.setInterval(ticktack, 1000)
if (matrix.piece) scheduler.setInterval(fall, stats.fallPeriod)
else generate()
}
}
function ticktack() {
timeCell.innerText = stats.timeFormat.format(stats.time)
}
function generate(piece) {
matrix.piece = piece || nextQueue.shift()
if (!piece && holdQueue.piece) holdQueue.drawPiece()
//lastActionSucceded = true
favicon.href = matrix.piece.favicon_href
if (matrix.piece.canMove(TRANSLATION.NONE)) {
scheduler.setInterval(fall, stats.fallPeriod)
} else {
gameOver() // block out
}
}
let playerActions = {
moveLeft: () => matrix.piece.move(TRANSLATION.LEFT),
moveRight: () => matrix.piece.move(TRANSLATION.RIGHT),
rotateClockwise: () => matrix.piece.rotate(ROTATION.CW),
rotateCounterclockwise: () => matrix.piece.rotate(ROTATION.CCW),
softDrop: () => matrix.piece.move(TRANSLATION.DOWN) && ++stats.score,
hardDrop: function() {
scheduler.clearTimeout(lockDown)
playSound(hardDropSound)
while (matrix.piece.move(TRANSLATION.DOWN, ROTATION.NONE, true)) stats.score += 2
matrixCard.classList.remove("hard-dropped-table-animation")
matrixCard.offsetHeight;
matrixCard.classList.add("hard-dropped-table-animation") // restart animation
lockDown()
return true
},
hold: function() {
if (matrix.piece.holdEnabled) {
scheduler.clearInterval(fall)
scheduler.clearTimeout(lockDown)
let piece = matrix.piece
piece.facing = FACING.NORTH
piece.locked = false
generate(holdQueue.piece)
matrix.piece.holdEnabled = false
holdQueue.piece = piece
}
},
pause: pauseSettings,
}
// Handle player inputs
const REPEATABLE_ACTIONS = [
playerActions.moveLeft,
playerActions.moveRight,
playerActions.softDrop
]
pressedKeys = new Set()
actionsQueue = []
function onkeydown(event) {
if (event.key in settings.keyBind) {
event.preventDefault()
if (!pressedKeys.has(event.key)) {
pressedKeys.add(event.key)
action = settings.keyBind[event.key]
/*if (action()) {
lastActionSucceded = true
} else if (lastActionSucceded || !(action in REPEATABLE_ACTIONS)) {
playSound(wallSound)
lastActionSucceded = false
}*/
action()
if (REPEATABLE_ACTIONS.includes(action)) {
actionsQueue.unshift(action)
scheduler.clearTimeout(repeat)
scheduler.clearInterval(autorepeat)
if (action == playerActions.softDrop) scheduler.setInterval(autorepeat, settings.fallPeriod/20)
else scheduler.setTimeout(repeat, settings.das)
}
matrix.drawPiece()
}
}
}
function repeat() {
if (actionsQueue.length) {
actionsQueue[0]()
scheduler.setInterval(autorepeat, settings.arr)
}
}
function autorepeat() {
if (actionsQueue.length) {
/*if (actionsQueue[0]()) {
lastActionSucceded = true
} else if (lastActionSucceded) {
wallSound.play()
lastActionSucceded = false
}*/
actionsQueue[0]()
}
else scheduler.clearInterval(autorepeat)
}
function onkeyup(event) {
if (event.key in settings.keyBind) {
event.preventDefault()
pressedKeys.delete(event.key)
action = settings.keyBind[event.key]
if (actionsQueue.includes(action)) {
actionsQueue.splice(actionsQueue.indexOf(action), 1)
scheduler.clearTimeout(repeat)
scheduler.clearInterval(autorepeat)
if (actionsQueue.length) {
if (actionsQueue[0] == playerActions.softDrop) scheduler.setInterval(autorepeat, settings.fallPeriod/20)
else scheduler.setTimeout(repeat, settings.das)
} else {
matrix.drawPiece()
}
}
}
}
function fall() {
matrix.piece.move(TRANSLATION.DOWN)
}
function lockDown() {
scheduler.clearTimeout(lockDown)
scheduler.clearInterval(fall)
if (matrix.lock()) {
stats.lockDown(matrix.piece.tSpin, matrix.clearLines())
generate()
} else {
gameOver() // lock out
}
}
onanimationend = function (event) {
event.target.classList.remove(event.animationName)
}
messagesSpan.onanimationend = function(event) {
event.target.remove()
}
function gameOver() {
matrix.piece.locked = true
matrix.drawPiece()
document.onkeydown = null
onblur = null
scheduler.clearInterval(ticktack)
playing = false
stats.show()
}
window.onbeforeunload = function(event) {
stats.save()
settings.save()
if (playing) return false;
}
// Play with 3D
let mousedown = false
let rX0 = -15
let rY0 = 0
let clientX0 = 0
let clientY0 = 0
sceneDiv.onmousedown = function(event) {
mousedown = true
rX0 = parseInt(getComputedStyle(screenRow).getPropertyValue("--rX"))
dy0 = parseInt(getComputedStyle(screenRow).getPropertyValue("--rY"))
clientX0 = event.clientX
clientY0 = event.clientY
}
sceneDiv.onmousemove = function(event) {
if (mousedown) {
event.preventDefault()
event.stopPropagation()
rX = (rX0 - 0.5 * (event.clientY - clientY0)) % 360
screenRow.style.setProperty("--rX", rX)
if (rX >= 0) {
screenRow.classList.remove("top")
screenRow.classList.add("bottom")
} else {
screenRow.classList.add("top")
screenRow.classList.remove("bottom")
}
rY = (rY0 + 0.5 * (event.clientX - clientX0)) % 360
screenRow.style.setProperty("--rY", rY)
if (rY <= 0) {
screenRow.classList.remove("left")
screenRow.classList.add("right")
} else {
screenRow.classList.add("left")
screenRow.classList.remove("right")
}
}
}
sceneDiv.onmouseup = document.onmouseleave = function(event) {
mousedown = false
}
sceneDiv.onwheel = function(event) {
event.preventDefault()
event.stopPropagation()
let tZ = parseInt(getComputedStyle(screenRow).getPropertyValue("--tZ"))
tZ -= event.deltaY
screenRow.style.setProperty("--tZ", tZ + "px")
}
+468
View File
@@ -0,0 +1,468 @@
/* Constants */
const TRANSLATION = {
NONE: [ 0, 0],
LEFT: [-1, 0],
RIGHT: [ 1, 0],
DOWN: [ 0, 1],
}
const ROTATION = {
CW: 1, // ClockWise
CCW: -1, // CounterClockWise
}
const T_SPIN = {
NONE: "",
MINI: "PETITE<br/>PIROUETTE",
T_SPIN: "PIROUETTE"
}
// score = AWARDED_LINE_CLEARS[tSpin][nbClearedLines]
const AWARDED_LINE_CLEARS = {
[T_SPIN.NONE]: [0, 1, 3, 5, 8],
[T_SPIN.MINI]: [1, 2],
[T_SPIN.T_SPIN]: [4, 8, 12, 16]
}
const CLEARED_LINES_NAMES = [
"",
"SOLO",
"DUO",
"TRIO",
"QUATUOR",
]
const DELAY = {
LOCK: 500,
FALL: 1000,
}
const FACING = {
NORTH: 0,
EAST: 1,
SOUTH: 2,
WEST: 3,
}
/* Customize Array to be use as position */
Object.defineProperties(Array.prototype, {
"x": {
get: function() { return this[0] },
set: function(x) { this[0] = x }
},
"y": {
get: function() { return this[1] },
set: function(y) { this[1] = y }
}
})
Array.prototype.add = function(other) { return this.map((x, i) => x + other[i]) }
Array.prototype.mul = function(k) { return this.map(x => k * x) }
Array.prototype.translate = function(vector) { return this.map(pos => pos.add(vector)) }
Array.prototype.rotate = function(rotation) { return [-rotation*this.y, rotation*this.x] }
Array.prototype.pick = function() { return this.splice(Math.floor(Math.random()*this.length), 1)[0] }
HTMLElement.prototype.addNewChild = function(tag, properties) {
let child = document.createElement(tag)
for (key in properties) {
child[key] = properties[key]
}
this.appendChild(child)
}
/* Classes */
class Scheduler {
constructor() {
this.intervalTasks = new Map()
this.timeoutTasks = new Map()
}
setInterval(func, delay, ...args) {
if (this.intervalTasks.has(func)) {
console.warn(`$func already in intervalTasks`)
return false
} else {
this.intervalTasks.set(func, window.setInterval(func, delay, ...args))
return true
}
}
setTimeout(func, delay, ...args) {
if (this.timeoutTasks.has(func)) {
console.warn(`$func already in timeoutTasks`)
return false
} else {
this.timeoutTasks.set(func, window.setTimeout(func, delay, ...args))
return true
}
}
clearInterval(func) {
if (this.intervalTasks.has(func)) {
window.clearInterval(this.intervalTasks.get(func))
this.intervalTasks.delete(func)
return true
} else {
return false
}
}
clearTimeout(func) {
if (this.timeoutTasks.has(func)) {
window.clearTimeout(this.timeoutTasks.get(func))
this.timeoutTasks.delete(func)
return true
} else {
return false
}
}
}
class MinoesTable {
constructor(id) {
this.table = document.getElementById(id)
Array.from(this.table.getElementsByTagName("tr")).forEach((tr, row) => {
tr.style.setProperty('--row', row)
Array.from(tr.getElementsByTagName("td")).forEach((td, column) => {
td.style.setProperty('--column', column)
})
})
this.rows = this.table.rows.length
this.columns = this.table.rows[0].childElementCount
}
init() {
this._piece = null
}
get piece() {
return this._piece
}
set piece(piece) {
this._piece = piece
this._piece.center = Array.from(this.init_center)
this.redraw()
this.drawPiece()
}
drawMino(position, className) {
this.table.rows[position.y].cells[position.x].className = className
}
drawPiece(piece=this.piece, className=piece.className) {
piece.minoesPosition[piece.facing]
.translate(piece.center)
.forEach(minoPosition => {
this.drawMino(minoPosition, className)
})
}
redraw() {
for (let y=0; y<this.rows; y++) {
for (let x=0; x<this.columns; x++) {
this.drawMino([x, y], "")
}
}
}
}
MinoesTable.prototype.init_center = [2, 2]
class HoldQueue extends MinoesTable {
constructor() {
super("holdTable")
}
drawPiece(piece=this.piece, className=piece.className) {
if (!matrix.piece.holdEnabled) {
className += " disabled"
}
super.drawPiece(piece, className)
}
}
class NextQueue extends MinoesTable {
constructor() {
super("nextTable")
}
init() {
this.pieces = this.init_centers.map(center => {
let piece = new Tetromino.pick()
piece.center = Array.from(center)
return piece
})
}
shift() {
let fistPiece = this.pieces.shift()
this.pieces.push(new Tetromino.pick())
this.pieces.forEach((piece, i) => {
piece.center = Array.from(this.init_centers[i])
})
this.redraw()
return fistPiece
}
redraw() {
super.redraw()
this.pieces.forEach((piece) => {
this.drawPiece(piece)
})
}
}
NextQueue.prototype.init_centers = [[2, 2], [2, 5], [2, 8], [2, 11], [2, 14]]
class Matrix extends MinoesTable {
constructor() {
super("matrixTable")
}
init() {
super.init()
this.blocks = Array(this.rows).fill().map(() => Array(this.columns))
this.redraw()
}
cellIsEmpty(position) {
return 0 <= position.x && position.x < this.columns && 0 <= position.y && position.y < this.rows && !this.blocks[position.y][position.x]
}
get piece() {
return this._piece
}
set piece(piece) {
this._piece = piece
this._piece.center = Array.from(this.init_center)
this.ghost = piece.ghost
this.redraw()
this.drawPiece()
}
drawPiece(piece=this.piece, className=piece.className) {
super.drawPiece(this.ghost, "")
this.ghost = piece.ghost
super.drawPiece(this.ghost)
if (piece.locked) className += " locking"
if (piece==this.piece && actionsQueue.length) className += " moving"
super.drawPiece(piece, className)
this.table.style.setProperty('--piece-column', this.piece.center.x)
this.table.style.setProperty('--piece-row', this.piece.center.y)
}
clearPiece(piece=this.piece, className="") {
super.drawPiece(piece, className)
}
redraw() {
for (let y=0; y<this.rows; y++) {
for (let x=0; x<this.columns; x++) {
if (this.table.rows[y].cells[x].classList != "trail-animation")
this.drawMino([x, y], this.blocks[y][x] || "")
}
}
}
lock() {
let blocksPosition = this.piece.minoesPosition[this.piece.facing].translate(this.piece.center)
if (blocksPosition.some(position => position.y >= 4)) {
blocksPosition.forEach(position => {
this.blocks[position.y][position.x] = "locked " + this.piece.className
this.drawMino(position, this.piece.className)
})
return true
} else {
return false
}
}
clearLines() {
let nbClearedLines = 0
for (let y=0; y<this.rows; y++) {
let row = this.blocks[y]
if (row.filter(lockedMino => lockedMino).length == this.columns) {
nbClearedLines++
this.blocks.splice(y, 1)
this.blocks.unshift(Array(matrix.columns))
this.table.rows[y].classList.add("cleared-line-animation")
}
}
this.redraw()
return nbClearedLines
}
}
Matrix.prototype.init_center = [4, 4]
class Tetromino {
static randomBag = []
static get pick() {
if (!this.randomBag.length) this.randomBag = [I, J, L, O, S, T, Z]
return this.randomBag.pick()
}
constructor(center, facing=0, className=this.constructor.name + " mino") {
this.center = center
this.className = className
this.facing = facing
this.lastRotation = false
this.rotationPoint4Used = false
this.holdEnabled = true
this.locked = false
}
canMove(translation, rotation=ROTATION.NONE) {
let testCenter = this.center.add(translation)
let testFacing = rotation? (this.facing + rotation + 4) % 4: this.facing
let testMinoesPosition = this.minoesPosition[testFacing]
if (testMinoesPosition
.translate(testCenter)
.every(minoPosition => matrix.cellIsEmpty(minoPosition)))
return {center: testCenter, facing: testFacing}
else
return false
}
move(translation, rotation=ROTATION.NONE, hardDropped=false) {
let success = this.canMove(translation, rotation)
if (success) {
scheduler.clearTimeout(lockDown)
matrix.clearPiece(this, hardDropped? "trail-animation" : "")
this.center = success.center
if (rotation) this.facing = success.facing
this.lastRotation = rotation
if (this.canMove(TRANSLATION.DOWN)) {
this.locked = false
} else {
this.locked = true
scheduler.setTimeout(lockDown, stats.lockDelay)
}
matrix.drawPiece()
return true
} else if (!hardDropped && translation == TRANSLATION.DOWN) {
this.locked = true
if (!scheduler.timeoutTasks.has(lockDown))
scheduler.setTimeout(lockDown, stats.lockDelay)
matrix.drawPiece()
}
}
rotate(rotation) {
return this.srs[this.facing][rotation].some((translation, rotationPoint) => {
if (this.move(translation, rotation)) {
if (rotationPoint == 4) this.rotationPoint4Used = true
favicon.href = this.favicon_href
return true
}
})
}
get ghost() {
let ghost = new this.constructor(Array.from(this.center), this.facing, "ghost " + this.className)
while (ghost.canMove(TRANSLATION.DOWN)) ghost.center.y++
return ghost
}
get favicon_href() {
return `favicons/${this.constructor.name}-${this.facing}.png`
}
get tSpin() {
return T_SPIN.NONE
}
}
// Super Rotation System
// freedom of movement = srs[piece.facing][rotation]
Tetromino.prototype.srs = [
{ [ROTATION.CW]: [[0, 0], [-1, 0], [-1, -1], [0, 2], [-1, 2]], [ROTATION.CCW]: [[0, 0], [ 1, 0], [ 1, -1], [0, 2], [ 1, 2]] },
{ [ROTATION.CW]: [[0, 0], [ 1, 0], [ 1, 1], [0, -2], [ 1, -2]], [ROTATION.CCW]: [[0, 0], [ 1, 0], [ 1, 1], [0, -2], [ 1, -2]] },
{ [ROTATION.CW]: [[0, 0], [ 1, 0], [ 1, -1], [0, 2], [ 1, 2]], [ROTATION.CCW]: [[0, 0], [-1, 0], [-1, -1], [0, 2], [-1, 2]] },
{ [ROTATION.CW]: [[0, 0], [-1, 0], [-1, 1], [0, -2], [-1, -2]], [ROTATION.CCW]: [[0, 0], [-1, 0], [-1, 1], [0, -2], [-1, -2]] },
]
class I extends Tetromino {}
I.prototype.minoesPosition = [
[[-1, 0], [0, 0], [1, 0], [2, 0]],
[[1, -1], [1, 0], [1, 1], [1, 2]],
[[-1, 1], [0, 1], [1, 1], [2, 1]],
[[0, -1], [0, 0], [0, 1], [0, 2]],
]
I.prototype.srs = [
{ [ROTATION.CW]: [[0, 0], [-2, 0], [ 1, 0], [-2, 1], [ 1, -2]], [ROTATION.CCW]: [[0, 0], [-1, 0], [ 2, 0], [-1, -2], [ 2, 1]] },
{ [ROTATION.CW]: [[0, 0], [-1, 0], [ 2, 0], [-1, -2], [ 2, 1]], [ROTATION.CCW]: [[0, 0], [ 2, 0], [-1, 0], [ 2, -1], [-1, 2]] },
{ [ROTATION.CW]: [[0, 0], [ 2, 0], [-1, 0], [ 2, -1], [-1, 2]], [ROTATION.CCW]: [[0, 0], [ 1, 0], [-2, 0], [ 1, 2], [-2, -1]] },
{ [ROTATION.CW]: [[0, 0], [ 1, 0], [-2, 0], [ 1, 2], [-2, -1]], [ROTATION.CCW]: [[0, 0], [-2, 0], [ 1, 0], [-2, 1], [ 1, -2]] },
]
class J extends Tetromino {}
J.prototype.minoesPosition = [
[[-1, -1], [-1, 0], [0, 0], [1, 0]],
[[ 0, -1], [1, -1], [0, 0], [0, 1]],
[[ 1, 1], [-1, 0], [0, 0], [1, 0]],
[[ 0, -1], [-1, 1], [0, 0], [0, 1]],
]
class L extends Tetromino {}
L.prototype.minoesPosition = [
[[-1, 0], [0, 0], [1, 0], [ 1, -1]],
[[0, -1], [0, 0], [0, 1], [ 1, 1]],
[[-1, 0], [0, 0], [1, 0], [-1, 1]],
[[0, -1], [0, 0], [0, 1], [-1, -1]],
]
class O extends Tetromino {}
O.prototype.minoesPosition = [
[[0, 0], [1, 0], [0, -1], [1, -1]]
]
O.prototype.srs = [
{[ROTATION.CW]: [], [ROTATION.CCW]: []}
]
class S extends Tetromino {}
S.prototype.minoesPosition = [
[[-1, 0], [0, 0], [0, -1], [1, -1]],
[[ 0, -1], [0, 0], [1, 0], [1, 1]],
[[-1, 1], [0, 0], [1, 0], [0, 1]],
[[-1, -1], [0, 0], [-1, 0], [0, 1]],
]
class T extends Tetromino {
get tSpin() {
if (this.lastRotation) {
let [a, b, c, d] = this.tSlots[this.facing]
.translate(this.center)
.map(minoPosition => !matrix.cellIsEmpty(minoPosition))
if (a && b && (c || d))
return T_SPIN.T_SPIN
else if (c && d && (a || b))
return this.rotationPoint4Used ? T_SPIN.T_SPIN : T_SPIN.MINI
}
return T_SPIN.NONE
}
}
T.prototype.minoesPosition = [
[[-1, 0], [0, 0], [1, 0], [0, -1]],
[[0, -1], [0, 0], [1, 0], [0, 1]],
[[-1, 0], [0, 0], [1, 0], [0, 1]],
[[0, -1], [0, 0], [0, 1], [-1, 0]],
]
T.prototype.tSlots = [
[[-1, -1], [ 1, -1], [ 1, 1], [-1, 1]],
[[ 1, -1], [ 1, 1], [-1, 1], [-1, -1]],
[[ 1, 1], [-1, 1], [-1, -1], [ 1, -1]],
[[-1, 1], [-1, -1], [ 1, -1], [ 1, 1]],
]
class Z extends Tetromino {}
Z.prototype.minoesPosition = [
[[-1, -1], [0, -1], [0, 0], [ 1, 0]],
[[ 1, -1], [1, 0], [0, 0], [ 0, 1]],
[[-1, 0], [0, 0], [0, 1], [ 1, 1]],
[[ 0, -1], [-1, 0], [0, 0], [-1, 1]]
]
+335
View File
@@ -0,0 +1,335 @@
const KEY_NAMES = new Proxy({
["ArrowLeft"] : "←",
["←"] : "ArrowLeft",
["ArrowRight"] : "→",
["→"] : "ArrowRight",
["ArrowUp"] : "↑",
["↑"] : "ArrowUp",
["ArrowDown"] : "↓",
["↓"] : "ArrowDown",
[" "] : "Espace",
["Espace"] : " ",
["Escape"] : "Échap.",
["Échap."] : "Escape",
["Backspace"] : "Ret. arrière",
["Ret. arrière"]: "Backspace",
["Enter"] : "Entrée",
["Entrée"] : "Enter",
}, {
get(target, key) {
return key in target? target[key] : key
}
})
class Settings {
constructor() {
this.form = settingsForm
this.load()
this.modal = new bootstrap.Modal('#settingsModal')
settingsModal.addEventListener('shown.bs.modal', () => resumeButton.focus())
}
load() {
this.form.querySelectorAll("[name]").forEach(element => {
if (element.name in localStorage)
element.value = localStorage[element.name]
})
window.document.selectedStyleSheetSet = stylesheetSelect.value
}
save() {
this.form.querySelectorAll("[name]").forEach(element => localStorage[element.name] = element.value)
}
init() {
this.form.onsubmit = newGame
levelInput.name = "startLevel"
levelInput.disabled = false
titleHeader.innerHTML = "QUATUOR"
resumeButton.innerHTML = "Jouer"
}
show() {
resumeButton.disabled = false
settings.form.classList.remove('was-validated')
settings.modal.show()
settings.form.reportValidity()
}
getInputs() {
for (let input of this.form.querySelectorAll("input[type='text']")) {
this[input.name] = KEY_NAMES[input.value]
}
for (let input of this.form.querySelectorAll("input[type='number'], input[type='range']")) {
this[input.name] = input.valueAsNumber
}
for (let input of this.form.querySelectorAll("input[type='checkbox']")) {
this[input.name] = input.checked == true
}
this.keyBind = new Proxy({}, {
get: (target, key) => target[key.toLowerCase()],
set: (target, key, value) => target[key.toLowerCase()] = value,
has: (target, key) => key.toLowerCase() in target
})
for (let actionName in playerActions) {
this.keyBind[settings[actionName]] = playerActions[actionName]
}
}
}
function changeKey(input) {
prevValue = input.value
input.value = ""
keyInputs = Array.from(input.form.querySelectorAll("input[type='text']"))
input.onkeydown = function (event) {
event.preventDefault()
input.value = KEY_NAMES[event.key]
keyInputs.forEach(input => {
input.setCustomValidity("")
input.classList.remove("is-invalid")
})
keyInputs.sort((input1, input2) => {
if(input1.value == input2.value) {
input1.setCustomValidity("Touche déjà utilisée")
input1.classList.add("is-invalid")
input2.setCustomValidity("Touche déjà utilisée")
input2.classList.add("is-invalid")
}
return input1.value > input2.value
})
if (input.checkValidity()) {
input.blur()
}
}
input.onblur = function (event) {
if (!input.value) input.value = prevValue
input.onkeydown = null
input.onblur = null
}
}
class Stats {
constructor() {
this.modal = new bootstrap.Modal('#statsModal')
this.load()
}
load() {
this.highScore = Number(localStorage["highScore"]) || 0
}
init() {
levelInput.value = localStorage["startLevel"] || 1
this.score = 0
this.goal = 0
this.combo = 0
this.b2b = 0
this.startTime = new Date()
this.lockDelay = DELAY.LOCK
this.totalClearedLines = 0
this.nbQuatuors = 0
this.nbTSpin = 0
this.maxCombo = 0
this.maxB2B = 0
}
set score(score) {
this._score = score
scoreCell.innerText = score.toLocaleString()
if (score > this.highScore) {
this.highScore = score
}
}
get score() {
return this._score
}
set highScore(highScore) {
this._highScore = highScore
highScoreCell.innerText = highScore.toLocaleString()
}
get highScore() {
return this._highScore
}
set level(level) {
this._level = level
this.goal += level * 5
if (level <= 20){
this.fallPeriod = 1000 * Math.pow(0.8 - ((level - 1) * 0.007), level - 1)
}
if (level > 15)
this.lockDelay = 500 * Math.pow(0.9, level - 15)
levelInput.value = level
levelCell.innerText = level
messagesSpan.addNewChild("div", { className: "show-level-animation", innerHTML: `<h1>NIVEAU<br/>${this.level}</h1>` })
}
get level() {
return this._level
}
set goal(goal) {
this._goal = goal
goalCell.innerText = goal
}
get goal() {
return this._goal
}
set combo(combo) {
this._combo = combo
if (combo > this.maxCombo) this.maxCombo = combo
}
get combo() {
return this._combo
}
set b2b(b2b) {
this._b2b = b2b
if (b2b > this.maxB2B) this.maxB2B = b2b
}
get b2b() {
return this._b2b
}
set time(time) {
this.startTime = new Date() - time
ticktack()
}
get time() {
return new Date() - this.startTime
}
lockDown(tSpin, nbClearedLines) {
this.totalClearedLines += nbClearedLines
if (nbClearedLines == 4) this.nbQuatuors++
if (tSpin == T_SPIN.T_SPIN) this.nbTSpin++
// Cleared lines & T-Spin
let awardedLineClears = AWARDED_LINE_CLEARS[tSpin][nbClearedLines]
let patternScore = 100 * this.level * awardedLineClears
if (tSpin) messagesSpan.addNewChild("div", {
className: "rotate-in-animation",
innerHTML: tSpin
})
if (nbClearedLines) messagesSpan.addNewChild("div", {
className: "zoom-in-animation",
innerHTML: CLEARED_LINES_NAMES[nbClearedLines]
})
if (patternScore) {
messagesSpan.addNewChild("div", {
className: "zoom-in-animation",
style: "animation-delay: .2s",
innerHTML: patternScore
})
this.score += patternScore
}
// Combo
if (nbClearedLines) {
this.combo++
if (this.combo >= 1) {
let comboScore = (nbClearedLines == 1 ? 20 : 50) * this.combo * this.level
if (this.combo == 1) {
messagesSpan.addNewChild("div", {
className: "zoom-in-animation",
style: "animation-delay: .4s",
innerHTML: `COMBO<br/>${comboScore}`
})
} else {
messagesSpan.addNewChild("div", {
className: "zoom-in-animation",
style: "animation-delay: .4s",
innerHTML: `COMBO x${this.combo}<br/>${comboScore}`
})
}
this.score += comboScore
}
} else {
this.combo = -1
}
// Back to back sequence
if ((nbClearedLines == 4) || (tSpin && nbClearedLines)) {
this.b2b++
if (this.b2b >= 1) {
let b2bScore = patternScore / 2
if (this.b2b == 1) {
messagesSpan.addNewChild("div", {
className: "zoom-in-animation",
style: "animation-delay: .4s",
innerHTML: `BOUT À BOUT<br/>${b2bScore}`
})
} else {
messagesSpan.addNewChild("div", {
className: "zoom-in-animation",
style: "animation-delay: .4s",
innerHTML: `BOUT À BOUT x${this.b2b}<br/>${b2bScore}`
})
}
this.score += b2bScore
}
} else if (nbClearedLines && !tSpin ) {
if (this.b2b >= 1) {
messagesSpan.addNewChild("div", {
className: "zoom-in-animation",
style: "animation-delay: .4s",
innerHTML: `FIN DU BOUT À BOUT`
})
}
this.b2b = -1
}
// Sound
if (sfxVolumeRange.value) {
if (nbClearedLines == 4) playSound(quatuorSound, this.combo)
else if (nbClearedLines) playSound(lineClearSound, this.combo)
if (tSpin) playSound(tSpinSound, this.combo)
}
this.goal -= awardedLineClears
if (this.goal <= 0) this.level++
}
show() {
let time = stats.time
statsModalScoreCell.innerText = this.score.toLocaleString()
statsModalHighScoreCell.innerText = this.highScore.toLocaleString()
statsModalLevelCell.innerText = this.level
statsModalTimeCell.innerText = this.timeFormat.format(time)
statsModaltotalClearedLines.innerText = this.totalClearedLines
statsModaltotalClearedLinesPM.innerText = (stats.totalClearedLines * 60000 / time).toFixed(2)
statsModalNbQuatuors.innerText = this.nbQuatuors
statsModalNbTSpin.innerText = this.nbTSpin
statsModalMaxCombo.innerText = this.maxCombo
statsModalMaxB2B.innerText = this.maxB2B
this.modal.show()
}
save() {
localStorage["highScore"] = this.highScore
}
}
Stats.prototype.timeFormat = new Intl.DateTimeFormat("fr-FR", {
hour: "numeric",
minute: "2-digit",
second: "2-digit",
timeZone: "UTC"
})
function playSound(sound, note=0) {
sound.currentTime = 0
sound.playbackRate = Math.pow(5/4, note)
sound.play()
}
+1 -1
View File
@@ -16,7 +16,7 @@ Copyright 2015, 2019, 2020 Google LLC. All Rights Reserved.
const OFFLINE_VERSION = 1;
const CACHE_NAME = "offline";
// Customize this with a different URL if needed.
const OFFLINE_URL = "index.html";
const OFFLINE_URL = "../index.html";
self.addEventListener("install", (event) => {
event.waitUntil(
BIN
View File
Binary file not shown.
BIN
View File
Binary file not shown.
BIN
View File
Binary file not shown.
BIN
View File
Binary file not shown.
BIN
View File
Binary file not shown.