nova64 0.2.4 → 0.2.6

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 (140) hide show
  1. package/README.md +25 -8
  2. package/bin/nova64.js +165 -0
  3. package/dist/assets/console-CY_kygm3.js +14 -0
  4. package/dist/assets/console-CY_kygm3.js.map +1 -0
  5. package/dist/assets/main-l0sNRNKZ.js.map +1 -0
  6. package/dist/assets/sky/studio/nx.png +0 -0
  7. package/dist/assets/sky/studio/ny.png +0 -0
  8. package/dist/assets/sky/studio/nz.png +0 -0
  9. package/dist/assets/sky/studio/px.png +0 -0
  10. package/dist/assets/sky/studio/py.png +0 -0
  11. package/dist/assets/sky/studio/pz.png +0 -0
  12. package/dist/assets/vanilla-Dcuy32gi.js +2 -0
  13. package/dist/assets/vanilla-Dcuy32gi.js.map +1 -0
  14. package/dist/console.html +899 -0
  15. package/dist/docs/BENCHMARK.md +77 -0
  16. package/dist/docs/CHEATSHEET.md +255 -0
  17. package/dist/docs/EFFECTS_API_GUIDE.md +577 -0
  18. package/dist/docs/EFFECTS_QUICK_REFERENCE.md +331 -0
  19. package/dist/docs/FONT_CHARACTER_REFERENCE.md +219 -0
  20. package/dist/docs/FREE_GLB_ASSETS.md +330 -0
  21. package/dist/docs/FULLSCREEN_BUTTON_FEATURE.md +296 -0
  22. package/dist/docs/GAMEPAD_SUPPORT.md +348 -0
  23. package/dist/docs/GAME_IMPROVEMENTS.md +278 -0
  24. package/dist/docs/GAME_QUALITY_STATUS.md +300 -0
  25. package/dist/docs/MIGRATION_GUIDE.md +553 -0
  26. package/dist/docs/NOVA64_3D_API.md +356 -0
  27. package/dist/docs/NOVA64_API_REFERENCE.md +1406 -0
  28. package/dist/docs/NOVA64_UI_API.md +503 -0
  29. package/dist/docs/UI_SYSTEM_SUMMARY.md +445 -0
  30. package/dist/docs/VOXEL_ENGINE_GUIDE.md +662 -0
  31. package/dist/docs/VOXEL_QUICK_REFERENCE.md +386 -0
  32. package/dist/docs/api-3d.html +750 -0
  33. package/dist/docs/api-effects.html +385 -0
  34. package/dist/docs/api-improvements.md +121 -0
  35. package/dist/docs/api-skybox.html +407 -0
  36. package/dist/docs/api-sprites.html +321 -0
  37. package/dist/docs/api-voxel.html +337 -0
  38. package/dist/docs/api.html +543 -0
  39. package/dist/docs/assets.html +306 -0
  40. package/dist/docs/audio.html +340 -0
  41. package/dist/docs/blogs.html +286 -0
  42. package/dist/docs/collision.html +316 -0
  43. package/dist/docs/console.html +247 -0
  44. package/dist/docs/editor.html +297 -0
  45. package/dist/docs/font.html +247 -0
  46. package/dist/docs/framebuffer.html +247 -0
  47. package/dist/docs/fullscreen-button.html +297 -0
  48. package/dist/docs/gpu-systems.html +247 -0
  49. package/dist/docs/index.html +580 -0
  50. package/dist/docs/input.html +491 -0
  51. package/dist/docs/physics.html +311 -0
  52. package/dist/docs/screens.html +311 -0
  53. package/dist/docs/storage.html +311 -0
  54. package/dist/docs/textinput.html +332 -0
  55. package/dist/docs/ui.html +488 -0
  56. package/dist/examples/3d-advanced/code.js +695 -0
  57. package/dist/examples/adventure-comic-3d/code.js +342 -0
  58. package/dist/examples/audio-lab/code.js +150 -0
  59. package/dist/examples/boids-flocking/code.js +270 -0
  60. package/dist/examples/crystal-cathedral-3d/code.js +706 -0
  61. package/dist/examples/cyberpunk-city-3d/code.js +1383 -0
  62. package/dist/examples/demoscene/README.md +192 -0
  63. package/dist/examples/demoscene/code.js +1081 -0
  64. package/dist/examples/demoscene/meta.json +21 -0
  65. package/dist/examples/dungeon-crawler-3d/code.js +1117 -0
  66. package/dist/examples/f-zero-nova-3d/code.js +865 -0
  67. package/dist/examples/f-zero-nova-3d/code_old.js +1555 -0
  68. package/dist/examples/fps-demo-3d/code.js +744 -0
  69. package/dist/examples/game-of-life-3d/code.js +338 -0
  70. package/dist/examples/generative-art/code.js +632 -0
  71. package/dist/examples/hello-3d/code.js +325 -0
  72. package/dist/examples/hello-skybox/code.js +183 -0
  73. package/dist/examples/hello-world/code.js +19 -0
  74. package/dist/examples/input-showcase/code.js +109 -0
  75. package/dist/examples/instancing-demo/code.js +315 -0
  76. package/dist/examples/minecraft-demo/code.js +387 -0
  77. package/dist/examples/model-viewer-3d/code.js +114 -0
  78. package/dist/examples/mystical-realm-3d/code.js +1203 -0
  79. package/dist/examples/nature-explorer-3d/code.js +1318 -0
  80. package/dist/examples/particles-demo/code.js +522 -0
  81. package/dist/examples/pbr-showcase/code.js +140 -0
  82. package/dist/examples/physics-demo-3d/code.js +948 -0
  83. package/dist/examples/screen-demo/code.js +267 -0
  84. package/dist/examples/shooter-demo-3d/code.js +1286 -0
  85. package/dist/examples/space-combat-3d/IMPLEMENTATION_SUMMARY.md +109 -0
  86. package/dist/examples/space-combat-3d/README.md +135 -0
  87. package/dist/examples/space-combat-3d/code.js +1332 -0
  88. package/dist/examples/space-harrier-3d/code.js +923 -0
  89. package/dist/examples/star-fox-nova-3d/code.js +1116 -0
  90. package/dist/examples/star-fox-nova-3d/code_backup.js +410 -0
  91. package/dist/examples/star-fox-nova-3d/code_broken.js +1821 -0
  92. package/dist/examples/storage-quest/code.js +209 -0
  93. package/dist/examples/strider-demo-3d/IMPROVEMENT_OPTIONS.md +285 -0
  94. package/dist/examples/strider-demo-3d/cache-test.html +132 -0
  95. package/dist/examples/strider-demo-3d/code-fixed.js +582 -0
  96. package/dist/examples/strider-demo-3d/code-old.js +1537 -0
  97. package/dist/examples/strider-demo-3d/code.js +1462 -0
  98. package/dist/examples/strider-demo-3d/code.js.bak2 +1169 -0
  99. package/dist/examples/strider-demo-3d/fix-game.sh +53 -0
  100. package/dist/examples/super-plumber-64/README.md +128 -0
  101. package/dist/examples/super-plumber-64/code.js +1185 -0
  102. package/dist/examples/super-plumber-64/index.html +88 -0
  103. package/dist/examples/test-2d-overlay/code.js +32 -0
  104. package/dist/examples/test-font/code.js +51 -0
  105. package/dist/examples/test-minimal/code.js +21 -0
  106. package/dist/examples/ui-demo/code.js +306 -0
  107. package/dist/examples/wing-commander-space/README.md +180 -0
  108. package/dist/examples/wing-commander-space/code.js +1285 -0
  109. package/dist/examples/wizardry-3d/CHANGELOG.md +366 -0
  110. package/dist/examples/wizardry-3d/code.js +3928 -0
  111. package/dist/index.html +666 -0
  112. package/dist/os9-shell/assets/index-DIHfrTaW.css +1 -0
  113. package/dist/os9-shell/assets/index-KchE_ngx.js +483 -0
  114. package/dist/os9-shell/assets/index-KchE_ngx.js.map +1 -0
  115. package/dist/os9-shell/index.html +23 -0
  116. package/dist/os9-shell/nova-icon.svg +12 -0
  117. package/index.html +6 -1
  118. package/package.json +37 -32
  119. package/public/assets/sky/studio/nx.png +0 -0
  120. package/public/assets/sky/studio/ny.png +0 -0
  121. package/public/assets/sky/studio/nz.png +0 -0
  122. package/public/assets/sky/studio/px.png +0 -0
  123. package/public/assets/sky/studio/py.png +0 -0
  124. package/public/assets/sky/studio/pz.png +0 -0
  125. package/public/os9-shell/assets/index-KchE_ngx.js +483 -0
  126. package/public/os9-shell/assets/index-KchE_ngx.js.map +1 -0
  127. package/public/os9-shell/index.html +10 -1
  128. package/runtime/api-2d.js +301 -21
  129. package/runtime/api-3d/pbr.js +45 -1
  130. package/runtime/api-3d.js +1 -0
  131. package/runtime/api-effects.js +90 -3
  132. package/runtime/api-gameutils.js +476 -0
  133. package/runtime/api-generative.js +610 -0
  134. package/runtime/api-skybox.js +54 -0
  135. package/runtime/api-voxel.js +139 -28
  136. package/runtime/gpu-threejs.js +13 -9
  137. package/runtime/ui.js +2 -2
  138. package/src/main.js +24 -1
  139. package/public/os9-shell/assets/index-B1Uvacma.js +0 -32825
  140. package/public/os9-shell/assets/index-B1Uvacma.js.map +0 -1
