nova64 0.2.5 → 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 +20 -0
  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,865 @@
1
+ // ⭐ F-ZERO NOVA 3D — Anti-Gravity Racing ⭐
2
+ // Ultimate Edition: Synthwave Neon Highway, Boost Mechanics, & Cinematic Bloom
3
+
4
+ // ── Configuration ──────────────────────────────────────────
5
+ const C = {
6
+ // Materials and Colors
7
+ shipBody: 0x0044ff,
8
+ shipCockpit: 0x22aaff,
9
+ shipEngine: 0x00ffff,
10
+ shipTrim: 0xffffff,
11
+ rivalBody: 0xee0033,
12
+ rivalEngine: 0xff4422,
13
+
14
+ bgFog: 0x040210, // Deep purple synthwave space
15
+ trackR1: 0x05051a,
16
+ trackR2: 0x0a0a22,
17
+ neonWall: 0xff00aa, // Magenta glowing rails
18
+ neonWallAlt: 0xaa00ff,
19
+ boostPad: 0x00ffaa,
20
+ spark: 0xffdd00,
21
+ };
22
+
23
+ let gameState = 'start'; // 'start', 'countdown', 'playing', 'crashed', 'finished'
24
+ let t = 0; // global time
25
+ let inputLock = 0;
26
+ let playerHit;
27
+ let speedLineInstanceId = null; // GPU instanced speed lines
28
+ let countdownTimer = 0;
29
+ let shake;
30
+
31
+ // ── Game State ─────────────────────────────────────────────
32
+ let g = {
33
+ speed: 0,
34
+ maxSpeed: 250,
35
+ boostSpeed: 400,
36
+ dist: 0,
37
+ health: 100,
38
+ energy: 100, // Used for boosting
39
+ rank: 10,
40
+ passCount: 0,
41
+
42
+ p: {
43
+ x: 0,
44
+ y: 1.5,
45
+ z: 0,
46
+ vx: 0,
47
+ roll: 0,
48
+ pitch: 0,
49
+ meshes: {},
50
+ isBoosting: false,
51
+ invuln: 0,
52
+ },
53
+
54
+ trackWidth: 40,
55
+ roadSegments: [],
56
+ borders: [],
57
+ props: [],
58
+ rivals: [],
59
+ particles: [],
60
+ speedLines: [],
61
+
62
+ propTimer: 0,
63
+ rivalTimer: 0,
64
+ };
65
+
66
+ // ── Initialization ─────────────────────────────────────────
67
+ export async function init() {
68
+ console.log('🏁 F-ZERO NOVA 3D (Ultimate) — Initializing Engine...');
69
+
70
+ // Super wide FOV for speed illusion
71
+ setCameraFOV(85);
72
+ setCameraPosition(0, 8, 15);
73
+ setCameraTarget(0, 0, -40);
74
+
75
+ // Atmospheric Lighting
76
+ setAmbientLight(0x222233, 1.5);
77
+ setLightDirection(0, -1, -0.5);
78
+ setLightColor(0xffaaaa);
79
+
80
+ // Deep synthwave fog
81
+ setFog(C.bgFog, 20, 300);
82
+
83
+ if (typeof createSpaceSkybox === 'function') {
84
+ createSpaceSkybox({ starCount: 1500, starSize: 2.0, nebulae: true, nebulaColor: 0xaa00aa });
85
+ }
86
+
87
+ // Neon over-glow
88
+ enableBloom(1.0, 0.4, 0.4);
89
+ if (typeof enableFXAA === 'function') enableFXAA();
90
+
91
+ buildPlayerShip();
92
+ playerHit = createHitState({ invulnDuration: 0.5, blinkRate: 40 });
93
+ shake = createShake({ decay: 4 });
94
+ initTrack();
95
+
96
+ // Pre-fill track props & speed lines
97
+ // Speed lines use GPU instancing (40 cubes → 1 instanced mesh)
98
+ speedLineInstanceId = createInstancedMesh('cube', 40, 0xaabbff, {
99
+ size: 1,
100
+ material: 'emissive',
101
+ emissive: 0xaabbff,
102
+ emissiveIntensity: 1.0,
103
+ });
104
+ for (let i = 0; i < 40; i++) spawnSpeedLine(i);
105
+ finalizeInstances(speedLineInstanceId);
106
+ for (let i = 0; i < 5; i++) {
107
+ g.propTimer += 0.5;
108
+ updateTrackSpawns(1);
109
+ g.rivalTimer += 0.5;
110
+ updateRivals(1);
111
+ }
112
+
113
+ initStartScreen();
114
+ }
115
+
116
+ // ── Entity Construction ────────────────────────────────────
117
+ function buildPlayerShip() {
118
+ const p = g.p;
119
+ // Sleek wedge shape (Metallic)
120
+ p.meshes.body = createCube(2.0, C.shipBody, [p.x, p.y, p.z], {
121
+ material: 'metallic',
122
+ metalness: 0.9,
123
+ roughness: 0.2,
124
+ });
125
+ setScale(p.meshes.body, 1.2, 0.4, 2.5);
126
+
127
+ p.meshes.wingL = createCube(1.0, C.shipTrim, [p.x - 1.8, p.y, p.z + 0.5], {
128
+ material: 'metallic',
129
+ });
130
+ setScale(p.meshes.wingL, 2.0, 0.1, 1.5);
131
+
132
+ p.meshes.wingR = createCube(1.0, C.shipTrim, [p.x + 1.8, p.y, p.z + 0.5], {
133
+ material: 'metallic',
134
+ });
135
+ setScale(p.meshes.wingR, 2.0, 0.1, 1.5);
136
+
137
+ p.meshes.cockpit = createCube(1.0, C.shipCockpit, [p.x, p.y + 0.5, p.z - 0.5], {
138
+ material: 'holographic',
139
+ transparent: true,
140
+ opacity: 0.8,
141
+ });
142
+ setScale(p.meshes.cockpit, 0.6, 0.5, 1.2);
143
+
144
+ // Twin Engines
145
+ p.meshes.engL = createCube(0.8, C.shipEngine, [p.x - 0.6, p.y, p.z + 2.5], {
146
+ material: 'emissive',
147
+ emissive: C.shipEngine,
148
+ });
149
+ p.meshes.engR = createCube(0.8, C.shipEngine, [p.x + 0.6, p.y, p.z + 2.5], {
150
+ material: 'emissive',
151
+ emissive: C.shipEngine,
152
+ });
153
+ }
154
+
155
+ function initTrack() {
156
+ const segLen = 40;
157
+ // Checkerboard pattern rolling floor
158
+ for (let i = 0; i < 15; i++) {
159
+ const z = -i * segLen + 20;
160
+ const color = i % 2 === 0 ? C.trackR1 : C.trackR2;
161
+ const mesh = createPlane(g.trackWidth * 1.5, segLen, color, [0, -1, z], {
162
+ material: 'standard',
163
+ roughness: 0.8,
164
+ });
165
+ rotateMesh(mesh, -Math.PI / 2, 0, 0);
166
+ g.roadSegments.push({ mesh, z, len: segLen, activeColor: color });
167
+
168
+ // Left & Right neon border rails
169
+ const wl = createCube(1.0, C.neonWall, [-g.trackWidth / 2 - 1, 0, z], {
170
+ material: 'emissive',
171
+ emissive: C.neonWall,
172
+ });
173
+ setScale(wl, 1.0, 2.0, segLen);
174
+ const wr = createCube(1.0, C.neonWall, [g.trackWidth / 2 + 1, 0, z], {
175
+ material: 'emissive',
176
+ emissive: C.neonWallAlt,
177
+ });
178
+ setScale(wr, 1.0, 2.0, segLen);
179
+ g.borders.push({ wl, wr, z, len: segLen });
180
+ }
181
+ }
182
+
183
+ function spawnRival(isFar = false) {
184
+ const x = (Math.random() - 0.5) * (g.trackWidth - 8);
185
+ const z = isFar ? -400 : -200 - Math.random() * 100;
186
+
187
+ // Basic rival shape
188
+ const body = createCube(2.0, C.rivalBody, [x, 1.5, z], { material: 'metallic', metalness: 0.8 });
189
+ setScale(body, 1.0, 0.5, 2.0);
190
+ const eng = createCube(0.8, C.rivalEngine, [x, 1.5, z + 2.0], {
191
+ material: 'emissive',
192
+ emissive: C.rivalEngine,
193
+ });
194
+ setScale(eng, 1.8, 0.6, 0.5);
195
+
196
+ const speedMultiplier = 0.5 + Math.random() * 0.4; // They travel at 50-90% of max speed
197
+
198
+ g.rivals.push({
199
+ meshes: [body, eng],
200
+ x,
201
+ y: 1.5,
202
+ z,
203
+ vx: (Math.random() - 0.5) * 10,
204
+ speed: g.maxSpeed * speedMultiplier,
205
+ passed: false,
206
+ });
207
+ }
208
+
209
+ function spawnSpeedLine(instanceIdx = -1) {
210
+ const x = (Math.random() - 0.5) * g.trackWidth * 1.4;
211
+ const y = 1 + Math.random() * 8;
212
+ const z = -400 + Math.random() * 450;
213
+ const len = 8.0 + Math.random() * 12;
214
+
215
+ const idx = instanceIdx >= 0 ? instanceIdx : g.speedLines.length;
216
+ if (speedLineInstanceId !== null) {
217
+ setInstanceTransform(speedLineInstanceId, idx, x, y, z, 0, 0, 0, 0.1, 0.1, len);
218
+ }
219
+ g.speedLines.push({ idx, x, y, z, len });
220
+ }
221
+
222
+ function spawnMine() {
223
+ const x = (Math.random() - 0.5) * 45;
224
+ const z = -400 - Math.random() * 200;
225
+
226
+ const mesh = createAdvancedCube(3, { material: 'emissive', emissive: 0xff0000, intensity: 2 }, [
227
+ x,
228
+ 0,
229
+ z,
230
+ ]);
231
+ g.props.push({ type: 'mine', mesh, x: x, z: z, active: true, throb: 0 });
232
+ }
233
+
234
+ function spawnBoostPad() {
235
+ const x = (Math.random() - 0.5) * (g.trackWidth - 10);
236
+ const z = -350;
237
+
238
+ const mesh = createPlane(6, 12, C.boostPad, [x, -0.9, z], {
239
+ material: 'emissive',
240
+ emissive: C.boostPad,
241
+ });
242
+ rotateMesh(mesh, -Math.PI / 2, 0, 0);
243
+
244
+ g.props.push({ type: 'boost', mesh, x, z, active: true });
245
+ }
246
+
247
+ function createSparks(cx, cy, cz, count) {
248
+ for (let i = 0; i < count; i++) {
249
+ const mesh = createCube(0.4, C.spark, [cx, cy, cz], {
250
+ material: 'emissive',
251
+ emissive: C.spark,
252
+ });
253
+ const spd = 20 + Math.random() * 30;
254
+ const a = Math.random() * Math.PI * 2;
255
+ g.particles.push({
256
+ mesh,
257
+ x: cx,
258
+ y: cy,
259
+ z: cz,
260
+ vx: Math.cos(a) * spd,
261
+ vy: Math.abs(Math.sin(a)) * spd + 10,
262
+ vz: Math.random() * 20 - 10,
263
+ life: 0.3 + Math.random() * 0.5,
264
+ maxLife: 0.8,
265
+ });
266
+ }
267
+ }
268
+
269
+ // ── Game Loop Update ───────────────────────────────────────
270
+ export function update(dt) {
271
+ t += dt;
272
+
273
+ if (gameState !== 'playing' && gameState !== 'countdown') {
274
+ if (inputLock > 0) inputLock -= dt;
275
+ updateAllButtons();
276
+ updateMenuAnim(dt);
277
+ if (gameState === 'start' && inputLock <= 0 && isKeyPressed('Space')) startGame();
278
+ return;
279
+ }
280
+
281
+ if (gameState === 'countdown') {
282
+ countdownTimer -= dt;
283
+ updateMenuAnim(dt);
284
+ if (countdownTimer <= 0) {
285
+ gameState = 'playing';
286
+ sfx('confirm');
287
+ } else if (countdownTimer < 1 || countdownTimer < 2 || countdownTimer < 3) {
288
+ // sfx on each count handled in draw
289
+ }
290
+ return;
291
+ }
292
+
293
+ // Ship logic
294
+ const p = g.p;
295
+ updateHitState(playerHit, dt);
296
+ updateShake(shake, dt);
297
+ if (g.health <= 0) {
298
+ createSparks(p.x, p.y, p.z, 40);
299
+ // Hide ALL ship parts
300
+ Object.values(p.meshes).forEach(m => m && setPosition(m, 1000, 0, 0));
301
+ gameState = 'crashed';
302
+ inputLock = 1.0;
303
+ sfx('death');
304
+ initGameOver();
305
+ return;
306
+ }
307
+
308
+ // Controls
309
+ let ix = 0;
310
+ if (key('ArrowLeft') || key('KeyA') || btn(14)) ix = -1;
311
+ if (key('ArrowRight') || key('KeyD') || btn(15)) ix = 1;
312
+
313
+ // Shifting/Drifting (L/R Bumpers or Q/E)
314
+ if (key('KeyQ') || btn(4)) ix -= 1.5;
315
+ if (key('KeyE') || btn(5)) ix += 1.5;
316
+
317
+ const latAccel = 120;
318
+ p.vx += ix * latAccel * dt;
319
+ p.vx *= 1 - 4.5 * dt; // handling friction
320
+
321
+ // Update speed automatically
322
+ let targetSpeed = g.maxSpeed;
323
+ if (p.isBoosting) targetSpeed = g.boostSpeed;
324
+
325
+ // Acceleration / declaration matching
326
+ if (g.speed < targetSpeed) g.speed += 150 * dt;
327
+ else g.speed -= 80 * dt;
328
+
329
+ // Engine energy passively recharges
330
+ g.energy = Math.min(100, g.energy + dt * 5);
331
+
332
+ // Manual boost (Space or A button)
333
+ if ((key('Space') || btn(0)) && g.energy > 30 && !p.isBoosting) {
334
+ p.isBoosting = true;
335
+ sfx('powerup');
336
+ }
337
+ if (p.isBoosting) {
338
+ g.energy -= 40 * dt;
339
+ if (g.energy <= 0) p.isBoosting = false;
340
+ } else if (!key('Space') && !btn(0)) {
341
+ p.isBoosting = false; // Release to cancel
342
+ }
343
+
344
+ p.x += p.vx * dt;
345
+
346
+ // Wall collisions
347
+ const bw = g.trackWidth / 2 - 1.5;
348
+ if (p.x < -bw) {
349
+ p.x = -bw;
350
+ p.vx = 40;
351
+ hitWall(-bw);
352
+ }
353
+ if (p.x > bw) {
354
+ p.x = bw;
355
+ p.vx = -40;
356
+ hitWall(bw);
357
+ }
358
+
359
+ p.roll += (-p.vx * 0.015 - p.roll) * 10 * dt; // Lean into turns
360
+
361
+ updatePlayerTransforms();
362
+
363
+ g.dist += g.speed * dt;
364
+
365
+ // Progress World
366
+ const relSpeed = g.speed * dt;
367
+ updateRoad(relSpeed);
368
+ updateTrackSpawns(dt);
369
+ updateRivals(dt);
370
+ updateProps(dt);
371
+ updateParticles(dt, relSpeed);
372
+
373
+ // Dynamic Camera (Lags slightly behind ship x-movement)
374
+ const [shakeX, shakeY] = getShakeOffset(shake);
375
+ const camX = p.x * 0.4 + p.vx * 0.05;
376
+ const fovWarp = p.isBoosting ? 100 : 85;
377
+ // Tween FOV for sense of speed
378
+ setCameraFOV(85 + (p.isBoosting ? 15 : 0) * Math.min(1, g.speed / g.boostSpeed));
379
+
380
+ const camY = 8 - p.pitch * 5;
381
+ setCameraPosition(camX + shakeX, camY + shakeY, 12);
382
+ setCameraTarget(camX * 1.5, 0, -40);
383
+
384
+ // Track win
385
+ if (g.dist > 30000 && g.rank <= 1) {
386
+ // reached end in 1st
387
+ gameState = 'finished';
388
+ inputLock = 1.0;
389
+ sfx('powerup');
390
+ initWinScreen();
391
+ }
392
+ }
393
+
394
+ function hitWall(xPos) {
395
+ g.health -= 5;
396
+ g.speed *= 0.6; // heavy slow down
397
+ playerHit.invulnTimer = 0.2;
398
+ createSparks(g.p.x + (xPos < 0 ? -1.5 : 1.5), g.p.y, g.p.z, 8);
399
+ triggerShake(shake, 0.5);
400
+ sfx('hit');
401
+ }
402
+
403
+ function updatePlayerTransforms() {
404
+ const p = g.p;
405
+ if (!p.meshes.body) return;
406
+
407
+ const r = p.roll;
408
+ const cr = Math.cos(r),
409
+ sr = Math.sin(r);
410
+
411
+ // Hover bob
412
+ p.y = 1.5 + Math.sin(t * 10) * 0.15;
413
+
414
+ const applyR = (ox, oy, oz) => [p.x + ox * cr - oy * sr, p.y + ox * sr + oy * cr, p.z + oz];
415
+
416
+ setPosition(p.meshes.body, ...applyR(0, 0, 0));
417
+ setRotation(p.meshes.body, p.pitch, 0, r);
418
+
419
+ setPosition(p.meshes.wingL, ...applyR(-1.8, 0, 0.5));
420
+ setRotation(p.meshes.wingL, p.pitch, 0, r);
421
+
422
+ setPosition(p.meshes.wingR, ...applyR(1.8, 0, 0.5));
423
+ setRotation(p.meshes.wingR, p.pitch, 0, r);
424
+
425
+ setPosition(p.meshes.cockpit, ...applyR(0, 0.5, -0.5));
426
+ setRotation(p.meshes.cockpit, p.pitch, 0, r);
427
+
428
+ // Glow flicker based on boosting
429
+ const engineScl = p.isBoosting ? 1.5 + Math.random() * 0.5 : 1.0 + Math.random() * 0.2;
430
+
431
+ setPosition(p.meshes.engL, ...applyR(-0.6, 0, 2.5));
432
+ setRotation(p.meshes.engL, p.pitch, 0, r);
433
+ setScale(p.meshes.engL, 0.8, 0.8, engineScl);
434
+
435
+ setPosition(p.meshes.engR, ...applyR(0.6, 0, 2.5));
436
+ setRotation(p.meshes.engR, p.pitch, 0, r);
437
+ setScale(p.meshes.engR, 0.8, 0.8, engineScl);
438
+ }
439
+
440
+ function updateRoad(distMovement) {
441
+ g.roadSegments.forEach(r => {
442
+ r.z += distMovement;
443
+ if (r.z > 30) r.z -= g.roadSegments.length * r.len;
444
+ // Emphasize speed by flattening road pitch based on Z
445
+ setPosition(r.mesh, 0, -1, r.z);
446
+ });
447
+
448
+ g.borders.forEach(b => {
449
+ b.z += distMovement;
450
+ if (b.z > 30) b.z -= g.borders.length * b.len;
451
+ setPosition(b.wl, -g.trackWidth / 2 - 1, 0, b.z);
452
+ setPosition(b.wr, g.trackWidth / 2 + 1, 0, b.z);
453
+ });
454
+ }
455
+
456
+ function updateTrackSpawns(dt) {
457
+ g.propTimer -= dt;
458
+ if (g.propTimer <= 0) {
459
+ if (Math.random() < 0.6) spawnBoostPad();
460
+ else if (Math.random() < 0.8) spawnMine();
461
+
462
+ g.propTimer = 1.0 + Math.random() * 2.0;
463
+ }
464
+ }
465
+
466
+ function updateRivals(dt) {
467
+ g.rivalTimer -= dt;
468
+ if (g.rivalTimer <= 0 && g.rivals.length < 8) {
469
+ spawnRival(true);
470
+ g.rivalTimer = 1.5 + Math.random() * 2.5;
471
+ }
472
+
473
+ const p = g.p;
474
+ for (let i = g.rivals.length - 1; i >= 0; i--) {
475
+ let r = g.rivals[i];
476
+
477
+ // Relative movement
478
+ const relSpeed = g.speed - r.speed;
479
+ r.z += relSpeed * dt;
480
+
481
+ r.x += r.vx * dt;
482
+ if (r.x < -18 || r.x > 18) r.vx *= -1;
483
+
484
+ r.y = 1.5 + Math.sin(t * 8 + i) * 0.3;
485
+
486
+ setPosition(r.meshes[0], r.x, r.y, r.z);
487
+ setRotation(r.meshes[0], 0, 0, -r.vx * 0.05);
488
+
489
+ setPosition(r.meshes[1], r.x, r.y, r.z + 2.0);
490
+ setRotation(r.meshes[1], 0, 0, -r.vx * 0.05);
491
+
492
+ // Passed mechanic
493
+ if (!r.passed && r.z > p.z + 2) {
494
+ r.passed = true;
495
+ g.passCount++;
496
+ g.rank = Math.max(1, 10 - g.passCount);
497
+ sfx('coin');
498
+ }
499
+
500
+ // Collision with player
501
+ if (!isInvulnerable(playerHit) && Math.abs(r.z - p.z) < 3.5 && Math.abs(r.x - p.x) < 2.5) {
502
+ g.health -= 15;
503
+ g.speed *= 0.7; // lose heavy momentum
504
+ triggerHit(playerHit);
505
+ triggerShake(shake, 0.8);
506
+ sfx('explosion');
507
+ r.vx = r.x > p.x ? 15 : -15; // knock away
508
+ createSparks((r.x + p.x) / 2, 1.5, p.z, 15);
509
+ }
510
+
511
+ // Clean up
512
+ if (r.z > 50) {
513
+ r.meshes.forEach(m => destroyMesh(m));
514
+ g.rivals.splice(i, 1);
515
+ }
516
+ }
517
+ }
518
+
519
+ function updateProps(dt) {
520
+ for (let i = g.props.length - 1; i >= 0; i--) {
521
+ let p = g.props[i];
522
+ p.z += g.speed * dt;
523
+ setPosition(p.mesh, p.x, -0.9, p.z);
524
+
525
+ // Boost Pad hit detection
526
+ if (
527
+ p.active &&
528
+ p.type === 'boost' &&
529
+ Math.abs(p.z - g.p.z) < 4.0 &&
530
+ Math.abs(p.x - g.p.x) < 4.0
531
+ ) {
532
+ p.active = false;
533
+ g.p.isBoosting = true;
534
+ g.energy = Math.min(100, g.energy + 50); // Restore energy
535
+ g.speed = g.boostSpeed + 50; // Extra burst
536
+ createSparks(g.p.x, -0.5, g.p.z, 20);
537
+ sfx('powerup');
538
+ }
539
+
540
+ // Mine collision detection
541
+ if (
542
+ p.active &&
543
+ p.type === 'mine' &&
544
+ !isInvulnerable(playerHit) &&
545
+ Math.abs(p.z - g.p.z) < 3.0 &&
546
+ Math.abs(p.x - g.p.x) < 3.0
547
+ ) {
548
+ p.active = false;
549
+ g.health -= 25;
550
+ g.speed *= 0.4;
551
+ triggerHit(playerHit);
552
+ triggerShake(shake, 1.2);
553
+ createSparks(p.x, 0, p.z, 30);
554
+ sfx('explosion');
555
+ }
556
+
557
+ if (p.z > 30) {
558
+ destroyMesh(p.mesh);
559
+ g.props.splice(i, 1);
560
+ }
561
+ }
562
+ }
563
+
564
+ function updateParticles(dt, ds) {
565
+ // Environmental speed stars/lines
566
+ let linesUpdated = false;
567
+ g.speedLines.forEach(sl => {
568
+ sl.z += ds * 1.5; // lines fly past faster
569
+ if (sl.z > 40) sl.z -= 500;
570
+ if (speedLineInstanceId !== null) {
571
+ setInstanceTransform(
572
+ speedLineInstanceId,
573
+ sl.idx,
574
+ sl.x,
575
+ sl.y,
576
+ sl.z,
577
+ 0,
578
+ 0,
579
+ 0,
580
+ 0.1,
581
+ 0.1,
582
+ sl.len
583
+ );
584
+ linesUpdated = true;
585
+ }
586
+ });
587
+ if (linesUpdated && speedLineInstanceId !== null) finalizeInstances(speedLineInstanceId);
588
+
589
+ // Active sparks
590
+ for (let i = g.particles.length - 1; i >= 0; i--) {
591
+ let p = g.particles[i];
592
+ p.x += p.vx * dt;
593
+ p.y += p.vy * dt;
594
+ p.z += (p.vz + g.speed) * dt;
595
+ p.vy -= 40 * dt; // gravity
596
+ p.life -= dt;
597
+
598
+ let a = Math.max(0.01, p.life / p.maxLife);
599
+ setScale(p.mesh, a, a, a);
600
+ setPosition(p.mesh, p.x, p.y, p.z);
601
+
602
+ if (p.life <= 0) {
603
+ destroyMesh(p.mesh);
604
+ g.particles.splice(i, 1);
605
+ }
606
+ }
607
+ }
608
+
609
+ function updateMenuAnim(dt) {
610
+ // Make ship hover idly on menu
611
+ updateRoad(200 * dt);
612
+ const p = g.p;
613
+ p.y = 1.8 + Math.sin(t * 3) * 0.3;
614
+ p.roll = Math.sin(t * 2) * 0.1;
615
+ p.pitch = Math.sin(t * 1.5) * 0.05;
616
+
617
+ if (gameState !== 'crashed') updatePlayerTransforms();
618
+ }
619
+
620
+ // ── Screen Rendering ───────────────────────────────────────
621
+ export function draw() {
622
+ if (gameState === 'start') {
623
+ drawStartScreen();
624
+ return;
625
+ }
626
+ if (gameState === 'countdown') {
627
+ drawCountdown();
628
+ return;
629
+ }
630
+ if (gameState === 'crashed') {
631
+ drawCrashedScreen();
632
+ return;
633
+ }
634
+ if (gameState === 'finished') {
635
+ drawFinishScreen();
636
+ return;
637
+ }
638
+
639
+ // Playing HUD
640
+ drawHUD();
641
+ }
642
+
643
+ function drawHUD() {
644
+ setFont('normal');
645
+ setTextAlign('left');
646
+
647
+ // Top HUD Bar
648
+ rect(0, 0, 640, 40, rgba8(0, 5, 20, 180), true);
649
+ line(0, 40, 640, 40, rgba8(255, 0, 150, 150));
650
+
651
+ drawTextShadow(`RANK ${g.rank}/10`, 20, 12, rgba8(0, 255, 200, 255), rgba8(0, 0, 0, 255), 2);
652
+
653
+ setFont('large');
654
+ setTextAlign('right');
655
+ const spdTxt = Math.floor(g.speed).toString().padStart(3, '0');
656
+ drawTextShadow(`${spdTxt} km/h`, 620, 10, rgba8(255, 255, 0, 255), rgba8(0, 0, 0, 255), 2);
657
+
658
+ // Progress Bar
659
+ const progPct = Math.min(1, g.dist / 30000);
660
+ rect(200, 16, 240, 8, rgba8(255, 255, 255, 50), true);
661
+ rect(200, 16, 240 * progPct, 8, rgba8(0, 255, 255, 255), true);
662
+
663
+ setFont('small');
664
+ setTextAlign('center');
665
+ drawText('COURSE PROGRESS', 320, 4, rgba8(200, 200, 255, 180));
666
+
667
+ // Bottom HUD elements
668
+ const bY = 320;
669
+
670
+ // Health
671
+ rect(20, bY, 150, 12, rgba8(50, 0, 0, 180), true);
672
+ const hpC = g.health > 40 ? rgba8(0, 255, 0, 255) : rgba8(255, 50, 50, 255);
673
+ rect(20, bY, 150 * (g.health / 100), 12, hpC, true);
674
+ rect(20, bY, 150, 12, rgba8(0, 0, 0, 0), false);
675
+ drawTextShadow('HULL', 20, bY - 14, rgba8(200, 200, 200, 255), rgba8(0, 0, 0, 255), 1);
676
+
677
+ // Boost Energy
678
+ rect(470, bY, 150, 12, rgba8(0, 10, 50, 180), true);
679
+ const engPct = Math.max(0, g.energy / 100);
680
+ rect(470, bY, 150 * engPct, 12, rgba8(0, 200, 255, 255), true);
681
+ if (g.p.isBoosting) {
682
+ rect(
683
+ 470,
684
+ bY,
685
+ 150 * engPct,
686
+ 12,
687
+ rgba8(255, 255, 255, Math.floor(Math.sin(t * 30) * 100 + 100)),
688
+ true
689
+ );
690
+ }
691
+ rect(470, bY, 150, 12, rgba8(0, 0, 0, 0), false);
692
+ drawTextShadow('BOOST POWER', 470, bY - 14, rgba8(200, 200, 200, 255), rgba8(0, 0, 0, 255), 1);
693
+
694
+ if (g.energy > 30 && !g.p.isBoosting && Math.sin(t * 4) > 0) {
695
+ drawText('PRESS SPACE', 545, bY + 16, rgba8(0, 255, 255, 180));
696
+ }
697
+
698
+ // Invuln and Damage Effects
699
+ if (isInvulnerable(playerHit)) {
700
+ rect(0, 0, 640, 360, rgba8(255, 0, 0, Math.floor(Math.sin(t * 40) * 50 + 50)), true);
701
+ }
702
+ if (g.speed > 350) {
703
+ // Hyperspeed blur effect via borders
704
+ const al = Math.min(60, g.speed - 350);
705
+ rect(0, 0, 640, 360, rgba8(0, 255, 255, al), true);
706
+ drawScanlines(10, 1);
707
+ }
708
+ }
709
+
710
+ function drawCountdown() {
711
+ drawHUD();
712
+ const c = Math.ceil(countdownTimer);
713
+ const label = c > 0 ? `${c}` : 'GO!';
714
+ const col = c > 0 ? rgba8(255, 255, 0, 255) : rgba8(0, 255, 100, 255);
715
+ setFont('huge');
716
+ setTextAlign('center');
717
+ drawGlowTextCentered(label, 320, 140, col, rgba8(0, 0, 0, 200), 6);
718
+ }
719
+
720
+ function drawStartScreen() {
721
+ // Vignette gradient
722
+ drawRadialGradient(320, 180, 400, rgba8(10, 0, 60, 50), rgba8(5, 0, 15, 240));
723
+
724
+ const bob = Math.sin(t * 3) * 6;
725
+
726
+ setFont('huge');
727
+ setTextAlign('center');
728
+ drawGlowTextCentered(
729
+ 'F-ZERO',
730
+ 320,
731
+ 60 + bob,
732
+ rgba8(0, 255, 200, 255),
733
+ rgba8(0, 150, 255, 200),
734
+ 4
735
+ );
736
+ drawGlowTextCentered(
737
+ 'NOVA 3D',
738
+ 320,
739
+ 130 + bob,
740
+ rgba8(255, 0, 150, 255),
741
+ rgba8(150, 0, 200, 200),
742
+ 2
743
+ );
744
+
745
+ const panel = createPanel(centerX(300), 190, 300, 100, {
746
+ bgColor: rgba8(10, 0, 30, 220),
747
+ borderColor: rgba8(0, 255, 255, 180),
748
+ borderWidth: 2,
749
+ });
750
+ drawPanel(panel);
751
+
752
+ setFont('small');
753
+ drawText('◆ ARROWS / WASD: Steer', 320, 210, uiColors.light);
754
+ drawText('◆ Q/E: Drift Step', 320, 230, uiColors.light);
755
+ drawText('◆ SPACE: Hyper Boost (Requires Power)', 320, 250, rgba8(0, 255, 255, 255));
756
+ drawText('◆ Avoid Rivals, Hit Green Pads!', 320, 270, rgba8(0, 255, 100, 255));
757
+
758
+ drawAllButtons();
759
+
760
+ const fa = Math.floor(Math.sin(t * 6) * 100 + 155);
761
+ drawTextShadow('PRESS SPACE TO RACE', 320, 330, rgba8(255, 255, 0, fa), rgba8(0, 0, 0, 255), 2);
762
+ drawScanlines(30, 2);
763
+ }
764
+
765
+ function drawCrashedScreen() {
766
+ rect(0, 0, 640, 360, rgba8(80, 0, 0, 120), true);
767
+ drawNoise(0, 0, 640, 360, 30, Math.floor(t * 10)); // Static
768
+
769
+ setFont('huge');
770
+ setTextAlign('center');
771
+ drawTextShadow('MACHINE DESTROYED', 320, 150, rgba8(255, 50, 50, 255), rgba8(0, 0, 0, 255), 4);
772
+
773
+ drawAllButtons();
774
+ drawScanlines(50, 3);
775
+ }
776
+
777
+ function drawFinishScreen() {
778
+ rect(0, 0, 640, 360, rgba8(0, 50, 20, 180), true);
779
+
780
+ setFont('huge');
781
+ setTextAlign('center');
782
+ drawGlowTextCentered(
783
+ 'COURSE CLEARED',
784
+ 320,
785
+ 120,
786
+ rgba8(0, 255, 100, 255),
787
+ rgba8(0, 150, 50, 200),
788
+ 3
789
+ );
790
+
791
+ setFont('large');
792
+ drawTextShadow(
793
+ `FINAL RANK: ${g.rank}`,
794
+ 320,
795
+ 180,
796
+ rgba8(255, 255, 0, 255),
797
+ rgba8(0, 0, 0, 255),
798
+ 2
799
+ );
800
+
801
+ drawAllButtons();
802
+ }
803
+
804
+ function startGame() {
805
+ gameState = 'countdown';
806
+ countdownTimer = 3.0;
807
+ inputLock = 0.3;
808
+ g.health = 100;
809
+ g.energy = 50;
810
+ g.dist = 0;
811
+ g.speed = 0;
812
+ g.rank = 10;
813
+ g.passCount = 0;
814
+
815
+ g.p.x = 0;
816
+ g.p.vx = 0;
817
+ // Restore ship meshes after crash
818
+ Object.values(g.p.meshes).forEach(m => m && setPosition(m, 0, 1.5, 0));
819
+ clearButtons();
820
+ sfx('select');
821
+ }
822
+
823
+ function initStartScreen() {
824
+ clearButtons();
825
+ createButton(centerX(220), 310, 220, 40, '▶ START ENGINE', startGame, {
826
+ normalColor: rgba8(200, 0, 150, 255),
827
+ hoverColor: rgba8(255, 50, 200, 255),
828
+ });
829
+ }
830
+
831
+ function initGameOver() {
832
+ clearButtons();
833
+ createButton(
834
+ centerX(240),
835
+ 220,
836
+ 240,
837
+ 50,
838
+ '↻ RETRY COURSE',
839
+ () => {
840
+ // Soft reset state then go to start menu
841
+ g.p.meshes.body && setPosition(g.p.meshes.body, 0, 1.5, 0); // Put ship back visually
842
+ gameState = 'start';
843
+ inputLock = 0.5;
844
+ initStartScreen();
845
+ },
846
+ { normalColor: rgba8(200, 50, 0, 255) }
847
+ );
848
+ }
849
+
850
+ function initWinScreen() {
851
+ clearButtons();
852
+ createButton(
853
+ centerX(240),
854
+ 240,
855
+ 240,
856
+ 50,
857
+ '↻ PLAY AGAIN',
858
+ () => {
859
+ gameState = 'start';
860
+ inputLock = 0.5;
861
+ initStartScreen();
862
+ },
863
+ { normalColor: rgba8(0, 150, 100, 255) }
864
+ );
865
+ }