@ludra/cli 0.5.0 → 0.8.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (107) hide show
  1. package/blocks/chasing-enemy/assets/slime.svg +21 -0
  2. package/blocks/chasing-enemy/block.json +64 -0
  3. package/blocks/chasing-enemy/demo.json +32 -0
  4. package/blocks/chasing-enemy/scripts/chaser.ts +19 -0
  5. package/blocks/coin-row/assets/coin.svg +22 -0
  6. package/blocks/coin-row/block.json +157 -0
  7. package/blocks/coin-row/demo.json +15 -0
  8. package/blocks/falling-platform/assets/falling-platform.svg +13 -0
  9. package/blocks/falling-platform/block.json +55 -0
  10. package/blocks/falling-platform/demo.json +24 -0
  11. package/blocks/falling-platform/scripts/falling-platform.ts +56 -0
  12. package/blocks/jump-pad/assets/jump-pad.svg +6 -0
  13. package/blocks/jump-pad/block.json +56 -0
  14. package/blocks/jump-pad/demo.json +17 -0
  15. package/blocks/jump-pad/scripts/jump-pad.ts +10 -0
  16. package/blocks/key-and-door/assets/door.svg +12 -0
  17. package/blocks/key-and-door/assets/key.svg +9 -0
  18. package/blocks/key-and-door/block.json +90 -0
  19. package/blocks/key-and-door/demo.json +31 -0
  20. package/blocks/key-and-door/scripts/key.ts +12 -0
  21. package/blocks/key-and-door/scripts/locked-door.ts +15 -0
  22. package/blocks/moving-platform/assets/lift.svg +9 -0
  23. package/blocks/moving-platform/block.json +38 -0
  24. package/blocks/moving-platform/demo.json +27 -0
  25. package/blocks/patrol-enemy/assets/slime.svg +21 -0
  26. package/blocks/patrol-enemy/block.json +60 -0
  27. package/blocks/patrol-enemy/demo.json +22 -0
  28. package/blocks/spikes/assets/spikes.svg +8 -0
  29. package/blocks/spikes/block.json +49 -0
  30. package/blocks/spikes/demo.json +16 -0
  31. package/blocks/teleporter/assets/teleporter.svg +14 -0
  32. package/blocks/teleporter/block.json +89 -0
  33. package/blocks/teleporter/demo.json +17 -0
  34. package/blocks/teleporter/scripts/teleporter.ts +17 -0
  35. package/blocks/time-limit/block.json +36 -0
  36. package/blocks/time-limit/demo.json +25 -0
  37. package/blocks/time-limit/scripts/time-limit.ts +16 -0
  38. package/blocks/turret/assets/bullet.svg +5 -0
  39. package/blocks/turret/assets/turret.svg +10 -0
  40. package/blocks/turret/block.json +59 -0
  41. package/blocks/turret/demo.json +35 -0
  42. package/blocks/turret/scripts/bullet.ts +11 -0
  43. package/blocks/turret/scripts/turret.ts +36 -0
  44. package/dist/blocks.d.ts +65 -0
  45. package/dist/blocks.js +95 -0
  46. package/dist/commands/blocks.d.ts +12 -0
  47. package/dist/commands/blocks.js +49 -0
  48. package/dist/commands/check.d.ts +1 -0
  49. package/dist/commands/check.js +57 -14
  50. package/dist/commands/dev.js +7 -2
  51. package/dist/commands/new.js +1 -1
  52. package/dist/commands/play.d.ts +1 -0
  53. package/dist/commands/play.js +12 -4
  54. package/dist/commands/review.d.ts +22 -0
  55. package/dist/commands/review.js +183 -0
  56. package/dist/commands/test.d.ts +1 -0
  57. package/dist/commands/test.js +20 -5
  58. package/dist/headless.d.ts +2 -4
  59. package/dist/headless.js +21 -3
  60. package/dist/index.js +25 -5
  61. package/dist/project.d.ts +18 -5
  62. package/dist/project.js +61 -21
  63. package/dist/server.js +22 -9
  64. package/package.json +4 -3
  65. package/templates/breakout/CLAUDE.md +43 -0
  66. package/templates/breakout/_gitignore +2 -0
  67. package/templates/breakout/ludra.json +5 -0
  68. package/templates/breakout/package.json +13 -0
  69. package/templates/breakout/scenes/main.scene.json +653 -0
  70. package/templates/breakout/scripts/backdrop.ts +101 -0
  71. package/templates/breakout/scripts/ball.ts +89 -0
  72. package/templates/breakout/scripts/brick.ts +51 -0
  73. package/templates/breakout/scripts/paddle.ts +53 -0
  74. package/templates/breakout/scripts/rules.ts +147 -0
  75. package/templates/breakout/scripts/wall.ts +21 -0
  76. package/templates/breakout/shaders/neon.frag +42 -0
  77. package/templates/breakout/tests/gameplay.test.json +31 -0
  78. package/templates/platformer/CLAUDE.md +13 -8
  79. package/templates/platformer/assets/lantern.svg +9 -0
  80. package/templates/platformer/package.json +1 -1
  81. package/templates/platformer/prefabs/coin.prefab.json +8 -0
  82. package/templates/platformer/prefabs/slime.prefab.json +9 -0
  83. package/templates/platformer/scenes/level-2.scene.json +413 -0
  84. package/templates/platformer/scenes/main.scene.json +356 -59
  85. package/templates/platformer/scripts/backdrop.ts +75 -0
  86. package/templates/platformer/scripts/hud.ts +111 -0
  87. package/templates/platformer/scripts/lantern.ts +35 -0
  88. package/templates/platformer/shaders/glow.frag +29 -0
  89. package/templates/platformer/tests/gameplay.test.json +45 -7
  90. package/templates/platformer/tests/level-2.test.json +49 -0
  91. package/templates/topdown/CLAUDE.md +39 -0
  92. package/templates/topdown/_gitignore +2 -0
  93. package/templates/topdown/assets/gem.svg +16 -0
  94. package/templates/topdown/assets/grass.svg +8 -0
  95. package/templates/topdown/assets/hedge.svg +10 -0
  96. package/templates/topdown/assets/hero.svg +23 -0
  97. package/templates/topdown/assets/portal.svg +15 -0
  98. package/templates/topdown/assets/slime.svg +21 -0
  99. package/templates/topdown/assets/thorns.svg +7 -0
  100. package/templates/topdown/ludra.json +5 -0
  101. package/templates/topdown/package.json +13 -0
  102. package/templates/topdown/scenes/main.scene.json +499 -0
  103. package/templates/topdown/scripts/hud.ts +94 -0
  104. package/templates/topdown/scripts/night.ts +67 -0
  105. package/templates/topdown/scripts/portal.ts +34 -0
  106. package/templates/topdown/shaders/dream.frag +31 -0
  107. package/templates/topdown/tests/gameplay.test.json +30 -0
