Spaces:
Sleeping
Sleeping
Upload 51 files
Browse filesThis view is limited to 50 files because it contains too many changes.
See raw diff
- .gitattributes +11 -0
- public/assets/backdrops/level-1/backdrop.png +0 -0
- public/assets/backdrops/level-10/backdrop.png +0 -0
- public/assets/backdrops/level-10/title.png +0 -0
- public/assets/backdrops/level-2/backdrop.png +0 -0
- public/assets/backdrops/level-3/backdrop.png +0 -0
- public/assets/backdrops/level-4/backdrop.png +0 -0
- public/assets/backdrops/level-5/backdrop.png +0 -0
- public/assets/backdrops/level-6/backdrop.png +0 -0
- public/assets/backdrops/level-7/backdrop.png +0 -0
- public/assets/backdrops/level-8/backdrop.png +0 -0
- public/assets/backdrops/level-9/backdrop.png +0 -0
- public/assets/blocks-sprite.png +0 -0
- public/assets/blocks.png +0 -0
- public/assets/crush-alt.png +0 -0
- public/assets/crush.png +0 -0
- public/assets/fonts/RetroArcade.ttf +0 -0
- public/assets/fonts/font.otf +0 -0
- public/assets/fonts/font.png +0 -0
- public/assets/fonts/thick_8x8.png +0 -0
- public/assets/fonts/thick_8x8.xml +276 -0
- public/assets/game-over.png +0 -0
- public/assets/music/level-1/track.mp3 +3 -0
- public/assets/music/level-10/track.mp3 +3 -0
- public/assets/music/level-2/track.mp3 +3 -0
- public/assets/music/level-3/track.mp3 +3 -0
- public/assets/music/level-4/track.mp3 +3 -0
- public/assets/music/level-5/track.mp3 +3 -0
- public/assets/music/level-6/track.mp3 +3 -0
- public/assets/music/level-7/track.mp3 +3 -0
- public/assets/music/level-8/track.mp3 +3 -0
- public/assets/music/level-9/track.mp3 +3 -0
- public/assets/title.png +0 -0
- public/assets/tv.png +3 -0
- public/template-generator.html +184 -0
- scripts/generate-placeholders.js +188 -0
- scripts/generate-template.js +170 -0
- scripts/test-canvas.js +24 -0
- src/config.js +13 -0
- src/constants.js +188 -0
- src/main.js +40 -0
- src/pipelines/TrinitronPipeline.js +190 -0
- src/scenes/GameScene.js +983 -0
- src/scenes/ModeSelectScene.js +125 -0
- src/scenes/PreloadScene.js +106 -0
- src/scenes/PreloadScene_new.js +52 -0
- src/shaderOverlay.js +212 -0
- src/shaders/trinitron-fragment.glsl +179 -0
- src/utils/BlockRenderer.js +128 -0
- src/utils/ColorExtractor.js +84 -0
.gitattributes
CHANGED
|
@@ -33,3 +33,14 @@ saved_model/**/* filter=lfs diff=lfs merge=lfs -text
|
|
| 33 |
*.zip filter=lfs diff=lfs merge=lfs -text
|
| 34 |
*.zst filter=lfs diff=lfs merge=lfs -text
|
| 35 |
*tfevents* filter=lfs diff=lfs merge=lfs -text
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 33 |
*.zip filter=lfs diff=lfs merge=lfs -text
|
| 34 |
*.zst filter=lfs diff=lfs merge=lfs -text
|
| 35 |
*tfevents* filter=lfs diff=lfs merge=lfs -text
|
| 36 |
+
public/assets/music/level-1/track.mp3 filter=lfs diff=lfs merge=lfs -text
|
| 37 |
+
public/assets/music/level-10/track.mp3 filter=lfs diff=lfs merge=lfs -text
|
| 38 |
+
public/assets/music/level-2/track.mp3 filter=lfs diff=lfs merge=lfs -text
|
| 39 |
+
public/assets/music/level-3/track.mp3 filter=lfs diff=lfs merge=lfs -text
|
| 40 |
+
public/assets/music/level-4/track.mp3 filter=lfs diff=lfs merge=lfs -text
|
| 41 |
+
public/assets/music/level-5/track.mp3 filter=lfs diff=lfs merge=lfs -text
|
| 42 |
+
public/assets/music/level-6/track.mp3 filter=lfs diff=lfs merge=lfs -text
|
| 43 |
+
public/assets/music/level-7/track.mp3 filter=lfs diff=lfs merge=lfs -text
|
| 44 |
+
public/assets/music/level-8/track.mp3 filter=lfs diff=lfs merge=lfs -text
|
| 45 |
+
public/assets/music/level-9/track.mp3 filter=lfs diff=lfs merge=lfs -text
|
| 46 |
+
public/assets/tv.png filter=lfs diff=lfs merge=lfs -text
|
public/assets/backdrops/level-1/backdrop.png
ADDED
|
public/assets/backdrops/level-10/backdrop.png
ADDED
|
public/assets/backdrops/level-10/title.png
ADDED
|
public/assets/backdrops/level-2/backdrop.png
ADDED
|
public/assets/backdrops/level-3/backdrop.png
ADDED
|
public/assets/backdrops/level-4/backdrop.png
ADDED
|
public/assets/backdrops/level-5/backdrop.png
ADDED
|
public/assets/backdrops/level-6/backdrop.png
ADDED
|
public/assets/backdrops/level-7/backdrop.png
ADDED
|
public/assets/backdrops/level-8/backdrop.png
ADDED
|
public/assets/backdrops/level-9/backdrop.png
ADDED
|
public/assets/blocks-sprite.png
ADDED
|
|
public/assets/blocks.png
ADDED
|
public/assets/crush-alt.png
ADDED
|
public/assets/crush.png
ADDED
|
public/assets/fonts/RetroArcade.ttf
ADDED
|
Binary file (22.7 kB). View file
|
|
|
public/assets/fonts/font.otf
ADDED
|
Binary file (9.46 kB). View file
|
|
|
public/assets/fonts/font.png
ADDED
|
public/assets/fonts/thick_8x8.png
ADDED
|
public/assets/fonts/thick_8x8.xml
ADDED
|
@@ -0,0 +1,276 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
<?xml version="1.0"?>
|
| 2 |
+
<font>
|
| 3 |
+
<info face="thick_8x8" size="10" bold="0" italic="0"/>
|
| 4 |
+
<common lineHeight="12" base="10" scaleW="104" scaleH="56" pages="1" packed="0"/>
|
| 5 |
+
<pages>
|
| 6 |
+
<page id="0" file="thick_8x8.png"/>
|
| 7 |
+
</pages>
|
| 8 |
+
<chars count="91">
|
| 9 |
+
|
| 10 |
+
<char id="65" x="0" y="0" width="8" height="8" page="0" xadvance="8" xoffset="0" yoffset="0"/>
|
| 11 |
+
<!-- A -->
|
| 12 |
+
|
| 13 |
+
<char id="66" x="8" y="0" width="8" height="8" page="0" xadvance="8" xoffset="0" yoffset="0"/>
|
| 14 |
+
<!-- B -->
|
| 15 |
+
|
| 16 |
+
<char id="67" x="16" y="0" width="8" height="8" page="0" xadvance="8" xoffset="0" yoffset="0"/>
|
| 17 |
+
<!-- C -->
|
| 18 |
+
|
| 19 |
+
<char id="68" x="24" y="0" width="8" height="8" page="0" xadvance="8" xoffset="0" yoffset="0"/>
|
| 20 |
+
<!-- D -->
|
| 21 |
+
|
| 22 |
+
<char id="69" x="32" y="0" width="8" height="8" page="0" xadvance="8" xoffset="0" yoffset="0"/>
|
| 23 |
+
<!-- E -->
|
| 24 |
+
|
| 25 |
+
<char id="70" x="40" y="0" width="8" height="8" page="0" xadvance="8" xoffset="0" yoffset="0"/>
|
| 26 |
+
<!-- F -->
|
| 27 |
+
|
| 28 |
+
<char id="71" x="48" y="0" width="8" height="8" page="0" xadvance="8" xoffset="0" yoffset="0"/>
|
| 29 |
+
<!-- G -->
|
| 30 |
+
|
| 31 |
+
<char id="72" x="56" y="0" width="8" height="8" page="0" xadvance="8" xoffset="0" yoffset="0"/>
|
| 32 |
+
<!-- H -->
|
| 33 |
+
|
| 34 |
+
<char id="73" x="64" y="0" width="8" height="8" page="0" xadvance="8" xoffset="0" yoffset="0"/>
|
| 35 |
+
<!-- I -->
|
| 36 |
+
|
| 37 |
+
<char id="74" x="72" y="0" width="8" height="8" page="0" xadvance="8" xoffset="0" yoffset="0"/>
|
| 38 |
+
<!-- J -->
|
| 39 |
+
|
| 40 |
+
<char id="75" x="80" y="0" width="8" height="8" page="0" xadvance="8" xoffset="0" yoffset="0"/>
|
| 41 |
+
<!-- K -->
|
| 42 |
+
|
| 43 |
+
<char id="76" x="88" y="0" width="8" height="8" page="0" xadvance="8" xoffset="0" yoffset="0"/>
|
| 44 |
+
<!-- L -->
|
| 45 |
+
|
| 46 |
+
<char id="77" x="96" y="0" width="8" height="8" page="0" xadvance="9" xoffset="0" yoffset="0"/>
|
| 47 |
+
<!-- M -->
|
| 48 |
+
|
| 49 |
+
<char id="78" x="0" y="8" width="8" height="8" page="0" xadvance="8" xoffset="0" yoffset="0"/>
|
| 50 |
+
<!-- N -->
|
| 51 |
+
|
| 52 |
+
<char id="79" x="8" y="8" width="8" height="8" page="0" xadvance="8" xoffset="0" yoffset="0"/>
|
| 53 |
+
<!-- O -->
|
| 54 |
+
|
| 55 |
+
<char id="80" x="16" y="8" width="8" height="8" page="0" xadvance="8" xoffset="0" yoffset="0"/>
|
| 56 |
+
<!-- P -->
|
| 57 |
+
|
| 58 |
+
<char id="81" x="24" y="8" width="8" height="8" page="0" xadvance="8" xoffset="0" yoffset="0"/>
|
| 59 |
+
<!-- Q -->
|
| 60 |
+
|
| 61 |
+
<char id="82" x="32" y="8" width="8" height="8" page="0" xadvance="8" xoffset="0" yoffset="0"/>
|
| 62 |
+
<!-- R -->
|
| 63 |
+
|
| 64 |
+
<char id="83" x="40" y="8" width="8" height="8" page="0" xadvance="8" xoffset="0" yoffset="0"/>
|
| 65 |
+
<!-- S -->
|
| 66 |
+
|
| 67 |
+
<char id="84" x="48" y="8" width="8" height="8" page="0" xadvance="8" xoffset="0" yoffset="0"/>
|
| 68 |
+
<!-- T -->
|
| 69 |
+
|
| 70 |
+
<char id="85" x="56" y="8" width="8" height="8" page="0" xadvance="8" xoffset="0" yoffset="0"/>
|
| 71 |
+
<!-- U -->
|
| 72 |
+
|
| 73 |
+
<char id="86" x="64" y="8" width="8" height="8" page="0" xadvance="8" xoffset="0" yoffset="0"/>
|
| 74 |
+
<!-- V -->
|
| 75 |
+
|
| 76 |
+
<char id="87" x="72" y="8" width="8" height="8" page="0" xadvance="9" xoffset="0" yoffset="0"/>
|
| 77 |
+
<!-- W -->
|
| 78 |
+
|
| 79 |
+
<char id="88" x="80" y="8" width="8" height="8" page="0" xadvance="8" xoffset="0" yoffset="0"/>
|
| 80 |
+
<!-- X -->
|
| 81 |
+
|
| 82 |
+
<char id="89" x="88" y="8" width="8" height="8" page="0" xadvance="8" xoffset="0" yoffset="0"/>
|
| 83 |
+
<!-- Y -->
|
| 84 |
+
|
| 85 |
+
<char id="90" x="96" y="8" width="8" height="8" page="0" xadvance="8" xoffset="0" yoffset="0"/>
|
| 86 |
+
<!-- Z -->
|
| 87 |
+
|
| 88 |
+
<char id="97" x="0" y="16" width="8" height="8" page="0" xadvance="8" xoffset="0" yoffset="0"/>
|
| 89 |
+
<!-- a -->
|
| 90 |
+
|
| 91 |
+
<char id="98" x="8" y="16" width="8" height="8" page="0" xadvance="8" xoffset="0" yoffset="0"/>
|
| 92 |
+
<!-- b -->
|
| 93 |
+
|
| 94 |
+
<char id="99" x="16" y="16" width="8" height="8" page="0" xadvance="8" xoffset="0" yoffset="0"/>
|
| 95 |
+
<!-- c -->
|
| 96 |
+
|
| 97 |
+
<char id="100" x="24" y="16" width="8" height="8" page="0" xadvance="8" xoffset="0" yoffset="0"/>
|
| 98 |
+
<!-- d -->
|
| 99 |
+
|
| 100 |
+
<char id="101" x="32" y="16" width="8" height="8" page="0" xadvance="8" xoffset="0" yoffset="0"/>
|
| 101 |
+
<!-- e -->
|
| 102 |
+
|
| 103 |
+
<char id="102" x="40" y="16" width="8" height="8" page="0" xadvance="7" xoffset="0" yoffset="0"/>
|
| 104 |
+
<!-- f -->
|
| 105 |
+
|
| 106 |
+
<char id="103" x="48" y="16" width="8" height="8" page="0" xadvance="8" xoffset="0" yoffset="0"/>
|
| 107 |
+
<!-- g -->
|
| 108 |
+
|
| 109 |
+
<char id="104" x="56" y="16" width="8" height="8" page="0" xadvance="8" xoffset="0" yoffset="0"/>
|
| 110 |
+
<!-- h -->
|
| 111 |
+
|
| 112 |
+
<char id="105" x="64" y="16" width="8" height="8" page="0" xadvance="5" xoffset="0" yoffset="0"/>
|
| 113 |
+
<!-- i -->
|
| 114 |
+
|
| 115 |
+
<char id="106" x="72" y="16" width="8" height="8" page="0" xadvance="7" xoffset="0" yoffset="0"/>
|
| 116 |
+
<!-- j -->
|
| 117 |
+
|
| 118 |
+
<char id="107" x="80" y="16" width="8" height="8" page="0" xadvance="8" xoffset="0" yoffset="0"/>
|
| 119 |
+
<!-- k -->
|
| 120 |
+
|
| 121 |
+
<char id="108" x="88" y="16" width="8" height="8" page="0" xadvance="5" xoffset="0" yoffset="0"/>
|
| 122 |
+
<!-- l -->
|
| 123 |
+
|
| 124 |
+
<char id="109" x="96" y="16" width="8" height="8" page="0" xadvance="9" xoffset="0" yoffset="0"/>
|
| 125 |
+
<!-- m -->
|
| 126 |
+
|
| 127 |
+
<char id="110" x="0" y="24" width="8" height="8" page="0" xadvance="8" xoffset="0" yoffset="0"/>
|
| 128 |
+
<!-- n -->
|
| 129 |
+
|
| 130 |
+
<char id="111" x="8" y="24" width="8" height="8" page="0" xadvance="8" xoffset="0" yoffset="0"/>
|
| 131 |
+
<!-- o -->
|
| 132 |
+
|
| 133 |
+
<char id="112" x="16" y="24" width="8" height="8" page="0" xadvance="8" xoffset="0" yoffset="0"/>
|
| 134 |
+
<!-- p -->
|
| 135 |
+
|
| 136 |
+
<char id="113" x="24" y="24" width="8" height="8" page="0" xadvance="8" xoffset="0" yoffset="0"/>
|
| 137 |
+
<!-- q -->
|
| 138 |
+
|
| 139 |
+
<char id="114" x="32" y="24" width="8" height="8" page="0" xadvance="8" xoffset="0" yoffset="0"/>
|
| 140 |
+
<!-- r -->
|
| 141 |
+
|
| 142 |
+
<char id="115" x="40" y="24" width="8" height="8" page="0" xadvance="8" xoffset="0" yoffset="0"/>
|
| 143 |
+
<!-- s -->
|
| 144 |
+
|
| 145 |
+
<char id="116" x="48" y="24" width="8" height="8" page="0" xadvance="8" xoffset="0" yoffset="0"/>
|
| 146 |
+
<!-- t -->
|
| 147 |
+
|
| 148 |
+
<char id="117" x="56" y="24" width="8" height="8" page="0" xadvance="8" xoffset="0" yoffset="0"/>
|
| 149 |
+
<!-- u -->
|
| 150 |
+
|
| 151 |
+
<char id="118" x="64" y="24" width="8" height="8" page="0" xadvance="8" xoffset="0" yoffset="0"/>
|
| 152 |
+
<!-- v -->
|
| 153 |
+
|
| 154 |
+
<char id="119" x="72" y="24" width="8" height="8" page="0" xadvance="9" xoffset="0" yoffset="0"/>
|
| 155 |
+
<!-- w -->
|
| 156 |
+
|
| 157 |
+
<char id="120" x="80" y="24" width="8" height="8" page="0" xadvance="8" xoffset="0" yoffset="0"/>
|
| 158 |
+
<!-- x -->
|
| 159 |
+
|
| 160 |
+
<char id="121" x="88" y="24" width="8" height="8" page="0" xadvance="8" xoffset="0" yoffset="0"/>
|
| 161 |
+
<!-- y -->
|
| 162 |
+
|
| 163 |
+
<char id="122" x="96" y="24" width="8" height="8" page="0" xadvance="8" xoffset="0" yoffset="0"/>
|
| 164 |
+
<!-- z -->
|
| 165 |
+
|
| 166 |
+
<char id="48" x="0" y="32" width="8" height="8" page="0" xadvance="8" xoffset="0" yoffset="0"/>
|
| 167 |
+
<!-- 0 -->
|
| 168 |
+
|
| 169 |
+
<char id="49" x="8" y="32" width="8" height="8" page="0" xadvance="8" xoffset="0" yoffset="0"/>
|
| 170 |
+
<!-- 1 -->
|
| 171 |
+
|
| 172 |
+
<char id="50" x="16" y="32" width="8" height="8" page="0" xadvance="8" xoffset="0" yoffset="0"/>
|
| 173 |
+
<!-- 2 -->
|
| 174 |
+
|
| 175 |
+
<char id="51" x="24" y="32" width="8" height="8" page="0" xadvance="8" xoffset="0" yoffset="0"/>
|
| 176 |
+
<!-- 3 -->
|
| 177 |
+
|
| 178 |
+
<char id="52" x="32" y="32" width="8" height="8" page="0" xadvance="8" xoffset="0" yoffset="0"/>
|
| 179 |
+
<!-- 4 -->
|
| 180 |
+
|
| 181 |
+
<char id="53" x="40" y="32" width="8" height="8" page="0" xadvance="8" xoffset="0" yoffset="0"/>
|
| 182 |
+
<!-- 5 -->
|
| 183 |
+
|
| 184 |
+
<char id="54" x="48" y="32" width="8" height="8" page="0" xadvance="8" xoffset="0" yoffset="0"/>
|
| 185 |
+
<!-- 6 -->
|
| 186 |
+
|
| 187 |
+
<char id="55" x="56" y="32" width="8" height="8" page="0" xadvance="8" xoffset="0" yoffset="0"/>
|
| 188 |
+
<!-- 7 -->
|
| 189 |
+
|
| 190 |
+
<char id="56" x="64" y="32" width="8" height="8" page="0" xadvance="8" xoffset="0" yoffset="0"/>
|
| 191 |
+
<!-- 8 -->
|
| 192 |
+
|
| 193 |
+
<char id="57" x="72" y="32" width="8" height="8" page="0" xadvance="8" xoffset="0" yoffset="0"/>
|
| 194 |
+
<!-- 9 -->
|
| 195 |
+
|
| 196 |
+
<char id="43" x="80" y="32" width="8" height="8" page="0" xadvance="8" xoffset="0" yoffset="0"/>
|
| 197 |
+
<!-- + -->
|
| 198 |
+
|
| 199 |
+
<char id="45" x="88" y="32" width="8" height="8" page="0" xadvance="8" xoffset="0" yoffset="0"/>
|
| 200 |
+
<!-- - -->
|
| 201 |
+
|
| 202 |
+
<char id="61" x="96" y="32" width="8" height="8" page="0" xadvance="8" xoffset="0" yoffset="0"/>
|
| 203 |
+
<!-- = -->
|
| 204 |
+
|
| 205 |
+
<char id="40" x="0" y="40" width="8" height="8" page="0" xadvance="8" xoffset="0" yoffset="0"/>
|
| 206 |
+
<!-- ( -->
|
| 207 |
+
|
| 208 |
+
<char id="41" x="8" y="40" width="8" height="8" page="0" xadvance="8" xoffset="0" yoffset="0"/>
|
| 209 |
+
<!-- ) -->
|
| 210 |
+
|
| 211 |
+
<char id="91" x="16" y="40" width="8" height="8" page="0" xadvance="8" xoffset="0" yoffset="0"/>
|
| 212 |
+
<!-- [ -->
|
| 213 |
+
|
| 214 |
+
<char id="93" x="24" y="40" width="8" height="8" page="0" xadvance="8" xoffset="0" yoffset="0"/>
|
| 215 |
+
<!-- ] -->
|
| 216 |
+
|
| 217 |
+
<char id="123" x="32" y="40" width="8" height="8" page="0" xadvance="8" xoffset="0" yoffset="0"/>
|
| 218 |
+
<!-- { -->
|
| 219 |
+
|
| 220 |
+
<char id="125" x="40" y="40" width="8" height="8" page="0" xadvance="8" xoffset="0" yoffset="0"/>
|
| 221 |
+
<!-- } -->
|
| 222 |
+
|
| 223 |
+
<char id="60" x="48" y="40" width="8" height="8" page="0" xadvance="8" xoffset="0" yoffset="0"/>
|
| 224 |
+
<!-- < -->
|
| 225 |
+
|
| 226 |
+
<char id="62" x="56" y="40" width="8" height="8" page="0" xadvance="8" xoffset="0" yoffset="0"/>
|
| 227 |
+
<!-- > -->
|
| 228 |
+
|
| 229 |
+
<char id="47" x="64" y="40" width="8" height="8" page="0" xadvance="6" xoffset="0" yoffset="0"/>
|
| 230 |
+
<!-- / -->
|
| 231 |
+
|
| 232 |
+
<char id="42" x="72" y="40" width="8" height="8" page="0" xadvance="8" xoffset="0" yoffset="0"/>
|
| 233 |
+
<!-- * -->
|
| 234 |
+
|
| 235 |
+
<char id="58" x="80" y="40" width="8" height="8" page="0" xadvance="8" xoffset="0" yoffset="0"/>
|
| 236 |
+
<!-- : -->
|
| 237 |
+
|
| 238 |
+
<char id="35" x="88" y="40" width="8" height="8" page="0" xadvance="8" xoffset="0" yoffset="0"/>
|
| 239 |
+
<!-- # -->
|
| 240 |
+
|
| 241 |
+
<char id="37" x="96" y="40" width="8" height="8" page="0" xadvance="8" xoffset="0" yoffset="0"/>
|
| 242 |
+
<!-- % -->
|
| 243 |
+
|
| 244 |
+
<char id="33" x="0" y="48" width="8" height="8" page="0" xadvance="4" xoffset="0" yoffset="0"/>
|
| 245 |
+
<!-- ! -->
|
| 246 |
+
|
| 247 |
+
<char id="63" x="8" y="48" width="8" height="8" page="0" xadvance="8" xoffset="0" yoffset="0"/>
|
| 248 |
+
<!-- ? -->
|
| 249 |
+
|
| 250 |
+
<char id="46" x="16" y="48" width="8" height="8" page="0" xadvance="4" xoffset="0" yoffset="0"/>
|
| 251 |
+
<!-- . -->
|
| 252 |
+
|
| 253 |
+
<char id="44" x="24" y="48" width="8" height="8" page="0" xadvance="6" xoffset="0" yoffset="0"/>
|
| 254 |
+
<!-- , -->
|
| 255 |
+
|
| 256 |
+
<char id="39" x="32" y="48" width="8" height="8" page="0" xadvance="4" xoffset="0" yoffset="0"/>
|
| 257 |
+
<!-- ' -->
|
| 258 |
+
|
| 259 |
+
<char id="34" x="40" y="48" width="8" height="8" page="0" xadvance="7" xoffset="0" yoffset="0"/>
|
| 260 |
+
<!-- " -->
|
| 261 |
+
|
| 262 |
+
<char id="64" x="48" y="48" width="8" height="8" page="0" xadvance="8" xoffset="0" yoffset="0"/>
|
| 263 |
+
<!-- @ -->
|
| 264 |
+
|
| 265 |
+
<char id="38" x="56" y="48" width="8" height="8" page="0" xadvance="8" xoffset="0" yoffset="0"/>
|
| 266 |
+
<!-- & -->
|
| 267 |
+
|
| 268 |
+
<char id="36" x="64" y="48" width="8" height="8" page="0" xadvance="8" xoffset="0" yoffset="0"/>
|
| 269 |
+
<!-- $ -->
|
| 270 |
+
|
| 271 |
+
<char id="32" x="72" y="48" width="8" height="8" page="0" xadvance="6" xoffset="0" yoffset="0"/>
|
| 272 |
+
<!-- -->
|
| 273 |
+
|
| 274 |
+
</chars>
|
| 275 |
+
</font>
|
| 276 |
+
|
public/assets/game-over.png
ADDED
|
public/assets/music/level-1/track.mp3
ADDED
|
@@ -0,0 +1,3 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
version https://git-lfs.github.com/spec/v1
|
| 2 |
+
oid sha256:2d7c6678c69eb655135dead8900255f92addcd43829573c8a5b51250c8362a66
|
| 3 |
+
size 3095196
|
public/assets/music/level-10/track.mp3
ADDED
|
@@ -0,0 +1,3 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
version https://git-lfs.github.com/spec/v1
|
| 2 |
+
oid sha256:bfe26f1a1561409094c22f299d56c0bc669081397a62e6b33b5ee8f0942ed2cf
|
| 3 |
+
size 3379882
|
public/assets/music/level-2/track.mp3
ADDED
|
@@ -0,0 +1,3 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
version https://git-lfs.github.com/spec/v1
|
| 2 |
+
oid sha256:0a963babd307c9dc6f4130cdf1a46a325a016e20c855f26b02147584fbd6db00
|
| 3 |
+
size 3552437
|
public/assets/music/level-3/track.mp3
ADDED
|
@@ -0,0 +1,3 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
version https://git-lfs.github.com/spec/v1
|
| 2 |
+
oid sha256:da2f3c48ba7204dbe4c8204e7ab975f0790096a02c44e8c9c251be2196043377
|
| 3 |
+
size 2951867
|
public/assets/music/level-4/track.mp3
ADDED
|
@@ -0,0 +1,3 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
version https://git-lfs.github.com/spec/v1
|
| 2 |
+
oid sha256:c5fe5a6007c042b9173e1b1a4c8cc34b642ad18ba7f0332d1c7ec7ee48717bc3
|
| 3 |
+
size 2826888
|
public/assets/music/level-5/track.mp3
ADDED
|
@@ -0,0 +1,3 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
version https://git-lfs.github.com/spec/v1
|
| 2 |
+
oid sha256:6d80b9049d1ec638813ae6332427ddda79843255b1fbc78fd8281791d8c1940f
|
| 3 |
+
size 2922595
|
public/assets/music/level-6/track.mp3
ADDED
|
@@ -0,0 +1,3 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
version https://git-lfs.github.com/spec/v1
|
| 2 |
+
oid sha256:dc4a2932dc8dcee1d2d69f122bf35bd3fe1a10fe3f271e30053f4ff0a36f4141
|
| 3 |
+
size 2504409
|
public/assets/music/level-7/track.mp3
ADDED
|
@@ -0,0 +1,3 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
version https://git-lfs.github.com/spec/v1
|
| 2 |
+
oid sha256:2ec0c8c525cfd451c194eded97ca17ded86be87a457e016732739c50376ebccd
|
| 3 |
+
size 2747379
|
public/assets/music/level-8/track.mp3
ADDED
|
@@ -0,0 +1,3 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
version https://git-lfs.github.com/spec/v1
|
| 2 |
+
oid sha256:4a7fdf42c917607f67c0ecc24869c59d7c19f3d87c33232b01b639b306593c11
|
| 3 |
+
size 3645189
|
public/assets/music/level-9/track.mp3
ADDED
|
@@ -0,0 +1,3 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
version https://git-lfs.github.com/spec/v1
|
| 2 |
+
oid sha256:e35ae825ee92e94e986e8e1cb63f96b42b3a9e58a87fa2b02e9e430cae54d30a
|
| 3 |
+
size 3038032
|
public/assets/title.png
ADDED
|
public/assets/tv.png
ADDED
|
Git LFS Details
|
public/template-generator.html
ADDED
|
@@ -0,0 +1,184 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
<!DOCTYPE html>
|
| 2 |
+
<html lang="en">
|
| 3 |
+
<head>
|
| 4 |
+
<meta charset="UTF-8">
|
| 5 |
+
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
| 6 |
+
<title>Backdrop Template Generator</title>
|
| 7 |
+
<style>
|
| 8 |
+
body {
|
| 9 |
+
margin: 20px;
|
| 10 |
+
font-family: monospace;
|
| 11 |
+
background: #222;
|
| 12 |
+
color: #fff;
|
| 13 |
+
}
|
| 14 |
+
canvas {
|
| 15 |
+
border: 2px solid #fff;
|
| 16 |
+
image-rendering: pixelated;
|
| 17 |
+
image-rendering: crisp-edges;
|
| 18 |
+
display: block;
|
| 19 |
+
margin: 20px 0;
|
| 20 |
+
}
|
| 21 |
+
button {
|
| 22 |
+
background: #4CAF50;
|
| 23 |
+
color: white;
|
| 24 |
+
padding: 10px 20px;
|
| 25 |
+
border: none;
|
| 26 |
+
cursor: pointer;
|
| 27 |
+
font-family: monospace;
|
| 28 |
+
font-size: 14px;
|
| 29 |
+
margin: 5px;
|
| 30 |
+
}
|
| 31 |
+
button:hover {
|
| 32 |
+
background: #45a049;
|
| 33 |
+
}
|
| 34 |
+
.info {
|
| 35 |
+
background: #333;
|
| 36 |
+
padding: 15px;
|
| 37 |
+
margin: 10px 0;
|
| 38 |
+
border-left: 4px solid #4CAF50;
|
| 39 |
+
}
|
| 40 |
+
</style>
|
| 41 |
+
</head>
|
| 42 |
+
<body>
|
| 43 |
+
<h1>Tetris Backdrop Template Generator</h1>
|
| 44 |
+
|
| 45 |
+
<div class="info">
|
| 46 |
+
<h3>Instructions:</h3>
|
| 47 |
+
<ol>
|
| 48 |
+
<li>Click "Download Template" to save the template image</li>
|
| 49 |
+
<li>Open the template in your image editor (Photoshop, GIMP, etc.)</li>
|
| 50 |
+
<li>The <strong style="color: #000;">BLACK area with MAGENTA border</strong> is the play area (80×160 pixels)</li>
|
| 51 |
+
<li>The play area will be BLACK in-game, so design around it</li>
|
| 52 |
+
<li>Create your artwork on layers below the template</li>
|
| 53 |
+
<li>Delete the template layer when done</li>
|
| 54 |
+
<li>Export as 256×224 PNG</li>
|
| 55 |
+
<li>Save to <code>public/assets/backdrops/level-X/backdrop.png</code></li>
|
| 56 |
+
</ol>
|
| 57 |
+
</div>
|
| 58 |
+
|
| 59 |
+
<canvas id="template" width="256" height="224"></canvas>
|
| 60 |
+
|
| 61 |
+
<button onclick="downloadTemplate()">Download Template (BACKDROP-TEMPLATE.png)</button>
|
| 62 |
+
<button onclick="downloadBlankTemplate()">Download Blank Template (no labels)</button>
|
| 63 |
+
|
| 64 |
+
<div class="info">
|
| 65 |
+
<h3>Specifications:</h3>
|
| 66 |
+
<ul>
|
| 67 |
+
<li><strong>Total Size:</strong> 256 × 224 pixels</li>
|
| 68 |
+
<li><strong>Play Area Position:</strong> X: 88, Y: 32</li>
|
| 69 |
+
<li><strong>Play Area Size:</strong> 80 × 160 pixels</li>
|
| 70 |
+
<li><strong>Grid:</strong> 10 blocks wide × 20 blocks tall</li>
|
| 71 |
+
<li><strong>Block Size:</strong> 8 × 8 pixels</li>
|
| 72 |
+
</ul>
|
| 73 |
+
</div>
|
| 74 |
+
|
| 75 |
+
<script>
|
| 76 |
+
const canvas = document.getElementById('template');
|
| 77 |
+
const ctx = canvas.getContext('2d');
|
| 78 |
+
|
| 79 |
+
function drawTemplate(includeLabels = true) {
|
| 80 |
+
// Background - light gray
|
| 81 |
+
ctx.fillStyle = '#CCCCCC';
|
| 82 |
+
ctx.fillRect(0, 0, 256, 224);
|
| 83 |
+
|
| 84 |
+
// Play area - BLACK (as it appears in game)
|
| 85 |
+
ctx.fillStyle = '#000000';
|
| 86 |
+
ctx.fillRect(88, 32, 80, 160);
|
| 87 |
+
|
| 88 |
+
// Magenta outline to show the boundary clearly
|
| 89 |
+
ctx.strokeStyle = '#FF00FF';
|
| 90 |
+
ctx.lineWidth = 2;
|
| 91 |
+
ctx.strokeRect(88, 32, 80, 160);
|
| 92 |
+
|
| 93 |
+
// Add grid lines in play area (light gray on black)
|
| 94 |
+
ctx.strokeStyle = '#444444';
|
| 95 |
+
ctx.lineWidth = 1;
|
| 96 |
+
// Vertical lines every 8 pixels
|
| 97 |
+
for (let x = 88; x <= 168; x += 8) {
|
| 98 |
+
ctx.beginPath();
|
| 99 |
+
ctx.moveTo(x, 32);
|
| 100 |
+
ctx.lineTo(x, 192);
|
| 101 |
+
ctx.stroke();
|
| 102 |
+
}
|
| 103 |
+
// Horizontal lines every 8 pixels
|
| 104 |
+
for (let y = 32; y <= 192; y += 8) {
|
| 105 |
+
ctx.beginPath();
|
| 106 |
+
ctx.moveTo(88, y);
|
| 107 |
+
ctx.lineTo(168, y);
|
| 108 |
+
ctx.stroke();
|
| 109 |
+
}
|
| 110 |
+
|
| 111 |
+
if (includeLabels) {
|
| 112 |
+
// Add labels
|
| 113 |
+
ctx.fillStyle = '#000000';
|
| 114 |
+
ctx.font = 'bold 12px monospace';
|
| 115 |
+
ctx.fillText('TETRIS BACKDROP TEMPLATE', 30, 15);
|
| 116 |
+
|
| 117 |
+
ctx.font = '10px monospace';
|
| 118 |
+
ctx.fillText('256 x 224 pixels', 85, 215);
|
| 119 |
+
|
| 120 |
+
// Play area label
|
| 121 |
+
ctx.fillStyle = '#00FF00';
|
| 122 |
+
ctx.font = 'bold 10px monospace';
|
| 123 |
+
ctx.fillText('PLAY AREA', 100, 110);
|
| 124 |
+
ctx.fillText('(BLACK)', 105, 122);
|
| 125 |
+
ctx.fillText('80 x 160', 105, 134);
|
| 126 |
+
|
| 127 |
+
// Coordinates
|
| 128 |
+
ctx.fillStyle = '#000000';
|
| 129 |
+
ctx.font = '8px monospace';
|
| 130 |
+
ctx.fillText('(88,32)', 90, 28);
|
| 131 |
+
ctx.fillText('(168,192)', 130, 200);
|
| 132 |
+
|
| 133 |
+
// UI area labels
|
| 134 |
+
ctx.fillStyle = '#666666';
|
| 135 |
+
ctx.font = '8px monospace';
|
| 136 |
+
ctx.fillText('SCORE/LEVEL', 10, 20);
|
| 137 |
+
ctx.fillText('AREA', 10, 30);
|
| 138 |
+
ctx.fillText('NEXT PIECE', 185, 20);
|
| 139 |
+
ctx.fillText('AREA', 185, 30);
|
| 140 |
+
}
|
| 141 |
+
|
| 142 |
+
// Corner markers
|
| 143 |
+
ctx.fillStyle = '#FF0000';
|
| 144 |
+
const markerSize = 6;
|
| 145 |
+
ctx.fillRect(88 - markerSize/2, 32 - markerSize/2, markerSize, markerSize);
|
| 146 |
+
ctx.fillRect(168 - markerSize/2, 32 - markerSize/2, markerSize, markerSize);
|
| 147 |
+
ctx.fillRect(88 - markerSize/2, 192 - markerSize/2, markerSize, markerSize);
|
| 148 |
+
ctx.fillRect(168 - markerSize/2, 192 - markerSize/2, markerSize, markerSize);
|
| 149 |
+
}
|
| 150 |
+
|
| 151 |
+
function downloadTemplate() {
|
| 152 |
+
ctx.clearRect(0, 0, 256, 224);
|
| 153 |
+
drawTemplate(true);
|
| 154 |
+
|
| 155 |
+
canvas.toBlob(function(blob) {
|
| 156 |
+
const url = URL.createObjectURL(blob);
|
| 157 |
+
const a = document.createElement('a');
|
| 158 |
+
a.href = url;
|
| 159 |
+
a.download = 'BACKDROP-TEMPLATE.png';
|
| 160 |
+
a.click();
|
| 161 |
+
URL.revokeObjectURL(url);
|
| 162 |
+
});
|
| 163 |
+
}
|
| 164 |
+
|
| 165 |
+
function downloadBlankTemplate() {
|
| 166 |
+
ctx.clearRect(0, 0, 256, 224);
|
| 167 |
+
drawTemplate(false);
|
| 168 |
+
|
| 169 |
+
canvas.toBlob(function(blob) {
|
| 170 |
+
const url = URL.createObjectURL(blob);
|
| 171 |
+
const a = document.createElement('a');
|
| 172 |
+
a.href = url;
|
| 173 |
+
a.download = 'BACKDROP-TEMPLATE-BLANK.png';
|
| 174 |
+
a.click();
|
| 175 |
+
URL.revokeObjectURL(url);
|
| 176 |
+
});
|
| 177 |
+
}
|
| 178 |
+
|
| 179 |
+
// Draw initial template
|
| 180 |
+
drawTemplate(true);
|
| 181 |
+
</script>
|
| 182 |
+
</body>
|
| 183 |
+
</html>
|
| 184 |
+
|
scripts/generate-placeholders.js
ADDED
|
@@ -0,0 +1,188 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
/**
|
| 2 |
+
* Generate placeholder assets for all 10 levels
|
| 3 |
+
* Run with: node scripts/generate-placeholders.js
|
| 4 |
+
*/
|
| 5 |
+
|
| 6 |
+
import fs from 'fs';
|
| 7 |
+
import path from 'path';
|
| 8 |
+
import { fileURLToPath } from 'url';
|
| 9 |
+
|
| 10 |
+
const __filename = fileURLToPath(import.meta.url);
|
| 11 |
+
const __dirname = path.dirname(__filename);
|
| 12 |
+
const projectRoot = path.join(__dirname, '..');
|
| 13 |
+
|
| 14 |
+
// Create directories
|
| 15 |
+
const assetsDir = path.join(projectRoot, 'public', 'assets');
|
| 16 |
+
const backdropsDir = path.join(assetsDir, 'backdrops');
|
| 17 |
+
const musicDir = path.join(assetsDir, 'music');
|
| 18 |
+
|
| 19 |
+
// Ensure directories exist
|
| 20 |
+
fs.mkdirSync(backdropsDir, { recursive: true });
|
| 21 |
+
fs.mkdirSync(musicDir, { recursive: true });
|
| 22 |
+
|
| 23 |
+
// Color palettes for each level
|
| 24 |
+
const levelPalettes = [
|
| 25 |
+
['#FF6B6B', '#4ECDC4', '#45B7D1', '#FFA07A', '#98D8C8', '#F7DC6F', '#BB8FCE'], // Level 1 - Warm
|
| 26 |
+
['#3498DB', '#E74C3C', '#2ECC71', '#F39C12', '#9B59B6', '#1ABC9C', '#E67E22'], // Level 2 - Primary
|
| 27 |
+
['#FF1744', '#00E676', '#2979FF', '#FFEA00', '#D500F9', '#00E5FF', '#FF9100'], // Level 3 - Neon
|
| 28 |
+
['#8E44AD', '#16A085', '#C0392B', '#F39C12', '#2980B9', '#27AE60', '#D35400'], // Level 4 - Deep
|
| 29 |
+
['#FF6F61', '#6B5B95', '#88B04B', '#F7CAC9', '#92A8D1', '#955251', '#B565A7'], // Level 5 - Pastel
|
| 30 |
+
['#34495E', '#E74C3C', '#ECF0F1', '#3498DB', '#2ECC71', '#F39C12', '#9B59B6'], // Level 6 - Modern
|
| 31 |
+
['#FF4500', '#FFD700', '#00CED1', '#FF1493', '#00FF00', '#1E90FF', '#FF69B4'], // Level 7 - Vibrant
|
| 32 |
+
['#8B4513', '#DAA520', '#CD853F', '#D2691E', '#B8860B', '#A0522D', '#DEB887'], // Level 8 - Earth
|
| 33 |
+
['#000080', '#4B0082', '#8B008B', '#9400D3', '#9932CC', '#BA55D3', '#DA70D6'], // Level 9 - Purple
|
| 34 |
+
['#FF0000', '#FF4500', '#FF6347', '#FF7F50', '#FFA500', '#FFD700', '#FFFF00'] // Level 10 - Fire
|
| 35 |
+
];
|
| 36 |
+
|
| 37 |
+
// Generate backdrop images using Canvas API (Node.js)
|
| 38 |
+
async function generateBackdrop(level, palette) {
|
| 39 |
+
const { createCanvas } = await import('canvas');
|
| 40 |
+
const canvas = createCanvas(256, 224);
|
| 41 |
+
const ctx = canvas.getContext('2d');
|
| 42 |
+
|
| 43 |
+
// Create gradient background
|
| 44 |
+
const gradient = ctx.createLinearGradient(0, 0, 256, 224);
|
| 45 |
+
gradient.addColorStop(0, palette[0]);
|
| 46 |
+
gradient.addColorStop(0.5, palette[1]);
|
| 47 |
+
gradient.addColorStop(1, palette[2]);
|
| 48 |
+
ctx.fillStyle = gradient;
|
| 49 |
+
ctx.fillRect(0, 0, 256, 224);
|
| 50 |
+
|
| 51 |
+
// Add some retro patterns
|
| 52 |
+
ctx.fillStyle = palette[3];
|
| 53 |
+
ctx.globalAlpha = 0.1;
|
| 54 |
+
for (let i = 0; i < 50; i++) {
|
| 55 |
+
const x = Math.random() * 256;
|
| 56 |
+
const y = Math.random() * 224;
|
| 57 |
+
const size = Math.random() * 20 + 5;
|
| 58 |
+
ctx.fillRect(x, y, size, size);
|
| 59 |
+
}
|
| 60 |
+
|
| 61 |
+
// Add level indicator in corner
|
| 62 |
+
ctx.globalAlpha = 0.3;
|
| 63 |
+
ctx.fillStyle = palette[4];
|
| 64 |
+
ctx.font = 'bold 48px monospace';
|
| 65 |
+
ctx.fillText(`L${level}`, 10, 50);
|
| 66 |
+
|
| 67 |
+
// PLAY AREA INDICATOR - Make it very clear
|
| 68 |
+
// Play area is at x:88, y:32, width:80, height:160
|
| 69 |
+
|
| 70 |
+
// Dark background for play area
|
| 71 |
+
ctx.globalAlpha = 0.4;
|
| 72 |
+
ctx.fillStyle = '#000000';
|
| 73 |
+
ctx.fillRect(88, 32, 80, 160);
|
| 74 |
+
|
| 75 |
+
// Bright border around play area
|
| 76 |
+
ctx.globalAlpha = 1.0;
|
| 77 |
+
ctx.strokeStyle = '#FFFF00'; // Bright yellow
|
| 78 |
+
ctx.lineWidth = 3;
|
| 79 |
+
ctx.strokeRect(88, 32, 80, 160);
|
| 80 |
+
|
| 81 |
+
// Add corner markers for extra visibility
|
| 82 |
+
ctx.fillStyle = '#FFFF00';
|
| 83 |
+
const markerSize = 8;
|
| 84 |
+
// Top-left corner
|
| 85 |
+
ctx.fillRect(88 - markerSize, 32 - markerSize, markerSize, markerSize);
|
| 86 |
+
// Top-right corner
|
| 87 |
+
ctx.fillRect(88 + 80, 32 - markerSize, markerSize, markerSize);
|
| 88 |
+
// Bottom-left corner
|
| 89 |
+
ctx.fillRect(88 - markerSize, 32 + 160, markerSize, markerSize);
|
| 90 |
+
// Bottom-right corner
|
| 91 |
+
ctx.fillRect(88 + 80, 32 + 160, markerSize, markerSize);
|
| 92 |
+
|
| 93 |
+
// Add text labels
|
| 94 |
+
ctx.globalAlpha = 0.8;
|
| 95 |
+
ctx.fillStyle = '#FFFFFF';
|
| 96 |
+
ctx.font = 'bold 10px monospace';
|
| 97 |
+
ctx.fillText('PLAY AREA', 92, 28);
|
| 98 |
+
ctx.fillText('80x160px', 95, 200);
|
| 99 |
+
ctx.fillText(`(${88},${32})`, 92, 44);
|
| 100 |
+
|
| 101 |
+
// Add grid lines inside play area to show it clearly
|
| 102 |
+
ctx.globalAlpha = 0.15;
|
| 103 |
+
ctx.strokeStyle = '#FFFFFF';
|
| 104 |
+
ctx.lineWidth = 1;
|
| 105 |
+
// Vertical lines every 8 pixels (block size)
|
| 106 |
+
for (let x = 88; x <= 168; x += 8) {
|
| 107 |
+
ctx.beginPath();
|
| 108 |
+
ctx.moveTo(x, 32);
|
| 109 |
+
ctx.lineTo(x, 192);
|
| 110 |
+
ctx.stroke();
|
| 111 |
+
}
|
| 112 |
+
// Horizontal lines every 8 pixels
|
| 113 |
+
for (let y = 32; y <= 192; y += 8) {
|
| 114 |
+
ctx.beginPath();
|
| 115 |
+
ctx.moveTo(88, y);
|
| 116 |
+
ctx.lineTo(168, y);
|
| 117 |
+
ctx.stroke();
|
| 118 |
+
}
|
| 119 |
+
|
| 120 |
+
return canvas.toBuffer('image/png');
|
| 121 |
+
}
|
| 122 |
+
|
| 123 |
+
// Generate silent MP3 placeholder (we'll create a minimal valid MP3)
|
| 124 |
+
function generateSilentMP3() {
|
| 125 |
+
// Minimal valid MP3 header for 1 second of silence
|
| 126 |
+
// This is a simplified approach - in production you'd use a proper audio library
|
| 127 |
+
const mp3Header = Buffer.from([
|
| 128 |
+
0xFF, 0xFB, 0x90, 0x00, // MP3 sync word and header
|
| 129 |
+
]);
|
| 130 |
+
|
| 131 |
+
// Create a small buffer with MP3 frame headers
|
| 132 |
+
const frames = 38; // Approximately 1 second at 44.1kHz
|
| 133 |
+
const frameSize = 417;
|
| 134 |
+
const buffer = Buffer.alloc(frames * frameSize);
|
| 135 |
+
|
| 136 |
+
for (let i = 0; i < frames; i++) {
|
| 137 |
+
mp3Header.copy(buffer, i * frameSize);
|
| 138 |
+
}
|
| 139 |
+
|
| 140 |
+
return buffer;
|
| 141 |
+
}
|
| 142 |
+
|
| 143 |
+
// Main generation function
|
| 144 |
+
async function generateAllAssets() {
|
| 145 |
+
console.log('Generating placeholder assets...\n');
|
| 146 |
+
|
| 147 |
+
// Check if canvas is available
|
| 148 |
+
let canvasAvailable = false;
|
| 149 |
+
try {
|
| 150 |
+
await import('canvas');
|
| 151 |
+
canvasAvailable = true;
|
| 152 |
+
} catch (e) {
|
| 153 |
+
console.log('⚠️ Canvas module not available. Install with: npm install canvas');
|
| 154 |
+
console.log(' Skipping backdrop generation. You can add your own PNG files.\n');
|
| 155 |
+
}
|
| 156 |
+
|
| 157 |
+
for (let level = 1; level <= 10; level++) {
|
| 158 |
+
const levelBackdropDir = path.join(backdropsDir, `level-${level}`);
|
| 159 |
+
const levelMusicDir = path.join(musicDir, `level-${level}`);
|
| 160 |
+
|
| 161 |
+
fs.mkdirSync(levelBackdropDir, { recursive: true });
|
| 162 |
+
fs.mkdirSync(levelMusicDir, { recursive: true });
|
| 163 |
+
|
| 164 |
+
// Generate backdrop
|
| 165 |
+
if (canvasAvailable) {
|
| 166 |
+
const backdropPath = path.join(levelBackdropDir, 'backdrop.png');
|
| 167 |
+
const backdropBuffer = await generateBackdrop(level, levelPalettes[level - 1]);
|
| 168 |
+
fs.writeFileSync(backdropPath, backdropBuffer);
|
| 169 |
+
console.log(`✓ Generated backdrop for level ${level}`);
|
| 170 |
+
} else {
|
| 171 |
+
console.log(`⊘ Skipped backdrop for level ${level} (canvas not available)`);
|
| 172 |
+
}
|
| 173 |
+
|
| 174 |
+
// Generate silent MP3
|
| 175 |
+
const musicPath = path.join(levelMusicDir, 'track.mp3');
|
| 176 |
+
const mp3Buffer = generateSilentMP3();
|
| 177 |
+
fs.writeFileSync(musicPath, mp3Buffer);
|
| 178 |
+
console.log(`✓ Generated music placeholder for level ${level}`);
|
| 179 |
+
}
|
| 180 |
+
|
| 181 |
+
console.log('\n✅ All placeholder assets generated!');
|
| 182 |
+
console.log('\nYou can now replace these files with your own:');
|
| 183 |
+
console.log(' - Backdrops: public/assets/backdrops/level-X/backdrop.png (256x224 pixels)');
|
| 184 |
+
console.log(' - Music: public/assets/music/level-X/track.mp3');
|
| 185 |
+
}
|
| 186 |
+
|
| 187 |
+
generateAllAssets().catch(console.error);
|
| 188 |
+
|
scripts/generate-template.js
ADDED
|
@@ -0,0 +1,170 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
/**
|
| 2 |
+
* Generate a simple template image showing the play area
|
| 3 |
+
* Run with: node scripts/generate-template.js
|
| 4 |
+
*/
|
| 5 |
+
|
| 6 |
+
import fs from 'fs';
|
| 7 |
+
import path from 'path';
|
| 8 |
+
import { fileURLToPath } from 'url';
|
| 9 |
+
|
| 10 |
+
const __filename = fileURLToPath(import.meta.url);
|
| 11 |
+
const __dirname = path.dirname(__filename);
|
| 12 |
+
const projectRoot = path.join(__dirname, '..');
|
| 13 |
+
|
| 14 |
+
async function generateTemplate() {
|
| 15 |
+
try {
|
| 16 |
+
const { createCanvas } = await import('canvas');
|
| 17 |
+
const canvas = createCanvas(256, 224);
|
| 18 |
+
const ctx = canvas.getContext('2d');
|
| 19 |
+
|
| 20 |
+
console.log('Canvas created:', canvas.width, 'x', canvas.height);
|
| 21 |
+
|
| 22 |
+
// Set global alpha to fully opaque
|
| 23 |
+
ctx.globalAlpha = 1.0;
|
| 24 |
+
|
| 25 |
+
// Background - light gray (with explicit RGB)
|
| 26 |
+
ctx.fillStyle = 'rgb(204, 204, 204)';
|
| 27 |
+
ctx.fillRect(0, 0, 256, 224);
|
| 28 |
+
|
| 29 |
+
// Play area - bright magenta/pink (easy to see and select in image editors)
|
| 30 |
+
ctx.fillStyle = 'rgb(255, 0, 255)';
|
| 31 |
+
ctx.fillRect(88, 32, 80, 160);
|
| 32 |
+
|
| 33 |
+
// Add grid lines in play area
|
| 34 |
+
ctx.strokeStyle = '#CC00CC';
|
| 35 |
+
ctx.lineWidth = 1;
|
| 36 |
+
// Vertical lines every 8 pixels
|
| 37 |
+
for (let x = 88; x <= 168; x += 8) {
|
| 38 |
+
ctx.beginPath();
|
| 39 |
+
ctx.moveTo(x, 32);
|
| 40 |
+
ctx.lineTo(x, 192);
|
| 41 |
+
ctx.stroke();
|
| 42 |
+
}
|
| 43 |
+
// Horizontal lines every 8 pixels
|
| 44 |
+
for (let y = 32; y <= 192; y += 8) {
|
| 45 |
+
ctx.beginPath();
|
| 46 |
+
ctx.moveTo(88, y);
|
| 47 |
+
ctx.lineTo(168, y);
|
| 48 |
+
ctx.stroke();
|
| 49 |
+
}
|
| 50 |
+
|
| 51 |
+
// Border around play area - black
|
| 52 |
+
ctx.strokeStyle = '#000000';
|
| 53 |
+
ctx.lineWidth = 2;
|
| 54 |
+
ctx.strokeRect(88, 32, 80, 160);
|
| 55 |
+
|
| 56 |
+
// Add labels
|
| 57 |
+
ctx.fillStyle = '#000000';
|
| 58 |
+
ctx.font = 'bold 12px sans-serif';
|
| 59 |
+
|
| 60 |
+
// Title
|
| 61 |
+
ctx.fillText('TETRIS BACKDROP TEMPLATE', 40, 15);
|
| 62 |
+
ctx.font = '10px sans-serif';
|
| 63 |
+
ctx.fillText('256 x 224 pixels', 85, 215);
|
| 64 |
+
|
| 65 |
+
// Play area label
|
| 66 |
+
ctx.fillStyle = '#FFFFFF';
|
| 67 |
+
ctx.font = 'bold 10px sans-serif';
|
| 68 |
+
ctx.fillText('PLAY AREA', 100, 110);
|
| 69 |
+
ctx.fillText('80 x 160', 105, 122);
|
| 70 |
+
|
| 71 |
+
// Coordinates
|
| 72 |
+
ctx.fillStyle = '#000000';
|
| 73 |
+
ctx.font = '8px sans-serif';
|
| 74 |
+
ctx.fillText('(88,32)', 90, 28);
|
| 75 |
+
ctx.fillText('(168,192)', 130, 200);
|
| 76 |
+
|
| 77 |
+
// UI area labels
|
| 78 |
+
ctx.fillStyle = '#666666';
|
| 79 |
+
ctx.font = '8px sans-serif';
|
| 80 |
+
ctx.fillText('SCORE/LEVEL', 10, 20);
|
| 81 |
+
ctx.fillText('AREA', 10, 30);
|
| 82 |
+
|
| 83 |
+
ctx.fillText('NEXT PIECE', 185, 20);
|
| 84 |
+
ctx.fillText('AREA', 185, 30);
|
| 85 |
+
|
| 86 |
+
// Corner markers
|
| 87 |
+
ctx.fillStyle = '#FF0000';
|
| 88 |
+
const markerSize = 6;
|
| 89 |
+
// Top-left
|
| 90 |
+
ctx.fillRect(88 - markerSize/2, 32 - markerSize/2, markerSize, markerSize);
|
| 91 |
+
// Top-right
|
| 92 |
+
ctx.fillRect(168 - markerSize/2, 32 - markerSize/2, markerSize, markerSize);
|
| 93 |
+
// Bottom-left
|
| 94 |
+
ctx.fillRect(88 - markerSize/2, 192 - markerSize/2, markerSize, markerSize);
|
| 95 |
+
// Bottom-right
|
| 96 |
+
ctx.fillRect(168 - markerSize/2, 192 - markerSize/2, markerSize, markerSize);
|
| 97 |
+
|
| 98 |
+
// Add dimension arrows
|
| 99 |
+
ctx.strokeStyle = '#000000';
|
| 100 |
+
ctx.lineWidth = 1;
|
| 101 |
+
ctx.fillStyle = '#000000';
|
| 102 |
+
|
| 103 |
+
// Width arrow (top)
|
| 104 |
+
ctx.beginPath();
|
| 105 |
+
ctx.moveTo(88, 25);
|
| 106 |
+
ctx.lineTo(168, 25);
|
| 107 |
+
ctx.stroke();
|
| 108 |
+
// Arrow heads
|
| 109 |
+
ctx.beginPath();
|
| 110 |
+
ctx.moveTo(88, 25);
|
| 111 |
+
ctx.lineTo(92, 23);
|
| 112 |
+
ctx.lineTo(92, 27);
|
| 113 |
+
ctx.closePath();
|
| 114 |
+
ctx.fill();
|
| 115 |
+
ctx.beginPath();
|
| 116 |
+
ctx.moveTo(168, 25);
|
| 117 |
+
ctx.lineTo(164, 23);
|
| 118 |
+
ctx.lineTo(164, 27);
|
| 119 |
+
ctx.closePath();
|
| 120 |
+
ctx.fill();
|
| 121 |
+
ctx.fillText('80px', 120, 23);
|
| 122 |
+
|
| 123 |
+
// Height arrow (left)
|
| 124 |
+
ctx.beginPath();
|
| 125 |
+
ctx.moveTo(82, 32);
|
| 126 |
+
ctx.lineTo(82, 192);
|
| 127 |
+
ctx.stroke();
|
| 128 |
+
// Arrow heads
|
| 129 |
+
ctx.beginPath();
|
| 130 |
+
ctx.moveTo(82, 32);
|
| 131 |
+
ctx.lineTo(80, 36);
|
| 132 |
+
ctx.lineTo(84, 36);
|
| 133 |
+
ctx.closePath();
|
| 134 |
+
ctx.fill();
|
| 135 |
+
ctx.beginPath();
|
| 136 |
+
ctx.moveTo(82, 192);
|
| 137 |
+
ctx.lineTo(80, 188);
|
| 138 |
+
ctx.lineTo(84, 188);
|
| 139 |
+
ctx.closePath();
|
| 140 |
+
ctx.fill();
|
| 141 |
+
|
| 142 |
+
ctx.save();
|
| 143 |
+
ctx.translate(75, 112);
|
| 144 |
+
ctx.rotate(-Math.PI / 2);
|
| 145 |
+
ctx.fillText('160px', -15, 0);
|
| 146 |
+
ctx.restore();
|
| 147 |
+
|
| 148 |
+
// Save the template
|
| 149 |
+
const templatePath = path.join(projectRoot, 'BACKDROP-TEMPLATE.png');
|
| 150 |
+
const buffer = canvas.toBuffer('image/png');
|
| 151 |
+
console.log('Buffer size:', buffer.length, 'bytes');
|
| 152 |
+
fs.writeFileSync(templatePath, buffer);
|
| 153 |
+
|
| 154 |
+
console.log('✅ Template created: BACKDROP-TEMPLATE.png');
|
| 155 |
+
console.log('');
|
| 156 |
+
console.log('The MAGENTA/PINK area (#FF00FF) is the play area.');
|
| 157 |
+
console.log('Use this template in your image editor:');
|
| 158 |
+
console.log(' 1. Open BACKDROP-TEMPLATE.png');
|
| 159 |
+
console.log(' 2. Create your artwork on layers below the template');
|
| 160 |
+
console.log(' 3. Delete or hide the template layer');
|
| 161 |
+
console.log(' 4. Export as 256x224 PNG');
|
| 162 |
+
console.log(' 5. Save to public/assets/backdrops/level-X/backdrop.png');
|
| 163 |
+
} catch (error) {
|
| 164 |
+
console.error('Error generating template:', error);
|
| 165 |
+
throw error;
|
| 166 |
+
}
|
| 167 |
+
}
|
| 168 |
+
|
| 169 |
+
generateTemplate().catch(console.error);
|
| 170 |
+
|
scripts/test-canvas.js
ADDED
|
@@ -0,0 +1,24 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import { createCanvas } from 'canvas';
|
| 2 |
+
import fs from 'fs';
|
| 3 |
+
|
| 4 |
+
const canvas = createCanvas(256, 224);
|
| 5 |
+
const ctx = canvas.getContext('2d');
|
| 6 |
+
|
| 7 |
+
// Fill with white
|
| 8 |
+
ctx.fillStyle = '#FFFFFF';
|
| 9 |
+
ctx.fillRect(0, 0, 256, 224);
|
| 10 |
+
|
| 11 |
+
// Draw a big red rectangle
|
| 12 |
+
ctx.fillStyle = '#FF0000';
|
| 13 |
+
ctx.fillRect(50, 50, 100, 100);
|
| 14 |
+
|
| 15 |
+
// Draw a blue circle
|
| 16 |
+
ctx.fillStyle = '#0000FF';
|
| 17 |
+
ctx.beginPath();
|
| 18 |
+
ctx.arc(200, 100, 30, 0, Math.PI * 2);
|
| 19 |
+
ctx.fill();
|
| 20 |
+
|
| 21 |
+
const buffer = canvas.toBuffer('image/png');
|
| 22 |
+
fs.writeFileSync('test-canvas.png', buffer);
|
| 23 |
+
console.log('Test image created:', buffer.length, 'bytes');
|
| 24 |
+
|
src/config.js
ADDED
|
@@ -0,0 +1,13 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
/**
|
| 2 |
+
* Game configuration
|
| 3 |
+
* Set different values for development vs production
|
| 4 |
+
*/
|
| 5 |
+
|
| 6 |
+
// Check if we're in production (deployed) or development (local)
|
| 7 |
+
const isProduction = import.meta.env.PROD;
|
| 8 |
+
|
| 9 |
+
export const CONFIG = {
|
| 10 |
+
// Number of lines needed to advance to the next level
|
| 11 |
+
LINES_PER_LEVEL: isProduction ? 15 : 2,
|
| 12 |
+
};
|
| 13 |
+
|
src/constants.js
ADDED
|
@@ -0,0 +1,188 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
// Game dimensions
|
| 2 |
+
export const GAME_WIDTH = 256;
|
| 3 |
+
export const GAME_HEIGHT = 224;
|
| 4 |
+
export const BORDER_OFFSET = 21; // Offset for 21px borders on each side
|
| 5 |
+
|
| 6 |
+
// Bitmap font character set
|
| 7 |
+
export const BITMAP_FONT_CHARS = 'ABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789';
|
| 8 |
+
|
| 9 |
+
// Grid configuration
|
| 10 |
+
export const BLOCK_SIZE = 8; // 8x8 pixel blocks for retro feel
|
| 11 |
+
export const GRID_WIDTH = 10;
|
| 12 |
+
export const GRID_HEIGHT = 20;
|
| 13 |
+
export const PLAY_AREA_WIDTH = GRID_WIDTH * BLOCK_SIZE; // 80 pixels
|
| 14 |
+
export const PLAY_AREA_HEIGHT = GRID_HEIGHT * BLOCK_SIZE; // 160 pixels
|
| 15 |
+
export const PLAY_AREA_X = 80 + BORDER_OFFSET; // Centered with room for UI + border offset
|
| 16 |
+
export const PLAY_AREA_Y = 28; // Room for header (moved up 20 pixels from original 48)
|
| 17 |
+
|
| 18 |
+
// Level progression
|
| 19 |
+
export const LINES_PER_LEVEL = 2; // Temporary for testing
|
| 20 |
+
export const MAX_LEVEL = 10;
|
| 21 |
+
|
| 22 |
+
// Tetromino shapes (NES Tetris style)
|
| 23 |
+
export const TETROMINOES = {
|
| 24 |
+
I: {
|
| 25 |
+
shape: [[1, 1, 1, 1]],
|
| 26 |
+
color: 0, // Will be replaced with palette color
|
| 27 |
+
name: 'I'
|
| 28 |
+
},
|
| 29 |
+
O: {
|
| 30 |
+
shape: [
|
| 31 |
+
[1, 1],
|
| 32 |
+
[1, 1]
|
| 33 |
+
],
|
| 34 |
+
color: 1,
|
| 35 |
+
name: 'O'
|
| 36 |
+
},
|
| 37 |
+
T: {
|
| 38 |
+
shape: [
|
| 39 |
+
[0, 1, 0],
|
| 40 |
+
[1, 1, 1]
|
| 41 |
+
],
|
| 42 |
+
color: 2,
|
| 43 |
+
name: 'T'
|
| 44 |
+
},
|
| 45 |
+
S: {
|
| 46 |
+
shape: [
|
| 47 |
+
[0, 1, 1],
|
| 48 |
+
[1, 1, 0]
|
| 49 |
+
],
|
| 50 |
+
color: 3,
|
| 51 |
+
name: 'S'
|
| 52 |
+
},
|
| 53 |
+
Z: {
|
| 54 |
+
shape: [
|
| 55 |
+
[1, 1, 0],
|
| 56 |
+
[0, 1, 1]
|
| 57 |
+
],
|
| 58 |
+
color: 4,
|
| 59 |
+
name: 'Z'
|
| 60 |
+
},
|
| 61 |
+
J: {
|
| 62 |
+
shape: [
|
| 63 |
+
[1, 0, 0],
|
| 64 |
+
[1, 1, 1]
|
| 65 |
+
],
|
| 66 |
+
color: 5,
|
| 67 |
+
name: 'J'
|
| 68 |
+
},
|
| 69 |
+
L: {
|
| 70 |
+
shape: [
|
| 71 |
+
[0, 0, 1],
|
| 72 |
+
[1, 1, 1]
|
| 73 |
+
],
|
| 74 |
+
color: 6,
|
| 75 |
+
name: 'L'
|
| 76 |
+
}
|
| 77 |
+
};
|
| 78 |
+
|
| 79 |
+
// Advanced mode tetrominoes (includes all classic + new shapes)
|
| 80 |
+
export const ADVANCED_TETROMINOES = {
|
| 81 |
+
...TETROMINOES,
|
| 82 |
+
// Small L (3 blocks) - top row
|
| 83 |
+
SMALL_L: {
|
| 84 |
+
shape: [
|
| 85 |
+
[1, 1],
|
| 86 |
+
[1, 0]
|
| 87 |
+
],
|
| 88 |
+
color: 0,
|
| 89 |
+
name: 'SMALL_L'
|
| 90 |
+
},
|
| 91 |
+
// Small L mirrored (3 blocks) - top row
|
| 92 |
+
SMALL_L_MIRROR: {
|
| 93 |
+
shape: [
|
| 94 |
+
[1, 1],
|
| 95 |
+
[0, 1]
|
| 96 |
+
],
|
| 97 |
+
color: 1,
|
| 98 |
+
name: 'SMALL_L_MIRROR'
|
| 99 |
+
},
|
| 100 |
+
// U shape (5 blocks) - middle left
|
| 101 |
+
U: {
|
| 102 |
+
shape: [
|
| 103 |
+
[1, 0, 1],
|
| 104 |
+
[1, 1, 1]
|
| 105 |
+
],
|
| 106 |
+
color: 2,
|
| 107 |
+
name: 'U'
|
| 108 |
+
},
|
| 109 |
+
// S shape (4 blocks) - middle right
|
| 110 |
+
S_ADVANCED: {
|
| 111 |
+
shape: [
|
| 112 |
+
[0, 1, 1],
|
| 113 |
+
[1, 1, 0]
|
| 114 |
+
],
|
| 115 |
+
color: 3,
|
| 116 |
+
name: 'S_ADVANCED'
|
| 117 |
+
},
|
| 118 |
+
// 2x2 with extra piece (5 blocks) - bottom left
|
| 119 |
+
BLOCK_PLUS: {
|
| 120 |
+
shape: [
|
| 121 |
+
[1, 1, 1],
|
| 122 |
+
[1, 1, 0]
|
| 123 |
+
],
|
| 124 |
+
color: 4,
|
| 125 |
+
name: 'BLOCK_PLUS'
|
| 126 |
+
},
|
| 127 |
+
// T with top extended (5 blocks) - bottom right
|
| 128 |
+
T_EXTENDED: {
|
| 129 |
+
shape: [
|
| 130 |
+
[0, 1, 0],
|
| 131 |
+
[0, 1, 1],
|
| 132 |
+
[0, 1, 0]
|
| 133 |
+
],
|
| 134 |
+
color: 5,
|
| 135 |
+
name: 'T_EXTENDED'
|
| 136 |
+
}
|
| 137 |
+
};
|
| 138 |
+
|
| 139 |
+
// NES Tetris scoring
|
| 140 |
+
export const SCORES = {
|
| 141 |
+
SINGLE: 40,
|
| 142 |
+
DOUBLE: 100,
|
| 143 |
+
TRIPLE: 300,
|
| 144 |
+
TETRIS: 1200,
|
| 145 |
+
SOFT_DROP: 1,
|
| 146 |
+
PERFECT_CLEAR: 10000 // Bonus for clearing entire field
|
| 147 |
+
};
|
| 148 |
+
|
| 149 |
+
// Game speeds (frames per drop) - Higher = slower
|
| 150 |
+
// Smooth progression, gets hard at level 6+
|
| 151 |
+
export const LEVEL_SPEEDS = [
|
| 152 |
+
90, // Level 1 - relaxed start
|
| 153 |
+
80, // Level 2
|
| 154 |
+
70, // Level 3
|
| 155 |
+
60, // Level 4
|
| 156 |
+
50, // Level 5
|
| 157 |
+
35, // Level 6 - starts getting hard
|
| 158 |
+
25, // Level 7
|
| 159 |
+
18, // Level 8
|
| 160 |
+
12, // Level 9
|
| 161 |
+
6 // Level 10 - very challenging
|
| 162 |
+
];
|
| 163 |
+
|
| 164 |
+
// Level titles for intro screens
|
| 165 |
+
export const LEVEL_TITLES = {
|
| 166 |
+
1: 'Low Earth Orbit',
|
| 167 |
+
2: 'Moon Surface',
|
| 168 |
+
3: 'Mars Horizon',
|
| 169 |
+
4: 'Asteroid Belt',
|
| 170 |
+
5: 'Jupiter Storms',
|
| 171 |
+
6: 'Deep Space Station',
|
| 172 |
+
7: 'Nebula Expanse',
|
| 173 |
+
8: 'Binary Star System',
|
| 174 |
+
9: 'Alien Megastructure',
|
| 175 |
+
10: 'Edge of the Universe'
|
| 176 |
+
};
|
| 177 |
+
|
| 178 |
+
// UI Layout - single panel to the right of play area
|
| 179 |
+
export const UI = {
|
| 180 |
+
// Panel positioned right of play area with 8px gap between frames
|
| 181 |
+
PANEL_X: PLAY_AREA_X + PLAY_AREA_WIDTH + 16,
|
| 182 |
+
PANEL_Y: PLAY_AREA_Y,
|
| 183 |
+
PANEL_WIDTH: 64,
|
| 184 |
+
PANEL_HEIGHT: PLAY_AREA_HEIGHT,
|
| 185 |
+
PADDING: 6,
|
| 186 |
+
LINE_HEIGHT: 20
|
| 187 |
+
};
|
| 188 |
+
|
src/main.js
ADDED
|
@@ -0,0 +1,40 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import Phaser from 'phaser';
|
| 2 |
+
import GameScene from './scenes/GameScene.js';
|
| 3 |
+
import PreloadScene from './scenes/PreloadScene.js';
|
| 4 |
+
import ModeSelectScene from './scenes/ModeSelectScene.js';
|
| 5 |
+
import { createShaderOverlay } from './shaderOverlay.js';
|
| 6 |
+
|
| 7 |
+
const config = {
|
| 8 |
+
type: Phaser.WEBGL,
|
| 9 |
+
width: 298, // 256 + 21px borders on each side (42px total)
|
| 10 |
+
height: 224,
|
| 11 |
+
parent: 'game-container',
|
| 12 |
+
backgroundColor: '#0a0a0a', // Dark grey for the borders
|
| 13 |
+
pixelArt: true,
|
| 14 |
+
roundPixels: true,
|
| 15 |
+
antialias: false,
|
| 16 |
+
fps: {
|
| 17 |
+
target: 60,
|
| 18 |
+
forceSetTimeOut: false
|
| 19 |
+
},
|
| 20 |
+
render: {
|
| 21 |
+
antialias: false,
|
| 22 |
+
pixelArt: true,
|
| 23 |
+
roundPixels: true,
|
| 24 |
+
antialiasGL: false
|
| 25 |
+
},
|
| 26 |
+
scale: {
|
| 27 |
+
mode: Phaser.Scale.NONE,
|
| 28 |
+
width: 298,
|
| 29 |
+
height: 224
|
| 30 |
+
},
|
| 31 |
+
scene: [PreloadScene, ModeSelectScene, GameScene]
|
| 32 |
+
};
|
| 33 |
+
|
| 34 |
+
const game = new Phaser.Game(config);
|
| 35 |
+
|
| 36 |
+
// Apply shader overlay to the scaled canvas
|
| 37 |
+
setTimeout(() => {
|
| 38 |
+
createShaderOverlay(game.canvas);
|
| 39 |
+
}, 100);
|
| 40 |
+
|
src/pipelines/TrinitronPipeline.js
ADDED
|
@@ -0,0 +1,190 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import Phaser from 'phaser';
|
| 2 |
+
|
| 3 |
+
const fragShader = `
|
| 4 |
+
precision mediump float;
|
| 5 |
+
|
| 6 |
+
uniform sampler2D uMainSampler;
|
| 7 |
+
uniform vec2 resolution;
|
| 8 |
+
uniform float time;
|
| 9 |
+
|
| 10 |
+
varying vec2 outTexCoord;
|
| 11 |
+
|
| 12 |
+
#define PI 3.14159265359
|
| 13 |
+
|
| 14 |
+
vec4 permute(vec4 t) {
|
| 15 |
+
return mod(((t * 34.0) + 1.0) * t, 289.0);
|
| 16 |
+
}
|
| 17 |
+
|
| 18 |
+
float noise3d(vec3 p) {
|
| 19 |
+
vec3 a = floor(p);
|
| 20 |
+
vec3 d = p - a;
|
| 21 |
+
d = d * d * (3.0 - 2.0 * d);
|
| 22 |
+
|
| 23 |
+
vec4 b = a.xxyy + vec4(0.0, 1.0, 0.0, 1.0);
|
| 24 |
+
vec4 k1 = permute(b.xyxy);
|
| 25 |
+
vec4 k2 = permute(k1.xyxy + b.zzww);
|
| 26 |
+
|
| 27 |
+
vec4 c = k2 + a.zzzz;
|
| 28 |
+
vec4 k3 = permute(c);
|
| 29 |
+
vec4 k4 = permute(c + 1.0);
|
| 30 |
+
|
| 31 |
+
vec4 o1 = fract(k3 * (1.0 / 41.0));
|
| 32 |
+
vec4 o2 = fract(k4 * (1.0 / 41.0));
|
| 33 |
+
|
| 34 |
+
vec4 o3_interp_z = o2 * d.z + o1 * (1.0 - d.z);
|
| 35 |
+
vec2 o4_interp_xy = o3_interp_z.yw * d.x + o3_interp_z.xz * (1.0 - d.x);
|
| 36 |
+
|
| 37 |
+
return o4_interp_xy.y * d.y + o4_interp_xy.x * (1.0 - d.y);
|
| 38 |
+
}
|
| 39 |
+
|
| 40 |
+
void main() {
|
| 41 |
+
float brightness = 2.5;
|
| 42 |
+
float red_balance = 1.0;
|
| 43 |
+
float green_balance = 0.85;
|
| 44 |
+
float blue_balance = 1.0;
|
| 45 |
+
|
| 46 |
+
float phosphorWidth = 2.50;
|
| 47 |
+
float phosphorHeight = 4.50;
|
| 48 |
+
float internalHorizontalGap = 1.0;
|
| 49 |
+
float columnGap = 0.2;
|
| 50 |
+
float verticalCellGap = 0.2;
|
| 51 |
+
float phosphorPower = 0.9;
|
| 52 |
+
|
| 53 |
+
float cell_noise_variation_amount = 0.025;
|
| 54 |
+
float cell_noise_scale_xy = 240.0;
|
| 55 |
+
float cell_noise_speed = 24.0;
|
| 56 |
+
float curvature_amount = 0.0;
|
| 57 |
+
|
| 58 |
+
vec2 fragCoord = gl_FragCoord.xy;
|
| 59 |
+
vec2 uv = outTexCoord;
|
| 60 |
+
vec2 centered_uv_output = uv - 0.5;
|
| 61 |
+
float r = length(centered_uv_output);
|
| 62 |
+
float distort_factor = 1.0 + curvature_amount * r * r;
|
| 63 |
+
vec2 centered_uv_source = centered_uv_output * distort_factor;
|
| 64 |
+
vec2 source_uv = centered_uv_source + 0.5;
|
| 65 |
+
vec2 fragCoord_warped = source_uv * resolution;
|
| 66 |
+
|
| 67 |
+
bool is_on_original_flat_screen = source_uv.x >= 0.0 && source_uv.x <= 1.0 &&
|
| 68 |
+
source_uv.y >= 0.0 && source_uv.y <= 1.0;
|
| 69 |
+
|
| 70 |
+
if (!is_on_original_flat_screen) {
|
| 71 |
+
gl_FragColor = vec4(0.0, 0.0, 0.0, 1.0);
|
| 72 |
+
return;
|
| 73 |
+
}
|
| 74 |
+
|
| 75 |
+
float fullCellWidth = 3.0 * phosphorWidth + 3.0 * internalHorizontalGap + columnGap;
|
| 76 |
+
float fullRowHeight = phosphorHeight + verticalCellGap;
|
| 77 |
+
|
| 78 |
+
float logical_cell_index_x = floor(fragCoord_warped.x / fullCellWidth);
|
| 79 |
+
float shift_y_offset = 0.0;
|
| 80 |
+
|
| 81 |
+
if (mod(logical_cell_index_x, 2.0) != 0.0) {
|
| 82 |
+
shift_y_offset = fullRowHeight / 2.0;
|
| 83 |
+
}
|
| 84 |
+
|
| 85 |
+
float effective_y_warped = fragCoord_warped.y + shift_y_offset;
|
| 86 |
+
float logical_row_index = floor(effective_y_warped / fullRowHeight);
|
| 87 |
+
|
| 88 |
+
float uv_cell_x = mod(fragCoord_warped.x, fullCellWidth);
|
| 89 |
+
if (uv_cell_x < 0.0) {
|
| 90 |
+
uv_cell_x += fullCellWidth;
|
| 91 |
+
}
|
| 92 |
+
|
| 93 |
+
float uv_row_y = mod(effective_y_warped, fullRowHeight);
|
| 94 |
+
if (uv_row_y < 0.0) {
|
| 95 |
+
uv_row_y += fullRowHeight;
|
| 96 |
+
}
|
| 97 |
+
|
| 98 |
+
vec3 video_color = texture2D(uMainSampler, source_uv).rgb;
|
| 99 |
+
video_color.r *= red_balance;
|
| 100 |
+
video_color.g *= green_balance;
|
| 101 |
+
video_color.b *= blue_balance;
|
| 102 |
+
|
| 103 |
+
vec3 final_color = vec3(0.0);
|
| 104 |
+
bool in_column_gap = uv_cell_x >= (3.0 * phosphorWidth + 3.0 * internalHorizontalGap);
|
| 105 |
+
bool in_vertical_gap = uv_row_y >= phosphorHeight;
|
| 106 |
+
|
| 107 |
+
if (!in_column_gap && !in_vertical_gap) {
|
| 108 |
+
float uv_cell_x_within_block = uv_cell_x;
|
| 109 |
+
vec3 phosphor_base_color = vec3(0.0);
|
| 110 |
+
float video_component_intensity = 0.0;
|
| 111 |
+
float current_phosphor_startX_in_block = -1.0;
|
| 112 |
+
float current_x_tracker = 0.0;
|
| 113 |
+
|
| 114 |
+
if (uv_cell_x_within_block >= current_x_tracker && uv_cell_x_within_block < current_x_tracker + phosphorWidth) {
|
| 115 |
+
phosphor_base_color = vec3(1.0, 0.0, 0.0);
|
| 116 |
+
video_component_intensity = video_color.r;
|
| 117 |
+
current_phosphor_startX_in_block = current_x_tracker;
|
| 118 |
+
}
|
| 119 |
+
current_x_tracker += phosphorWidth + internalHorizontalGap;
|
| 120 |
+
|
| 121 |
+
if (uv_cell_x_within_block >= current_x_tracker && uv_cell_x_within_block < current_x_tracker + phosphorWidth) {
|
| 122 |
+
phosphor_base_color = vec3(0.0, 1.0, 0.0);
|
| 123 |
+
video_component_intensity = video_color.g;
|
| 124 |
+
current_phosphor_startX_in_block = current_x_tracker;
|
| 125 |
+
}
|
| 126 |
+
current_x_tracker += phosphorWidth + internalHorizontalGap;
|
| 127 |
+
|
| 128 |
+
if (uv_cell_x_within_block >= current_x_tracker && uv_cell_x_within_block < current_x_tracker + phosphorWidth) {
|
| 129 |
+
phosphor_base_color = vec3(0.0, 0.0, 1.0);
|
| 130 |
+
video_component_intensity = video_color.b;
|
| 131 |
+
current_phosphor_startX_in_block = current_x_tracker;
|
| 132 |
+
}
|
| 133 |
+
|
| 134 |
+
if (current_phosphor_startX_in_block >= 0.0) {
|
| 135 |
+
float x_in_phosphor = (uv_cell_x_within_block - current_phosphor_startX_in_block) / phosphorWidth;
|
| 136 |
+
float horizontal_intensity_factor = pow(sin(x_in_phosphor * PI), phosphorPower);
|
| 137 |
+
float y_in_phosphor_band = uv_row_y / phosphorHeight;
|
| 138 |
+
float vertical_intensity_factor = (phosphorHeight > 0.0) ? pow(sin(y_in_phosphor_band * PI), phosphorPower) : 1.0;
|
| 139 |
+
float total_intensity_factor = horizontal_intensity_factor * vertical_intensity_factor;
|
| 140 |
+
final_color = phosphor_base_color * video_component_intensity * total_intensity_factor;
|
| 141 |
+
}
|
| 142 |
+
}
|
| 143 |
+
|
| 144 |
+
vec3 noise_pos = vec3(logical_cell_index_x * cell_noise_scale_xy,
|
| 145 |
+
logical_row_index * cell_noise_scale_xy,
|
| 146 |
+
time * cell_noise_speed);
|
| 147 |
+
|
| 148 |
+
vec3 cell_noise_rgb;
|
| 149 |
+
cell_noise_rgb.r = noise3d(noise_pos);
|
| 150 |
+
cell_noise_rgb.g = noise3d(noise_pos + vec3(19.0, 0.0, 0.0));
|
| 151 |
+
cell_noise_rgb.b = noise3d(noise_pos + vec3(0.0, 13.0, 0.0));
|
| 152 |
+
cell_noise_rgb = cell_noise_rgb * 2.0 - 1.0;
|
| 153 |
+
final_color += cell_noise_rgb * cell_noise_variation_amount;
|
| 154 |
+
|
| 155 |
+
final_color *= brightness;
|
| 156 |
+
float edge_darken_strength = 0.1;
|
| 157 |
+
float vignette_factor = 1.0 - dot(centered_uv_output, centered_uv_output) * edge_darken_strength * 2.0;
|
| 158 |
+
vignette_factor = clamp(vignette_factor, 0.0, 1.0);
|
| 159 |
+
final_color *= vignette_factor;
|
| 160 |
+
|
| 161 |
+
final_color = clamp(final_color, 0.0, 1.0);
|
| 162 |
+
gl_FragColor = vec4(final_color, 1.0);
|
| 163 |
+
}
|
| 164 |
+
`;
|
| 165 |
+
|
| 166 |
+
export default class TrinitronPipeline extends Phaser.Renderer.WebGL.Pipelines.PostFXPipeline {
|
| 167 |
+
constructor(game) {
|
| 168 |
+
super({
|
| 169 |
+
name: 'TrinitronPipeline',
|
| 170 |
+
game: game,
|
| 171 |
+
renderTarget: true,
|
| 172 |
+
fragShader: fragShader,
|
| 173 |
+
uniforms: [
|
| 174 |
+
'uMainSampler',
|
| 175 |
+
'resolution',
|
| 176 |
+
'time'
|
| 177 |
+
]
|
| 178 |
+
});
|
| 179 |
+
}
|
| 180 |
+
|
| 181 |
+
onPreRender() {
|
| 182 |
+
// Use the actual canvas display size (after scaling), not the game resolution
|
| 183 |
+
const canvas = this.game.canvas;
|
| 184 |
+
const displayWidth = canvas.width;
|
| 185 |
+
const displayHeight = canvas.height;
|
| 186 |
+
this.set2f('resolution', displayWidth, displayHeight);
|
| 187 |
+
this.set1f('time', this.game.loop.time / 1000);
|
| 188 |
+
}
|
| 189 |
+
}
|
| 190 |
+
|
src/scenes/GameScene.js
ADDED
|
@@ -0,0 +1,983 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import Phaser from 'phaser';
|
| 2 |
+
import {
|
| 3 |
+
GAME_WIDTH, GAME_HEIGHT, BLOCK_SIZE, GRID_WIDTH, GRID_HEIGHT,
|
| 4 |
+
PLAY_AREA_X, PLAY_AREA_Y, PLAY_AREA_WIDTH, PLAY_AREA_HEIGHT,
|
| 5 |
+
TETROMINOES, ADVANCED_TETROMINOES, SCORES, LEVEL_SPEEDS, MAX_LEVEL, UI, BORDER_OFFSET, LEVEL_TITLES
|
| 6 |
+
} from '../constants.js';
|
| 7 |
+
import ColorExtractor from '../utils/ColorExtractor.js';
|
| 8 |
+
import SpriteBlockRenderer from '../utils/SpriteBlockRenderer.js';
|
| 9 |
+
import SoundGenerator from '../utils/SoundGenerator.js';
|
| 10 |
+
import { CONFIG } from '../config.js';
|
| 11 |
+
|
| 12 |
+
export default class GameScene extends Phaser.Scene {
|
| 13 |
+
constructor() { super({ key: 'GameScene' }); }
|
| 14 |
+
|
| 15 |
+
create() {
|
| 16 |
+
// Get game mode from registry (set by ModeSelectScene)
|
| 17 |
+
this.gameMode = this.registry.get('gameMode') || 'classic';
|
| 18 |
+
this.tetrominoes = this.gameMode === 'advanced' ? ADVANCED_TETROMINOES : TETROMINOES;
|
| 19 |
+
|
| 20 |
+
// CRITICAL: Ensure canvas has focus and can receive keyboard events
|
| 21 |
+
this.game.canvas.setAttribute('tabindex', '1');
|
| 22 |
+
this.game.canvas.focus();
|
| 23 |
+
this.game.canvas.style.outline = 'none';
|
| 24 |
+
|
| 25 |
+
// Visual indicator for focus loss
|
| 26 |
+
this.focusWarning = null;
|
| 27 |
+
|
| 28 |
+
// Re-focus on any click
|
| 29 |
+
this.game.canvas.addEventListener('click', () => {
|
| 30 |
+
this.game.canvas.focus();
|
| 31 |
+
if (this.focusWarning) {
|
| 32 |
+
this.focusWarning.destroy();
|
| 33 |
+
this.focusWarning = null;
|
| 34 |
+
}
|
| 35 |
+
});
|
| 36 |
+
|
| 37 |
+
// Monitor focus state
|
| 38 |
+
this.game.canvas.addEventListener('blur', () => {
|
| 39 |
+
console.log('Canvas lost focus!');
|
| 40 |
+
if (!this.focusWarning) {
|
| 41 |
+
this.focusWarning = this.add.text(GAME_WIDTH / 2 + BORDER_OFFSET, 10, 'CLICK TO FOCUS', {
|
| 42 |
+
fontSize: '8px',
|
| 43 |
+
color: '#ff0000',
|
| 44 |
+
backgroundColor: '#000000'
|
| 45 |
+
}).setOrigin(0.5).setDepth(300);
|
| 46 |
+
}
|
| 47 |
+
});
|
| 48 |
+
|
| 49 |
+
this.game.canvas.addEventListener('focus', () => {
|
| 50 |
+
console.log('Canvas gained focus');
|
| 51 |
+
if (this.focusWarning) {
|
| 52 |
+
this.focusWarning.destroy();
|
| 53 |
+
this.focusWarning = null;
|
| 54 |
+
}
|
| 55 |
+
});
|
| 56 |
+
|
| 57 |
+
// Re-focus if window regains focus
|
| 58 |
+
window.addEventListener('focus', () => {
|
| 59 |
+
this.game.canvas.focus();
|
| 60 |
+
});
|
| 61 |
+
|
| 62 |
+
this.grid = this.createEmptyGrid();
|
| 63 |
+
this.score = 0; this.level = 1; this.lines = 0; this.gameOver = false;
|
| 64 |
+
this.clearing = false;
|
| 65 |
+
this.dropCounter = 0; this.dropInterval = LEVEL_SPEEDS[0];
|
| 66 |
+
this.softDropping = false; this.softDropCounter = 0;
|
| 67 |
+
this.inputEnabled = true;
|
| 68 |
+
this.currentPiece = null; this.nextPiece = null;
|
| 69 |
+
this.currentX = 0; this.currentY = 0;
|
| 70 |
+
this.blockSprites = []; this.ghostSprites = [];
|
| 71 |
+
this.setupInput();
|
| 72 |
+
this.loadLevel(this.level, false); // Load level first without intro
|
| 73 |
+
this.createUI(); // Create UI after level is loaded
|
| 74 |
+
this.spawnPiece(); this.nextPiece = this.getRandomPiece();
|
| 75 |
+
this.updateNextPieceDisplay();
|
| 76 |
+
|
| 77 |
+
// Show intro animation after everything is set up
|
| 78 |
+
this.showLevelIntro();
|
| 79 |
+
}
|
| 80 |
+
|
| 81 |
+
createEmptyGrid() {
|
| 82 |
+
const grid = [];
|
| 83 |
+
for (let y = 0; y < GRID_HEIGHT; y++) { grid[y] = []; for (let x = 0; x < GRID_WIDTH; x++) grid[y][x] = 0; }
|
| 84 |
+
return grid;
|
| 85 |
+
}
|
| 86 |
+
|
| 87 |
+
loadLevel(level, showIntro = false) {
|
| 88 |
+
if (this.currentMusic) this.currentMusic.stop();
|
| 89 |
+
const backdropKey = `backdrop-${level}`;
|
| 90 |
+
if (this.backdrop) this.backdrop.destroy();
|
| 91 |
+
this.backdrop = this.add.image(BORDER_OFFSET, 0, backdropKey).setOrigin(0, 0);
|
| 92 |
+
this.backdrop.setDisplaySize(GAME_WIDTH, GAME_HEIGHT);
|
| 93 |
+
this.backdrop.setDepth(-1);
|
| 94 |
+
this.colorPalette = ColorExtractor.extractPalette(this, backdropKey);
|
| 95 |
+
this.createBlockTextures();
|
| 96 |
+
const musicKey = `music-${level}`;
|
| 97 |
+
this.currentMusic = this.sound.add(musicKey, { loop: true, volume: 0.5 });
|
| 98 |
+
this.currentMusic.play();
|
| 99 |
+
this.redrawGrid();
|
| 100 |
+
|
| 101 |
+
if (showIntro) {
|
| 102 |
+
this.showLevelIntro();
|
| 103 |
+
}
|
| 104 |
+
}
|
| 105 |
+
|
| 106 |
+
showLevelIntro() {
|
| 107 |
+
// Immediately move containers off-screen (before any delay)
|
| 108 |
+
if (this.playAreaContainer) {
|
| 109 |
+
this.playAreaContainer.y = -GAME_HEIGHT;
|
| 110 |
+
}
|
| 111 |
+
if (this.uiPanelContainer) {
|
| 112 |
+
this.uiPanelContainer.y = -GAME_HEIGHT;
|
| 113 |
+
}
|
| 114 |
+
|
| 115 |
+
// Hide all block sprites (current piece and grid)
|
| 116 |
+
this.blockSprites.forEach(sprite => sprite.setVisible(false));
|
| 117 |
+
this.ghostSprites.forEach(sprite => sprite.setVisible(false));
|
| 118 |
+
|
| 119 |
+
// Disable input temporarily
|
| 120 |
+
this.inputEnabled = false;
|
| 121 |
+
|
| 122 |
+
// Show level title text
|
| 123 |
+
const levelTitle = LEVEL_TITLES[this.level] || 'Unknown';
|
| 124 |
+
const levelText = this.createBitmapText(GAME_WIDTH / 2 + BORDER_OFFSET, GAME_HEIGHT / 2 - 10, `LEVEL ${this.level}`, 16);
|
| 125 |
+
levelText.setOrigin(0.5);
|
| 126 |
+
levelText.setDepth(201);
|
| 127 |
+
|
| 128 |
+
const titleText = this.createBitmapText(GAME_WIDTH / 2 + BORDER_OFFSET, GAME_HEIGHT / 2 + 10, levelTitle, 10);
|
| 129 |
+
titleText.setOrigin(0.5);
|
| 130 |
+
titleText.setDepth(201);
|
| 131 |
+
|
| 132 |
+
// Wait 1 second showing backdrop and title
|
| 133 |
+
this.time.delayedCall(1500, () => {
|
| 134 |
+
// Fade out title texts
|
| 135 |
+
this.tweens.add({
|
| 136 |
+
targets: [levelText, titleText],
|
| 137 |
+
alpha: 0,
|
| 138 |
+
duration: 300,
|
| 139 |
+
onComplete: () => {
|
| 140 |
+
levelText.destroy();
|
| 141 |
+
titleText.destroy();
|
| 142 |
+
}
|
| 143 |
+
});
|
| 144 |
+
|
| 145 |
+
// Play woosh sound
|
| 146 |
+
SoundGenerator.playWoosh();
|
| 147 |
+
|
| 148 |
+
// Animate play area falling in
|
| 149 |
+
if (this.playAreaContainer) {
|
| 150 |
+
this.tweens.add({
|
| 151 |
+
targets: this.playAreaContainer,
|
| 152 |
+
y: 0,
|
| 153 |
+
duration: 600,
|
| 154 |
+
ease: 'Bounce.easeOut'
|
| 155 |
+
});
|
| 156 |
+
}
|
| 157 |
+
|
| 158 |
+
// Animate UI panel falling in (slightly delayed)
|
| 159 |
+
if (this.uiPanelContainer) {
|
| 160 |
+
this.tweens.add({
|
| 161 |
+
targets: this.uiPanelContainer,
|
| 162 |
+
y: 0,
|
| 163 |
+
duration: 600,
|
| 164 |
+
delay: 100,
|
| 165 |
+
ease: 'Bounce.easeOut',
|
| 166 |
+
onComplete: () => {
|
| 167 |
+
// Show blocks and re-enable input after animations complete
|
| 168 |
+
this.blockSprites.forEach(sprite => sprite.setVisible(true));
|
| 169 |
+
this.ghostSprites.forEach(sprite => sprite.setVisible(true));
|
| 170 |
+
this.inputEnabled = true;
|
| 171 |
+
}
|
| 172 |
+
});
|
| 173 |
+
}
|
| 174 |
+
});
|
| 175 |
+
}
|
| 176 |
+
|
| 177 |
+
createBlockTextures() {
|
| 178 |
+
const enhanced = SpriteBlockRenderer.enhancePalette(this.colorPalette);
|
| 179 |
+
this.colorPalette = enhanced;
|
| 180 |
+
Object.keys(this.tetrominoes).forEach((key, i) => {
|
| 181 |
+
// Remove old textures if they exist
|
| 182 |
+
if (this.textures.exists(`block-${key}`)) {
|
| 183 |
+
this.textures.remove(`block-${key}`);
|
| 184 |
+
}
|
| 185 |
+
if (this.textures.exists(`ghost-${key}`)) {
|
| 186 |
+
this.textures.remove(`ghost-${key}`);
|
| 187 |
+
}
|
| 188 |
+
SpriteBlockRenderer.createBlockTexture(this, this.colorPalette, this.level, `block-${key}`, i);
|
| 189 |
+
SpriteBlockRenderer.createBlockTexture(this, this.colorPalette, this.level, `ghost-${key}`, i);
|
| 190 |
+
});
|
| 191 |
+
}
|
| 192 |
+
|
| 193 |
+
setupInput() {
|
| 194 |
+
// Simple polling - use Phaser's built-in JustDown
|
| 195 |
+
this.cursors = this.input.keyboard.createCursorKeys();
|
| 196 |
+
this.spaceKey = this.input.keyboard.addKey(Phaser.Input.Keyboard.KeyCodes.SPACE);
|
| 197 |
+
this.pKey = this.input.keyboard.addKey(Phaser.Input.Keyboard.KeyCodes.P);
|
| 198 |
+
|
| 199 |
+
// DAS settings for left/right auto-repeat when HOLDING
|
| 200 |
+
this.dasDelay = 16; // Frames before repeat starts (longer delay)
|
| 201 |
+
this.dasSpeed = 4; // Frames between repeats (slower repeat)
|
| 202 |
+
this.leftHoldCounter = 0;
|
| 203 |
+
this.rightHoldCounter = 0;
|
| 204 |
+
|
| 205 |
+
// Grace period to prevent double-taps
|
| 206 |
+
this.moveGracePeriod = 2; // Minimum frames between moves
|
| 207 |
+
this.leftGraceCounter = 0;
|
| 208 |
+
this.rightGraceCounter = 0;
|
| 209 |
+
|
| 210 |
+
this.paused = false;
|
| 211 |
+
}
|
| 212 |
+
|
| 213 |
+
createBitmapText(x, y, text, size = 10) {
|
| 214 |
+
const t = this.add.bitmapText(x, y, 'pixel-font', text.toUpperCase(), size);
|
| 215 |
+
t.texture.setFilter(Phaser.Textures.FilterMode.NEAREST);
|
| 216 |
+
return t;
|
| 217 |
+
}
|
| 218 |
+
|
| 219 |
+
createUI() {
|
| 220 |
+
// Create container for play area (so it can be animated as a unit)
|
| 221 |
+
this.playAreaContainer = this.add.container(0, 0);
|
| 222 |
+
const playAreaGraphics = this.add.graphics();
|
| 223 |
+
this.drawNESFrame(playAreaGraphics, PLAY_AREA_X - 2, PLAY_AREA_Y - 2, PLAY_AREA_WIDTH + 5, PLAY_AREA_HEIGHT + 4);
|
| 224 |
+
this.playAreaContainer.add(playAreaGraphics);
|
| 225 |
+
|
| 226 |
+
// Create container for right-side UI panels
|
| 227 |
+
this.uiPanelContainer = this.add.container(0, 0);
|
| 228 |
+
const panelGraphics = this.add.graphics();
|
| 229 |
+
|
| 230 |
+
// UI text positions - align first frame with play area top
|
| 231 |
+
const frameWidth = UI.PANEL_WIDTH - 3;
|
| 232 |
+
const x = UI.PANEL_X + UI.PADDING;
|
| 233 |
+
let y = PLAY_AREA_Y; // Align with play area top
|
| 234 |
+
|
| 235 |
+
// SCORE frame
|
| 236 |
+
this.drawNESFrame(panelGraphics, UI.PANEL_X, y - 2, frameWidth, 26);
|
| 237 |
+
const scoreLabel = this.createBitmapText(x, y + 2, 'SCORE');
|
| 238 |
+
y += 12;
|
| 239 |
+
this.scoreText = this.createBitmapText(x, y + 2, '000000');
|
| 240 |
+
y += 12 + 12; // 12px vertical space
|
| 241 |
+
|
| 242 |
+
// LEVEL frame
|
| 243 |
+
this.drawNESFrame(panelGraphics, UI.PANEL_X, y - 2, frameWidth, 26);
|
| 244 |
+
const levelLabel = this.createBitmapText(x, y + 2, 'LEVEL');
|
| 245 |
+
y += 12;
|
| 246 |
+
this.levelText = this.createBitmapText(x, y + 2, '1');
|
| 247 |
+
y += 12 + 12; // 12px vertical space
|
| 248 |
+
|
| 249 |
+
// LINES frame
|
| 250 |
+
this.drawNESFrame(panelGraphics, UI.PANEL_X, y - 2, frameWidth, 26);
|
| 251 |
+
const linesLabel = this.createBitmapText(x, y + 2, 'LINES');
|
| 252 |
+
y += 12;
|
| 253 |
+
this.linesText = this.createBitmapText(x, y + 2, '0');
|
| 254 |
+
y += 12 + 12; // 12px vertical space
|
| 255 |
+
|
| 256 |
+
// NEXT frame
|
| 257 |
+
const nextFrameHeight = 42; // Enough for piece preview + 2px top padding
|
| 258 |
+
this.drawNESFrame(panelGraphics, UI.PANEL_X, y - 2, frameWidth, nextFrameHeight);
|
| 259 |
+
this.nextPieceText = this.createBitmapText(x, y + 2, 'NEXT');
|
| 260 |
+
this.nextPieceY = y + 16;
|
| 261 |
+
this.nextPieceX = x;
|
| 262 |
+
|
| 263 |
+
// Add all UI elements to the container
|
| 264 |
+
this.uiPanelContainer.add([
|
| 265 |
+
panelGraphics,
|
| 266 |
+
scoreLabel,
|
| 267 |
+
this.scoreText,
|
| 268 |
+
levelLabel,
|
| 269 |
+
this.levelText,
|
| 270 |
+
linesLabel,
|
| 271 |
+
this.linesText,
|
| 272 |
+
this.nextPieceText
|
| 273 |
+
]);
|
| 274 |
+
}
|
| 275 |
+
|
| 276 |
+
drawNESFrame(g, x, y, w, h) {
|
| 277 |
+
g.fillStyle(0x000000, 1); g.fillRect(x, y, w, h);
|
| 278 |
+
g.lineStyle(2, 0xAAAAAA, 1); g.strokeRect(x, y, w, h);
|
| 279 |
+
g.lineStyle(1, 0x555555, 1); g.strokeRect(x + 2, y + 2, w - 4, h - 4);
|
| 280 |
+
g.lineStyle(1, 0xFFFFFF, 1); g.beginPath(); g.moveTo(x + 1, y + h - 1); g.lineTo(x + 1, y + 1); g.lineTo(x + w - 1, y + 1); g.strokePath();
|
| 281 |
+
g.lineStyle(1, 0x333333, 1); g.beginPath(); g.moveTo(x + w - 1, y + 1); g.lineTo(x + w - 1, y + h - 1); g.lineTo(x + 1, y + h - 1); g.strokePath();
|
| 282 |
+
}
|
| 283 |
+
|
| 284 |
+
getRandomPiece() {
|
| 285 |
+
const keys = Object.keys(this.tetrominoes);
|
| 286 |
+
return JSON.parse(JSON.stringify(this.tetrominoes[keys[Math.floor(Math.random() * keys.length)]]));
|
| 287 |
+
}
|
| 288 |
+
|
| 289 |
+
spawnPiece() {
|
| 290 |
+
this.currentPiece = this.nextPiece ? this.nextPiece : this.getRandomPiece();
|
| 291 |
+
this.nextPiece = this.getRandomPiece();
|
| 292 |
+
this.currentX = Math.floor(GRID_WIDTH / 2) - Math.floor(this.currentPiece.shape[0].length / 2);
|
| 293 |
+
this.currentY = 0;
|
| 294 |
+
if (this.checkCollision(this.currentPiece, this.currentX, this.currentY)) { this.gameOver = true; this.handleGameOver(); }
|
| 295 |
+
this.updateNextPieceDisplay();
|
| 296 |
+
}
|
| 297 |
+
|
| 298 |
+
update(time, delta) {
|
| 299 |
+
if (this.gameOver || !this.inputEnabled) return;
|
| 300 |
+
|
| 301 |
+
// Pause check - always available
|
| 302 |
+
if (Phaser.Input.Keyboard.JustDown(this.pKey)) {
|
| 303 |
+
this.togglePause();
|
| 304 |
+
}
|
| 305 |
+
|
| 306 |
+
if (this.clearing || this.paused) return;
|
| 307 |
+
|
| 308 |
+
this.handleInput();
|
| 309 |
+
this.dropCounter++;
|
| 310 |
+
if (this.dropCounter >= this.dropInterval) { this.dropCounter = 0; this.moveDown(); }
|
| 311 |
+
this.renderPiece();
|
| 312 |
+
}
|
| 313 |
+
|
| 314 |
+
togglePause() {
|
| 315 |
+
this.paused = !this.paused;
|
| 316 |
+
if (this.paused) {
|
| 317 |
+
this.pauseOverlay = this.add.rectangle(GAME_WIDTH / 2 + BORDER_OFFSET, GAME_HEIGHT / 2, GAME_WIDTH, GAME_HEIGHT, 0x000000, 0.8);
|
| 318 |
+
this.pauseOverlay.setDepth(100);
|
| 319 |
+
this.pauseText = this.createBitmapText(GAME_WIDTH / 2 + BORDER_OFFSET, GAME_HEIGHT / 2, 'PAUSED');
|
| 320 |
+
this.pauseText.setOrigin(0.5).setDepth(101);
|
| 321 |
+
this.pauseHintText = this.createBitmapText(GAME_WIDTH / 2 + BORDER_OFFSET, GAME_HEIGHT / 2 + 12, 'PRESS P');
|
| 322 |
+
this.pauseHintText.setOrigin(0.5).setDepth(101);
|
| 323 |
+
if (this.currentMusic) this.currentMusic.pause();
|
| 324 |
+
} else {
|
| 325 |
+
if (this.pauseOverlay) { this.pauseOverlay.destroy(); this.pauseOverlay = null; }
|
| 326 |
+
if (this.pauseText) { this.pauseText.destroy(); this.pauseText = null; }
|
| 327 |
+
if (this.pauseHintText) { this.pauseHintText.destroy(); this.pauseHintText = null; }
|
| 328 |
+
if (this.currentMusic) this.currentMusic.resume();
|
| 329 |
+
}
|
| 330 |
+
}
|
| 331 |
+
|
| 332 |
+
handleInput() {
|
| 333 |
+
// Rotation - JustDown ensures one action per press
|
| 334 |
+
if (Phaser.Input.Keyboard.JustDown(this.cursors.up)) {
|
| 335 |
+
this.rotatePiece();
|
| 336 |
+
}
|
| 337 |
+
|
| 338 |
+
// Hard drop - JustDown ensures one action per press
|
| 339 |
+
if (Phaser.Input.Keyboard.JustDown(this.spaceKey)) {
|
| 340 |
+
this.hardDrop();
|
| 341 |
+
}
|
| 342 |
+
|
| 343 |
+
// Soft drop (down key held) - continuous action
|
| 344 |
+
if (this.cursors.down.isDown) {
|
| 345 |
+
if (!this.softDropping) { this.softDropping = true; this.softDropCounter = 0; }
|
| 346 |
+
this.softDropCounter++;
|
| 347 |
+
if (this.softDropCounter >= 2) {
|
| 348 |
+
this.softDropCounter = 0;
|
| 349 |
+
if (this.moveDown()) {
|
| 350 |
+
this.score += SCORES.SOFT_DROP;
|
| 351 |
+
this.updateUI();
|
| 352 |
+
SoundGenerator.playSoftDrop();
|
| 353 |
+
}
|
| 354 |
+
}
|
| 355 |
+
} else {
|
| 356 |
+
this.softDropping = false;
|
| 357 |
+
this.softDropCounter = 0;
|
| 358 |
+
}
|
| 359 |
+
|
| 360 |
+
// Decrement grace counters
|
| 361 |
+
if (this.leftGraceCounter > 0) this.leftGraceCounter--;
|
| 362 |
+
if (this.rightGraceCounter > 0) this.rightGraceCounter--;
|
| 363 |
+
|
| 364 |
+
// LEFT - JustDown for first press, then auto-repeat when held
|
| 365 |
+
if (Phaser.Input.Keyboard.JustDown(this.cursors.left) && this.leftGraceCounter === 0) {
|
| 366 |
+
this.moveLeft();
|
| 367 |
+
this.leftHoldCounter = 0;
|
| 368 |
+
this.leftGraceCounter = this.moveGracePeriod;
|
| 369 |
+
} else if (this.cursors.left.isDown && this.leftGraceCounter === 0) {
|
| 370 |
+
this.leftHoldCounter++;
|
| 371 |
+
if (this.leftHoldCounter >= this.dasDelay && (this.leftHoldCounter - this.dasDelay) % this.dasSpeed === 0) {
|
| 372 |
+
this.moveLeft();
|
| 373 |
+
this.leftGraceCounter = this.moveGracePeriod;
|
| 374 |
+
}
|
| 375 |
+
} else if (!this.cursors.left.isDown) {
|
| 376 |
+
this.leftHoldCounter = 0;
|
| 377 |
+
}
|
| 378 |
+
|
| 379 |
+
// RIGHT - JustDown for first press, then auto-repeat when held
|
| 380 |
+
if (Phaser.Input.Keyboard.JustDown(this.cursors.right) && this.rightGraceCounter === 0) {
|
| 381 |
+
this.moveRight();
|
| 382 |
+
this.rightHoldCounter = 0;
|
| 383 |
+
this.rightGraceCounter = this.moveGracePeriod;
|
| 384 |
+
} else if (this.cursors.right.isDown && this.rightGraceCounter === 0) {
|
| 385 |
+
this.rightHoldCounter++;
|
| 386 |
+
if (this.rightHoldCounter >= this.dasDelay && (this.rightHoldCounter - this.dasDelay) % this.dasSpeed === 0) {
|
| 387 |
+
this.moveRight();
|
| 388 |
+
this.rightGraceCounter = this.moveGracePeriod;
|
| 389 |
+
}
|
| 390 |
+
} else if (!this.cursors.right.isDown) {
|
| 391 |
+
this.rightHoldCounter = 0;
|
| 392 |
+
}
|
| 393 |
+
}
|
| 394 |
+
|
| 395 |
+
moveLeft() { if (!this.checkCollision(this.currentPiece, this.currentX - 1, this.currentY)) { this.currentX--; SoundGenerator.playMove(); } }
|
| 396 |
+
moveRight() { if (!this.checkCollision(this.currentPiece, this.currentX + 1, this.currentY)) { this.currentX++; SoundGenerator.playMove(); } }
|
| 397 |
+
moveDown() { if (!this.checkCollision(this.currentPiece, this.currentX, this.currentY + 1)) { this.currentY++; return true; } else { this.lockPiece(); return false; } }
|
| 398 |
+
hardDrop() { while (!this.checkCollision(this.currentPiece, this.currentX, this.currentY + 1)) this.currentY++; SoundGenerator.playDrop(); this.lockPiece(); }
|
| 399 |
+
|
| 400 |
+
rotatePiece() {
|
| 401 |
+
const rotated = this.getRotatedPiece(this.currentPiece);
|
| 402 |
+
|
| 403 |
+
// Try rotation at current position
|
| 404 |
+
if (!this.checkCollision(rotated, this.currentX, this.currentY)) {
|
| 405 |
+
this.currentPiece = rotated;
|
| 406 |
+
SoundGenerator.playRotate();
|
| 407 |
+
return;
|
| 408 |
+
}
|
| 409 |
+
|
| 410 |
+
// Wall kick: try shifting right
|
| 411 |
+
if (!this.checkCollision(rotated, this.currentX + 1, this.currentY)) {
|
| 412 |
+
this.currentPiece = rotated;
|
| 413 |
+
this.currentX++;
|
| 414 |
+
SoundGenerator.playRotate();
|
| 415 |
+
return;
|
| 416 |
+
}
|
| 417 |
+
|
| 418 |
+
// Wall kick: try shifting left
|
| 419 |
+
if (!this.checkCollision(rotated, this.currentX - 1, this.currentY)) {
|
| 420 |
+
this.currentPiece = rotated;
|
| 421 |
+
this.currentX--;
|
| 422 |
+
SoundGenerator.playRotate();
|
| 423 |
+
return;
|
| 424 |
+
}
|
| 425 |
+
|
| 426 |
+
// Wall kick: try shifting right 2 spaces (for I-piece)
|
| 427 |
+
if (!this.checkCollision(rotated, this.currentX + 2, this.currentY)) {
|
| 428 |
+
this.currentPiece = rotated;
|
| 429 |
+
this.currentX += 2;
|
| 430 |
+
SoundGenerator.playRotate();
|
| 431 |
+
return;
|
| 432 |
+
}
|
| 433 |
+
|
| 434 |
+
// Wall kick: try shifting left 2 spaces (for I-piece)
|
| 435 |
+
if (!this.checkCollision(rotated, this.currentX - 2, this.currentY)) {
|
| 436 |
+
this.currentPiece = rotated;
|
| 437 |
+
this.currentX -= 2;
|
| 438 |
+
SoundGenerator.playRotate();
|
| 439 |
+
return;
|
| 440 |
+
}
|
| 441 |
+
|
| 442 |
+
// Rotation failed - no valid position found
|
| 443 |
+
}
|
| 444 |
+
|
| 445 |
+
getRotatedPiece(piece) {
|
| 446 |
+
const rotated = JSON.parse(JSON.stringify(piece));
|
| 447 |
+
const shape = piece.shape;
|
| 448 |
+
const rows = shape.length;
|
| 449 |
+
const cols = shape[0].length;
|
| 450 |
+
const newShape = [];
|
| 451 |
+
for (let x = 0; x < cols; x++) { newShape[x] = []; for (let y = rows - 1; y >= 0; y--) newShape[x][rows - 1 - y] = shape[y][x]; }
|
| 452 |
+
rotated.shape = newShape;
|
| 453 |
+
return rotated;
|
| 454 |
+
}
|
| 455 |
+
|
| 456 |
+
checkCollision(piece, x, y) {
|
| 457 |
+
const shape = piece.shape;
|
| 458 |
+
for (let row = 0; row < shape.length; row++) {
|
| 459 |
+
for (let col = 0; col < shape[row].length; col++) {
|
| 460 |
+
if (shape[row][col]) {
|
| 461 |
+
const gridX = x + col;
|
| 462 |
+
const gridY = y + row;
|
| 463 |
+
if (gridX < 0 || gridX >= GRID_WIDTH || gridY >= GRID_HEIGHT) return true;
|
| 464 |
+
if (gridY >= 0 && this.grid[gridY][gridX]) return true;
|
| 465 |
+
}
|
| 466 |
+
}
|
| 467 |
+
}
|
| 468 |
+
return false;
|
| 469 |
+
}
|
| 470 |
+
|
| 471 |
+
lockPiece() {
|
| 472 |
+
const shape = this.currentPiece.shape;
|
| 473 |
+
for (let row = 0; row < shape.length; row++) {
|
| 474 |
+
for (let col = 0; col < shape[row].length; col++) {
|
| 475 |
+
if (shape[row][col]) {
|
| 476 |
+
const gridX = this.currentX + col;
|
| 477 |
+
const gridY = this.currentY + row;
|
| 478 |
+
if (gridY >= 0) this.grid[gridY][gridX] = this.currentPiece.name;
|
| 479 |
+
}
|
| 480 |
+
}
|
| 481 |
+
}
|
| 482 |
+
this.checkAndClearLines();
|
| 483 |
+
}
|
| 484 |
+
|
| 485 |
+
checkAndClearLines() {
|
| 486 |
+
// Find complete lines - a line is complete ONLY if every cell is filled
|
| 487 |
+
const completeLines = [];
|
| 488 |
+
for (let y = 0; y < GRID_HEIGHT; y++) {
|
| 489 |
+
let isComplete = true;
|
| 490 |
+
for (let x = 0; x < GRID_WIDTH; x++) {
|
| 491 |
+
if (!this.grid[y][x]) {
|
| 492 |
+
isComplete = false;
|
| 493 |
+
break;
|
| 494 |
+
}
|
| 495 |
+
}
|
| 496 |
+
if (isComplete) {
|
| 497 |
+
console.log(`Line ${y} is complete:`, JSON.stringify(this.grid[y]));
|
| 498 |
+
completeLines.push(y);
|
| 499 |
+
}
|
| 500 |
+
}
|
| 501 |
+
|
| 502 |
+
if (completeLines.length > 0) {
|
| 503 |
+
console.log('Complete lines found:', completeLines);
|
| 504 |
+
console.log('Grid state:', JSON.stringify(this.grid));
|
| 505 |
+
}
|
| 506 |
+
|
| 507 |
+
if (completeLines.length === 0) {
|
| 508 |
+
this.spawnPiece();
|
| 509 |
+
this.redrawGrid();
|
| 510 |
+
return;
|
| 511 |
+
}
|
| 512 |
+
|
| 513 |
+
// Block game updates during line clear
|
| 514 |
+
this.clearing = true;
|
| 515 |
+
|
| 516 |
+
// Play sound based on number of lines cleared
|
| 517 |
+
SoundGenerator.playLineClear(completeLines.length);
|
| 518 |
+
|
| 519 |
+
// Show the locked piece first
|
| 520 |
+
this.redrawGrid();
|
| 521 |
+
|
| 522 |
+
// Run the line clear animation, then apply changes
|
| 523 |
+
this.animateLineClear(completeLines);
|
| 524 |
+
}
|
| 525 |
+
|
| 526 |
+
animateLineClear(completeLines) {
|
| 527 |
+
// Create crush animation for each block
|
| 528 |
+
const crushSprites = [];
|
| 529 |
+
const texturesToCleanup = [];
|
| 530 |
+
|
| 531 |
+
completeLines.forEach(y => {
|
| 532 |
+
for (let x = 0; x < GRID_WIDTH; x++) {
|
| 533 |
+
const blockType = this.grid[y][x];
|
| 534 |
+
if (!blockType) continue;
|
| 535 |
+
|
| 536 |
+
const px = PLAY_AREA_X + x * BLOCK_SIZE;
|
| 537 |
+
const py = PLAY_AREA_Y + y * BLOCK_SIZE;
|
| 538 |
+
|
| 539 |
+
// Get the block's color from the palette
|
| 540 |
+
const colorIndex = blockType - 1;
|
| 541 |
+
const color = this.colorPalette[colorIndex % this.colorPalette.length];
|
| 542 |
+
|
| 543 |
+
// Create unique crush animation frames for this specific block instance
|
| 544 |
+
const uniqueId = `${Date.now()}-${x}-${y}-${Math.random().toString(36).substr(2, 9)}`;
|
| 545 |
+
const frames = [];
|
| 546 |
+
for (let f = 0; f < 5; f++) {
|
| 547 |
+
const frameKey = `crush-${uniqueId}-${f}`;
|
| 548 |
+
SpriteBlockRenderer.createCrushTexture(this, color, f, frameKey);
|
| 549 |
+
frames.push(frameKey);
|
| 550 |
+
texturesToCleanup.push(frameKey);
|
| 551 |
+
}
|
| 552 |
+
|
| 553 |
+
// Create sprite starting with frame 4 (most intact)
|
| 554 |
+
const sprite = this.add.sprite(px, py, frames[4]).setOrigin(0, 0);
|
| 555 |
+
sprite.setDepth(50);
|
| 556 |
+
crushSprites.push({ sprite, frames });
|
| 557 |
+
}
|
| 558 |
+
});
|
| 559 |
+
|
| 560 |
+
// Cycle through the 5 crush frames in REVERSE: 4 -> 3 -> 2 -> 1 -> 0
|
| 561 |
+
let frameCounter = 4;
|
| 562 |
+
|
| 563 |
+
this.time.addEvent({
|
| 564 |
+
delay: 75, // 75ms per frame (twice as fast)
|
| 565 |
+
repeat: 4, // repeat 4 times = 5 total callbacks (frames 4,3,2,1,0)
|
| 566 |
+
callback: () => {
|
| 567 |
+
if (frameCounter > 0) {
|
| 568 |
+
frameCounter--;
|
| 569 |
+
crushSprites.forEach(crushData => {
|
| 570 |
+
crushData.sprite.setTexture(crushData.frames[frameCounter]);
|
| 571 |
+
});
|
| 572 |
+
}
|
| 573 |
+
}
|
| 574 |
+
});
|
| 575 |
+
|
| 576 |
+
// After all 5 frames (4 shows immediately, then 3,2,1,0 at 75ms each = 300ms total), clean up
|
| 577 |
+
this.time.delayedCall(350, () => {
|
| 578 |
+
crushSprites.forEach(crushData => {
|
| 579 |
+
crushData.sprite.destroy();
|
| 580 |
+
});
|
| 581 |
+
|
| 582 |
+
// Clean up all textures
|
| 583 |
+
texturesToCleanup.forEach(frameKey => {
|
| 584 |
+
if (this.textures.exists(frameKey)) {
|
| 585 |
+
this.textures.remove(frameKey);
|
| 586 |
+
}
|
| 587 |
+
});
|
| 588 |
+
|
| 589 |
+
this.finishLineClear(completeLines);
|
| 590 |
+
});
|
| 591 |
+
}
|
| 592 |
+
|
| 593 |
+
finishLineClear(completeLines) {
|
| 594 |
+
// Apply the grid changes first
|
| 595 |
+
const validLines = completeLines.filter(y => {
|
| 596 |
+
if (y < 0 || y >= GRID_HEIGHT) return false;
|
| 597 |
+
for (let x = 0; x < GRID_WIDTH; x++) {
|
| 598 |
+
if (!this.grid[y][x]) return false;
|
| 599 |
+
}
|
| 600 |
+
return true;
|
| 601 |
+
});
|
| 602 |
+
|
| 603 |
+
if (validLines.length === 0) {
|
| 604 |
+
console.warn('No valid lines to clear after validation');
|
| 605 |
+
this.clearing = false;
|
| 606 |
+
this.spawnPiece();
|
| 607 |
+
this.redrawGrid();
|
| 608 |
+
return;
|
| 609 |
+
}
|
| 610 |
+
|
| 611 |
+
// Build new grid
|
| 612 |
+
const newGrid = [];
|
| 613 |
+
const linesToRemove = new Set(validLines);
|
| 614 |
+
|
| 615 |
+
for (let i = 0; i < validLines.length; i++) {
|
| 616 |
+
newGrid.push(new Array(GRID_WIDTH).fill(0));
|
| 617 |
+
}
|
| 618 |
+
|
| 619 |
+
for (let y = 0; y < GRID_HEIGHT; y++) {
|
| 620 |
+
if (!linesToRemove.has(y)) {
|
| 621 |
+
newGrid.push([...this.grid[y]]);
|
| 622 |
+
}
|
| 623 |
+
}
|
| 624 |
+
|
| 625 |
+
this.grid = newGrid;
|
| 626 |
+
|
| 627 |
+
// Now animate the falling blocks
|
| 628 |
+
// Rebuild sprites from new grid state
|
| 629 |
+
this.redrawGrid();
|
| 630 |
+
|
| 631 |
+
// Animate all sprites falling into place
|
| 632 |
+
const sortedLines = [...validLines].sort((a, b) => a - b);
|
| 633 |
+
|
| 634 |
+
this.blockSprites.forEach(sprite => {
|
| 635 |
+
const spriteGridY = Math.floor((sprite.y - PLAY_AREA_Y) / BLOCK_SIZE);
|
| 636 |
+
|
| 637 |
+
// Count how many cleared lines were below this sprite's ORIGINAL position
|
| 638 |
+
let linesBelowCount = 0;
|
| 639 |
+
sortedLines.forEach(clearedY => {
|
| 640 |
+
if (clearedY > spriteGridY - validLines.length) {
|
| 641 |
+
linesBelowCount++;
|
| 642 |
+
}
|
| 643 |
+
});
|
| 644 |
+
|
| 645 |
+
if (linesBelowCount > 0) {
|
| 646 |
+
// Start sprite higher, then animate down to current position
|
| 647 |
+
const startY = sprite.y - (linesBelowCount * BLOCK_SIZE);
|
| 648 |
+
sprite.y = startY;
|
| 649 |
+
|
| 650 |
+
this.tweens.add({
|
| 651 |
+
targets: sprite,
|
| 652 |
+
y: sprite.y + (linesBelowCount * BLOCK_SIZE),
|
| 653 |
+
duration: 150,
|
| 654 |
+
ease: 'Bounce.easeOut'
|
| 655 |
+
});
|
| 656 |
+
}
|
| 657 |
+
});
|
| 658 |
+
|
| 659 |
+
// Wait for fall animation, then finish
|
| 660 |
+
this.time.delayedCall(160, () => {
|
| 661 |
+
this.finishScoring(validLines);
|
| 662 |
+
});
|
| 663 |
+
}
|
| 664 |
+
|
| 665 |
+
finishScoring(validLines) {
|
| 666 |
+
// Update score
|
| 667 |
+
this.lines += validLines.length;
|
| 668 |
+
const levelMultiplier = this.level;
|
| 669 |
+
switch (validLines.length) {
|
| 670 |
+
case 1: this.score += SCORES.SINGLE * levelMultiplier; break;
|
| 671 |
+
case 2: this.score += SCORES.DOUBLE * levelMultiplier; break;
|
| 672 |
+
case 3: this.score += SCORES.TRIPLE * levelMultiplier; break;
|
| 673 |
+
case 4: this.score += SCORES.TETRIS * levelMultiplier; break;
|
| 674 |
+
}
|
| 675 |
+
|
| 676 |
+
// Check for perfect clear (entire grid is empty)
|
| 677 |
+
const isPerfectClear = this.grid.every(row => row.every(cell => cell === 0));
|
| 678 |
+
if (isPerfectClear) {
|
| 679 |
+
this.score += SCORES.PERFECT_CLEAR * levelMultiplier;
|
| 680 |
+
// Show perfect clear message
|
| 681 |
+
const perfectText = this.createBitmapText(GAME_WIDTH / 2 + BORDER_OFFSET, GAME_HEIGHT / 2, 'PERFECT CLEAR!', 12);
|
| 682 |
+
perfectText.setOrigin(0.5);
|
| 683 |
+
perfectText.setDepth(150);
|
| 684 |
+
perfectText.setTint(0xFFD700); // Gold color
|
| 685 |
+
|
| 686 |
+
// Animate the text
|
| 687 |
+
this.tweens.add({
|
| 688 |
+
targets: perfectText,
|
| 689 |
+
scale: 1.5,
|
| 690 |
+
alpha: 0,
|
| 691 |
+
duration: 2000,
|
| 692 |
+
ease: 'Power2',
|
| 693 |
+
onComplete: () => perfectText.destroy()
|
| 694 |
+
});
|
| 695 |
+
|
| 696 |
+
// Play special sound
|
| 697 |
+
SoundGenerator.playLevelUp();
|
| 698 |
+
}
|
| 699 |
+
|
| 700 |
+
// Check for level up
|
| 701 |
+
const newLevel = Math.min(MAX_LEVEL, Math.floor(this.lines / CONFIG.LINES_PER_LEVEL) + 1);
|
| 702 |
+
if (newLevel > this.level) {
|
| 703 |
+
this.level = newLevel;
|
| 704 |
+
this.dropInterval = LEVEL_SPEEDS[this.level - 1];
|
| 705 |
+
SoundGenerator.playLevelUp();
|
| 706 |
+
|
| 707 |
+
// Exciting level transition!
|
| 708 |
+
this.showLevelTransition(newLevel);
|
| 709 |
+
} else {
|
| 710 |
+
this.updateUI();
|
| 711 |
+
this.clearing = false;
|
| 712 |
+
this.spawnPiece();
|
| 713 |
+
}
|
| 714 |
+
}
|
| 715 |
+
|
| 716 |
+
|
| 717 |
+
|
| 718 |
+
showLevelTransition(newLevel) {
|
| 719 |
+
// Keep game paused during transition
|
| 720 |
+
this.clearing = true;
|
| 721 |
+
|
| 722 |
+
// Black screen overlay
|
| 723 |
+
const blackScreen = this.add.rectangle(GAME_WIDTH / 2 + BORDER_OFFSET, GAME_HEIGHT / 2, GAME_WIDTH, GAME_HEIGHT, 0x000000);
|
| 724 |
+
blackScreen.setDepth(200);
|
| 725 |
+
blackScreen.setAlpha(0);
|
| 726 |
+
|
| 727 |
+
// Fade to black
|
| 728 |
+
this.tweens.add({
|
| 729 |
+
targets: blackScreen,
|
| 730 |
+
alpha: 1,
|
| 731 |
+
duration: 300,
|
| 732 |
+
ease: 'Power2',
|
| 733 |
+
onComplete: () => {
|
| 734 |
+
// Pre-load the new level's palette and create preview blocks
|
| 735 |
+
const backdropKey = `backdrop-${newLevel}`;
|
| 736 |
+
const rawPalette = ColorExtractor.extractPalette(this, backdropKey);
|
| 737 |
+
const newPalette = SpriteBlockRenderer.enhancePalette(rawPalette);
|
| 738 |
+
|
| 739 |
+
// Level up text
|
| 740 |
+
const levelText = this.createBitmapText(GAME_WIDTH / 2 + BORDER_OFFSET, 60, `LEVEL ${newLevel}`, 20);
|
| 741 |
+
levelText.setOrigin(0.5);
|
| 742 |
+
levelText.setDepth(201);
|
| 743 |
+
levelText.setAlpha(0);
|
| 744 |
+
|
| 745 |
+
// Subtitle - show level title
|
| 746 |
+
const levelTitle = LEVEL_TITLES[newLevel] || 'Unknown';
|
| 747 |
+
const subtitle = this.createBitmapText(GAME_WIDTH / 2 + BORDER_OFFSET, 85, levelTitle, 10);
|
| 748 |
+
subtitle.setOrigin(0.5);
|
| 749 |
+
subtitle.setDepth(201);
|
| 750 |
+
subtitle.setAlpha(0);
|
| 751 |
+
|
| 752 |
+
// Create preview blocks showing new level's style
|
| 753 |
+
const previewBlocks = [];
|
| 754 |
+
const startX = GAME_WIDTH / 2 + BORDER_OFFSET - 32; // Center 8 blocks (8*8 = 64px wide)
|
| 755 |
+
const startY = 120;
|
| 756 |
+
|
| 757 |
+
for (let i = 0; i < 7; i++) {
|
| 758 |
+
const x = startX + i * 10;
|
| 759 |
+
const y = startY;
|
| 760 |
+
const blockKey = `preview-block-${i}`;
|
| 761 |
+
|
| 762 |
+
// Create block texture with new level's palette
|
| 763 |
+
SpriteBlockRenderer.createBlockTexture(this, newPalette, newLevel, blockKey, i);
|
| 764 |
+
|
| 765 |
+
const block = this.add.sprite(x, y, blockKey).setOrigin(0, 0);
|
| 766 |
+
block.setDepth(201);
|
| 767 |
+
block.setAlpha(0);
|
| 768 |
+
block.setScale(0.5);
|
| 769 |
+
previewBlocks.push({ sprite: block, key: blockKey });
|
| 770 |
+
}
|
| 771 |
+
|
| 772 |
+
// Animate text and blocks in
|
| 773 |
+
this.tweens.add({
|
| 774 |
+
targets: [levelText, subtitle],
|
| 775 |
+
alpha: 1,
|
| 776 |
+
duration: 400,
|
| 777 |
+
ease: 'Power2'
|
| 778 |
+
});
|
| 779 |
+
|
| 780 |
+
this.tweens.add({
|
| 781 |
+
targets: previewBlocks.map(b => b.sprite),
|
| 782 |
+
alpha: 1,
|
| 783 |
+
scale: 1,
|
| 784 |
+
duration: 500,
|
| 785 |
+
delay: 200,
|
| 786 |
+
ease: 'Back.easeOut',
|
| 787 |
+
onComplete: () => {
|
| 788 |
+
// Hold for a moment
|
| 789 |
+
this.time.delayedCall(1200, () => {
|
| 790 |
+
// Fade out text and preview blocks only (keep black screen)
|
| 791 |
+
this.tweens.add({
|
| 792 |
+
targets: [levelText, subtitle, ...previewBlocks.map(b => b.sprite)],
|
| 793 |
+
alpha: 0,
|
| 794 |
+
duration: 300,
|
| 795 |
+
onComplete: () => {
|
| 796 |
+
// Clean up text and preview blocks
|
| 797 |
+
levelText.destroy();
|
| 798 |
+
subtitle.destroy();
|
| 799 |
+
previewBlocks.forEach(b => {
|
| 800 |
+
b.sprite.destroy();
|
| 801 |
+
if (this.textures.exists(b.key)) {
|
| 802 |
+
this.textures.remove(b.key);
|
| 803 |
+
}
|
| 804 |
+
});
|
| 805 |
+
|
| 806 |
+
// Black screen stays for a moment
|
| 807 |
+
this.time.delayedCall(300, () => {
|
| 808 |
+
// Destroy old level elements while screen is black
|
| 809 |
+
this.blockSprites.forEach(sprite => sprite.destroy());
|
| 810 |
+
this.blockSprites = [];
|
| 811 |
+
this.ghostSprites.forEach(sprite => sprite.destroy());
|
| 812 |
+
this.ghostSprites = [];
|
| 813 |
+
|
| 814 |
+
// Load new level (no intro yet)
|
| 815 |
+
this.loadLevel(newLevel, false);
|
| 816 |
+
this.updateUI();
|
| 817 |
+
this.clearing = false;
|
| 818 |
+
this.spawnPiece();
|
| 819 |
+
|
| 820 |
+
// IMMEDIATELY hide UI containers before fading out black screen
|
| 821 |
+
if (this.playAreaContainer) {
|
| 822 |
+
this.playAreaContainer.y = -GAME_HEIGHT;
|
| 823 |
+
}
|
| 824 |
+
if (this.uiPanelContainer) {
|
| 825 |
+
this.uiPanelContainer.y = -GAME_HEIGHT;
|
| 826 |
+
}
|
| 827 |
+
this.blockSprites.forEach(sprite => sprite.setVisible(false));
|
| 828 |
+
this.ghostSprites.forEach(sprite => sprite.setVisible(false));
|
| 829 |
+
this.inputEnabled = false;
|
| 830 |
+
|
| 831 |
+
// Fade out black screen to reveal ONLY the backdrop
|
| 832 |
+
this.tweens.add({
|
| 833 |
+
targets: blackScreen,
|
| 834 |
+
alpha: 0,
|
| 835 |
+
duration: 500,
|
| 836 |
+
ease: 'Power2',
|
| 837 |
+
onComplete: () => {
|
| 838 |
+
blackScreen.destroy();
|
| 839 |
+
// Now show the intro animation (UI falls in)
|
| 840 |
+
// Wait 1 second showing just the backdrop
|
| 841 |
+
this.time.delayedCall(1000, () => {
|
| 842 |
+
// Play woosh sound
|
| 843 |
+
SoundGenerator.playWoosh();
|
| 844 |
+
|
| 845 |
+
// Animate play area falling in
|
| 846 |
+
if (this.playAreaContainer) {
|
| 847 |
+
this.tweens.add({
|
| 848 |
+
targets: this.playAreaContainer,
|
| 849 |
+
y: 0,
|
| 850 |
+
duration: 600,
|
| 851 |
+
ease: 'Bounce.easeOut'
|
| 852 |
+
});
|
| 853 |
+
}
|
| 854 |
+
|
| 855 |
+
// Animate UI panel falling in (slightly delayed)
|
| 856 |
+
if (this.uiPanelContainer) {
|
| 857 |
+
this.tweens.add({
|
| 858 |
+
targets: this.uiPanelContainer,
|
| 859 |
+
y: 0,
|
| 860 |
+
duration: 600,
|
| 861 |
+
delay: 100,
|
| 862 |
+
ease: 'Bounce.easeOut',
|
| 863 |
+
onComplete: () => {
|
| 864 |
+
// Show blocks and re-enable input after animations complete
|
| 865 |
+
this.blockSprites.forEach(sprite => sprite.setVisible(true));
|
| 866 |
+
this.ghostSprites.forEach(sprite => sprite.setVisible(true));
|
| 867 |
+
this.inputEnabled = true;
|
| 868 |
+
}
|
| 869 |
+
});
|
| 870 |
+
}
|
| 871 |
+
});
|
| 872 |
+
}
|
| 873 |
+
});
|
| 874 |
+
});
|
| 875 |
+
}
|
| 876 |
+
});
|
| 877 |
+
});
|
| 878 |
+
}
|
| 879 |
+
});
|
| 880 |
+
}
|
| 881 |
+
});
|
| 882 |
+
}
|
| 883 |
+
|
| 884 |
+
redrawGrid() {
|
| 885 |
+
this.blockSprites.forEach(sprite => sprite.destroy());
|
| 886 |
+
this.blockSprites = [];
|
| 887 |
+
for (let y = 0; y < GRID_HEIGHT; y++) {
|
| 888 |
+
for (let x = 0; x < GRID_WIDTH; x++) {
|
| 889 |
+
if (this.grid[y][x]) {
|
| 890 |
+
const blockType = this.grid[y][x];
|
| 891 |
+
const sprite = this.add.sprite(PLAY_AREA_X + x * BLOCK_SIZE, PLAY_AREA_Y + y * BLOCK_SIZE, `block-${blockType}`).setOrigin(0, 0);
|
| 892 |
+
sprite.setDepth(2);
|
| 893 |
+
this.blockSprites.push(sprite);
|
| 894 |
+
}
|
| 895 |
+
}
|
| 896 |
+
}
|
| 897 |
+
}
|
| 898 |
+
|
| 899 |
+
renderPiece() {
|
| 900 |
+
this.blockSprites.forEach(sprite => { if (sprite.getData('current')) sprite.destroy(); });
|
| 901 |
+
this.blockSprites = this.blockSprites.filter(s => !s.getData('current'));
|
| 902 |
+
this.ghostSprites.forEach(sprite => sprite.destroy());
|
| 903 |
+
this.ghostSprites = [];
|
| 904 |
+
if (!this.currentPiece) return;
|
| 905 |
+
if (this.level === 1) {
|
| 906 |
+
let ghostY = this.currentY;
|
| 907 |
+
while (!this.checkCollision(this.currentPiece, this.currentX, ghostY + 1)) ghostY++;
|
| 908 |
+
const shape = this.currentPiece.shape;
|
| 909 |
+
for (let row = 0; row < shape.length; row++) {
|
| 910 |
+
for (let col = 0; col < shape[row].length; col++) {
|
| 911 |
+
if (shape[row][col]) {
|
| 912 |
+
const x = PLAY_AREA_X + (this.currentX + col) * BLOCK_SIZE;
|
| 913 |
+
const y = PLAY_AREA_Y + (ghostY + row) * BLOCK_SIZE;
|
| 914 |
+
const sprite = this.add.sprite(x, y, `block-${this.currentPiece.name}`).setOrigin(0, 0);
|
| 915 |
+
sprite.setAlpha(0.3);
|
| 916 |
+
sprite.setDepth(1);
|
| 917 |
+
this.ghostSprites.push(sprite);
|
| 918 |
+
}
|
| 919 |
+
}
|
| 920 |
+
}
|
| 921 |
+
}
|
| 922 |
+
const shape = this.currentPiece.shape;
|
| 923 |
+
for (let row = 0; row < shape.length; row++) {
|
| 924 |
+
for (let col = 0; col < shape[row].length; col++) {
|
| 925 |
+
if (shape[row][col]) {
|
| 926 |
+
const x = PLAY_AREA_X + (this.currentX + col) * BLOCK_SIZE;
|
| 927 |
+
const y = PLAY_AREA_Y + (this.currentY + row) * BLOCK_SIZE;
|
| 928 |
+
const sprite = this.add.sprite(x, y, `block-${this.currentPiece.name}`).setOrigin(0, 0);
|
| 929 |
+
sprite.setData('current', true);
|
| 930 |
+
sprite.setDepth(2);
|
| 931 |
+
this.blockSprites.push(sprite);
|
| 932 |
+
}
|
| 933 |
+
}
|
| 934 |
+
}
|
| 935 |
+
}
|
| 936 |
+
|
| 937 |
+
updateNextPieceDisplay() {
|
| 938 |
+
if (this.nextPieceSprites) this.nextPieceSprites.forEach(sprite => sprite.destroy());
|
| 939 |
+
this.nextPieceSprites = [];
|
| 940 |
+
if (!this.nextPiece) return;
|
| 941 |
+
const shape = this.nextPiece.shape;
|
| 942 |
+
const startX = this.nextPieceX;
|
| 943 |
+
const startY = this.nextPieceY;
|
| 944 |
+
for (let row = 0; row < shape.length; row++) {
|
| 945 |
+
for (let col = 0; col < shape[row].length; col++) {
|
| 946 |
+
if (shape[row][col]) {
|
| 947 |
+
const x = startX + col * BLOCK_SIZE;
|
| 948 |
+
const y = startY + row * BLOCK_SIZE;
|
| 949 |
+
const sprite = this.add.sprite(x, y, `block-${this.nextPiece.name}`).setOrigin(0, 0);
|
| 950 |
+
sprite.setDepth(20);
|
| 951 |
+
this.nextPieceSprites.push(sprite);
|
| 952 |
+
// Add to UI container so it animates with the panel
|
| 953 |
+
if (this.uiPanelContainer) {
|
| 954 |
+
this.uiPanelContainer.add(sprite);
|
| 955 |
+
}
|
| 956 |
+
}
|
| 957 |
+
}
|
| 958 |
+
}
|
| 959 |
+
}
|
| 960 |
+
|
| 961 |
+
updateUI() {
|
| 962 |
+
const scoreStr = this.score.toString().padStart(6, '0');
|
| 963 |
+
this.scoreText.setText(scoreStr);
|
| 964 |
+
this.levelText.setText(this.level.toString());
|
| 965 |
+
this.linesText.setText(this.lines.toString());
|
| 966 |
+
}
|
| 967 |
+
|
| 968 |
+
handleGameOver() {
|
| 969 |
+
if (this.currentMusic) this.currentMusic.stop();
|
| 970 |
+
SoundGenerator.playGameOver();
|
| 971 |
+
|
| 972 |
+
// Display game over image (256x224, fills the game area)
|
| 973 |
+
const gameOverImage = this.add.image(BORDER_OFFSET, 0, 'game-over');
|
| 974 |
+
gameOverImage.setOrigin(0, 0);
|
| 975 |
+
gameOverImage.setDisplaySize(GAME_WIDTH, GAME_HEIGHT);
|
| 976 |
+
gameOverImage.setDepth(100);
|
| 977 |
+
gameOverImage.texture.setFilter(Phaser.Textures.FilterMode.NEAREST);
|
| 978 |
+
|
| 979 |
+
this.input.keyboard.once('keydown-SPACE', () => {
|
| 980 |
+
this.scene.start('PreloadScene');
|
| 981 |
+
});
|
| 982 |
+
}
|
| 983 |
+
}
|
src/scenes/ModeSelectScene.js
ADDED
|
@@ -0,0 +1,125 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import Phaser from 'phaser';
|
| 2 |
+
import { GAME_WIDTH, GAME_HEIGHT, BORDER_OFFSET } from '../constants.js';
|
| 3 |
+
import SoundGenerator from '../utils/SoundGenerator.js';
|
| 4 |
+
|
| 5 |
+
export default class ModeSelectScene extends Phaser.Scene {
|
| 6 |
+
constructor() {
|
| 7 |
+
super({ key: 'ModeSelectScene' });
|
| 8 |
+
}
|
| 9 |
+
|
| 10 |
+
create() {
|
| 11 |
+
// Use the title backdrop
|
| 12 |
+
const titleImage = this.add.image(BORDER_OFFSET, 0, 'title');
|
| 13 |
+
titleImage.setOrigin(0, 0);
|
| 14 |
+
titleImage.texture.setFilter(Phaser.Textures.FilterMode.NEAREST);
|
| 15 |
+
|
| 16 |
+
// Dim the background by 50%
|
| 17 |
+
const dimOverlay = this.add.rectangle(GAME_WIDTH / 2 + BORDER_OFFSET, GAME_HEIGHT / 2, GAME_WIDTH, GAME_HEIGHT, 0x000000, 0.5);
|
| 18 |
+
dimOverlay.setDepth(5);
|
| 19 |
+
|
| 20 |
+
// Title
|
| 21 |
+
const titleText = this.add.bitmapText(GAME_WIDTH / 2 + BORDER_OFFSET, 60, 'pixel-font', 'MODE SELECT', 10).setOrigin(0.5);
|
| 22 |
+
titleText.texture.setFilter(Phaser.Textures.FilterMode.NEAREST);
|
| 23 |
+
titleText.setDepth(10);
|
| 24 |
+
|
| 25 |
+
// Classic mode option
|
| 26 |
+
this.classicText = this.add.bitmapText(GAME_WIDTH / 2 + BORDER_OFFSET, 100, 'pixel-font', '> CLASSIC', 10).setOrigin(0.5);
|
| 27 |
+
this.classicText.texture.setFilter(Phaser.Textures.FilterMode.NEAREST);
|
| 28 |
+
this.classicText.setDepth(10);
|
| 29 |
+
this.classicText.setInteractive({ useHandCursor: true });
|
| 30 |
+
|
| 31 |
+
const classicDesc = this.add.bitmapText(GAME_WIDTH / 2 + BORDER_OFFSET, 115, 'pixel-font', '7 STANDARD PIECES', 10).setOrigin(0.5);
|
| 32 |
+
classicDesc.texture.setFilter(Phaser.Textures.FilterMode.NEAREST);
|
| 33 |
+
classicDesc.setDepth(10);
|
| 34 |
+
|
| 35 |
+
// Advanced mode option
|
| 36 |
+
this.advancedText = this.add.bitmapText(GAME_WIDTH / 2 + BORDER_OFFSET, 145, 'pixel-font', ' ADVANCED', 10).setOrigin(0.5);
|
| 37 |
+
this.advancedText.texture.setFilter(Phaser.Textures.FilterMode.NEAREST);
|
| 38 |
+
this.advancedText.setDepth(10);
|
| 39 |
+
this.advancedText.setInteractive({ useHandCursor: true });
|
| 40 |
+
|
| 41 |
+
const advancedDesc = this.add.bitmapText(GAME_WIDTH / 2 + BORDER_OFFSET, 160, 'pixel-font', 'EXTRA UNIQUE PIECES', 10).setOrigin(0.5);
|
| 42 |
+
advancedDesc.texture.setFilter(Phaser.Textures.FilterMode.NEAREST);
|
| 43 |
+
advancedDesc.setDepth(10);
|
| 44 |
+
|
| 45 |
+
// Track selected mode
|
| 46 |
+
this.selectedMode = 'classic';
|
| 47 |
+
|
| 48 |
+
// Hover effects
|
| 49 |
+
this.classicText.on('pointerover', () => {
|
| 50 |
+
if (this.selectedMode !== 'classic') {
|
| 51 |
+
SoundGenerator.playMove();
|
| 52 |
+
this.selectedMode = 'classic';
|
| 53 |
+
this.updateSelection();
|
| 54 |
+
}
|
| 55 |
+
});
|
| 56 |
+
|
| 57 |
+
this.advancedText.on('pointerover', () => {
|
| 58 |
+
if (this.selectedMode !== 'advanced') {
|
| 59 |
+
SoundGenerator.playMove();
|
| 60 |
+
this.selectedMode = 'advanced';
|
| 61 |
+
this.updateSelection();
|
| 62 |
+
}
|
| 63 |
+
});
|
| 64 |
+
|
| 65 |
+
// Click handlers
|
| 66 |
+
this.classicText.on('pointerdown', () => {
|
| 67 |
+
SoundGenerator.playRotate();
|
| 68 |
+
this.startGame('classic');
|
| 69 |
+
});
|
| 70 |
+
|
| 71 |
+
this.advancedText.on('pointerdown', () => {
|
| 72 |
+
SoundGenerator.playRotate();
|
| 73 |
+
this.startGame('advanced');
|
| 74 |
+
});
|
| 75 |
+
|
| 76 |
+
// Keyboard controls
|
| 77 |
+
const upKey = this.input.keyboard.addKey(Phaser.Input.Keyboard.KeyCodes.UP);
|
| 78 |
+
const downKey = this.input.keyboard.addKey(Phaser.Input.Keyboard.KeyCodes.DOWN);
|
| 79 |
+
const spaceKey = this.input.keyboard.addKey(Phaser.Input.Keyboard.KeyCodes.SPACE);
|
| 80 |
+
const enterKey = this.input.keyboard.addKey(Phaser.Input.Keyboard.KeyCodes.ENTER);
|
| 81 |
+
|
| 82 |
+
upKey.on('down', () => {
|
| 83 |
+
if (this.selectedMode !== 'classic') {
|
| 84 |
+
SoundGenerator.playMove();
|
| 85 |
+
this.selectedMode = 'classic';
|
| 86 |
+
this.updateSelection();
|
| 87 |
+
}
|
| 88 |
+
});
|
| 89 |
+
|
| 90 |
+
downKey.on('down', () => {
|
| 91 |
+
if (this.selectedMode !== 'advanced') {
|
| 92 |
+
SoundGenerator.playMove();
|
| 93 |
+
this.selectedMode = 'advanced';
|
| 94 |
+
this.updateSelection();
|
| 95 |
+
}
|
| 96 |
+
});
|
| 97 |
+
|
| 98 |
+
spaceKey.on('down', () => {
|
| 99 |
+
SoundGenerator.playRotate();
|
| 100 |
+
this.startGame(this.selectedMode);
|
| 101 |
+
});
|
| 102 |
+
|
| 103 |
+
enterKey.on('down', () => {
|
| 104 |
+
SoundGenerator.playRotate();
|
| 105 |
+
this.startGame(this.selectedMode);
|
| 106 |
+
});
|
| 107 |
+
}
|
| 108 |
+
|
| 109 |
+
updateSelection() {
|
| 110 |
+
if (this.selectedMode === 'classic') {
|
| 111 |
+
this.classicText.setText('> CLASSIC');
|
| 112 |
+
this.advancedText.setText(' ADVANCED');
|
| 113 |
+
} else {
|
| 114 |
+
this.classicText.setText(' CLASSIC');
|
| 115 |
+
this.advancedText.setText('> ADVANCED');
|
| 116 |
+
}
|
| 117 |
+
}
|
| 118 |
+
|
| 119 |
+
startGame(mode) {
|
| 120 |
+
// Store the selected mode in the registry so GameScene can access it
|
| 121 |
+
this.registry.set('gameMode', mode);
|
| 122 |
+
this.scene.start('GameScene');
|
| 123 |
+
}
|
| 124 |
+
}
|
| 125 |
+
|
src/scenes/PreloadScene.js
ADDED
|
@@ -0,0 +1,106 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import Phaser from 'phaser';
|
| 2 |
+
import { MAX_LEVEL, GAME_WIDTH, GAME_HEIGHT, BORDER_OFFSET } from '../constants.js';
|
| 3 |
+
|
| 4 |
+
export default class PreloadScene extends Phaser.Scene {
|
| 5 |
+
constructor() {
|
| 6 |
+
super({ key: 'PreloadScene' });
|
| 7 |
+
}
|
| 8 |
+
|
| 9 |
+
preload() {
|
| 10 |
+
// Create loading screen
|
| 11 |
+
const loadingText = this.add.text(GAME_WIDTH / 2 + BORDER_OFFSET, GAME_HEIGHT / 2 - 20, 'LOADING...', {
|
| 12 |
+
fontSize: '16px',
|
| 13 |
+
color: '#ffffff',
|
| 14 |
+
fontFamily: 'monospace'
|
| 15 |
+
}).setOrigin(0.5);
|
| 16 |
+
|
| 17 |
+
const progressText = this.add.text(GAME_WIDTH / 2 + BORDER_OFFSET, GAME_HEIGHT / 2 + 10, '0%', {
|
| 18 |
+
fontSize: '14px',
|
| 19 |
+
color: '#ffffff',
|
| 20 |
+
fontFamily: 'monospace'
|
| 21 |
+
}).setOrigin(0.5);
|
| 22 |
+
|
| 23 |
+
// Progress bar
|
| 24 |
+
const progressBar = this.add.graphics();
|
| 25 |
+
const progressBox = this.add.graphics();
|
| 26 |
+
progressBox.fillStyle(0x222222, 0.8);
|
| 27 |
+
progressBox.fillRect(GAME_WIDTH / 2 + BORDER_OFFSET - 80, GAME_HEIGHT / 2 + 30, 160, 20);
|
| 28 |
+
|
| 29 |
+
// Update progress
|
| 30 |
+
this.load.on('progress', (value) => {
|
| 31 |
+
progressText.setText(Math.floor(value * 100) + '%');
|
| 32 |
+
progressBar.clear();
|
| 33 |
+
progressBar.fillStyle(0xffffff, 1);
|
| 34 |
+
progressBar.fillRect(GAME_WIDTH / 2 + BORDER_OFFSET - 78, GAME_HEIGHT / 2 + 32, 156 * value, 16);
|
| 35 |
+
});
|
| 36 |
+
|
| 37 |
+
this.load.on('complete', () => {
|
| 38 |
+
progressBar.destroy();
|
| 39 |
+
progressBox.destroy();
|
| 40 |
+
loadingText.destroy();
|
| 41 |
+
progressText.destroy();
|
| 42 |
+
});
|
| 43 |
+
|
| 44 |
+
// Load title screen
|
| 45 |
+
this.load.image('title', 'assets/title.png');
|
| 46 |
+
|
| 47 |
+
// Load game over screen
|
| 48 |
+
this.load.image('game-over', 'assets/game-over.png');
|
| 49 |
+
|
| 50 |
+
// Load block sprite sheet (grayscale with depth)
|
| 51 |
+
this.load.image('blocks-spritesheet', 'assets/blocks-sprite.png');
|
| 52 |
+
|
| 53 |
+
// Load crush animation sprite (40x8px = 5 frames of 8x8px)
|
| 54 |
+
this.load.image('crush-spritesheet', 'assets/crush.png');
|
| 55 |
+
|
| 56 |
+
// Load bitmap font (Thick 8x8 from frostyfreeze)
|
| 57 |
+
this.load.bitmapFont('pixel-font', 'assets/fonts/thick_8x8.png', 'assets/fonts/thick_8x8.xml');
|
| 58 |
+
|
| 59 |
+
// Load backdrops for all levels
|
| 60 |
+
for (let i = 1; i <= MAX_LEVEL; i++) {
|
| 61 |
+
this.load.image(`backdrop-${i}`, `assets/backdrops/level-${i}/backdrop.png`);
|
| 62 |
+
}
|
| 63 |
+
|
| 64 |
+
// Load music for all levels
|
| 65 |
+
for (let i = 1; i <= MAX_LEVEL; i++) {
|
| 66 |
+
this.load.audio(`music-${i}`, `assets/music/level-${i}/track.mp3`);
|
| 67 |
+
}
|
| 68 |
+
}
|
| 69 |
+
|
| 70 |
+
create() {
|
| 71 |
+
// Title image fills entire screen (256x224), offset by border
|
| 72 |
+
const titleImage = this.add.image(BORDER_OFFSET, 0, 'title');
|
| 73 |
+
titleImage.setOrigin(0, 0);
|
| 74 |
+
titleImage.texture.setFilter(Phaser.Textures.FilterMode.NEAREST);
|
| 75 |
+
|
| 76 |
+
// "Press space to start" text - positioned in bottom third
|
| 77 |
+
const startText = this.add.bitmapText(GAME_WIDTH / 2 + BORDER_OFFSET, GAME_HEIGHT * 0.7 + 20, 'pixel-font', 'PRESS SPACE TO START', 10).setOrigin(0.5);
|
| 78 |
+
startText.texture.setFilter(Phaser.Textures.FilterMode.NEAREST);
|
| 79 |
+
startText.setDepth(10);
|
| 80 |
+
|
| 81 |
+
// Demo mode timer - start demo after 10 seconds of inactivity
|
| 82 |
+
this.demoTimer = this.time.delayedCall(10000, () => {
|
| 83 |
+
this.startDemoMode();
|
| 84 |
+
});
|
| 85 |
+
|
| 86 |
+
// Blinking effect for start text
|
| 87 |
+
this.tweens.add({
|
| 88 |
+
targets: startText,
|
| 89 |
+
alpha: 0.3,
|
| 90 |
+
duration: 600,
|
| 91 |
+
yoyo: true,
|
| 92 |
+
repeat: -1
|
| 93 |
+
});
|
| 94 |
+
|
| 95 |
+
// Credits text - positioned below start text in bottom third
|
| 96 |
+
const creditsText = this.add.bitmapText(GAME_WIDTH / 2 + BORDER_OFFSET, GAME_HEIGHT * 0.8 + 20, 'pixel-font', 'BY MARCO VAN HYLCKAMA VLIEG', 10).setOrigin(0.5);
|
| 97 |
+
creditsText.texture.setFilter(Phaser.Textures.FilterMode.NEAREST);
|
| 98 |
+
creditsText.setDepth(10);
|
| 99 |
+
|
| 100 |
+
// Wait for space key to start
|
| 101 |
+
this.input.keyboard.once('keydown-SPACE', () => {
|
| 102 |
+
this.scene.start('ModeSelectScene');
|
| 103 |
+
});
|
| 104 |
+
}
|
| 105 |
+
}
|
| 106 |
+
|
src/scenes/PreloadScene_new.js
ADDED
|
@@ -0,0 +1,52 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import Phaser from 'phaser';
|
| 2 |
+
import { MAX_LEVEL } from '../constants.js';
|
| 3 |
+
|
| 4 |
+
export default class PreloadScene extends Phaser.Scene {
|
| 5 |
+
constructor() {
|
| 6 |
+
super({ key: 'PreloadScene' });
|
| 7 |
+
}
|
| 8 |
+
|
| 9 |
+
preload() {
|
| 10 |
+
// Load pixel font first
|
| 11 |
+
this.load.font('retro', 'assets/fonts/font.otf', 'opentype');
|
| 12 |
+
|
| 13 |
+
// Create loading text
|
| 14 |
+
const loadingText = this.add.text(128, 112, 'LOADING...', {
|
| 15 |
+
fontFamily: 'monospace',
|
| 16 |
+
fontSize: '8px',
|
| 17 |
+
color: '#ffffff'
|
| 18 |
+
}).setOrigin(0.5);
|
| 19 |
+
|
| 20 |
+
// Load block sprite sheet (64x8px, 10 sprites of 8x8 each)
|
| 21 |
+
this.load.image('blocks-spritesheet', 'assets/blocks.png');
|
| 22 |
+
|
| 23 |
+
// Load backdrops for all levels
|
| 24 |
+
for (let i = 1; i <= MAX_LEVEL; i++) {
|
| 25 |
+
this.load.image(`backdrop-${i}`, `assets/backdrops/level-${i}/backdrop.png`);
|
| 26 |
+
}
|
| 27 |
+
|
| 28 |
+
// Load music for all levels
|
| 29 |
+
for (let i = 1; i <= MAX_LEVEL; i++) {
|
| 30 |
+
this.load.audio(`music-${i}`, `assets/music/level-${i}/track.mp3`);
|
| 31 |
+
}
|
| 32 |
+
|
| 33 |
+
// Update loading progress
|
| 34 |
+
this.load.on('progress', (value) => {
|
| 35 |
+
loadingText.setText(`LOADING... ${Math.floor(value * 100)}%`);
|
| 36 |
+
});
|
| 37 |
+
|
| 38 |
+
this.load.on('complete', () => {
|
| 39 |
+
// Update to use pixel font once loaded
|
| 40 |
+
loadingText.setFontFamily('retro');
|
| 41 |
+
loadingText.setText('PRESS SPACE TO START');
|
| 42 |
+
});
|
| 43 |
+
}
|
| 44 |
+
|
| 45 |
+
create() {
|
| 46 |
+
// Wait for space key to start
|
| 47 |
+
this.input.keyboard.once('keydown-SPACE', () => {
|
| 48 |
+
this.scene.start('GameScene');
|
| 49 |
+
});
|
| 50 |
+
}
|
| 51 |
+
}
|
| 52 |
+
|
src/shaderOverlay.js
ADDED
|
@@ -0,0 +1,212 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
// This creates a WebGL canvas overlay that applies the Trinitron shader to the final scaled output
|
| 2 |
+
|
| 3 |
+
const vertexShaderSource = `
|
| 4 |
+
attribute vec2 a_position;
|
| 5 |
+
attribute vec2 a_texCoord;
|
| 6 |
+
varying vec2 v_texCoord;
|
| 7 |
+
|
| 8 |
+
void main() {
|
| 9 |
+
gl_Position = vec4(a_position, 0.0, 1.0);
|
| 10 |
+
v_texCoord = a_texCoord;
|
| 11 |
+
}
|
| 12 |
+
`;
|
| 13 |
+
|
| 14 |
+
const fragmentShaderSource = `
|
| 15 |
+
precision mediump float;
|
| 16 |
+
|
| 17 |
+
uniform sampler2D u_texture;
|
| 18 |
+
uniform vec2 u_resolution;
|
| 19 |
+
uniform float u_time;
|
| 20 |
+
|
| 21 |
+
varying vec2 v_texCoord;
|
| 22 |
+
|
| 23 |
+
#define PI 3.14159265359
|
| 24 |
+
|
| 25 |
+
// Random noise function for static
|
| 26 |
+
float random(vec2 co) {
|
| 27 |
+
return fract(sin(dot(co.xy, vec2(12.9898, 78.233))) * 43758.5453);
|
| 28 |
+
}
|
| 29 |
+
|
| 30 |
+
void main() {
|
| 31 |
+
vec2 uv = v_texCoord;
|
| 32 |
+
|
| 33 |
+
// CRT curvature - subtle but noticeable
|
| 34 |
+
vec2 centered = uv - 0.5;
|
| 35 |
+
float curvature = 0.06; // Curvature amount (reduced by half from 0.12)
|
| 36 |
+
|
| 37 |
+
// Apply barrel distortion
|
| 38 |
+
float r2 = centered.x * centered.x + centered.y * centered.y;
|
| 39 |
+
float distortion = 1.0 + curvature * r2;
|
| 40 |
+
vec2 curvedUV = centered * distortion + 0.5;
|
| 41 |
+
|
| 42 |
+
// Check if we're outside the original screen bounds (black borders)
|
| 43 |
+
if (curvedUV.x < 0.0 || curvedUV.x > 1.0 || curvedUV.y < 0.0 || curvedUV.y > 1.0) {
|
| 44 |
+
gl_FragColor = vec4(0.0, 0.0, 0.0, 1.0);
|
| 45 |
+
return;
|
| 46 |
+
}
|
| 47 |
+
|
| 48 |
+
vec3 color = texture2D(u_texture, curvedUV).rgb;
|
| 49 |
+
|
| 50 |
+
// Add static noise - using larger blocks for grainier effect
|
| 51 |
+
// Divide by 4.0 to make static "pixels" 4x4 screen pixels
|
| 52 |
+
vec2 staticCoord = floor(gl_FragCoord.xy / 4.0);
|
| 53 |
+
float staticNoise = random(staticCoord + vec2(u_time * 100.0)) * 0.06; // Slightly lower intensity
|
| 54 |
+
color += vec3(staticNoise);
|
| 55 |
+
|
| 56 |
+
// Add flicker (brightness variation over time) - multiple frequencies for realism
|
| 57 |
+
float flicker = sin(u_time * 12.0) * 0.015 + sin(u_time * 5.7) * 0.0125 + sin(u_time * 23.3) * 0.0075;
|
| 58 |
+
color *= (1.0 + flicker);
|
| 59 |
+
|
| 60 |
+
// 480i scanline effect - simulating classic CRT TV
|
| 61 |
+
float scanline = gl_FragCoord.y;
|
| 62 |
+
|
| 63 |
+
// Calculate scanline width to achieve ~480 scanlines for current resolution
|
| 64 |
+
// For 556px height, we want 480 scanlines: 556/480 ≈ 1.16 pixels per scanline
|
| 65 |
+
float scanlineWidth = 2.0;
|
| 66 |
+
float scanlineIntensity = 0.7; // How dark the scanlines are
|
| 67 |
+
float scanlineMod = mod(scanline, scanlineWidth);
|
| 68 |
+
|
| 69 |
+
// Make scanline darker for half the pixels
|
| 70 |
+
float scanlineFactor = 1.0;
|
| 71 |
+
if (scanlineMod < 1.0) {
|
| 72 |
+
scanlineFactor = 1.0 - scanlineIntensity;
|
| 73 |
+
}
|
| 74 |
+
|
| 75 |
+
// Apply scanlines
|
| 76 |
+
color *= scanlineFactor;
|
| 77 |
+
|
| 78 |
+
// Slight bloom/glow on bright areas (CRT phosphor persistence)
|
| 79 |
+
float brightness = (color.r + color.g + color.b) / 3.0;
|
| 80 |
+
color *= 1.0 + (brightness * 0.15);
|
| 81 |
+
|
| 82 |
+
// Vignette (darker edges like a CRT tube)
|
| 83 |
+
float vignette = 1.0 - dot(centered, centered) * 0.4;
|
| 84 |
+
color *= vignette;
|
| 85 |
+
|
| 86 |
+
// Slight color shift for CRT feel
|
| 87 |
+
color.r *= 1.02;
|
| 88 |
+
color.b *= 0.98;
|
| 89 |
+
|
| 90 |
+
gl_FragColor = vec4(color, 1.0);
|
| 91 |
+
}
|
| 92 |
+
`;
|
| 93 |
+
|
| 94 |
+
export function createShaderOverlay(gameCanvas) {
|
| 95 |
+
console.log('Creating shader overlay for canvas:', gameCanvas);
|
| 96 |
+
|
| 97 |
+
// Create overlay canvas
|
| 98 |
+
const overlay = document.createElement('canvas');
|
| 99 |
+
overlay.style.position = 'absolute';
|
| 100 |
+
overlay.style.pointerEvents = 'none';
|
| 101 |
+
overlay.style.zIndex = '1000';
|
| 102 |
+
|
| 103 |
+
// Position it over the game canvas
|
| 104 |
+
const updateOverlayPosition = () => {
|
| 105 |
+
const rect = gameCanvas.getBoundingClientRect();
|
| 106 |
+
overlay.style.left = rect.left + 'px';
|
| 107 |
+
overlay.style.top = rect.top + 'px';
|
| 108 |
+
overlay.width = rect.width;
|
| 109 |
+
overlay.height = rect.height;
|
| 110 |
+
overlay.style.width = rect.width + 'px';
|
| 111 |
+
overlay.style.height = rect.height + 'px';
|
| 112 |
+
};
|
| 113 |
+
|
| 114 |
+
document.body.appendChild(overlay);
|
| 115 |
+
updateOverlayPosition();
|
| 116 |
+
|
| 117 |
+
// Update on resize
|
| 118 |
+
window.addEventListener('resize', updateOverlayPosition);
|
| 119 |
+
|
| 120 |
+
const gl = overlay.getContext('webgl') || overlay.getContext('experimental-webgl');
|
| 121 |
+
if (!gl) {
|
| 122 |
+
console.error('WebGL not supported');
|
| 123 |
+
return null;
|
| 124 |
+
}
|
| 125 |
+
|
| 126 |
+
console.log('WebGL context created, overlay size:', overlay.width, 'x', overlay.height);
|
| 127 |
+
|
| 128 |
+
// Compile shaders
|
| 129 |
+
function compileShader(source, type) {
|
| 130 |
+
const shader = gl.createShader(type);
|
| 131 |
+
gl.shaderSource(shader, source);
|
| 132 |
+
gl.compileShader(shader);
|
| 133 |
+
if (!gl.getShaderParameter(shader, gl.COMPILE_STATUS)) {
|
| 134 |
+
console.error('Shader compile error:', gl.getShaderInfoLog(shader));
|
| 135 |
+
gl.deleteShader(shader);
|
| 136 |
+
return null;
|
| 137 |
+
}
|
| 138 |
+
return shader;
|
| 139 |
+
}
|
| 140 |
+
|
| 141 |
+
const vertexShader = compileShader(vertexShaderSource, gl.VERTEX_SHADER);
|
| 142 |
+
const fragmentShader = compileShader(fragmentShaderSource, gl.FRAGMENT_SHADER);
|
| 143 |
+
|
| 144 |
+
const program = gl.createProgram();
|
| 145 |
+
gl.attachShader(program, vertexShader);
|
| 146 |
+
gl.attachShader(program, fragmentShader);
|
| 147 |
+
gl.linkProgram(program);
|
| 148 |
+
|
| 149 |
+
if (!gl.getProgramParameter(program, gl.LINK_STATUS)) {
|
| 150 |
+
console.error('Program link error:', gl.getProgramInfoLog(program));
|
| 151 |
+
return null;
|
| 152 |
+
}
|
| 153 |
+
|
| 154 |
+
gl.useProgram(program);
|
| 155 |
+
|
| 156 |
+
// Set up geometry (flip Y coordinate for texture)
|
| 157 |
+
const positions = new Float32Array([
|
| 158 |
+
-1, -1, 0, 1,
|
| 159 |
+
1, -1, 1, 1,
|
| 160 |
+
-1, 1, 0, 0,
|
| 161 |
+
1, 1, 1, 0
|
| 162 |
+
]);
|
| 163 |
+
|
| 164 |
+
const buffer = gl.createBuffer();
|
| 165 |
+
gl.bindBuffer(gl.ARRAY_BUFFER, buffer);
|
| 166 |
+
gl.bufferData(gl.ARRAY_BUFFER, positions, gl.STATIC_DRAW);
|
| 167 |
+
|
| 168 |
+
const positionLoc = gl.getAttribLocation(program, 'a_position');
|
| 169 |
+
const texCoordLoc = gl.getAttribLocation(program, 'a_texCoord');
|
| 170 |
+
|
| 171 |
+
gl.enableVertexAttribArray(positionLoc);
|
| 172 |
+
gl.vertexAttribPointer(positionLoc, 2, gl.FLOAT, false, 16, 0);
|
| 173 |
+
|
| 174 |
+
gl.enableVertexAttribArray(texCoordLoc);
|
| 175 |
+
gl.vertexAttribPointer(texCoordLoc, 2, gl.FLOAT, false, 16, 8);
|
| 176 |
+
|
| 177 |
+
// Create texture from game canvas
|
| 178 |
+
const texture = gl.createTexture();
|
| 179 |
+
gl.bindTexture(gl.TEXTURE_2D, texture);
|
| 180 |
+
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_S, gl.CLAMP_TO_EDGE);
|
| 181 |
+
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_T, gl.CLAMP_TO_EDGE);
|
| 182 |
+
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, gl.LINEAR);
|
| 183 |
+
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MAG_FILTER, gl.LINEAR);
|
| 184 |
+
|
| 185 |
+
const resolutionLoc = gl.getUniformLocation(program, 'u_resolution');
|
| 186 |
+
const timeLoc = gl.getUniformLocation(program, 'u_time');
|
| 187 |
+
const borderWidthLoc = gl.getUniformLocation(program, 'u_borderWidth');
|
| 188 |
+
|
| 189 |
+
// Render loop
|
| 190 |
+
function render() {
|
| 191 |
+
updateOverlayPosition();
|
| 192 |
+
|
| 193 |
+
// Copy game canvas to texture
|
| 194 |
+
gl.bindTexture(gl.TEXTURE_2D, texture);
|
| 195 |
+
gl.texImage2D(gl.TEXTURE_2D, 0, gl.RGBA, gl.RGBA, gl.UNSIGNED_BYTE, gameCanvas);
|
| 196 |
+
|
| 197 |
+
// Set uniforms
|
| 198 |
+
gl.uniform2f(resolutionLoc, overlay.width, overlay.height);
|
| 199 |
+
gl.uniform1f(timeLoc, performance.now() / 1000);
|
| 200 |
+
|
| 201 |
+
// Draw
|
| 202 |
+
gl.viewport(0, 0, overlay.width, overlay.height);
|
| 203 |
+
gl.drawArrays(gl.TRIANGLE_STRIP, 0, 4);
|
| 204 |
+
|
| 205 |
+
requestAnimationFrame(render);
|
| 206 |
+
}
|
| 207 |
+
|
| 208 |
+
render();
|
| 209 |
+
|
| 210 |
+
return overlay;
|
| 211 |
+
}
|
| 212 |
+
|
src/shaders/trinitron-fragment.glsl
ADDED
|
@@ -0,0 +1,179 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
precision mediump float;
|
| 2 |
+
|
| 3 |
+
// Texture and coordinates
|
| 4 |
+
uniform sampler2D uMainSampler;
|
| 5 |
+
varying vec2 outTexCoord;
|
| 6 |
+
|
| 7 |
+
// Uniforms for shader parameters
|
| 8 |
+
uniform vec2 resolution;
|
| 9 |
+
uniform float time;
|
| 10 |
+
|
| 11 |
+
#define PI 3.14159265359
|
| 12 |
+
|
| 13 |
+
// --- Noise Helper Function (Permutation) ---
|
| 14 |
+
vec4 permute(vec4 t) {
|
| 15 |
+
return mod(((t * 34.0) + 1.0) * t, 289.0);
|
| 16 |
+
}
|
| 17 |
+
|
| 18 |
+
// --- 3D Noise Function ---
|
| 19 |
+
float noise3d(vec3 p) {
|
| 20 |
+
vec3 a = floor(p);
|
| 21 |
+
vec3 d = p - a;
|
| 22 |
+
d = d * d * (3.0 - 2.0 * d);
|
| 23 |
+
|
| 24 |
+
vec4 b = a.xxyy + vec4(0.0, 1.0, 0.0, 1.0);
|
| 25 |
+
vec4 k1 = permute(b.xyxy);
|
| 26 |
+
vec4 k2 = permute(k1.xyxy + b.zzww);
|
| 27 |
+
|
| 28 |
+
vec4 c = k2 + a.zzzz;
|
| 29 |
+
vec4 k3 = permute(c);
|
| 30 |
+
vec4 k4 = permute(c + 1.0);
|
| 31 |
+
|
| 32 |
+
vec4 o1 = fract(k3 * (1.0 / 41.0));
|
| 33 |
+
vec4 o2 = fract(k4 * (1.0 / 41.0));
|
| 34 |
+
|
| 35 |
+
vec4 o3_interp_z = o2 * d.z + o1 * (1.0 - d.z);
|
| 36 |
+
vec2 o4_interp_xy = o3_interp_z.yw * d.x + o3_interp_z.xz * (1.0 - d.x);
|
| 37 |
+
|
| 38 |
+
return o4_interp_xy.y * d.y + o4_interp_xy.x * (1.0 - d.y);
|
| 39 |
+
}
|
| 40 |
+
|
| 41 |
+
void main() {
|
| 42 |
+
// --- Configuration Parameters ---
|
| 43 |
+
float brightness = 2.5;
|
| 44 |
+
float red_balance = 1.0;
|
| 45 |
+
float green_balance = 0.85;
|
| 46 |
+
float blue_balance = 1.0;
|
| 47 |
+
|
| 48 |
+
// Custom settings as requested
|
| 49 |
+
float phosphorWidth = 2.50;
|
| 50 |
+
float phosphorHeight = 4.50;
|
| 51 |
+
float internalHorizontalGap = 1.0;
|
| 52 |
+
float columnGap = 0.2;
|
| 53 |
+
float verticalCellGap = 0.2;
|
| 54 |
+
float phosphorPower = 0.9;
|
| 55 |
+
|
| 56 |
+
float cell_noise_variation_amount = 0.025;
|
| 57 |
+
float cell_noise_scale_xy = 240.0;
|
| 58 |
+
float cell_noise_speed = 24.0;
|
| 59 |
+
float curvature_amount = 0.0; // Set to 0 as requested
|
| 60 |
+
|
| 61 |
+
// --- Apply Curvature Distortion ---
|
| 62 |
+
vec2 fragCoord = gl_FragCoord.xy;
|
| 63 |
+
vec2 uv = outTexCoord;
|
| 64 |
+
vec2 centered_uv_output = uv - 0.5;
|
| 65 |
+
float r = length(centered_uv_output);
|
| 66 |
+
float distort_factor = 1.0 + curvature_amount * r * r;
|
| 67 |
+
vec2 centered_uv_source = centered_uv_output * distort_factor;
|
| 68 |
+
vec2 source_uv = centered_uv_source + 0.5;
|
| 69 |
+
vec2 fragCoord_warped = source_uv * resolution;
|
| 70 |
+
|
| 71 |
+
// --- Check if Warped Coordinate is on the "Flat Screen" ---
|
| 72 |
+
bool is_on_original_flat_screen = source_uv.x >= 0.0 && source_uv.x <= 1.0 &&
|
| 73 |
+
source_uv.y >= 0.0 && source_uv.y <= 1.0;
|
| 74 |
+
|
| 75 |
+
if (!is_on_original_flat_screen) {
|
| 76 |
+
gl_FragColor = vec4(0.0, 0.0, 0.0, 1.0);
|
| 77 |
+
return;
|
| 78 |
+
}
|
| 79 |
+
|
| 80 |
+
// --- Calculated Grid Dimensions ---
|
| 81 |
+
float fullCellWidth = 3.0 * phosphorWidth + 3.0 * internalHorizontalGap + columnGap;
|
| 82 |
+
float fullRowHeight = phosphorHeight + verticalCellGap;
|
| 83 |
+
|
| 84 |
+
// --- Calculate Logical Grid Positions ---
|
| 85 |
+
float logical_cell_index_x = floor(fragCoord_warped.x / fullCellWidth);
|
| 86 |
+
float shift_y_offset = 0.0;
|
| 87 |
+
|
| 88 |
+
if (mod(logical_cell_index_x, 2.0) != 0.0) {
|
| 89 |
+
shift_y_offset = fullRowHeight / 2.0;
|
| 90 |
+
}
|
| 91 |
+
|
| 92 |
+
float effective_y_warped = fragCoord_warped.y + shift_y_offset;
|
| 93 |
+
float logical_row_index = floor(effective_y_warped / fullRowHeight);
|
| 94 |
+
|
| 95 |
+
float uv_cell_x = mod(fragCoord_warped.x, fullCellWidth);
|
| 96 |
+
if (uv_cell_x < 0.0) {
|
| 97 |
+
uv_cell_x += fullCellWidth;
|
| 98 |
+
}
|
| 99 |
+
|
| 100 |
+
float uv_row_y = mod(effective_y_warped, fullRowHeight);
|
| 101 |
+
if (uv_row_y < 0.0) {
|
| 102 |
+
uv_row_y += fullRowHeight;
|
| 103 |
+
}
|
| 104 |
+
|
| 105 |
+
// --- Video Sampling and Color Balancing ---
|
| 106 |
+
vec3 video_color = texture2D(uMainSampler, source_uv).rgb;
|
| 107 |
+
video_color.r *= red_balance;
|
| 108 |
+
video_color.g *= green_balance;
|
| 109 |
+
video_color.b *= blue_balance;
|
| 110 |
+
|
| 111 |
+
// --- Determine if inside a Phosphor Area ---
|
| 112 |
+
vec3 final_color = vec3(0.0);
|
| 113 |
+
bool in_column_gap = uv_cell_x >= (3.0 * phosphorWidth + 3.0 * internalHorizontalGap);
|
| 114 |
+
bool in_vertical_gap = uv_row_y >= phosphorHeight;
|
| 115 |
+
|
| 116 |
+
if (!in_column_gap && !in_vertical_gap) {
|
| 117 |
+
float uv_cell_x_within_block = uv_cell_x;
|
| 118 |
+
vec3 phosphor_base_color = vec3(0.0);
|
| 119 |
+
float video_component_intensity = 0.0;
|
| 120 |
+
float current_phosphor_startX_in_block = -1.0;
|
| 121 |
+
float current_x_tracker = 0.0;
|
| 122 |
+
|
| 123 |
+
// Red phosphor area
|
| 124 |
+
if (uv_cell_x_within_block >= current_x_tracker && uv_cell_x_within_block < current_x_tracker + phosphorWidth) {
|
| 125 |
+
phosphor_base_color = vec3(1.0, 0.0, 0.0);
|
| 126 |
+
video_component_intensity = video_color.r;
|
| 127 |
+
current_phosphor_startX_in_block = current_x_tracker;
|
| 128 |
+
}
|
| 129 |
+
current_x_tracker += phosphorWidth + internalHorizontalGap;
|
| 130 |
+
|
| 131 |
+
// Green phosphor area
|
| 132 |
+
if (uv_cell_x_within_block >= current_x_tracker && uv_cell_x_within_block < current_x_tracker + phosphorWidth) {
|
| 133 |
+
phosphor_base_color = vec3(0.0, 1.0, 0.0);
|
| 134 |
+
video_component_intensity = video_color.g;
|
| 135 |
+
current_phosphor_startX_in_block = current_x_tracker;
|
| 136 |
+
}
|
| 137 |
+
current_x_tracker += phosphorWidth + internalHorizontalGap;
|
| 138 |
+
|
| 139 |
+
// Blue phosphor area
|
| 140 |
+
if (uv_cell_x_within_block >= current_x_tracker && uv_cell_x_within_block < current_x_tracker + phosphorWidth) {
|
| 141 |
+
phosphor_base_color = vec3(0.0, 0.0, 1.0);
|
| 142 |
+
video_component_intensity = video_color.b;
|
| 143 |
+
current_phosphor_startX_in_block = current_x_tracker;
|
| 144 |
+
}
|
| 145 |
+
|
| 146 |
+
if (current_phosphor_startX_in_block >= 0.0) {
|
| 147 |
+
float x_in_phosphor = (uv_cell_x_within_block - current_phosphor_startX_in_block) / phosphorWidth;
|
| 148 |
+
float horizontal_intensity_factor = pow(sin(x_in_phosphor * PI), phosphorPower);
|
| 149 |
+
float y_in_phosphor_band = uv_row_y / phosphorHeight;
|
| 150 |
+
float vertical_intensity_factor = (phosphorHeight > 0.0) ? pow(sin(y_in_phosphor_band * PI), phosphorPower) : 1.0;
|
| 151 |
+
float total_intensity_factor = horizontal_intensity_factor * vertical_intensity_factor;
|
| 152 |
+
final_color = phosphor_base_color * video_component_intensity * total_intensity_factor;
|
| 153 |
+
}
|
| 154 |
+
}
|
| 155 |
+
|
| 156 |
+
// --- Apply Cell-Based RGB Analog Noise ---
|
| 157 |
+
vec3 noise_pos = vec3(logical_cell_index_x * cell_noise_scale_xy,
|
| 158 |
+
logical_row_index * cell_noise_scale_xy,
|
| 159 |
+
time * cell_noise_speed);
|
| 160 |
+
|
| 161 |
+
vec3 cell_noise_rgb;
|
| 162 |
+
cell_noise_rgb.r = noise3d(noise_pos);
|
| 163 |
+
cell_noise_rgb.g = noise3d(noise_pos + vec3(19.0, 0.0, 0.0));
|
| 164 |
+
cell_noise_rgb.b = noise3d(noise_pos + vec3(0.0, 13.0, 0.0));
|
| 165 |
+
cell_noise_rgb = cell_noise_rgb * 2.0 - 1.0;
|
| 166 |
+
final_color += cell_noise_rgb * cell_noise_variation_amount;
|
| 167 |
+
|
| 168 |
+
// --- Apply Overall Brightness and Effects ---
|
| 169 |
+
final_color *= brightness;
|
| 170 |
+
float edge_darken_strength = 0.1;
|
| 171 |
+
float vignette_factor = 1.0 - dot(centered_uv_output, centered_uv_output) * edge_darken_strength * 2.0;
|
| 172 |
+
vignette_factor = clamp(vignette_factor, 0.0, 1.0);
|
| 173 |
+
final_color *= vignette_factor;
|
| 174 |
+
|
| 175 |
+
// --- Output ---
|
| 176 |
+
final_color = clamp(final_color, 0.0, 1.0);
|
| 177 |
+
gl_FragColor = vec4(final_color, 1.0);
|
| 178 |
+
}
|
| 179 |
+
|
src/utils/BlockRenderer.js
ADDED
|
@@ -0,0 +1,128 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import { BLOCK_SIZE } from '../constants.js';
|
| 2 |
+
|
| 3 |
+
/**
|
| 4 |
+
* Renders Tetris blocks with different pixel art styles per level
|
| 5 |
+
*/
|
| 6 |
+
export default class BlockRenderer {
|
| 7 |
+
/**
|
| 8 |
+
* Create a block texture with a specific style
|
| 9 |
+
* @param {Phaser.Scene} scene - The Phaser scene
|
| 10 |
+
* @param {number} color - The color in hex format
|
| 11 |
+
* @param {number} level - Current level (1-10) determines style
|
| 12 |
+
* @param {string} key - Texture key to create
|
| 13 |
+
*/
|
| 14 |
+
static createBlockTexture(scene, color, level, key) {
|
| 15 |
+
const graphics = scene.make.graphics({ x: 0, y: 0, add: false });
|
| 16 |
+
|
| 17 |
+
// Extract RGB components
|
| 18 |
+
const r = (color >> 16) & 0xFF;
|
| 19 |
+
const g = (color >> 8) & 0xFF;
|
| 20 |
+
const b = color & 0xFF;
|
| 21 |
+
|
| 22 |
+
// Create lighter and darker shades
|
| 23 |
+
const lightColor = Phaser.Display.Color.GetColor(
|
| 24 |
+
Math.min(255, r + 60),
|
| 25 |
+
Math.min(255, g + 60),
|
| 26 |
+
Math.min(255, b + 60)
|
| 27 |
+
);
|
| 28 |
+
const darkColor = Phaser.Display.Color.GetColor(
|
| 29 |
+
Math.max(0, r - 60),
|
| 30 |
+
Math.max(0, g - 60),
|
| 31 |
+
Math.max(0, b - 60)
|
| 32 |
+
);
|
| 33 |
+
|
| 34 |
+
// Different styles based on level
|
| 35 |
+
const style = (level - 1) % 5; // 5 different styles cycling
|
| 36 |
+
|
| 37 |
+
switch (style) {
|
| 38 |
+
case 0: // Classic with border
|
| 39 |
+
this.drawClassicBlock(graphics, color, lightColor, darkColor);
|
| 40 |
+
break;
|
| 41 |
+
case 1: // Gradient style
|
| 42 |
+
this.drawGradientBlock(graphics, color, lightColor, darkColor);
|
| 43 |
+
break;
|
| 44 |
+
case 2: // Dotted pattern
|
| 45 |
+
this.drawDottedBlock(graphics, color, lightColor);
|
| 46 |
+
break;
|
| 47 |
+
case 3: // Checkered
|
| 48 |
+
this.drawCheckeredBlock(graphics, color, darkColor);
|
| 49 |
+
break;
|
| 50 |
+
case 4: // Outlined
|
| 51 |
+
this.drawOutlinedBlock(graphics, color, lightColor, darkColor);
|
| 52 |
+
break;
|
| 53 |
+
}
|
| 54 |
+
|
| 55 |
+
graphics.generateTexture(key, BLOCK_SIZE, BLOCK_SIZE);
|
| 56 |
+
graphics.destroy();
|
| 57 |
+
}
|
| 58 |
+
|
| 59 |
+
static drawClassicBlock(graphics, color, lightColor, darkColor) {
|
| 60 |
+
// Fill
|
| 61 |
+
graphics.fillStyle(color);
|
| 62 |
+
graphics.fillRect(0, 0, BLOCK_SIZE, BLOCK_SIZE);
|
| 63 |
+
|
| 64 |
+
// Light edge (top-left)
|
| 65 |
+
graphics.fillStyle(lightColor);
|
| 66 |
+
graphics.fillRect(0, 0, BLOCK_SIZE, 1);
|
| 67 |
+
graphics.fillRect(0, 0, 1, BLOCK_SIZE);
|
| 68 |
+
|
| 69 |
+
// Dark edge (bottom-right)
|
| 70 |
+
graphics.fillStyle(darkColor);
|
| 71 |
+
graphics.fillRect(0, BLOCK_SIZE - 1, BLOCK_SIZE, 1);
|
| 72 |
+
graphics.fillRect(BLOCK_SIZE - 1, 0, 1, BLOCK_SIZE);
|
| 73 |
+
}
|
| 74 |
+
|
| 75 |
+
static drawGradientBlock(graphics, color, lightColor, darkColor) {
|
| 76 |
+
// Create gradient effect with horizontal bands
|
| 77 |
+
for (let y = 0; y < BLOCK_SIZE; y++) {
|
| 78 |
+
const ratio = y / BLOCK_SIZE;
|
| 79 |
+
const r = Phaser.Math.Linear((lightColor >> 16) & 0xFF, (darkColor >> 16) & 0xFF, ratio);
|
| 80 |
+
const g = Phaser.Math.Linear((lightColor >> 8) & 0xFF, (darkColor >> 8) & 0xFF, ratio);
|
| 81 |
+
const b = Phaser.Math.Linear(lightColor & 0xFF, darkColor & 0xFF, ratio);
|
| 82 |
+
graphics.fillStyle(Phaser.Display.Color.GetColor(r, g, b));
|
| 83 |
+
graphics.fillRect(0, y, BLOCK_SIZE, 1);
|
| 84 |
+
}
|
| 85 |
+
}
|
| 86 |
+
|
| 87 |
+
static drawDottedBlock(graphics, color, lightColor) {
|
| 88 |
+
graphics.fillStyle(color);
|
| 89 |
+
graphics.fillRect(0, 0, BLOCK_SIZE, BLOCK_SIZE);
|
| 90 |
+
|
| 91 |
+
// Add dots
|
| 92 |
+
graphics.fillStyle(lightColor);
|
| 93 |
+
for (let y = 1; y < BLOCK_SIZE; y += 3) {
|
| 94 |
+
for (let x = 1; x < BLOCK_SIZE; x += 3) {
|
| 95 |
+
graphics.fillRect(x, y, 1, 1);
|
| 96 |
+
}
|
| 97 |
+
}
|
| 98 |
+
}
|
| 99 |
+
|
| 100 |
+
static drawCheckeredBlock(graphics, color, darkColor) {
|
| 101 |
+
for (let y = 0; y < BLOCK_SIZE; y += 2) {
|
| 102 |
+
for (let x = 0; x < BLOCK_SIZE; x += 2) {
|
| 103 |
+
const useMain = (x + y) % 4 === 0;
|
| 104 |
+
graphics.fillStyle(useMain ? color : darkColor);
|
| 105 |
+
graphics.fillRect(x, y, 2, 2);
|
| 106 |
+
}
|
| 107 |
+
}
|
| 108 |
+
}
|
| 109 |
+
|
| 110 |
+
static drawOutlinedBlock(graphics, color, lightColor, darkColor) {
|
| 111 |
+
// Fill
|
| 112 |
+
graphics.fillStyle(color);
|
| 113 |
+
graphics.fillRect(1, 1, BLOCK_SIZE - 2, BLOCK_SIZE - 2);
|
| 114 |
+
|
| 115 |
+
// Thick outline
|
| 116 |
+
graphics.fillStyle(darkColor);
|
| 117 |
+
graphics.fillRect(0, 0, BLOCK_SIZE, 1);
|
| 118 |
+
graphics.fillRect(0, 0, 1, BLOCK_SIZE);
|
| 119 |
+
graphics.fillRect(0, BLOCK_SIZE - 1, BLOCK_SIZE, 1);
|
| 120 |
+
graphics.fillRect(BLOCK_SIZE - 1, 0, 1, BLOCK_SIZE);
|
| 121 |
+
|
| 122 |
+
// Inner highlight
|
| 123 |
+
graphics.fillStyle(lightColor);
|
| 124 |
+
graphics.fillRect(2, 2, BLOCK_SIZE - 4, 1);
|
| 125 |
+
graphics.fillRect(2, 2, 1, BLOCK_SIZE - 4);
|
| 126 |
+
}
|
| 127 |
+
}
|
| 128 |
+
|
src/utils/ColorExtractor.js
ADDED
|
@@ -0,0 +1,84 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
/**
|
| 2 |
+
* Extracts dominant colors from an image to create a palette for Tetris blocks
|
| 3 |
+
*/
|
| 4 |
+
export default class ColorExtractor {
|
| 5 |
+
/**
|
| 6 |
+
* Extract 7 dominant colors from a texture
|
| 7 |
+
* @param {Phaser.Scene} scene - The Phaser scene
|
| 8 |
+
* @param {string} textureKey - The key of the loaded texture
|
| 9 |
+
* @returns {number[]} Array of 7 color values in hex format
|
| 10 |
+
*/
|
| 11 |
+
static extractPalette(scene, textureKey) {
|
| 12 |
+
const texture = scene.textures.get(textureKey);
|
| 13 |
+
const source = texture.getSourceImage();
|
| 14 |
+
|
| 15 |
+
// Create a temporary canvas to analyze the image
|
| 16 |
+
const canvas = document.createElement('canvas');
|
| 17 |
+
const ctx = canvas.getContext('2d');
|
| 18 |
+
canvas.width = source.width;
|
| 19 |
+
canvas.height = source.height;
|
| 20 |
+
ctx.drawImage(source, 0, 0);
|
| 21 |
+
|
| 22 |
+
const imageData = ctx.getImageData(0, 0, canvas.width, canvas.height);
|
| 23 |
+
const pixels = imageData.data;
|
| 24 |
+
|
| 25 |
+
// Sample pixels (every 4th pixel for performance)
|
| 26 |
+
const colorMap = new Map();
|
| 27 |
+
for (let i = 0; i < pixels.length; i += 16) { // RGBA, skip every 4 pixels
|
| 28 |
+
const r = pixels[i];
|
| 29 |
+
const g = pixels[i + 1];
|
| 30 |
+
const b = pixels[i + 2];
|
| 31 |
+
const a = pixels[i + 3];
|
| 32 |
+
|
| 33 |
+
// Skip transparent pixels
|
| 34 |
+
if (a < 128) continue;
|
| 35 |
+
|
| 36 |
+
// Quantize colors to reduce similar shades
|
| 37 |
+
const qr = Math.round(r / 32) * 32;
|
| 38 |
+
const qg = Math.round(g / 32) * 32;
|
| 39 |
+
const qb = Math.round(b / 32) * 32;
|
| 40 |
+
|
| 41 |
+
const colorKey = (qr << 16) | (qg << 8) | qb;
|
| 42 |
+
colorMap.set(colorKey, (colorMap.get(colorKey) || 0) + 1);
|
| 43 |
+
}
|
| 44 |
+
|
| 45 |
+
// Sort colors by frequency
|
| 46 |
+
const sortedColors = Array.from(colorMap.entries())
|
| 47 |
+
.sort((a, b) => b[1] - a[1])
|
| 48 |
+
.map(entry => entry[0]);
|
| 49 |
+
|
| 50 |
+
// Get top 7 colors, ensuring variety
|
| 51 |
+
const palette = [];
|
| 52 |
+
for (let i = 0; i < sortedColors.length && palette.length < 7; i++) {
|
| 53 |
+
const color = sortedColors[i];
|
| 54 |
+
|
| 55 |
+
// Ensure color is not too dark (visible on dark backgrounds)
|
| 56 |
+
const r = (color >> 16) & 0xFF;
|
| 57 |
+
const g = (color >> 8) & 0xFF;
|
| 58 |
+
const b = color & 0xFF;
|
| 59 |
+
const brightness = (r + g + b) / 3;
|
| 60 |
+
|
| 61 |
+
if (brightness > 70) { // Skip dark colors - minimum brightness threshold
|
| 62 |
+
palette.push(color);
|
| 63 |
+
}
|
| 64 |
+
}
|
| 65 |
+
|
| 66 |
+
// Fill remaining slots with vibrant defaults if needed
|
| 67 |
+
const defaultColors = [
|
| 68 |
+
0x00F0F0, // Cyan
|
| 69 |
+
0xF0F000, // Yellow
|
| 70 |
+
0xA000F0, // Purple
|
| 71 |
+
0x00F000, // Green
|
| 72 |
+
0xF00000, // Red
|
| 73 |
+
0x0000F0, // Blue
|
| 74 |
+
0xF0A000 // Orange
|
| 75 |
+
];
|
| 76 |
+
|
| 77 |
+
while (palette.length < 7) {
|
| 78 |
+
palette.push(defaultColors[palette.length]);
|
| 79 |
+
}
|
| 80 |
+
|
| 81 |
+
return palette;
|
| 82 |
+
}
|
| 83 |
+
}
|
| 84 |
+
|