@@ -0,0 +1,1185 @@
1
+ // ⭐ SUPER PLUMBER 64 — 3D Platforming Adventure ⭐
2
+ // Multi-zone world: Grassland → Desert Ruins → Ice Mountain
3
+ // Power-ups, moving platforms, 4 enemy types, double jump, checkpoints
4
+
5
+ // ── Constants ────────────────────────────────────────────────
6
+ const C = {
7
+ sky: 0x44aaff,
8
+ grass: 0x228811,
9
+ dirt: 0x553311,
10
+ sand: 0xccaa55,
11
+ sandstone: 0xbb8833,
12
+ ice: 0x88bbdd,
13
+ snow: 0xddeeff,
14
+ lava: 0xff3300,
15
+ plumberHat: 0xff0000,
16
+ plumberFace: 0xffccaa,
17
+ plumberBody: 0x0022cc,
18
+ coin: 0xffcc00,
19
+ goomba: 0xaa4400,
20
+ koopa: 0x22aa22,
21
+ flyGuy: 0xdd44dd,
22
+ chomp: 0x222222,
23
+ brick: 0xcc6600,
24
+ water: 0x0055ff,
25
+ star: 0xffff00,
26
+ mushroom: 0xff4444,
27
+ speedShoe: 0x44ddff,
28
+ };
29
+
30
+ let gameState = 'start';
31
+ let t = 0;
32
+ let inputLock = 0;
33
+ let playerHit;
34
+ let shake;
35
+
36
+ let g = {
37
+ score: 0,
38
+ coins: 0,
39
+ totalCoins: 0,
40
+ health: 3,
41
+ maxHealth: 3,
42
+ zone: 0,
43
+ checkpoint: null,
44
+ hasDoubleJump: false,
45
+ starTimer: 0,
46
+ speedTimer: 0,
47
+
48
+ p: {
49
+ x: 0,
50
+ y: 10,
51
+ z: 0,
52
+ vx: 0,
53
+ vy: 0,
54
+ vz: 0,
55
+ rotY: 0,
56
+ isGrounded: false,
57
+ jumpTimer: 0,
58
+ jumpsLeft: 1,
59
+ meshGroup: [],
60
+ },
61
+
62
+ platforms: [],
63
+ movingPlatforms: [],
64
+ coinsList: [],
65
+ enemies: [],
66
+ particles: [],
67
+ powerups: [],
68
+ checkpoints: [],
69
+ hazards: [],
70
+ worldMeshes: [],
71
+ };
72
+
73
+ // ── Initialization ─────────────────────────────────────────
74
+ export function init() {
75
+ setCameraFOV(60);
76
+ setCameraPosition(0, 12, 18);
77
+ setCameraTarget(0, 2, -5);
78
+
79
+ setAmbientLight(0xffffff, 0.75);
80
+ setLightDirection(1, 2, 1);
81
+ setLightColor(0xffffff);
82
+
83
+ setFog(C.sky, 60, 180);
84
+ enableBloom(0.4, 0.5, 0.4);
85
+ enableFXAA();
86
+
87
+ shake = createShake({ decay: 0.88, maxMag: 6 });
88
+ playerHit = createHitState({ invulnDuration: 1.5, blinkRate: 47 });
89
+
90
+ resetGame();
91
+ }
92
+
93
+ function resetGame() {
94
+ // Destroy old meshes
95
+ for (const m of g.worldMeshes) destroyMesh(m);
96
+ for (const p of g.platforms) if (p.mesh) destroyMesh(p.mesh);
97
+ for (const mp of g.movingPlatforms) if (mp.mesh) destroyMesh(mp.mesh);
98
+ for (const c of g.coinsList) if (c.mesh) destroyMesh(c.mesh);
99
+ for (const e of g.enemies) e.meshes.forEach(m => destroyMesh(m));
100
+ for (const part of g.particles) if (part.mesh) destroyMesh(part.mesh);
101
+ for (const pu of g.powerups) if (pu.mesh) destroyMesh(pu.mesh);
102
+ for (const cp of g.checkpoints) if (cp.mesh) destroyMesh(cp.mesh);
103
+ for (const h of g.hazards) if (h.mesh) destroyMesh(h.mesh);
104
+ if (g.p.meshGroup)
105
+ g.p.meshGroup.forEach(part => {
106
+ if (part.m) destroyMesh(part.m);
107
+ });
108
+
109
+ g.score = 0;
110
+ g.coins = 0;
111
+ g.totalCoins = 0;
112
+ g.health = 3;
113
+ g.maxHealth = 3;
114
+ g.zone = 0;
115
+ g.checkpoint = null;
116
+ g.hasDoubleJump = false;
117
+ g.starTimer = 0;
118
+ g.speedTimer = 0;
119
+
120
+ g.p.x = 0;
121
+ g.p.y = 10;
122
+ g.p.z = 0;
123
+ g.p.vx = 0;
124
+ g.p.vy = 0;
125
+ g.p.vz = 0;
126
+ g.p.rotY = Math.PI;
127
+ g.p.isGrounded = false;
128
+ g.p.jumpTimer = 0;
129
+ g.p.jumpsLeft = 1;
130
+ g.p.meshGroup = [];
131
+
132
+ g.platforms = [];
133
+ g.movingPlatforms = [];
134
+ g.coinsList = [];
135
+ g.enemies = [];
136
+ g.particles = [];
137
+ g.powerups = [];
138
+ g.checkpoints = [];
139
+ g.hazards = [];
140
+ g.worldMeshes = [];
141
+
142
+ playerHit.invulnTimer = 0;
143
+
144
+ buildWorld();
145
+ buildPlumber();
146
+ inputLock = 0.3;
147
+ }
148
+
149
+ // ── World Building ─────────────────────────────────────────
150
+ function buildWorld() {
151
+ // Water plane
152
+ const water = createPlane(500, 500, C.water, [0, -8, 0], {
153
+ material: 'standard',
154
+ transparent: true,
155
+ opacity: 0.5,
156
+ });
157
+ rotateMesh(water, -Math.PI / 2, 0, 0);
158
+ g.worldMeshes.push(water);
159
+
160
+ buildGrassland();
161
+ buildDesertRuins();
162
+ buildIceMountain();
163
+
164
+ g.totalCoins = g.coinsList.length;
165
+ }
166
+
167
+ function buildGrassland() {
168
+ // ── Zone 1: Grassland ──
169
+ // Starting island
170
+ addPlatform(0, -1, 0, 30, 2, 30, C.grass);
171
+
172
+ // Trees (decorative)
173
+ addTree(8, 1, 8);
174
+ addTree(-10, 1, 5);
175
+ addTree(-6, 1, -8);
176
+
177
+ // Floating brick blocks with coins
178
+ addPlatform(0, 6, -10, 3, 2, 3, C.brick, true);
179
+ addPlatform(-6, 6, -10, 3, 2, 3, C.brick, true);
180
+ addPlatform(6, 6, -10, 3, 2, 3, C.brick, true);
181
+ spawnCoin(-6, 9, -10);
182
+ spawnCoin(0, 9, -10);
183
+ spawnCoin(6, 9, -10);
184
+
185
+ // Coin trail across grass
186
+ for (let i = -3; i <= 3; i++) spawnCoin(i * 3, 2, -3);
187
+
188
+ // Stepping stone path
189
+ addPlatform(0, 2, -22, 8, 1.5, 8, C.grass);
190
+ addPlatform(-12, 5, -30, 6, 1, 6, C.grass);
191
+ addPlatform(0, 8, -38, 8, 1, 8, C.grass);
192
+ spawnCoin(0, 4, -22);
193
+ spawnCoin(-12, 7, -30);
194
+ spawnCoin(0, 10, -38);
195
+
196
+ // Moving platform bridge
197
+ addMovingPlatform(10, 4, -22, 5, 1, 5, C.grass, 'x', 8, 1.5);
198
+ spawnCoin(10, 7, -22);
199
+
200
+ // Mushroom power-up on stepping stone
201
+ spawnPowerup(-12, 7, -30, 'mushroom');
202
+
203
+ // Trampolines
204
+ addPlatform(12, 1, -10, 4, 0.5, 4, 0xff4488, false, true); // trampoline
205
+
206
+ // Enemies
207
+ spawnGoomba(5, 1, 5);
208
+ spawnGoomba(-5, 1, 5);
209
+ spawnGoomba(0, 10, -38);
210
+
211
+ // Hill to zone 2
212
+ addPlatform(0, 6, -55, 20, 12, 18, C.dirt);
213
+ addPlatform(0, 12, -55, 22, 1, 20, C.grass);
214
+ spawnCoin(0, 14, -55);
215
+ for (let i = 0; i < 6; i++) {
216
+ const a = (i / 6) * Math.PI * 2;
217
+ spawnCoin(Math.cos(a) * 6, 14, -55 + Math.sin(a) * 6);
218
+ }
219
+
220
+ // Checkpoint at hill top
221
+ addCheckpoint(0, 13.5, -48);
222
+
223
+ // Koopa on hill
224
+ spawnKoopa(5, 13, -55);
225
+ spawnKoopa(-5, 13, -55);
226
+ }
227
+
228
+ function buildDesertRuins() {
229
+ // ── Zone 2: Desert Ruins ──
230
+ const dz = -80;
231
+
232
+ // Desert floor
233
+ addPlatform(0, 10, dz, 40, 2, 40, C.sand);
234
+
235
+ // Sandstone ruins/pillars
236
+ addPlatform(-12, 16, dz + 5, 3, 10, 3, C.sandstone);
237
+ addPlatform(12, 16, dz + 5, 3, 10, 3, C.sandstone);
238
+ addPlatform(0, 21, dz + 5, 28, 1.5, 5, C.sandstone); // Bridge between pillars
239
+ for (let i = -2; i <= 2; i++) spawnCoin(i * 5, 23, dz + 5);
240
+
241
+ // Star power-up on bridge
242
+ spawnPowerup(0, 23, dz + 5, 'star');
243
+
244
+ // Pyramid (stepped)
245
+ addPlatform(0, 12, dz - 10, 16, 2, 16, C.sandstone);
246
+ addPlatform(0, 14, dz - 10, 12, 2, 12, C.sandstone);
247
+ addPlatform(0, 16, dz - 10, 8, 2, 8, C.sandstone);
248
+ addPlatform(0, 18, dz - 10, 4, 2, 4, C.sandstone);
249
+ spawnCoin(0, 20, dz - 10);
250
+
251
+ // Lava pits (hazards)
252
+ addHazard(-10, 11.5, dz - 5, 6, 0.5, 6, C.lava, 5);
253
+ addHazard(10, 11.5, dz + 10, 6, 0.5, 6, C.lava, 5);
254
+
255
+ // Floating ruins coins
256
+ addPlatform(-15, 15, dz - 8, 4, 1, 4, C.sandstone);
257
+ addPlatform(15, 15, dz + 8, 4, 1, 4, C.sandstone);
258
+ spawnCoin(-15, 17, dz - 8);
259
+ spawnCoin(15, 17, dz + 8);
260
+
261
+ // Moving platforms over lava
262
+ addMovingPlatform(-10, 14, dz - 5, 4, 1, 4, C.sandstone, 'z', 5, 1.2);
263
+ addMovingPlatform(10, 14, dz + 10, 4, 1, 4, C.sandstone, 'x', 5, 1.0);
264
+
265
+ // Double jump power-up (speed shoes) hidden behind pyramid
266
+ spawnPowerup(0, 12, dz - 18, 'speedShoe');
267
+
268
+ // Enemies
269
+ spawnKoopa(-8, 12, dz + 8);
270
+ spawnKoopa(8, 12, dz - 3);
271
+ spawnFlyGuy(0, 18, dz);
272
+ spawnFlyGuy(-8, 16, dz + 12);
273
+
274
+ // Checkpoint
275
+ addCheckpoint(0, 12, dz + 15);
276
+
277
+ // Bridge to Zone 3
278
+ for (let i = 0; i < 5; i++) {
279
+ addPlatform(0, 12 + i * 2, dz - 22 - i * 8, 8, 1, 6, i % 2 === 0 ? C.sand : C.sandstone);
280
+ spawnCoin(0, 14 + i * 2, dz - 22 - i * 8);
281
+ }
282
+ }
283
+
284
+ function buildIceMountain() {
285
+ // ── Zone 3: Ice Mountain ──
286
+ const iz = -160;
287
+ const iy = 22;
288
+
289
+ // Ice base
290
+ addPlatform(0, iy, iz, 35, 2, 35, C.ice);
291
+
292
+ // Icy floor is slippery (handled in physics)
293
+ // Snowdrift decorations
294
+ addTree(10, iy + 1, iz + 10, true);
295
+ addTree(-8, iy + 1, iz - 5, true);
296
+
297
+ // Ice pillars
298
+ addPlatform(-10, iy + 6, iz - 8, 3, 10, 3, C.snow);
299
+ addPlatform(10, iy + 6, iz + 8, 3, 10, 3, C.snow);
300
+
301
+ // Staircase of ice platforms
302
+ for (let i = 0; i < 6; i++) {
303
+ const angle = (i / 6) * Math.PI;
304
+ const px = Math.cos(angle) * 10;
305
+ const pz = iz + Math.sin(angle) * 10;
306
+ addPlatform(px, iy + 3 + i * 3, pz, 5, 1, 5, C.ice);
307
+ spawnCoin(px, iy + 5 + i * 3, pz);
308
+ }
309
+
310
+ // Summit platform
311
+ addPlatform(0, iy + 22, iz, 12, 2, 12, C.snow);
312
+ spawnCoin(0, iy + 25, iz);
313
+
314
+ // Moving platforms spiraling up
315
+ addMovingPlatform(8, iy + 10, iz, 4, 1, 4, C.ice, 'x', 6, 0.8);
316
+ addMovingPlatform(-8, iy + 16, iz - 5, 4, 1, 4, C.ice, 'z', 6, 1.0);
317
+
318
+ // Star power-up near summit
319
+ spawnPowerup(0, iy + 25, iz, 'star');
320
+
321
+ // Boss: Chain Chomp at summit
322
+ spawnChomp(0, iy + 24, iz);
323
+
324
+ // Enemies
325
+ spawnGoomba(5, iy + 1, iz + 5);
326
+ spawnGoomba(-5, iy + 1, iz - 5);
327
+ spawnFlyGuy(8, iy + 8, iz);
328
+ spawnKoopa(-8, iy + 1, iz + 8);
329
+
330
+ // Checkpoint at base
331
+ addCheckpoint(0, iy + 1.5, iz + 12);
332
+
333
+ // Final coin ring at summit
334
+ for (let i = 0; i < 8; i++) {
335
+ const a = (i / 8) * Math.PI * 2;
336
+ spawnCoin(Math.cos(a) * 4, iy + 25, iz + Math.sin(a) * 4);
337
+ }
338
+ }
339
+
340
+ // ── Level Helpers ──────────────────────────────────────────
341
+ function addPlatform(x, y, z, sx, sy, sz, color, isBrick = false, isTrampoline = false) {
342
+ const mesh = createCube(1, color, [x, y, z], {
343
+ material: 'standard',
344
+ roughness: isBrick ? 0.9 : isTrampoline ? 0.3 : 0.6,
345
+ });
346
+ setScale(mesh, sx, sy, sz);
347
+ g.platforms.push({ mesh, x, y, z, sx, sy, sz, isBrick, isTrampoline, destroyed: false });
348
+ }
349
+
350
+ function addMovingPlatform(x, y, z, sx, sy, sz, color, axis, range, speed) {
351
+ const mesh = createCube(1, color, [x, y, z], { material: 'standard', roughness: 0.5 });
352
+ setScale(mesh, sx, sy, sz);
353
+ const glow = createCube(1, 0xffff88, [x, y + sy / 2 + 0.1, z], {
354
+ material: 'emissive',
355
+ emissive: 0xffff88,
356
+ });
357
+ setScale(glow, sx * 0.8, 0.1, sz * 0.8);
358
+ g.worldMeshes.push(glow);
359
+ g.movingPlatforms.push({
360
+ mesh,
361
+ glow,
362
+ x,
363
+ y,
364
+ z,
365
+ sx,
366
+ sy,
367
+ sz,
368
+ axis,
369
+ range,
370
+ speed,
371
+ origin: axis === 'x' ? x : axis === 'z' ? z : y,
372
+ t: Math.random() * 6,
373
+ });
374
+ }
375
+
376
+ function addHazard(x, y, z, sx, sy, sz, color, damage) {
377
+ const mesh = createCube(1, color, [x, y, z], { material: 'emissive', emissive: color });
378
+ setScale(mesh, sx, sy, sz);
379
+ g.hazards.push({ mesh, x, y, z, sx, sy, sz, damage });
380
+ }
381
+
382
+ function addCheckpoint(x, y, z) {
383
+ const pole = createCylinder(0.15, 0.15, 3, 0x888888, [x, y + 1.5, z], { material: 'standard' });
384
+ g.worldMeshes.push(pole);
385
+ const flag = createCube(1, 0xff4444, [x + 0.6, y + 2.5, z], {
386
+ material: 'emissive',
387
+ emissive: 0xff4444,
388
+ });
389
+ setScale(flag, 1.2, 0.6, 0.1);
390
+ g.checkpoints.push({ mesh: flag, x, y, z, active: false });
391
+ }
392
+
393
+ function addTree(x, y, z, snowy = false) {
394
+ const trunk = createCylinder(0.3, 0.3, 3, 0x664422, [x, y + 1.5, z], {
395
+ material: 'standard',
396
+ roughness: 0.9,
397
+ });
398
+ g.worldMeshes.push(trunk);
399
+ const leaves = createSphere(1.5, snowy ? 0xaaccbb : 0x228833, [x, y + 4, z], 8, {
400
+ material: 'standard',
401
+ roughness: 0.8,
402
+ });
403
+ g.worldMeshes.push(leaves);
404
+ if (snowy) {
405
+ const snowCap = createSphere(1.6, 0xeeeeff, [x, y + 4.5, z], 8, { material: 'standard' });
406
+ setScale(snowCap, 1, 0.4, 1);
407
+ g.worldMeshes.push(snowCap);
408
+ }
409
+ }
410
+
411
+ // ── Coins & Powerups ───────────────────────────────────────
412
+ function spawnCoin(x, y, z) {
413
+ const mesh = createSphere(0.8, C.coin, [x, y, z], 12, {
414
+ material: 'metallic',
415
+ metalness: 0.8,
416
+ roughness: 0.2,
417
+ });
418
+ setScale(mesh, 1, 1, 0.2);
419
+ g.coinsList.push({ mesh, x, y, z, t: Math.random() * 10, collected: false });
420
+ }
421
+
422
+ function spawnPowerup(x, y, z, type) {
423
+ let color = type === 'star' ? C.star : type === 'mushroom' ? C.mushroom : C.speedShoe;
424
+ const mesh = createSphere(0.6, color, [x, y + 0.5, z], 12, {
425
+ material: 'emissive',
426
+ emissive: color,
427
+ });
428
+ g.powerups.push({ mesh, x, y: y + 0.5, z, type, collected: false, t: 0 });
429
+ }
430
+
431
+ // ── Enemies ────────────────────────────────────────────────
432
+ function spawnGoomba(x, y, z) {
433
+ const body = createSphere(1.0, C.goomba, [x, y + 0.6, z], 8, { material: 'standard' });
434
+ setScale(body, 1, 0.7, 1);
435
+ const eyeL = createCube(0.2, 0x000000, [x - 0.3, y + 0.9, z + 0.8], { material: 'standard' });
436
+ const eyeR = createCube(0.2, 0x000000, [x + 0.3, y + 0.9, z + 0.8], { material: 'standard' });
437
+ g.enemies.push({
438
+ meshes: [body, eyeL, eyeR],
439
+ type: 'goomba',
440
+ x,
441
+ y,
442
+ z,
443
+ vx: (Math.random() > 0.5 ? 1 : -1) * 3,
444
+ hp: 1,
445
+ startX: x,
446
+ alive: true,
447
+ patrolRange: 10,
448
+ });
449
+ }
450
+
451
+ function spawnKoopa(x, y, z) {
452
+ const shell = createSphere(0.8, C.koopa, [x, y + 0.5, z], 8, {
453
+ material: 'standard',
454
+ roughness: 0.3,
455
+ });
456
+ const head = createSphere(0.4, 0xffcc66, [x, y + 1.2, z + 0.5], 8, { material: 'standard' });
457
+ g.enemies.push({
458
+ meshes: [shell, head],
459
+ type: 'koopa',
460
+ x,
461
+ y,
462
+ z,
463
+ vx: (Math.random() > 0.5 ? 1 : -1) * 4,
464
+ hp: 2,
465
+ startX: x,
466
+ alive: true,
467
+ patrolRange: 8,
468
+ });
469
+ }
470
+
471
+ function spawnFlyGuy(x, y, z) {
472
+ const body = createSphere(0.7, C.flyGuy, [x, y, z], 8, {
473
+ material: 'emissive',
474
+ emissive: C.flyGuy,
475
+ });
476
+ const wingL = createCube(0.1, 0xffffff, [x - 0.8, y + 0.2, z], { material: 'standard' });
477
+ setScale(wingL, 8, 1, 3);
478
+ const wingR = createCube(0.1, 0xffffff, [x + 0.8, y + 0.2, z], { material: 'standard' });
479
+ setScale(wingR, 8, 1, 3);
480
+ g.enemies.push({
481
+ meshes: [body, wingL, wingR],
482
+ type: 'flyguy',
483
+ x,
484
+ y,
485
+ z,
486
+ orbAngle: Math.random() * Math.PI * 2,
487
+ startY: y,
488
+ startX: x,
489
+ startZ: z,
490
+ alive: true,
491
+ hp: 1,
492
+ patrolRange: 6,
493
+ });
494
+ }
495
+
496
+ function spawnChomp(x, y, z) {
497
+ const body = createSphere(2.0, C.chomp, [x, y + 2, z], 12, {
498
+ material: 'metallic',
499
+ metalness: 0.7,
500
+ roughness: 0.3,
501
+ });
502
+ const eyeL = createSphere(0.4, 0xffffff, [x - 0.7, y + 3, z + 1.5], 8, {
503
+ material: 'emissive',
504
+ emissive: 0xffffff,
505
+ });
506
+ const eyeR = createSphere(0.4, 0xffffff, [x + 0.7, y + 3, z + 1.5], 8, {
507
+ material: 'emissive',
508
+ emissive: 0xffffff,
509
+ });
510
+ const pupilL = createSphere(0.2, 0xff0000, [x - 0.7, y + 3, z + 1.8], 8, {
511
+ material: 'emissive',
512
+ emissive: 0xff0000,
513
+ });
514
+ const pupilR = createSphere(0.2, 0xff0000, [x + 0.7, y + 3, z + 1.8], 8, {
515
+ material: 'emissive',
516
+ emissive: 0xff0000,
517
+ });
518
+ // Chain
519
+ const chain = createCylinder(0.15, 0.15, 4, 0x444444, [x, y, z], {
520
+ material: 'metallic',
521
+ metalness: 0.9,
522
+ });
523
+ g.enemies.push({
524
+ meshes: [body, eyeL, eyeR, pupilL, pupilR, chain],
525
+ type: 'chomp',
526
+ x,
527
+ y,
528
+ z,
529
+ startX: x,
530
+ startZ: z,
531
+ orbAngle: 0,
532
+ alive: true,
533
+ hp: 5,
534
+ patrolRange: 6,
535
+ });
536
+ }
537
+
538
+ // ── Plumber Build ──────────────────────────────────────────
539
+ function buildPlumber() {
540
+ const body = createCube(1.2, C.plumberBody, [0, 0, 0], { material: 'standard', roughness: 0.8 });
541
+ setScale(body, 1, 1.2, 1);
542
+ const head = createSphere(0.8, C.plumberFace, [0, 1.2, 0], 12, { material: 'standard' });
543
+ const hat = createSphere(0.82, C.plumberHat, [0, 1.5, 0], 12, { material: 'standard' });
544
+ setScale(hat, 1, 0.5, 1);
545
+ const brim = createCube(1, C.plumberHat, [0, 1.4, 0.5], { material: 'standard' });
546
+ setScale(brim, 1.2, 0.1, 0.6);
547
+
548
+ g.p.meshGroup = [
549
+ { m: body, ox: 0, oy: 0.6, oz: 0 },
550
+ { m: head, ox: 0, oy: 1.6, oz: 0 },
551
+ { m: hat, ox: 0, oy: 2.0, oz: 0 },
552
+ { m: brim, ox: 0, oy: 1.9, oz: 0.5 },
553
+ ];
554
+ }
555
+
556
+ function createFX(cx, cy, cz, color, count = 10, speed = 10) {
557
+ for (let i = 0; i < count; i++) {
558
+ const mesh = createCube(0.4, color, [cx, cy, cz], { material: 'emissive', emissive: color });
559
+ const a = Math.random() * Math.PI * 2;
560
+ const a2 = Math.random() * Math.PI - Math.PI / 2;
561
+ g.particles.push({
562
+ mesh,
563
+ x: cx,
564
+ y: cy,
565
+ z: cz,
566
+ vx: Math.cos(a) * Math.cos(a2) * speed,
567
+ vy: Math.abs(Math.sin(a2)) * speed + 5,
568
+ vz: Math.sin(a) * Math.cos(a2) * speed,
569
+ life: 0.3 + Math.random() * 0.4,
570
+ });
571
+ }
572
+ }
573
+
574
+ // ── Game Loop ──────────────────────────────────────────────
575
+ export function update(dt) {
576
+ t += dt;
577
+ updateShake(shake, dt);
578
+
579
+ if (gameState !== 'playing') {
580
+ if (inputLock > 0) inputLock -= dt;
581
+ if (gameState === 'start' && inputLock <= 0 && (keyp('Space') || keyp('Enter'))) startGame();
582
+ if (
583
+ (gameState === 'gameover' || gameState === 'win') &&
584
+ inputLock <= 0 &&
585
+ (keyp('Space') || keyp('Enter'))
586
+ ) {
587
+ resetGame();
588
+ gameState = 'playing';
589
+ }
590
+ return;
591
+ }
592
+
593
+ const p = g.p;
594
+ updateHitState(playerHit, dt);
595
+
596
+ // Power-up timers
597
+ if (g.starTimer > 0) g.starTimer -= dt;
598
+ if (g.speedTimer > 0) g.speedTimer -= dt;
599
+
600
+ // Falling check — generous threshold so falls feel recoverable
601
+ if (p.y < -20) respawnPlayer();
602
+
603
+ handlePlayerInput(dt);
604
+ updateMovingPlatforms(dt);
605
+ updatePhysics(dt);
606
+ updatePlayerMeshes();
607
+ updateCoins(dt);
608
+ updateEnemies(dt);
609
+ updatePowerups(dt);
610
+ updateCheckpoints();
611
+ updateParticles(dt);
612
+
613
+ // Zone detection for atmosphere
614
+ const zoneNow = getZone(p.z);
615
+ if (zoneNow !== g.zone) {
616
+ g.zone = zoneNow;
617
+ updateAtmosphere(zoneNow);
618
+ }
619
+
620
+ // Camera
621
+ const sx = shake.offsetX || 0;
622
+ const sy = shake.offsetY || 0;
623
+ setCameraPosition(p.x + sx * 0.03, p.y + 6 + sy * 0.03, p.z + 12);
624
+ setCameraTarget(p.x, p.y + 1, p.z - 4);
625
+ }
626
+
627
+ function getZone(z) {
628
+ if (z > -65) return 0; // Grassland
629
+ if (z > -130) return 1; // Desert
630
+ return 2; // Ice
631
+ }
632
+
633
+ function updateAtmosphere(zone) {
634
+ if (zone === 0) {
635
+ setFog(0x44aaff, 60, 180);
636
+ setAmbientLight(0xffffff, 0.75);
637
+ } else if (zone === 1) {
638
+ setFog(0xddaa66, 50, 150);
639
+ setAmbientLight(0xffeedd, 0.85);
640
+ } else {
641
+ setFog(0x889fbb, 40, 130);
642
+ setAmbientLight(0xccddff, 0.6);
643
+ }
644
+ }
645
+
646
+ function handlePlayerInput(dt) {
647
+ const p = g.p;
648
+ let ax = 0,
649
+ az = 0;
650
+
651
+ if (key('ArrowLeft') || key('KeyA') || btn(0)) ax = -1;
652
+ if (key('ArrowRight') || key('KeyD') || btn(1)) ax = 1;
653
+ if (key('ArrowUp') || key('KeyW') || btn(2)) az = -1;
654
+ if (key('ArrowDown') || key('KeyS') || btn(3)) az = 1;
655
+
656
+ const speedMult = g.speedTimer > 0 ? 1.6 : 1.0;
657
+ const moveSpd = 32 * speedMult;
658
+ p.vx += ax * moveSpd * dt;
659
+ p.vz += az * moveSpd * dt;
660
+
661
+ // Friction — generous air control for fun platforming
662
+ const zone = getZone(p.z);
663
+ const isIce = zone === 2;
664
+ const fricMult = isIce ? 0.4 : 1.0;
665
+ const fric = p.isGrounded ? 6 * fricMult : 0.8;
666
+ p.vx *= 1 - fric * dt;
667
+ p.vz *= 1 - fric * dt;
668
+
669
+ if (Math.abs(p.vx) > 0.1 || Math.abs(p.vz) > 0.1) p.rotY = Math.atan2(p.vx, p.vz);
670
+
671
+ // Jump / double jump — floaty Mario-style arcs
672
+ const maxJumps = g.hasDoubleJump ? 2 : 1;
673
+ if ((keyp('Space') || keyp('KeyZ')) && p.jumpsLeft > 0 && p.jumpTimer <= 0) {
674
+ p.vy = p.jumpsLeft === maxJumps ? 18 : 14;
675
+ p.isGrounded = false;
676
+ p.jumpsLeft--;
677
+ p.jumpTimer = 0.12;
678
+ sfx('jump');
679
+ createFX(p.x, p.y, p.z, 0xffffff, 5, 2);
680
+ }
681
+ if (p.jumpTimer > 0) p.jumpTimer -= dt;
682
+ }
683
+
684
+ function updateMovingPlatforms(dt) {
685
+ for (const mp of g.movingPlatforms) {
686
+ mp.t += dt * mp.speed;
687
+ const offset = Math.sin(mp.t) * mp.range;
688
+ if (mp.axis === 'x') {
689
+ mp.x = mp.origin + offset;
690
+ setPosition(mp.mesh, mp.x, mp.y, mp.z);
691
+ setPosition(mp.glow, mp.x, mp.y + mp.sy / 2 + 0.1, mp.z);
692
+ } else if (mp.axis === 'z') {
693
+ mp.z = mp.origin + offset;
694
+ setPosition(mp.mesh, mp.x, mp.y, mp.z);
695
+ setPosition(mp.glow, mp.x, mp.y + mp.sy / 2 + 0.1, mp.z);
696
+ } else {
697
+ mp.y = mp.origin + offset;
698
+ setPosition(mp.mesh, mp.x, mp.y, mp.z);
699
+ setPosition(mp.glow, mp.x, mp.y + mp.sy / 2 + 0.1, mp.z);
700
+ }
701
+ }
702
+ }
703
+
704
+ function updatePhysics(dt) {
705
+ const p = g.p;
706
+ // Lighter gravity = floaty fun jumps (35 vs Mario's ~38)
707
+ // Variable: fall faster than rise for snappy-yet-floaty feel
708
+ const grav = p.vy > 0 ? 35 : 45;
709
+ p.vy -= grav * dt;
710
+ p.isGrounded = false;
711
+
712
+ p.y += p.vy * dt;
713
+ handleCollisions('y');
714
+ p.x += p.vx * dt;
715
+ handleCollisions('x');
716
+ p.z += p.vz * dt;
717
+ handleCollisions('z');
718
+ }
719
+
720
+ function handleCollisions(axis) {
721
+ const p = g.p;
722
+ const pr = 0.6,
723
+ ph = 2.0;
724
+
725
+ const collide = plat => {
726
+ if (plat.destroyed) return;
727
+ const hx = plat.sx / 2,
728
+ hy = plat.sy / 2,
729
+ hz = plat.sz / 2;
730
+ const overlapX = p.x + pr > plat.x - hx && p.x - pr < plat.x + hx;
731
+ const overlapY = p.y + ph > plat.y - hy && p.y < plat.y + hy;
732
+ const overlapZ = p.z + pr > plat.z - hz && p.z - pr < plat.z + hz;
733
+
734
+ if (overlapX && overlapY && overlapZ) {
735
+ if (axis === 'y') {
736
+ if (p.vy < 0) {
737
+ p.y = plat.y + hy;
738
+ p.vy = 0;
739
+ p.isGrounded = true;
740
+ p.jumpsLeft = g.hasDoubleJump ? 2 : 1;
741
+ if (plat.isTrampoline) {
742
+ p.vy = 25;
743
+ p.isGrounded = false;
744
+ p.jumpsLeft = g.hasDoubleJump ? 2 : 1;
745
+ sfx('jump');
746
+ createFX(p.x, p.y, p.z, 0xff88ff, 8, 4);
747
+ }
748
+ } else if (p.vy > 0) {
749
+ p.y = plat.y - hy - ph;
750
+ p.vy = -2;
751
+ if (plat.isBrick) {
752
+ createFX(plat.x, plat.y, plat.z, C.brick, 15, 10);
753
+ g.score += 50;
754
+ sfx('explosion');
755
+ destroyMesh(plat.mesh);
756
+ plat.destroyed = true;
757
+ }
758
+ }
759
+ } else if (axis === 'x') {
760
+ if (p.vx > 0) {
761
+ p.x = plat.x - hx - pr;
762
+ p.vx = 0;
763
+ } else if (p.vx < 0) {
764
+ p.x = plat.x + hx + pr;
765
+ p.vx = 0;
766
+ }
767
+ } else if (axis === 'z') {
768
+ if (p.vz > 0) {
769
+ p.z = plat.z - hz - pr;
770
+ p.vz = 0;
771
+ } else if (p.vz < 0) {
772
+ p.z = plat.z + hz + pr;
773
+ p.vz = 0;
774
+ }
775
+ }
776
+ }
777
+ };
778
+
779
+ g.platforms.forEach(collide);
780
+ g.movingPlatforms.forEach(collide);
781
+
782
+ // Cleanup destroyed
783
+ for (let i = g.platforms.length - 1; i >= 0; i--) {
784
+ if (g.platforms[i].destroyed) g.platforms.splice(i, 1);
785
+ }
786
+
787
+ // Hazard collision
788
+ if (axis === 'y') {
789
+ for (const h of g.hazards) {
790
+ const hx = h.sx / 2,
791
+ hy = h.sy / 2,
792
+ hz = h.sz / 2;
793
+ if (
794
+ p.x + pr > h.x - hx &&
795
+ p.x - pr < h.x + hx &&
796
+ p.y + ph > h.y - hy &&
797
+ p.y < h.y + hy &&
798
+ p.z + pr > h.z - hz &&
799
+ p.z - pr < h.z + hz
800
+ ) {
801
+ takeDamage();
802
+ p.vy = 15;
803
+ }
804
+ }
805
+ }
806
+ }
807
+
808
+ function updatePlayerMeshes() {
809
+ const p = g.p;
810
+ const visible = isVisible(playerHit, t);
811
+ const cr = Math.cos(p.rotY),
812
+ sr = Math.sin(p.rotY);
813
+
814
+ p.meshGroup.forEach(part => {
815
+ if (!visible) {
816
+ setPosition(part.m, 0, -100, 0);
817
+ return;
818
+ }
819
+ const rx = part.ox * cr + part.oz * sr;
820
+ const rz = -part.ox * sr + part.oz * cr;
821
+ let animY = 0;
822
+ if (p.isGrounded && (Math.abs(p.vx) > 1 || Math.abs(p.vz) > 1))
823
+ animY = Math.abs(Math.sin(t * 15)) * 0.2;
824
+ setPosition(part.m, p.x + rx, p.y + part.oy + animY, p.z + rz);
825
+ setRotation(part.m, 0, p.rotY, 0);
826
+ });
827
+ }
828
+
829
+ function updateCoins(dt) {
830
+ for (let i = g.coinsList.length - 1; i >= 0; i--) {
831
+ const c = g.coinsList[i];
832
+ if (c.collected) continue;
833
+ c.t += dt;
834
+ c.y += Math.sin(c.t * 5) * 0.005;
835
+ setPosition(c.mesh, c.x, c.y, c.z);
836
+ setRotation(c.mesh, 0, c.t * 3, 0);
837
+
838
+ const dx = c.x - g.p.x,
839
+ dy = c.y - (g.p.y + 1),
840
+ dz = c.z - g.p.z;
841
+ if (dx * dx + dy * dy + dz * dz < 4) {
842
+ g.score += 100;
843
+ g.coins++;
844
+ createFX(c.x, c.y, c.z, C.coin, 8, 8);
845
+ sfx('coin');
846
+ destroyMesh(c.mesh);
847
+ c.collected = true;
848
+
849
+ if (g.coins >= g.totalCoins) {
850
+ gameState = 'win';
851
+ inputLock = 1.5;
852
+ sfx('powerup');
853
+ }
854
+ }
855
+ }
856
+ }
857
+
858
+ function updatePowerups(dt) {
859
+ for (const pu of g.powerups) {
860
+ if (pu.collected) continue;
861
+ pu.t += dt;
862
+ const bob = Math.sin(pu.t * 4) * 0.3;
863
+ setPosition(pu.mesh, pu.x, pu.y + bob, pu.z);
864
+ setRotation(pu.mesh, 0, pu.t * 2, 0);
865
+
866
+ const dx = pu.x - g.p.x,
867
+ dy = pu.y + bob - (g.p.y + 1),
868
+ dz = pu.z - g.p.z;
869
+ if (dx * dx + dy * dy + dz * dz < 3) {
870
+ pu.collected = true;
871
+ destroyMesh(pu.mesh);
872
+ sfx('powerup');
873
+ createFX(
874
+ pu.x,
875
+ pu.y,
876
+ pu.z,
877
+ pu.type === 'star' ? C.star : pu.type === 'mushroom' ? C.mushroom : C.speedShoe,
878
+ 15,
879
+ 8
880
+ );
881
+
882
+ if (pu.type === 'mushroom') {
883
+ g.health = Math.min(g.health + 1, g.maxHealth);
884
+ } else if (pu.type === 'star') {
885
+ g.starTimer = 8;
886
+ triggerShake(shake, 3);
887
+ } else if (pu.type === 'speedShoe') {
888
+ g.speedTimer = 12;
889
+ g.hasDoubleJump = true;
890
+ }
891
+ }
892
+ }
893
+ }
894
+
895
+ function updateCheckpoints() {
896
+ for (const cp of g.checkpoints) {
897
+ if (cp.active) continue;
898
+ const dx = cp.x - g.p.x,
899
+ dz = cp.z - g.p.z;
900
+ if (dx * dx + dz * dz < 9 && Math.abs(g.p.y - cp.y) < 4) {
901
+ cp.active = true;
902
+ g.checkpoint = { x: cp.x, y: cp.y + 2, z: cp.z };
903
+ sfx('powerup');
904
+ createFX(cp.x, cp.y + 2, cp.z, 0x44ff44, 12, 6);
905
+ // Turn flag green
906
+ destroyMesh(cp.mesh);
907
+ cp.mesh = createCube(1, 0x44ff44, [cp.x + 0.6, cp.y + 2.5, cp.z], {
908
+ material: 'emissive',
909
+ emissive: 0x44ff44,
910
+ });
911
+ setScale(cp.mesh, 1.2, 0.6, 0.1);
912
+ }
913
+ }
914
+ }
915
+
916
+ function updateEnemies(dt) {
917
+ const p = g.p;
918
+ for (let i = g.enemies.length - 1; i >= 0; i--) {
919
+ const e = g.enemies[i];
920
+ if (!e.alive) continue;
921
+
922
+ // Movement by type
923
+ if (e.type === 'goomba' || e.type === 'koopa') {
924
+ e.x += e.vx * dt;
925
+ if (Math.abs(e.x - e.startX) > e.patrolRange) e.vx *= -1;
926
+ const bobY = Math.sin(t * 10 + i) * 0.01;
927
+ e.y += bobY;
928
+ e.meshes[0] && setPosition(e.meshes[0], e.x, e.y + 0.6, e.z);
929
+ if (e.type === 'goomba') {
930
+ e.meshes[1] && setPosition(e.meshes[1], e.x - 0.3, e.y + 0.9, e.z + 0.8);
931
+ e.meshes[2] && setPosition(e.meshes[2], e.x + 0.3, e.y + 0.9, e.z + 0.8);
932
+ } else {
933
+ e.meshes[1] && setPosition(e.meshes[1], e.x, e.y + 1.2, e.z + 0.5);
934
+ }
935
+ } else if (e.type === 'flyguy') {
936
+ e.orbAngle += dt * 2;
937
+ e.x = e.startX + Math.cos(e.orbAngle) * e.patrolRange;
938
+ e.z = e.startZ + Math.sin(e.orbAngle) * e.patrolRange * 0.5;
939
+ const fy = e.startY + Math.sin(t * 3 + i) * 1.5;
940
+ e.meshes[0] && setPosition(e.meshes[0], e.x, fy, e.z);
941
+ const wingFlap = Math.sin(t * 20) * 0.3;
942
+ e.meshes[1] && setPosition(e.meshes[1], e.x - 0.8, fy + 0.2 + wingFlap, e.z);
943
+ e.meshes[2] && setPosition(e.meshes[2], e.x + 0.8, fy + 0.2 - wingFlap, e.z);
944
+ e.y = fy - 0.6;
945
+ } else if (e.type === 'chomp') {
946
+ e.orbAngle += dt * 1.5;
947
+ e.x = e.startX + Math.cos(e.orbAngle) * 4;
948
+ e.z = e.startZ + Math.sin(e.orbAngle) * 4;
949
+ const bounce = Math.abs(Math.sin(t * 5)) * 1.5;
950
+ e.meshes[0] && setPosition(e.meshes[0], e.x, e.y + 2 + bounce, e.z);
951
+ e.meshes[1] && setPosition(e.meshes[1], e.x - 0.7, e.y + 3 + bounce, e.z + 1.5);
952
+ e.meshes[2] && setPosition(e.meshes[2], e.x + 0.7, e.y + 3 + bounce, e.z + 1.5);
953
+ e.meshes[3] && setPosition(e.meshes[3], e.x - 0.7, e.y + 3 + bounce, e.z + 1.8);
954
+ e.meshes[4] && setPosition(e.meshes[4], e.x + 0.7, e.y + 3 + bounce, e.z + 1.8);
955
+ e.meshes[5] && setPosition(e.meshes[5], e.x, e.y + 1, e.z);
956
+ }
957
+
958
+ // Collision with player
959
+ const dx = e.x - p.x;
960
+ const eHeight = e.type === 'chomp' ? 2 : 0.6;
961
+ const dy = e.y + eHeight - (p.y + 1);
962
+ const dz = e.z - p.z;
963
+ const dist2 = dx * dx + dy * dy + dz * dz;
964
+ const hitRange = e.type === 'chomp' ? 5 : 2.5;
965
+
966
+ if (dist2 < hitRange) {
967
+ if (p.vy < 0 && p.y > e.y + eHeight - 0.5) {
968
+ // Stomp!
969
+ e.hp--;
970
+ p.vy = 14;
971
+ triggerShake(shake, e.type === 'chomp' ? 5 : 2);
972
+ sfx('hit');
973
+ g.score += e.type === 'chomp' ? 500 : 200;
974
+
975
+ if (e.hp <= 0) {
976
+ e.alive = false;
977
+ createFX(
978
+ e.x,
979
+ e.y + 1,
980
+ e.z,
981
+ e.type === 'chomp'
982
+ ? C.chomp
983
+ : e.type === 'koopa'
984
+ ? C.koopa
985
+ : e.type === 'flyguy'
986
+ ? C.flyGuy
987
+ : C.goomba,
988
+ 20,
989
+ 12
990
+ );
991
+ sfx('explosion');
992
+ e.meshes.forEach(m => destroyMesh(m));
993
+ g.enemies.splice(i, 1);
994
+ if (e.type === 'chomp') {
995
+ g.score += 1000;
996
+ createFX(e.x, e.y + 2, e.z, C.star, 30, 15);
997
+ }
998
+ } else {
999
+ createFX(e.x, e.y + 1, e.z, 0xffaa44, 8, 6);
1000
+ }
1001
+ } else if (!isInvulnerable(playerHit) && g.starTimer <= 0) {
1002
+ takeDamage();
1003
+ } else if (g.starTimer > 0) {
1004
+ // Star power kills on touch
1005
+ e.alive = false;
1006
+ e.hp = 0;
1007
+ createFX(e.x, e.y + 1, e.z, C.star, 20, 12);
1008
+ sfx('explosion');
1009
+ e.meshes.forEach(m => destroyMesh(m));
1010
+ g.enemies.splice(i, 1);
1011
+ g.score += 300;
1012
+ }
1013
+ }
1014
+ }
1015
+ }
1016
+
1017
+ function takeDamage() {
1018
+ if (!triggerHit(playerHit)) return;
1019
+ g.health--;
1020
+ g.p.vy = 10;
1021
+ triggerShake(shake, 4);
1022
+ sfx('hit');
1023
+ createFX(g.p.x, g.p.y + 1, g.p.z, C.plumberFace, 15);
1024
+ if (g.health <= 0) {
1025
+ gameState = 'gameover';
1026
+ inputLock = 1.5;
1027
+ g.p.meshGroup.forEach(part => setPosition(part.m, 0, -100, 0));
1028
+ sfx('explosion');
1029
+ }
1030
+ }
1031
+
1032
+ function respawnPlayer() {
1033
+ takeDamage();
1034
+ if (gameState === 'gameover') return;
1035
+ const cp = g.checkpoint || { x: 0, y: 10, z: 0 };
1036
+ g.p.x = cp.x;
1037
+ g.p.y = cp.y;
1038
+ g.p.z = cp.z;
1039
+ g.p.vx = 0;
1040
+ g.p.vy = 0;
1041
+ g.p.vz = 0;
1042
+ }
1043
+
1044
+ function updateParticles(dt) {
1045
+ for (let i = g.particles.length - 1; i >= 0; i--) {
1046
+ const p = g.particles[i];
1047
+ p.x += p.vx * dt;
1048
+ p.y += p.vy * dt;
1049
+ p.z += p.vz * dt;
1050
+ p.vy -= 15 * dt;
1051
+ p.life -= dt;
1052
+ const a = Math.max(0.01, p.life);
1053
+ setScale(p.mesh, a, a, a);
1054
+ setPosition(p.mesh, p.x, p.y, p.z);
1055
+ if (p.life <= 0) {
1056
+ destroyMesh(p.mesh);
1057
+ g.particles.splice(i, 1);
1058
+ }
1059
+ }
1060
+ }
1061
+
1062
+ // ── Screens & UI ───────────────────────────────────────────
1063
+ export function draw() {
1064
+ if (gameState === 'start') {
1065
+ drawStartScreen();
1066
+ return;
1067
+ }
1068
+ if (gameState === 'gameover') {
1069
+ drawGameOver();
1070
+ return;
1071
+ }
1072
+ if (gameState === 'win') {
1073
+ drawWinScreen();
1074
+ return;
1075
+ }
1076
+ drawHUD();
1077
+ }
1078
+
1079
+ function drawHUD() {
1080
+ // Coin counter
1081
+ rectfill(14, 10, 110, 22, rgba8(0, 0, 0, 140));
1082
+ print(`COINS ${g.coins}/${g.totalCoins}`, 20, 14, rgba8(255, 220, 50, 255));
1083
+
1084
+ // Score
1085
+ rectfill(500, 10, 130, 22, rgba8(0, 0, 0, 140));
1086
+ print(`SCORE ${String(g.score).padStart(6, '0')}`, 506, 14, rgba8(255, 255, 255, 255));
1087
+
1088
+ // Health hearts
1089
+ for (let i = 0; i < g.maxHealth; i++) {
1090
+ const color = i < g.health ? rgba8(255, 50, 50, 255) : rgba8(60, 60, 60, 150);
1091
+ rectfill(20 + i * 24, 38, 18, 16, color);
1092
+ rect(20 + i * 24, 38, 18, 16, rgba8(255, 255, 255, 100), false);
1093
+ }
1094
+
1095
+ // Zone name
1096
+ const zoneNames = ['GRASSLAND', 'DESERT RUINS', 'ICE MOUNTAIN'];
1097
+ const zone = getZone(g.p.z);
1098
+ print(zoneNames[zone], 280, 348, rgba8(200, 200, 200, 160));
1099
+
1100
+ // Power-up indicators
1101
+ if (g.starTimer > 0) {
1102
+ const flash = Math.sin(t * 10) > 0;
1103
+ rectfill(270, 10, 100, 18, flash ? rgba8(255, 255, 0, 180) : rgba8(200, 150, 0, 180));
1104
+ print(`STAR ${Math.ceil(g.starTimer)}s`, 280, 12, rgba8(0, 0, 0, 255));
1105
+ }
1106
+ if (g.speedTimer > 0) {
1107
+ rectfill(270, 30, 100, 18, rgba8(50, 200, 255, 160));
1108
+ print(`SPEED ${Math.ceil(g.speedTimer)}s`, 276, 32, rgba8(0, 0, 0, 255));
1109
+ }
1110
+ if (g.hasDoubleJump) {
1111
+ print('2x JUMP', 560, 38, rgba8(100, 220, 255, 200));
1112
+ }
1113
+ }
1114
+
1115
+ function drawStartScreen() {
1116
+ rectfill(0, 0, 640, 360, rgba8(0, 0, 0, 180));
1117
+ drawGlowText(
1118
+ 'SUPER PLUMBER',
1119
+ 165,
1120
+ 45 + Math.sin(t * 3) * 5,
1121
+ rgba8(255, 50, 50),
1122
+ rgba8(150, 0, 0)
1123
+ );
1124
+ printCentered('NOVA 64', 320, 95 + Math.sin(t * 3) * 5, rgba8(255, 200, 0));
1125
+
1126
+ rectfill(140, 130, 360, 120, rgba8(10, 20, 50, 220));
1127
+ rect(140, 130, 360, 120, rgba8(100, 150, 255, 200), false);
1128
+
1129
+ printCentered('3 ZONES: Grassland Desert Ice', 320, 140, rgba8(200, 200, 255));
1130
+ printCentered('WASD / Arrows = Move', 320, 162, rgba8(200, 200, 200));
1131
+ printCentered('SPACE = Jump (collect Speed Shoes for 2x)', 320, 178, rgba8(200, 200, 200));
1132
+ printCentered('Stomp enemies! Collect ALL coins to win!', 320, 194, rgba8(255, 255, 100));
1133
+ printCentered('Power-ups: Star, Mushroom, Speed Shoes', 320, 214, rgba8(100, 255, 150));
1134
+ printCentered('Touch checkpoints to save your position!', 320, 230, rgba8(255, 180, 100));
1135
+
1136
+ const pulse = Math.sin(t * 3) * 0.5 + 0.5;
1137
+ printCentered(
1138
+ 'PRESS SPACE TO PLAY',
1139
+ 320,
1140
+ 290,
1141
+ rgba8(255, 255, 100, Math.floor(100 + pulse * 155))
1142
+ );
1143
+ }
1144
+
1145
+ function drawGameOver() {
1146
+ rectfill(0, 0, 640, 360, rgba8(40, 0, 0, 220));
1147
+ drawGlowText('GAME OVER', 220, 80, rgba8(255, 50, 50), rgba8(150, 0, 0));
1148
+ printCentered(
1149
+ `Score: ${g.score} | Coins: ${g.coins}/${g.totalCoins}`,
1150
+ 320,
1151
+ 150,
1152
+ rgba8(200, 200, 200)
1153
+ );
1154
+ const zoneNames = ['Grassland', 'Desert Ruins', 'Ice Mountain'];
1155
+ printCentered(`Reached: ${zoneNames[getZone(g.p.z)]}`, 320, 175, rgba8(180, 180, 200));
1156
+ const pulse = Math.sin(t * 2) * 0.5 + 0.5;
1157
+ printCentered(
1158
+ 'PRESS SPACE TO TRY AGAIN',
1159
+ 320,
1160
+ 240,
1161
+ rgba8(200, 150, 150, Math.floor(120 + pulse * 135))
1162
+ );
1163
+ }
1164
+
1165
+ function drawWinScreen() {
1166
+ rectfill(0, 0, 640, 360, rgba8(0, 20, 0, 200));
1167
+ drawGlowText('COURSE CLEAR!', 180, 60, rgba8(50, 255, 100), rgba8(0, 100, 0));
1168
+ printCentered(`FINAL SCORE: ${g.score}`, 320, 130, rgba8(255, 220, 50));
1169
+ printCentered(`ALL ${g.totalCoins} COINS COLLECTED!`, 320, 160, rgba8(255, 255, 100));
1170
+ const rating =
1171
+ g.score > 15000 ? 'S RANK!' : g.score > 10000 ? 'A RANK' : g.score > 5000 ? 'B RANK' : 'C RANK';
1172
+ drawGlowText(rating, 260, 200, rgba8(255, 215, 0), rgba8(180, 140, 0));
1173
+ const pulse = Math.sin(t * 2) * 0.5 + 0.5;
1174
+ printCentered(
1175
+ 'PRESS SPACE TO PLAY AGAIN',
1176
+ 320,
1177
+ 280,
1178
+ rgba8(200, 255, 200, Math.floor(120 + pulse * 135))
1179
+ );
1180
+ }
1181
+
1182
+ function startGame() {
1183
+ gameState = 'playing';
1184
+ sfx('confirm');
1185
+ }