@@ -1,20 +1,146 @@
1
1
  {
2
- "name": "Level 1",
3
- "world": { "width": 1920, "height": 540, "gravity": 1800, "background": "#1b2140" },
2
+ "name": "Night Meadow",
3
+ "world": {
4
+ "width": 1920,
5
+ "height": 540,
6
+ "gravity": 1800,
7
+ "background": "#1b2140",
8
+ "lives": 3
9
+ },
10
+ "fonts": {
11
+ "Fredoka": "google"
12
+ },
13
+ "postfx": {
14
+ "shader": "shaders/glow.frag",
15
+ "uniforms": {
16
+ "bloom": 1.3,
17
+ "warmth": 0.7,
18
+ "vignette": 1
19
+ }
20
+ },
4
21
  "effects": {
5
- "jump": { "sound": "jump", "particles": { "count": 6, "color": "#cfd8ff", "speed": 60, "direction": 90, "spread": 150, "life": 0.25, "size": 3, "gravity": 0 } },
6
- "land": { "sound": "thud", "particles": { "count": 5, "color": "#cfd8ff", "speed": 50, "direction": -90, "spread": 160, "life": 0.2, "size": 2, "gravity": 300 } },
7
- "collect": { "sound": "coin", "particles": { "count": 14, "color": "#ffd166", "speed": 140, "life": 0.45, "size": 3, "gravity": 200 } },
8
- "stomp": { "sound": "hit", "shake": 5, "particles": { "count": 18, "color": "#c77dff", "speed": 180, "life": 0.5, "size": 4 } },
9
- "die": { "sound": "explosion", "shake": 10, "particles": { "count": 30, "color": "#ff5d73", "speed": 220, "life": 0.7, "size": 4 } },
10
- "win": { "sound": "powerup", "shake": 3, "particles": { "count": 60, "color": "#4cc9f0", "speed": 260, "life": 1.2, "size": 4, "gravity": 150 } }
22
+ "jump": {
23
+ "sound": "jump",
24
+ "particles": {
25
+ "count": 6,
26
+ "color": "#cfd8ff",
27
+ "speed": 60,
28
+ "direction": 90,
29
+ "spread": 150,
30
+ "life": 0.25,
31
+ "size": 3,
32
+ "gravity": 0
33
+ }
34
+ },
35
+ "land": {
36
+ "sound": "thud",
37
+ "particles": {
38
+ "count": 5,
39
+ "color": "#cfd8ff",
40
+ "speed": 50,
41
+ "direction": -90,
42
+ "spread": 160,
43
+ "life": 0.2,
44
+ "size": 2,
45
+ "gravity": 300
46
+ }
47
+ },
48
+ "collect": {
49
+ "sound": "coin",
50
+ "particles": {
51
+ "count": 14,
52
+ "color": "#ffd166",
53
+ "speed": 140,
54
+ "life": 0.45,
55
+ "size": 3,
56
+ "gravity": 200
57
+ }
58
+ },
59
+ "stomp": {
60
+ "sound": "hit",
61
+ "shake": 5,
62
+ "particles": {
63
+ "count": 18,
64
+ "color": "#c77dff",
65
+ "speed": 180,
66
+ "life": 0.5,
67
+ "size": 4
68
+ }
69
+ },
70
+ "die": {
71
+ "sound": "explosion",
72
+ "shake": 10,
73
+ "particles": {
74
+ "count": 30,
75
+ "color": "#ff5d73",
76
+ "speed": 220,
77
+ "life": 0.7,
78
+ "size": 4
79
+ }
80
+ },
81
+ "win": {
82
+ "sound": "powerup",
83
+ "shake": 3,
84
+ "particles": {
85
+ "count": 60,
86
+ "color": "#4cc9f0",
87
+ "speed": 260,
88
+ "life": 1.2,
89
+ "size": 4,
90
+ "gravity": 150
91
+ }
92
+ },
93
+ "checkpoint": {
94
+ "sound": "powerup",
95
+ "particles": {
96
+ "count": 24,
97
+ "color": "#ffd166",
98
+ "speed": 120,
99
+ "life": 0.7,
100
+ "size": 3,
101
+ "gravity": -60
102
+ }
103
+ },
104
+ "gameover": {
105
+ "sound": "explosion",
106
+ "shake": 12
107
+ }
11
108
  },
12
109
  "entities": [
110
+ {
111
+ "id": "sky",
112
+ "components": {
113
+ "transform": {
114
+ "x": 0,
115
+ "y": 0
116
+ },
117
+ "script": {
118
+ "src": "scripts/backdrop.ts",
119
+ "params": {
120
+ "sky": [
121
+ "#0b1030",
122
+ "#2d3366"
123
+ ],
124
+ "hills": [
125
+ "#27306a",
126
+ "#1a2148"
127
+ ]
128
+ }
129
+ }
130
+ }
131
+ },
13
132
  {
14
133
  "id": "player",
15
134
  "components": {
16
- "transform": { "x": 80, "y": 482 },
17
- "body": { "type": "dynamic", "width": 28, "height": 36 },
135
+ "transform": {
136
+ "x": 80,
137
+ "y": 482
138
+ },
139
+ "body": {
140
+ "type": "dynamic",
141
+ "width": 28,
142
+ "height": 36
143
+ },
18
144
  "sprite": {
19
145
  "color": "#7cf29c",
20
146
  "image": "assets/player.svg",
@@ -27,127 +153,298 @@
27
153
  "fps": 10,
28
154
  "layer": 3
29
155
  },
30
- "platformer": { "speed": 240, "jumpSpeed": 680 },
156
+ "platformer": {
157
+ "speed": 240,
158
+ "jumpSpeed": 680
159
+ },
31
160
  "camera": {}
32
161
  }
33
162
  },
34
163
  {
35
164
  "id": "ground-start",
36
165
  "components": {
37
- "transform": { "x": 380, "y": 520 },
38
- "body": { "type": "static", "width": 760, "height": 40 },
39
- "sprite": { "color": "#3b4766", "image": "assets/ground.svg", "tile": true }
166
+ "transform": {
167
+ "x": 380,
168
+ "y": 520
169
+ },
170
+ "body": {
171
+ "type": "static",
172
+ "width": 760,
173
+ "height": 40
174
+ },
175
+ "sprite": {
176
+ "color": "#3b4766",
177
+ "image": "assets/ground.svg",
178
+ "tile": true
179
+ }
40
180
  }
41
181
  },
42
182
  {
43
183
  "id": "ground-end",
44
184
  "components": {
45
- "transform": { "x": 1450, "y": 520 },
46
- "body": { "type": "static", "width": 940, "height": 40 },
47
- "sprite": { "color": "#3b4766", "image": "assets/ground.svg", "tile": true }
185
+ "transform": {
186
+ "x": 1450,
187
+ "y": 520
188
+ },
189
+ "body": {
190
+ "type": "static",
191
+ "width": 940,
192
+ "height": 40
193
+ },
194
+ "sprite": {
195
+ "color": "#3b4766",
196
+ "image": "assets/ground.svg",
197
+ "tile": true
198
+ }
48
199
  }
49
200
  },
50
201
  {
51
202
  "id": "wall-left",
52
203
  "components": {
53
- "transform": { "x": -10, "y": 270 },
54
- "body": { "type": "static", "width": 20, "height": 540 }
204
+ "transform": {
205
+ "x": -10,
206
+ "y": 270
207
+ },
208
+ "body": {
209
+ "type": "static",
210
+ "width": 20,
211
+ "height": 540
212
+ }
55
213
  }
56
214
  },
57
215
  {
58
216
  "id": "wall-right",
59
217
  "components": {
60
- "transform": { "x": 1930, "y": 270 },
61
- "body": { "type": "static", "width": 20, "height": 540 }
218
+ "transform": {
219
+ "x": 1930,
220
+ "y": 270
221
+ },
222
+ "body": {
223
+ "type": "static",
224
+ "width": 20,
225
+ "height": 540
226
+ }
62
227
  }
63
228
  },
64
229
  {
65
230
  "id": "ledge",
66
231
  "components": {
67
- "transform": { "x": 300, "y": 410 },
68
- "body": { "type": "static", "width": 160, "height": 20 },
69
- "sprite": { "color": "#56658f", "image": "assets/platform.svg", "tile": true }
232
+ "transform": {
233
+ "x": 300,
234
+ "y": 410
235
+ },
236
+ "body": {
237
+ "type": "static",
238
+ "width": 160,
239
+ "height": 20
240
+ },
241
+ "sprite": {
242
+ "color": "#56658f",
243
+ "image": "assets/platform.svg",
244
+ "tile": true
245
+ }
70
246
  }
71
247
  },
72
248
  {
73
249
  "id": "coin-1",
250
+ "prefab": "coin",
74
251
  "components": {
75
- "transform": { "x": 300, "y": 370 },
76
- "body": { "type": "static", "width": 20, "height": 20, "sensor": true },
77
- "sprite": { "shape": "circle", "color": "#ffd166", "image": "assets/coin.svg", "frameWidth": 20, "idle": "0-3", "layer": 1 },
78
- "collectible": { "score": 1 }
252
+ "transform": {
253
+ "x": 300,
254
+ "y": 370
255
+ }
79
256
  }
80
257
  },
81
258
  {
82
259
  "id": "spikes",
83
260
  "components": {
84
- "transform": { "x": 560, "y": 492 },
85
- "body": { "type": "static", "width": 80, "height": 16, "sensor": true },
86
- "sprite": { "color": "#ff5d73", "image": "assets/spikes.svg", "tile": true, "layer": 1 },
261
+ "transform": {
262
+ "x": 560,
263
+ "y": 492
264
+ },
265
+ "body": {
266
+ "type": "static",
267
+ "width": 80,
268
+ "height": 16,
269
+ "sensor": true
270
+ },
271
+ "sprite": {
272
+ "color": "#ff5d73",
273
+ "image": "assets/spikes.svg",
274
+ "tile": true,
275
+ "layer": 1
276
+ },
87
277
  "hazard": {}
88
278
  }
89
279
  },
90
280
  {
91
281
  "id": "lift",
92
282
  "components": {
93
- "transform": { "x": 800, "y": 440 },
94
- "body": { "type": "kinematic", "width": 120, "height": 20 },
95
- "sprite": { "color": "#8fa3d9", "image": "assets/lift.svg", "tile": true },
96
- "patrol": { "axis": "x", "distance": 140, "speed": 90 }
283
+ "transform": {
284
+ "x": 800,
285
+ "y": 440
286
+ },
287
+ "body": {
288
+ "type": "kinematic",
289
+ "width": 120,
290
+ "height": 20
291
+ },
292
+ "sprite": {
293
+ "color": "#8fa3d9",
294
+ "image": "assets/lift.svg",
295
+ "tile": true
296
+ },
297
+ "patrol": {
298
+ "axis": "x",
299
+ "distance": 140,
300
+ "speed": 90
301
+ }
97
302
  }
98
303
  },
99
304
  {
100
305
  "id": "coin-2",
306
+ "prefab": "coin",
101
307
  "components": {
102
- "transform": { "x": 870, "y": 390 },
103
- "body": { "type": "static", "width": 20, "height": 20, "sensor": true },
104
- "sprite": { "shape": "circle", "color": "#ffd166", "image": "assets/coin.svg", "frameWidth": 20, "idle": "0-3", "layer": 1 },
105
- "collectible": { "score": 1 }
308
+ "transform": {
309
+ "x": 870,
310
+ "y": 390
311
+ }
106
312
  }
107
313
  },
108
314
  {
109
315
  "id": "slime",
316
+ "prefab": "slime",
110
317
  "components": {
111
- "transform": { "x": 1150, "y": 487 },
112
- "body": { "type": "kinematic", "width": 32, "height": 26, "sensor": true },
113
- "sprite": { "color": "#c77dff", "image": "assets/slime.svg", "frameWidth": 32, "idle": "0", "run": "0-1", "fps": 4, "layer": 2 },
114
- "patrol": { "axis": "x", "distance": 200, "speed": 70 },
115
- "hazard": { "stompable": true, "score": 1 }
318
+ "transform": {
319
+ "x": 1150,
320
+ "y": 487
321
+ }
116
322
  }
117
323
  },
118
324
  {
119
325
  "id": "step-1",
120
326
  "components": {
121
- "transform": { "x": 1500, "y": 410 },
122
- "body": { "type": "static", "width": 140, "height": 20 },
123
- "sprite": { "color": "#56658f", "image": "assets/platform.svg", "tile": true }
327
+ "transform": {
328
+ "x": 1500,
329
+ "y": 410
330
+ },
331
+ "body": {
332
+ "type": "static",
333
+ "width": 140,
334
+ "height": 20
335
+ },
336
+ "sprite": {
337
+ "color": "#56658f",
338
+ "image": "assets/platform.svg",
339
+ "tile": true
340
+ }
124
341
  }
125
342
  },
126
343
  {
127
344
  "id": "step-2",
128
345
  "components": {
129
- "transform": { "x": 1680, "y": 330 },
130
- "body": { "type": "static", "width": 140, "height": 20 },
131
- "sprite": { "color": "#56658f", "image": "assets/platform.svg", "tile": true }
346
+ "transform": {
347
+ "x": 1680,
348
+ "y": 330
349
+ },
350
+ "body": {
351
+ "type": "static",
352
+ "width": 140,
353
+ "height": 20
354
+ },
355
+ "sprite": {
356
+ "color": "#56658f",
357
+ "image": "assets/platform.svg",
358
+ "tile": true
359
+ }
132
360
  }
133
361
  },
134
362
  {
135
363
  "id": "coin-3",
364
+ "prefab": "coin",
136
365
  "components": {
137
- "transform": { "x": 1680, "y": 290 },
138
- "body": { "type": "static", "width": 20, "height": 20, "sensor": true },
139
- "sprite": { "shape": "circle", "color": "#ffd166", "image": "assets/coin.svg", "frameWidth": 20, "idle": "0-3", "layer": 1 },
140
- "collectible": { "score": 1 }
366
+ "transform": {
367
+ "x": 1680,
368
+ "y": 290
369
+ }
370
+ }
371
+ },
372
+ {
373
+ "id": "lantern",
374
+ "components": {
375
+ "transform": {
376
+ "x": 1060,
377
+ "y": 468
378
+ },
379
+ "body": {
380
+ "type": "static",
381
+ "width": 24,
382
+ "height": 64,
383
+ "sensor": true
384
+ },
385
+ "sprite": {
386
+ "color": "#6b4f3a",
387
+ "image": "assets/lantern.svg",
388
+ "layer": 1
389
+ },
390
+ "checkpoint": {},
391
+ "script": {
392
+ "src": "scripts/lantern.ts"
393
+ }
141
394
  }
142
395
  },
143
396
  {
144
397
  "id": "flag",
145
398
  "components": {
146
- "transform": { "x": 1860, "y": 460 },
147
- "body": { "type": "static", "width": 24, "height": 80, "sensor": true },
148
- "sprite": { "color": "#4cc9f0", "image": "assets/flag.svg", "frameWidth": 24, "idle": "0-1", "fps": 3, "flip": false, "layer": 1 },
149
- "emitter": { "rate": 6, "color": "#fff3b0", "speed": 30, "spread": 70, "life": 1.2, "size": 3, "gravity": -20 },
150
- "goal": { "requireScore": 4 }
399
+ "transform": {
400
+ "x": 1860,
401
+ "y": 460
402
+ },
403
+ "body": {
404
+ "type": "static",
405
+ "width": 24,
406
+ "height": 80,
407
+ "sensor": true
408
+ },
409
+ "sprite": {
410
+ "color": "#4cc9f0",
411
+ "image": "assets/flag.svg",
412
+ "frameWidth": 24,
413
+ "idle": "0-1",
414
+ "fps": 3,
415
+ "flip": false,
416
+ "layer": 1
417
+ },
418
+ "emitter": {
419
+ "rate": 6,
420
+ "color": "#fff3b0",
421
+ "speed": 30,
422
+ "spread": 70,
423
+ "life": 1.2,
424
+ "size": 3,
425
+ "gravity": -20
426
+ },
427
+ "goal": {
428
+ "requireScore": 4,
429
+ "next": "scenes/level-2.scene.json"
430
+ }
431
+ }
432
+ },
433
+ {
434
+ "id": "hud",
435
+ "components": {
436
+ "transform": {
437
+ "x": 0,
438
+ "y": 0
439
+ },
440
+ "script": {
441
+ "src": "scripts/hud.ts",
442
+ "params": {
443
+ "title": "Night Meadow",
444
+ "level": 1,
445
+ "lives": 3
446
+ }
447
+ }
151
448
  }
152
449
  }
153
450
  ]
@@ -0,0 +1,75 @@
1
+ import type { Script } from "@ludra/core";
2
+
3
+ /** A stable pseudo-random number per index, so stars and hills don't jitter. */
4
+ function hash(n: number): number {
5
+ const x = Math.sin(n * 157.3 + 21.9) * 43758.5453;
6
+ return x - Math.floor(x);
7
+ }
8
+
9
+ /**
10
+ * A night sky that moves at its own pace behind the level: stars, a glowing
11
+ * moon and two rows of hills scrolling slower than the camera (parallax).
12
+ * params.sky and params.hills set the palette, so each level can have its own night.
13
+ */
14
+ export default {
15
+ draw({ g, view, time, params }) {
16
+ const [top, bottom] = params.sky as [string, string];
17
+ const [far, near] = params.hills as [string, string];
18
+ // Draw in screen space: the sky stays put while the level scrolls.
19
+ g.translate(view.cameraX, view.cameraY);
20
+ const { width: w, height: h } = view;
21
+
22
+ const sky = g.createLinearGradient(0, 0, 0, h);
23
+ sky.addColorStop(0, top);
24
+ sky.addColorStop(1, bottom);
25
+ g.fillStyle = sky;
26
+ g.fillRect(0, 0, w, h);
27
+
28
+ for (let i = 0; i < 90; i++) {
29
+ // Stars drift a little with the camera, the slowest layer of all.
30
+ const x = (((hash(i) * w * 2 - view.cameraX * 0.05) % w) + w) % w;
31
+ g.globalAlpha = 0.3 + 0.6 * Math.abs(Math.sin(time * (0.5 + hash(i + 9)) + i));
32
+ g.fillStyle = "#e8eeff";
33
+ g.fillRect(x, hash(i + 40) * h * 0.6, i % 9 === 0 ? 2 : 1, i % 9 === 0 ? 2 : 1);
34
+ }
35
+ g.globalAlpha = 1;
36
+
37
+ const moonX = w * 0.78 - view.cameraX * 0.03;
38
+ const glow = g.createRadialGradient(moonX, 90, 10, moonX, 90, 140);
39
+ glow.addColorStop(0, "rgba(255, 244, 214, 0.35)");
40
+ glow.addColorStop(1, "rgba(255, 244, 214, 0)");
41
+ g.fillStyle = glow;
42
+ g.fillRect(moonX - 140, -50, 280, 280);
43
+ g.fillStyle = "#fff4d6";
44
+ g.shadowColor = "#fff4d6";
45
+ g.shadowBlur = 30;
46
+ g.beginPath();
47
+ g.arc(moonX, 90, 34, 0, Math.PI * 2);
48
+ g.fill();
49
+ g.shadowBlur = 0;
50
+ g.fillStyle = "rgba(200, 190, 170, 0.35)";
51
+ for (const [dx, dy, r] of [[-10, -8, 7], [12, 6, 5], [-2, 14, 4]] as const) {
52
+ g.beginPath();
53
+ g.arc(moonX + dx, 90 + dy, r, 0, Math.PI * 2);
54
+ g.fill();
55
+ }
56
+
57
+ // Two rows of rolling hills; the nearer row scrolls faster.
58
+ const hills = (color: string, speed: number, base: number, height: number, seed: number) => {
59
+ g.fillStyle = color;
60
+ g.beginPath();
61
+ g.moveTo(0, h);
62
+ const offset = view.cameraX * speed;
63
+ for (let x = 0; x <= w + 20; x += 20) {
64
+ const u = (x + offset) / 160;
65
+ const y = base - height * (0.55 + 0.45 * Math.sin(u + seed) * Math.cos(u * 0.37 + seed * 2));
66
+ g.lineTo(x, y);
67
+ }
68
+ g.lineTo(w, h);
69
+ g.closePath();
70
+ g.fill();
71
+ };
72
+ hills(far, 0.15, h * 0.72, 90, 1);
73
+ hills(near, 0.35, h * 0.86, 70, 4);
74
+ },
75
+ } satisfies Script;