broadfield-dev commited on
Commit
78475cb
·
verified ·
1 Parent(s): e5f2822

Upload 51 files

Browse files
This view is limited to 50 files because it contains too many changes.   See raw diff
Files changed (50) hide show
  1. .gitattributes +11 -0
  2. public/assets/backdrops/level-1/backdrop.png +0 -0
  3. public/assets/backdrops/level-10/backdrop.png +0 -0
  4. public/assets/backdrops/level-10/title.png +0 -0
  5. public/assets/backdrops/level-2/backdrop.png +0 -0
  6. public/assets/backdrops/level-3/backdrop.png +0 -0
  7. public/assets/backdrops/level-4/backdrop.png +0 -0
  8. public/assets/backdrops/level-5/backdrop.png +0 -0
  9. public/assets/backdrops/level-6/backdrop.png +0 -0
  10. public/assets/backdrops/level-7/backdrop.png +0 -0
  11. public/assets/backdrops/level-8/backdrop.png +0 -0
  12. public/assets/backdrops/level-9/backdrop.png +0 -0
  13. public/assets/blocks-sprite.png +0 -0
  14. public/assets/blocks.png +0 -0
  15. public/assets/crush-alt.png +0 -0
  16. public/assets/crush.png +0 -0
  17. public/assets/fonts/RetroArcade.ttf +0 -0
  18. public/assets/fonts/font.otf +0 -0
  19. public/assets/fonts/font.png +0 -0
  20. public/assets/fonts/thick_8x8.png +0 -0
  21. public/assets/fonts/thick_8x8.xml +276 -0
  22. public/assets/game-over.png +0 -0
  23. public/assets/music/level-1/track.mp3 +3 -0
  24. public/assets/music/level-10/track.mp3 +3 -0
  25. public/assets/music/level-2/track.mp3 +3 -0
  26. public/assets/music/level-3/track.mp3 +3 -0
  27. public/assets/music/level-4/track.mp3 +3 -0
  28. public/assets/music/level-5/track.mp3 +3 -0
  29. public/assets/music/level-6/track.mp3 +3 -0
  30. public/assets/music/level-7/track.mp3 +3 -0
  31. public/assets/music/level-8/track.mp3 +3 -0
  32. public/assets/music/level-9/track.mp3 +3 -0
  33. public/assets/title.png +0 -0
  34. public/assets/tv.png +3 -0
  35. public/template-generator.html +184 -0
  36. scripts/generate-placeholders.js +188 -0
  37. scripts/generate-template.js +170 -0
  38. scripts/test-canvas.js +24 -0
  39. src/config.js +13 -0
  40. src/constants.js +188 -0
  41. src/main.js +40 -0
  42. src/pipelines/TrinitronPipeline.js +190 -0
  43. src/scenes/GameScene.js +983 -0
  44. src/scenes/ModeSelectScene.js +125 -0
  45. src/scenes/PreloadScene.js +106 -0
  46. src/scenes/PreloadScene_new.js +52 -0
  47. src/shaderOverlay.js +212 -0
  48. src/shaders/trinitron-fragment.glsl +179 -0
  49. src/utils/BlockRenderer.js +128 -0
  50. 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

  • SHA256: 3708b47de0b5b463d8817900ea7d406f316aaedf29f4ab26d3451f02ae27d6e2
  • Pointer size: 131 Bytes
  • Size of remote file: 171 kB
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
+