nova64 0.2.5 → 0.2.7

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 (185) 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/dist/runtime/api-2d.js +1158 -0
  118. package/dist/runtime/api-3d/camera.js +73 -0
  119. package/dist/runtime/api-3d/instancing.js +180 -0
  120. package/dist/runtime/api-3d/lights.js +51 -0
  121. package/dist/runtime/api-3d/materials.js +47 -0
  122. package/dist/runtime/api-3d/models.js +84 -0
  123. package/dist/runtime/api-3d/particles.js +296 -0
  124. package/dist/runtime/api-3d/pbr.js +113 -0
  125. package/dist/runtime/api-3d/primitives.js +304 -0
  126. package/dist/runtime/api-3d/scene.js +169 -0
  127. package/dist/runtime/api-3d/transforms.js +161 -0
  128. package/dist/runtime/api-3d.js +166 -0
  129. package/dist/runtime/api-effects.js +840 -0
  130. package/dist/runtime/api-gameutils.js +476 -0
  131. package/dist/runtime/api-generative.js +610 -0
  132. package/dist/runtime/api-presets.js +85 -0
  133. package/dist/runtime/api-skybox.js +232 -0
  134. package/dist/runtime/api-sprites.js +100 -0
  135. package/dist/runtime/api-voxel.js +712 -0
  136. package/dist/runtime/api.js +201 -0
  137. package/dist/runtime/assets.js +27 -0
  138. package/dist/runtime/audio.js +114 -0
  139. package/dist/runtime/collision.js +47 -0
  140. package/dist/runtime/console.js +101 -0
  141. package/dist/runtime/editor.js +233 -0
  142. package/dist/runtime/font.js +233 -0
  143. package/dist/runtime/framebuffer.js +28 -0
  144. package/dist/runtime/fullscreen-button.js +185 -0
  145. package/dist/runtime/gpu-canvas2d.js +47 -0
  146. package/dist/runtime/gpu-threejs.js +643 -0
  147. package/dist/runtime/gpu-webgl2.js +310 -0
  148. package/dist/runtime/index.d.ts +682 -0
  149. package/dist/runtime/index.js +22 -0
  150. package/dist/runtime/input.js +225 -0
  151. package/dist/runtime/logger.js +60 -0
  152. package/dist/runtime/physics.js +101 -0
  153. package/dist/runtime/screens.js +213 -0
  154. package/dist/runtime/storage.js +38 -0
  155. package/dist/runtime/store.js +151 -0
  156. package/dist/runtime/textinput.js +68 -0
  157. package/dist/runtime/ui/buttons.js +124 -0
  158. package/dist/runtime/ui/panels.js +105 -0
  159. package/dist/runtime/ui/text.js +86 -0
  160. package/dist/runtime/ui/widgets.js +141 -0
  161. package/dist/runtime/ui.js +111 -0
  162. package/index.html +6 -1
  163. package/package.json +9 -2
  164. package/public/assets/sky/studio/nx.png +0 -0
  165. package/public/assets/sky/studio/ny.png +0 -0
  166. package/public/assets/sky/studio/nz.png +0 -0
  167. package/public/assets/sky/studio/px.png +0 -0
  168. package/public/assets/sky/studio/py.png +0 -0
  169. package/public/assets/sky/studio/pz.png +0 -0
  170. package/public/os9-shell/assets/index-KchE_ngx.js +483 -0
  171. package/public/os9-shell/assets/index-KchE_ngx.js.map +1 -0
  172. package/public/os9-shell/index.html +10 -1
  173. package/runtime/api-2d.js +301 -21
  174. package/runtime/api-3d/pbr.js +45 -1
  175. package/runtime/api-3d.js +1 -0
  176. package/runtime/api-effects.js +90 -3
  177. package/runtime/api-gameutils.js +476 -0
  178. package/runtime/api-generative.js +610 -0
  179. package/runtime/api-skybox.js +54 -0
  180. package/runtime/api-voxel.js +139 -28
  181. package/runtime/gpu-threejs.js +13 -9
  182. package/runtime/ui.js +2 -2
  183. package/src/main.js +20 -0
  184. package/public/os9-shell/assets/index-B1Uvacma.js +0 -32825
  185. 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
+ }