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,1116 @@
1
+ // ⭐ STAR FOX NOVA 64 — Space Combat Rail Shooter ⭐
2
+ // Ultimate 3D Edition with Holographic Materials, Barrel Rolls, and Cinematic Lighting
3
+
4
+ // ── State ──────────────────────────────────────────────
5
+ let gameState = 'start'; // 'start' | 'playing' | 'gameover'
6
+ let gameTime = 0;
7
+ let inputLockout = 0.6;
8
+ let playerHit;
9
+ let weaponCD;
10
+
11
+ const C = {
12
+ // Ship (Modern Metallic)
13
+ shipBody: 0xcccccc,
14
+ shipWing: 0x0055ff,
15
+ shipEngine: 0x00ffee,
16
+ shipCockpit: 0x00ffff,
17
+ // Enemies
18
+ drone: 0x222222,
19
+ droneEye: 0xff0044,
20
+ droneWing: 0x990033,
21
+ // Projectiles
22
+ laser: 0x00ffcc,
23
+ enemyShot: 0xff0044,
24
+ // Environment
25
+ asteroid: 0x443333,
26
+ ring: 0xffdd00,
27
+ star: 0xffffff,
28
+ // FX
29
+ explosion: 0xff4411,
30
+ spark: 0xffffaa,
31
+ };
32
+
33
+ let game = {
34
+ // Arwing
35
+ player: {
36
+ x: 0,
37
+ y: 5,
38
+ z: 0,
39
+ vx: 0,
40
+ vy: 0,
41
+ roll: 0,
42
+ barrelRoll: 0,
43
+ isBarrelRolling: false,
44
+ rollSpeed: 0,
45
+ health: 100,
46
+ weaponTimer: 0,
47
+ meshes: {},
48
+ invuln: 0,
49
+ },
50
+ speed: 60,
51
+ distance: 0,
52
+ score: 0,
53
+ wave: 1,
54
+ kills: 0,
55
+
56
+ // World
57
+ gridPlanes: [],
58
+ asteroids: [],
59
+ enemies: [],
60
+ bullets: [],
61
+ enemyBullets: [],
62
+ particles: [],
63
+ rings: [],
64
+ boss: null,
65
+ bossSpawned: false,
66
+ trails: [],
67
+
68
+ enemySpawnTimer: 0,
69
+ ringSpawnTimer: 0,
70
+ };
71
+
72
+ // ── Init ───────────────────────────────────────────────
73
+ export async function init() {
74
+ console.log('🚀 STAR FOX NOVA 64 (Ultimate) — Loading...');
75
+
76
+ // Camera — dynamic lag behind player
77
+ setCameraPosition(0, 12, 22);
78
+ setCameraTarget(0, 4, -30);
79
+ setCameraFOV(75);
80
+
81
+ // Lighting — dramatic space lighting
82
+ setAmbientLight(0x111122, 1.2);
83
+ setLightDirection(-0.5, -1, -0.5);
84
+ setLightColor(0xffffff);
85
+
86
+ // Deep space fog
87
+ setFog(0x020308, 50, 300);
88
+
89
+ if (typeof createSpaceSkybox === 'function') {
90
+ createSpaceSkybox({ starCount: 3000, starSize: 2.5, nebulae: true, nebulaColor: 0x1a0044 });
91
+ }
92
+
93
+ // Post-processing — cinematic space feel
94
+ enableBloom(1.2, 0.4, 0.4); // Intense bloom for lasers/engines
95
+ if (typeof enableFXAA === 'function') enableFXAA();
96
+ if (typeof enableVignette === 'function') enableVignette(1.2, 0.92);
97
+
98
+ // Build world
99
+ createGridFloor();
100
+ createArwing();
101
+ playerHit = createHitState({ invulnDuration: 1.0, blinkRate: 30 });
102
+ weaponCD = createCooldown(0.1);
103
+ for (let i = 0; i < 25; i++) spawnAsteroid(true);
104
+
105
+ // Start screen
106
+ initStartScreen();
107
+ console.log('✅ STAR FOX NOVA 64 — Ready!');
108
+ }
109
+
110
+ // GPU instancing for grid floor tiles
111
+ let gridInstanceA = null; // dark tiles
112
+ let gridInstanceB = null; // lighter tiles
113
+
114
+ // ── World Building ─────────────────────────────────────
115
+ function createGridFloor() {
116
+ const cols = 20,
117
+ rows = 35,
118
+ size = 5;
119
+ const startX = -(cols * size) / 2;
120
+
121
+ // 700 planes → 2 instanced meshes (350 each)
122
+ const halfCount = (cols * rows) / 2;
123
+ gridInstanceA = createInstancedMesh('plane', halfCount, 0x050a14, {
124
+ width: size,
125
+ height: size,
126
+ material: 'standard',
127
+ roughness: 0.8,
128
+ });
129
+ gridInstanceB = createInstancedMesh('plane', halfCount, 0x081020, {
130
+ width: size,
131
+ height: size,
132
+ material: 'standard',
133
+ roughness: 0.8,
134
+ });
135
+
136
+ let idxA = 0,
137
+ idxB = 0;
138
+ for (let r = 0; r < rows; r++) {
139
+ for (let c = 0; c < cols; c++) {
140
+ const alt = (r + c) % 2 === 0;
141
+ const x = startX + c * size + size / 2;
142
+ const z = 15 - r * size - size / 2;
143
+ if (alt) {
144
+ setInstanceTransform(gridInstanceA, idxA, x, -4, z, -Math.PI / 2, 0, 0);
145
+ game.gridPlanes.push({ instanceId: gridInstanceA, index: idxA, x, z });
146
+ idxA++;
147
+ } else {
148
+ setInstanceTransform(gridInstanceB, idxB, x, -4, z, -Math.PI / 2, 0, 0);
149
+ game.gridPlanes.push({ instanceId: gridInstanceB, index: idxB, x, z });
150
+ idxB++;
151
+ }
152
+ }
153
+ }
154
+ finalizeInstances(gridInstanceA);
155
+ finalizeInstances(gridInstanceB);
156
+ }
157
+
158
+ function createArwing() {
159
+ const p = game.player;
160
+ const px = p.x,
161
+ py = p.y,
162
+ pz = p.z;
163
+
164
+ // Fuselage (Metallic)
165
+ p.meshes.body = createCube(1.8, C.shipBody, [px, py, pz], {
166
+ material: 'metallic',
167
+ metalness: 0.9,
168
+ roughness: 0.2,
169
+ });
170
+ setScale(p.meshes.body, 0.8, 0.5, 2.5);
171
+
172
+ // Cockpit canopy (Glass/Holographic)
173
+ p.meshes.cockpit = createSphere(0.5, C.shipCockpit, [px, py + 0.4, pz + 0.8], 12, {
174
+ material: 'holographic',
175
+ transparent: true,
176
+ opacity: 0.8,
177
+ emissive: 0x0044aa,
178
+ });
179
+ setScale(p.meshes.cockpit, 0.8, 0.6, 1.2);
180
+
181
+ // Wings (Metallic Blue)
182
+ p.meshes.wingL = createCube(1, C.shipWing, [px - 2.2, py - 0.1, pz - 0.3], {
183
+ material: 'metallic',
184
+ metalness: 0.8,
185
+ });
186
+ setScale(p.meshes.wingL, 2.8, 0.1, 1.8);
187
+
188
+ p.meshes.wingR = createCube(1, C.shipWing, [px + 2.2, py - 0.1, pz - 0.3], {
189
+ material: 'metallic',
190
+ metalness: 0.8,
191
+ });
192
+ setScale(p.meshes.wingR, 2.8, 0.1, 1.8);
193
+
194
+ // Engine glow pods (Emissive)
195
+ p.meshes.engineL = createCube(0.4, C.shipEngine, [px - 1.2, py - 0.15, pz - 2.0], {
196
+ material: 'emissive',
197
+ emissive: C.shipEngine,
198
+ });
199
+ setScale(p.meshes.engineL, 0.8, 0.6, 1.0);
200
+
201
+ p.meshes.engineR = createCube(0.4, C.shipEngine, [px + 1.2, py - 0.15, pz - 2.0], {
202
+ material: 'emissive',
203
+ emissive: C.shipEngine,
204
+ });
205
+ setScale(p.meshes.engineR, 0.8, 0.6, 1.0);
206
+
207
+ // Tail fin
208
+ p.meshes.tail = createCube(0.3, C.shipWing, [px, py + 0.8, pz - 1.8], { material: 'metallic' });
209
+ setScale(p.meshes.tail, 0.15, 1.4, 1.2);
210
+ }
211
+
212
+ function spawnAsteroid(randomZ = false) {
213
+ const side = Math.random() > 0.5 ? 1 : -1;
214
+ const x = side * (5 + Math.random() * 35);
215
+ const y = -2 + Math.random() * 15;
216
+ const z = randomZ ? 10 - Math.random() * 250 : -250 - Math.random() * 50;
217
+ const sz = 1.5 + Math.random() * 4;
218
+
219
+ const mesh = createSphere(sz, C.asteroid, [x, y, z], 6, { material: 'standard', roughness: 0.9 });
220
+ game.asteroids.push({
221
+ mesh,
222
+ x,
223
+ y,
224
+ z,
225
+ sz,
226
+ rotSpeed: (Math.random() - 0.5) * 2.5,
227
+ rotAxisX: Math.random(),
228
+ rotAxisY: Math.random(),
229
+ });
230
+ }
231
+
232
+ function spawnEnemy() {
233
+ const x = (Math.random() - 0.5) * 40;
234
+ const y = 4 + Math.random() * 14;
235
+ const z = -200 - Math.random() * 40;
236
+
237
+ const core = createSphere(1.8, C.drone, [x, y, z], 8, { material: 'metallic', metalness: 0.8 });
238
+ const eye = createSphere(0.8, C.droneEye, [x, y, z + 1.5], 8, {
239
+ material: 'emissive',
240
+ emissive: C.droneEye,
241
+ });
242
+ const wL = createCube(1, C.droneWing, [x - 2.5, y, z], { material: 'metallic' });
243
+ setScale(wL, 2.5, 0.15, 0.8);
244
+ const wR = createCube(1, C.droneWing, [x + 2.5, y, z], { material: 'metallic' });
245
+ setScale(wR, 2.5, 0.15, 0.8);
246
+
247
+ game.enemies.push({
248
+ parts: [
249
+ { mesh: core, ox: 0, oy: 0, oz: 0 },
250
+ { mesh: eye, ox: 0, oy: 0, oz: 1.5 },
251
+ { mesh: wL, ox: -2.5, oy: 0, oz: 0 },
252
+ { mesh: wR, ox: 2.5, oy: 0, oz: 0 },
253
+ ],
254
+ x,
255
+ y,
256
+ z,
257
+ health: 30,
258
+ vx: (Math.random() - 0.5) * 18,
259
+ vy: (Math.random() - 0.5) * 10,
260
+ vz: 40 + Math.random() * 30,
261
+ timer: 0,
262
+ });
263
+ }
264
+
265
+ function spawnBoss() {
266
+ const x = 0;
267
+ const y = 8;
268
+ const z = -150;
269
+ const core = createCube(6, 0xff0000, [x, y, z], {
270
+ material: 'metallic',
271
+ metalness: 0.9,
272
+ roughness: 0.1,
273
+ });
274
+ const eyeL = createSphere(1.5, 0xffffff, [x - 2, y + 1, z + 3], 8, {
275
+ material: 'emissive',
276
+ emissive: 0xffffff,
277
+ intensity: 2,
278
+ });
279
+ const eyeR = createSphere(1.5, 0xffffff, [x + 2, y + 1, z + 3], 8, {
280
+ material: 'emissive',
281
+ emissive: 0xffffff,
282
+ intensity: 2,
283
+ });
284
+ const mouth = createCube(3, 0x000000, [x, y - 2, z + 2.5], { material: 'standard' });
285
+ setScale(mouth, 1, 0.2, 1);
286
+
287
+ game.boss = {
288
+ parts: [
289
+ { mesh: core, ox: 0, oy: 0, oz: 0 },
290
+ { mesh: eyeL, ox: -2, oy: 1, oz: 3 },
291
+ { mesh: eyeR, ox: 2, oy: 1, oz: 3 },
292
+ { mesh: mouth, ox: 0, oy: -2, oz: 2.5 },
293
+ ],
294
+ x,
295
+ y,
296
+ z,
297
+ hp: 50,
298
+ maxHp: 50,
299
+ vx: 0,
300
+ vy: 0,
301
+ vz: 10,
302
+ timer: 0,
303
+ };
304
+ game.bossSpawned = true;
305
+ }
306
+
307
+ function updateBoss(dt) {
308
+ if (!game.boss) return;
309
+ const b = game.boss;
310
+ b.timer += dt;
311
+
312
+ if (b.z < -40) b.z += b.vz * dt;
313
+ else {
314
+ b.x = Math.sin(b.timer * 0.5) * 15;
315
+ b.y = 8 + Math.cos(b.timer * 0.7) * 5;
316
+
317
+ if (b.timer > 2 && Math.random() < 0.05) {
318
+ fireEnemyShot(b.x - 2, b.y + 1, b.z + 3);
319
+ fireEnemyShot(b.x + 2, b.y + 1, b.z + 3);
320
+ }
321
+ }
322
+
323
+ b.parts.forEach(part => {
324
+ setPosition(part.mesh, b.x + part.ox, b.y + part.oy, b.z + part.oz);
325
+ });
326
+ }
327
+
328
+ function spawnRing() {
329
+ const x = (Math.random() - 0.5) * 30;
330
+ const y = 3 + Math.random() * 12;
331
+ const z = -200 - Math.random() * 20;
332
+
333
+ // Emissive glowing ring
334
+ const mesh =
335
+ typeof createTorus === 'function'
336
+ ? createTorus(x, y, z, 2.0, 0.35, C.ring)
337
+ : createSphere(2.0, C.ring, [x, y, z], 8, { material: 'emissive', emissive: 0xffaa00 });
338
+
339
+ if (typeof createTorus !== 'function') setScale(mesh, 1.0, 1.0, 0.1);
340
+ game.rings.push({ mesh, x, y, z, collected: false });
341
+ }
342
+
343
+ // ── Shooting ───────────────────────────────────────────
344
+ function fireLaser() {
345
+ const p = game.player;
346
+ // Twin lasers from wing tips
347
+ for (const offX of [-2.0, 2.0]) {
348
+ // Apply roll to offsets
349
+ const r = p.roll + p.barrelRoll;
350
+ const cosR = Math.cos(r);
351
+ const sinR = Math.sin(r);
352
+
353
+ // Rotate laser origin around ship center based on roll
354
+ const actualOffX = offX * cosR;
355
+ const actualOffY = offX * sinR;
356
+
357
+ const bx = p.x + actualOffX,
358
+ by = p.y - actualOffY,
359
+ bz = p.z - 2;
360
+ const mesh = createCube(0.6, C.laser, [bx, by, bz], {
361
+ material: 'emissive',
362
+ emissive: C.laser,
363
+ });
364
+ setScale(mesh, 0.3, 0.3, 5.0);
365
+ game.bullets.push({ mesh, x: bx, y: by, z: bz, vz: -240, life: 2.0 });
366
+ }
367
+ sfx('laser');
368
+ }
369
+
370
+ function fireEnemyShot(ex, ey, ez) {
371
+ const p = game.player;
372
+ const dx = p.x - ex,
373
+ dy = p.y - ey,
374
+ dz = p.z - ez;
375
+ const d = Math.sqrt(dx * dx + dy * dy + dz * dz) || 1;
376
+ const spd = 75;
377
+
378
+ const mesh = createSphere(0.8, C.enemyShot, [ex, ey, ez], 6, {
379
+ material: 'emissive',
380
+ emissive: C.enemyShot,
381
+ });
382
+ setScale(mesh, 0.6, 0.6, 2.5);
383
+ // Optional: align projectile to velocity vector
384
+
385
+ game.enemyBullets.push({
386
+ mesh,
387
+ x: ex,
388
+ y: ey,
389
+ z: ez,
390
+ vx: (dx / d) * spd,
391
+ vy: (dy / d) * spd,
392
+ vz: (dz / d) * spd,
393
+ life: 3.5,
394
+ });
395
+ }
396
+
397
+ function createExplosion(x, y, z, color, count = 16) {
398
+ for (let i = 0; i < count; i++) {
399
+ const mesh = createCube(0.6, color, [x, y, z], { material: 'emissive', emissive: color });
400
+ const spd = 15 + Math.random() * 25;
401
+ const a1 = Math.random() * Math.PI * 2;
402
+ const a2 = Math.random() * Math.PI * 2;
403
+ game.particles.push({
404
+ mesh,
405
+ x,
406
+ y,
407
+ z,
408
+ vx: Math.cos(a1) * Math.sin(a2) * spd,
409
+ vy: Math.sin(a1) * spd * 0.8,
410
+ vz: Math.cos(a1) * Math.cos(a2) * spd,
411
+ life: 0.5 + Math.random() * 0.6,
412
+ maxLife: 1.1,
413
+ isTrail: false,
414
+ });
415
+ }
416
+ }
417
+
418
+ function spawnTrail() {
419
+ const p = game.player;
420
+ for (const offX of [-1.2, 1.2]) {
421
+ const r = p.roll + p.barrelRoll;
422
+ const actualOffX = offX * Math.cos(r) - -0.15 * Math.sin(r);
423
+ const actualOffY = -(offX * Math.sin(r) + -0.15 * Math.cos(r));
424
+
425
+ let x = p.x + actualOffX;
426
+ let y = p.y + actualOffY;
427
+ let z = p.z + 2.0;
428
+
429
+ const mesh = createCube(0.5, C.shipEngine, [x, y, z], {
430
+ material: 'emissive',
431
+ emissive: C.shipEngine,
432
+ });
433
+ game.particles.push({
434
+ mesh,
435
+ x,
436
+ y,
437
+ z,
438
+ vx: (Math.random() - 0.5) * 2,
439
+ vy: (Math.random() - 0.5) * 2,
440
+ vz: game.speed * 0.8,
441
+ life: 0.2 + Math.random() * 0.1,
442
+ maxLife: 0.3,
443
+ isTrail: true,
444
+ });
445
+ }
446
+ }
447
+
448
+ // ── Update ─────────────────────────────────────────────
449
+ export function update(dt) {
450
+ gameTime += dt;
451
+
452
+ if (gameState === 'start' || gameState === 'gameover') {
453
+ if (inputLockout > 0) inputLockout -= dt;
454
+ updateAllButtons();
455
+ updateGrid(dt * 0.3);
456
+ updateArwingIdle(dt);
457
+ if (inputLockout <= 0 && isKeyPressed('Space')) startGame();
458
+ return;
459
+ }
460
+
461
+ // Playing
462
+ game.distance += game.speed * dt;
463
+ game.score += dt * 25;
464
+ game.speed = Math.min(110, 60 + game.score * 0.0015);
465
+
466
+ // Increase difficulty
467
+ if (game.kills > 0 && game.kills % 15 === 0 && game.wave < 10) {
468
+ game.wave = Math.floor(game.kills / 15) + 1;
469
+ }
470
+
471
+ updateArwing(dt);
472
+ updateGrid(dt);
473
+ updateAsteroids(dt);
474
+ updateEnemies(dt);
475
+ updateBullets(dt);
476
+ updateEnemyBullets(dt);
477
+ updateParticles(dt);
478
+ updateRings(dt);
479
+
480
+ // Dynamic camera follow
481
+ const camTag = game.player;
482
+ const cx = camTag.x * 0.3;
483
+ const cy = camTag.y * 0.5 + 8;
484
+ setCameraPosition(cx, cy, 22);
485
+ setCameraTarget(cx * 0.5, camTag.y * 0.8, -30);
486
+ }
487
+
488
+ function startGame() {
489
+ if (gameState === 'playing') return;
490
+ gameState = 'playing';
491
+ inputLockout = 0.3;
492
+
493
+ game.score = 0;
494
+ game.kills = 0;
495
+ game.wave = 1;
496
+ game.speed = 60;
497
+ game.player.health = 100;
498
+ playerHit.invulnTimer = 0;
499
+ weaponCD.remaining = 0;
500
+ game.enemySpawnTimer = 0;
501
+ game.ringSpawnTimer = 0;
502
+ game.bossSpawned = false;
503
+ if (game.boss) {
504
+ game.boss.parts.forEach(p => destroyMesh(p.mesh));
505
+ game.boss = null;
506
+ }
507
+
508
+ // Clean up old objects
509
+ game.enemies.forEach(e => e.parts.forEach(p => destroyMesh(p.mesh)));
510
+ game.bullets.forEach(b => destroyMesh(b.mesh));
511
+ game.enemyBullets.forEach(b => destroyMesh(b.mesh));
512
+ game.particles.forEach(p => destroyMesh(p.mesh));
513
+ game.rings.forEach(r => destroyMesh(r.mesh));
514
+ game.enemies = [];
515
+ game.bullets = [];
516
+ game.enemyBullets = [];
517
+ game.particles = [];
518
+ game.rings = [];
519
+
520
+ clearButtons();
521
+ }
522
+
523
+ // ── Arwing Movement ────────────────────────────────────
524
+ function updateArwing(dt) {
525
+ const p = game.player;
526
+
527
+ if (p.health <= 0) {
528
+ createExplosion(p.x, p.y, p.z, C.explosion, 30);
529
+ createExplosion(p.x, p.y, p.z, 0xffffff, 10);
530
+ // Hide all ship parts
531
+ Object.values(game.player.meshes).forEach(m => m && setPosition(m, 1000, 0, 0));
532
+ gameState = 'gameover';
533
+ inputLockout = 1.0;
534
+ sfx('death');
535
+ initGameOverScreen();
536
+ return;
537
+ }
538
+
539
+ updateHitState(playerHit, dt);
540
+
541
+ // Barrel Roll Logic (Press Q/E or trigger dynamically)
542
+ if (!p.isBarrelRolling && (isKeyPressed('KeyQ') || isKeyPressed('KeyE') || btnp(1) || btnp(2))) {
543
+ p.isBarrelRolling = true;
544
+ p.rollSpeed = isKeyPressed('KeyE') ? -Math.PI * 6 : Math.PI * 6; // Fast spin
545
+ playerHit.invulnTimer = Math.max(playerHit.invulnTimer, 0.5); // Invincible during roll!
546
+ sfx('jump');
547
+ }
548
+
549
+ if (p.isBarrelRolling) {
550
+ p.barrelRoll += p.rollSpeed * dt;
551
+ if (Math.abs(p.barrelRoll) >= Math.PI * 2) {
552
+ p.barrelRoll = 0;
553
+ p.isBarrelRolling = false;
554
+ }
555
+ }
556
+
557
+ // Input — smooth velocity-based movement
558
+ let ix = 0,
559
+ iy = 0;
560
+ if (key('ArrowLeft') || key('KeyA') || btn(14) || btn(12)) ix = -1;
561
+ if (key('ArrowRight') || key('KeyD') || btn(15) || btn(13)) ix = 1;
562
+ if (key('ArrowUp') || key('KeyW') || btn(12) || btn(14)) iy = 1;
563
+ if (key('ArrowDown') || key('KeyS') || btn(13) || btn(15)) iy = -1;
564
+
565
+ const accel = 140,
566
+ friction = 5.0;
567
+ p.vx += ix * accel * dt;
568
+ p.vy += iy * accel * dt;
569
+ p.vx *= 1 - friction * dt;
570
+ p.vy *= 1 - friction * dt;
571
+
572
+ // Apply movement
573
+ p.x += p.vx * dt;
574
+ p.y += p.vy * dt;
575
+
576
+ // Clamp to play area
577
+ if (p.x < -24) {
578
+ p.x = -24;
579
+ p.vx = 0;
580
+ }
581
+ if (p.x > 24) {
582
+ p.x = 24;
583
+ p.vx = 0;
584
+ }
585
+ if (p.y < 1) {
586
+ p.y = 1;
587
+ p.vy = 0;
588
+ }
589
+ if (p.y > 20) {
590
+ p.y = 20;
591
+ p.vy = 0;
592
+ }
593
+
594
+ // Rolling tilt from lateral movement
595
+ const targetRoll = -p.vx * 0.05;
596
+ p.roll += (targetRoll - p.roll) * 8 * dt;
597
+
598
+ // Position all ship meshes
599
+ positionArwing(p, p.roll + p.barrelRoll);
600
+
601
+ // Engine trails
602
+ if (Math.random() < 0.6) spawnTrail();
603
+
604
+ // Shooting
605
+ updateCooldown(weaponCD, dt);
606
+ if ((key('Space') || btn(0)) && useCooldown(weaponCD)) {
607
+ fireLaser();
608
+ }
609
+ }
610
+
611
+ function updateArwingIdle(dt) {
612
+ const p = game.player;
613
+ const bob = Math.sin(gameTime * 2.0) * 0.8;
614
+ const sway = Math.sin(gameTime * 1.2) * 0.5;
615
+ const idleRoll = Math.sin(gameTime * 1.5) * 0.08;
616
+
617
+ p.x = sway;
618
+ p.y = 6 + bob;
619
+ positionArwing(p, idleRoll);
620
+ }
621
+
622
+ function positionArwing(p, totalRoll) {
623
+ const m = p.meshes;
624
+ const rx = 0;
625
+ const rz = totalRoll;
626
+
627
+ // Helper to rotate local offsets around Z
628
+ const applyRot = (ox, oy, oz) => {
629
+ const cosR = Math.cos(rz),
630
+ sinR = Math.sin(rz);
631
+ return [p.x + ox * cosR - oy * sinR, p.y + ox * sinR + oy * cosR, p.z + oz];
632
+ };
633
+
634
+ if (!m.body) return; // Skip if dead/hidden
635
+
636
+ setPosition(m.body, ...applyRot(0, 0, 0));
637
+ setRotation(m.body, 0, 0, rz);
638
+
639
+ setPosition(m.cockpit, ...applyRot(0, 0.4, 0.8));
640
+ setRotation(m.cockpit, 0, 0, rz);
641
+
642
+ setPosition(m.wingL, ...applyRot(-2.2, -0.1, -0.3));
643
+ setRotation(m.wingL, 0, 0, rz);
644
+
645
+ setPosition(m.wingR, ...applyRot(2.2, -0.1, -0.3));
646
+ setRotation(m.wingR, 0, 0, rz);
647
+
648
+ setPosition(m.engineL, ...applyRot(-1.2, -0.15, -2.0));
649
+ setRotation(m.engineL, 0, 0, rz);
650
+
651
+ setPosition(m.engineR, ...applyRot(1.2, -0.15, -2.0));
652
+ setRotation(m.engineR, 0, 0, rz);
653
+
654
+ setPosition(m.tail, ...applyRot(0, 0.8, -1.8));
655
+ setRotation(m.tail, 0, 0, rz);
656
+ }
657
+
658
+ // ── World Updates ──────────────────────────────────────
659
+ function updateGrid(dt) {
660
+ const total = 35 * 5;
661
+ game.gridPlanes.forEach(g => {
662
+ g.z += game.speed * dt;
663
+ if (g.z > 15) g.z -= total;
664
+ setInstanceTransform(g.instanceId, g.index, g.x, -4, g.z, -Math.PI / 2, 0, 0);
665
+ });
666
+ if (gridInstanceA) finalizeInstances(gridInstanceA);
667
+ if (gridInstanceB) finalizeInstances(gridInstanceB);
668
+ }
669
+
670
+ function updateAsteroids(dt) {
671
+ for (let i = game.asteroids.length - 1; i >= 0; i--) {
672
+ const a = game.asteroids[i];
673
+ a.z += game.speed * dt;
674
+ rotateMesh(a.mesh, a.rotSpeed * dt * a.rotAxisX, a.rotSpeed * dt * a.rotAxisY, 0);
675
+ setPosition(a.mesh, a.x, a.y, a.z);
676
+
677
+ if (a.z > 25) {
678
+ destroyMesh(a.mesh);
679
+ game.asteroids.splice(i, 1);
680
+ spawnAsteroid(false);
681
+ }
682
+ }
683
+ }
684
+
685
+ function updateEnemies(dt) {
686
+ if (game.wave >= 3 && !game.bossSpawned && game.enemies.length === 0) spawnBoss();
687
+ if (game.boss) updateBoss(dt);
688
+ game.enemySpawnTimer -= dt;
689
+ if (game.enemySpawnTimer <= 0 && game.enemies.length < 10) {
690
+ spawnEnemy();
691
+ game.enemySpawnTimer = Math.max(0.6, 2.5 - game.wave * 0.2);
692
+ }
693
+
694
+ for (let i = game.enemies.length - 1; i >= 0; i--) {
695
+ const e = game.enemies[i];
696
+ e.timer += dt;
697
+
698
+ e.x += e.vx * dt;
699
+ e.y += e.vy * dt;
700
+ e.z += e.vz * dt;
701
+
702
+ // Bounce off edges with roll effect
703
+ if (e.x < -36 || e.x > 36) e.vx *= -1;
704
+ if (e.y < 2 || e.y > 22) e.vy *= -1;
705
+
706
+ const bob = Math.sin(e.timer * 4) * 1.5;
707
+ const bank = -e.vx * 0.05;
708
+
709
+ e.parts.forEach((part, idx) => {
710
+ // Basic rotation application for drone parts
711
+ const ox = part.ox,
712
+ oy = part.oy;
713
+ const rx = e.x + ox * Math.cos(bank) - oy * Math.sin(bank);
714
+ const ry = e.y + ox * Math.sin(bank) + oy * Math.cos(bank) + bob;
715
+ setPosition(part.mesh, rx, ry, e.z + part.oz);
716
+ setRotation(part.mesh, 0, 0, bank);
717
+ });
718
+
719
+ // Predict and shoot
720
+ if (e.timer > 1.0 && Math.random() < 0.02) {
721
+ fireEnemyShot(e.x, e.y + bob, e.z);
722
+ }
723
+
724
+ if (e.z > 30) {
725
+ e.parts.forEach(part => destroyMesh(part.mesh));
726
+ game.enemies.splice(i, 1);
727
+ }
728
+ }
729
+ }
730
+
731
+ function updateBullets(dt) {
732
+ for (let i = game.bullets.length - 1; i >= 0; i--) {
733
+ const b = game.bullets[i];
734
+ b.z += b.vz * dt;
735
+ b.life -= dt;
736
+ setPosition(b.mesh, b.x, b.y, b.z);
737
+
738
+ if (b.life <= 0 || b.z < -250) {
739
+ destroyMesh(b.mesh);
740
+ game.bullets.splice(i, 1);
741
+ continue;
742
+ }
743
+
744
+ let hit = false;
745
+
746
+ // Boss Collision
747
+ if (game.boss) {
748
+ const boss = game.boss;
749
+ const dist = Math.hypot(b.x - boss.x, b.z - boss.z);
750
+ if (dist < 5 && Math.abs(b.y - boss.y) < 5) {
751
+ boss.hp -= 10;
752
+ hit = true;
753
+ createExplosion(b.x, b.y, b.z, C.spark, 3);
754
+ sfx('hit');
755
+ if (boss.hp <= 0) {
756
+ game.score += 5000;
757
+ createExplosion(boss.x, boss.y, boss.z, 0xffffff, 50);
758
+ boss.parts.forEach(p => destroyMesh(p.mesh));
759
+ game.boss = null;
760
+ game.wave++;
761
+ sfx('explosion');
762
+ }
763
+ }
764
+ }
765
+
766
+ for (let j = game.enemies.length - 1; j >= 0; j--) {
767
+ const e = game.enemies[j];
768
+ if (Math.abs(b.x - e.x) < 3.5 && Math.abs(b.y - e.y) < 3.0 && Math.abs(b.z - e.z) < 4.5) {
769
+ e.health -= 20; // stronger lasers
770
+ hit = true;
771
+ if (e.health <= 0) {
772
+ createExplosion(e.x, e.y, e.z, C.explosion);
773
+ createExplosion(e.x, e.y, e.z, 0xffbb00, 8); // secondary burst
774
+ game.score += 500;
775
+ game.kills++;
776
+ sfx('explosion');
777
+ e.parts.forEach(part => destroyMesh(part.mesh));
778
+ game.enemies.splice(j, 1);
779
+ } else {
780
+ createExplosion(b.x, b.y, b.z, C.spark, 4); // hit spark
781
+ sfx('hit');
782
+ }
783
+ break;
784
+ }
785
+ }
786
+
787
+ if (!hit) {
788
+ for (let j = game.asteroids.length - 1; j >= 0; j--) {
789
+ const a = game.asteroids[j];
790
+ const hitDist = a.sz + 1.5;
791
+ if (
792
+ Math.abs(b.x - a.x) < hitDist &&
793
+ Math.abs(b.y - a.y) < hitDist &&
794
+ Math.abs(b.z - a.z) < hitDist
795
+ ) {
796
+ createExplosion(b.x, b.y, b.z, C.spark, 5);
797
+ if (a.sz < 2.5) {
798
+ // Break medium/small ones
799
+ createExplosion(a.x, a.y, a.z, C.asteroid, 8);
800
+ game.score += 150;
801
+ destroyMesh(a.mesh);
802
+ game.asteroids.splice(j, 1);
803
+ spawnAsteroid(false);
804
+ }
805
+ hit = true;
806
+ break;
807
+ }
808
+ }
809
+ }
810
+
811
+ if (hit) {
812
+ destroyMesh(b.mesh);
813
+ game.bullets.splice(i, 1);
814
+ }
815
+ }
816
+ }
817
+
818
+ function updateEnemyBullets(dt) {
819
+ const p = game.player;
820
+ for (let i = game.enemyBullets.length - 1; i >= 0; i--) {
821
+ const b = game.enemyBullets[i];
822
+ b.x += b.vx * dt;
823
+ b.y += b.vy * dt;
824
+ b.z += b.vz * dt;
825
+ b.life -= dt;
826
+
827
+ // Rotate projectile based on velocity
828
+ const yaw = Math.atan2(b.vx, b.vz);
829
+ const pitch = Math.atan2(-b.vy, Math.sqrt(b.vx * b.vx + b.vz * b.vz));
830
+ setRotation(b.mesh, pitch, yaw, 0);
831
+ setPosition(b.mesh, b.x, b.y, b.z);
832
+
833
+ if (
834
+ isInvulnerable(playerHit) === false &&
835
+ Math.abs(b.x - p.x) < 2.0 &&
836
+ Math.abs(b.y - p.y) < 1.8 &&
837
+ Math.abs(b.z - p.z) < 2.5
838
+ ) {
839
+ p.health -= 25;
840
+ triggerHit(playerHit);
841
+ sfx('hit');
842
+ createExplosion(p.x, p.y, p.z, C.explosion, 8);
843
+ destroyMesh(b.mesh);
844
+ game.enemyBullets.splice(i, 1);
845
+ continue;
846
+ }
847
+
848
+ if (b.life <= 0 || b.z > 30) {
849
+ destroyMesh(b.mesh);
850
+ game.enemyBullets.splice(i, 1);
851
+ }
852
+ }
853
+ }
854
+
855
+ function updateParticles(dt) {
856
+ for (let i = game.particles.length - 1; i >= 0; i--) {
857
+ const p = game.particles[i];
858
+ p.x += p.vx * dt;
859
+ p.y += p.vy * dt;
860
+ p.z += p.vz * dt;
861
+ p.life -= dt;
862
+ setPosition(p.mesh, p.x, p.y, p.z);
863
+
864
+ if (p.isTrail) {
865
+ const s = Math.max(0.01, p.life / p.maxLife) * 0.5;
866
+ setScale(p.mesh, s, s, s * 2);
867
+ } else {
868
+ const s = Math.max(0.01, p.life / p.maxLife);
869
+ setScale(p.mesh, s, s, s);
870
+ }
871
+
872
+ if (p.life <= 0) {
873
+ destroyMesh(p.mesh);
874
+ game.particles.splice(i, 1);
875
+ }
876
+ }
877
+ }
878
+
879
+ function updateRings(dt) {
880
+ game.ringSpawnTimer -= dt;
881
+ if (game.ringSpawnTimer <= 0 && game.rings.length < 4) {
882
+ spawnRing();
883
+ game.ringSpawnTimer = 3 + Math.random() * 3;
884
+ }
885
+
886
+ const p = game.player;
887
+ for (let i = game.rings.length - 1; i >= 0; i--) {
888
+ const r = game.rings[i];
889
+ r.z += game.speed * dt;
890
+
891
+ rotateMesh(r.mesh, 0, gameTime * 3, 0); // Active spinning
892
+ setPosition(r.mesh, r.x, r.y, r.z);
893
+
894
+ if (Math.abs(r.x - p.x) < 3.5 && Math.abs(r.y - p.y) < 3.5 && Math.abs(r.z - p.z) < 3.5) {
895
+ game.score += 1000;
896
+ game.player.health = Math.min(100, game.player.health + 10); // Heal
897
+ createExplosion(r.x, r.y, r.z, C.ring, 12);
898
+ sfx('coin');
899
+ destroyMesh(r.mesh);
900
+ game.rings.splice(i, 1);
901
+ continue;
902
+ }
903
+
904
+ if (r.z > 30) {
905
+ destroyMesh(r.mesh);
906
+ game.rings.splice(i, 1);
907
+ }
908
+ }
909
+ }
910
+
911
+ // ── Screens ────────────────────────────────────────────
912
+ function initStartScreen() {
913
+ clearButtons();
914
+ createButton(
915
+ centerX(260),
916
+ 250,
917
+ 260,
918
+ 52,
919
+ '▶ LAUNCH ARWING',
920
+ () => {
921
+ startGame();
922
+ },
923
+ {
924
+ normalColor: rgba8(0, 180, 255, 255),
925
+ hoverColor: rgba8(60, 220, 255, 255),
926
+ pressedColor: rgba8(0, 120, 200, 255),
927
+ }
928
+ );
929
+ }
930
+
931
+ function initGameOverScreen() {
932
+ clearButtons();
933
+ createButton(
934
+ centerX(220),
935
+ 265,
936
+ 220,
937
+ 50,
938
+ '↻ MISSION RETRY',
939
+ () => {
940
+ gameState = 'start';
941
+ inputLockout = 0.6;
942
+ initStartScreen();
943
+ },
944
+ {
945
+ normalColor: rgba8(220, 50, 50, 255),
946
+ hoverColor: rgba8(255, 80, 80, 255),
947
+ pressedColor: rgba8(180, 30, 30, 255),
948
+ }
949
+ );
950
+ }
951
+
952
+ // ── Draw ───────────────────────────────────────────────
953
+ export function draw() {
954
+ if (gameState === 'start') {
955
+ drawStartScreen();
956
+ return;
957
+ }
958
+
959
+ if (gameState === 'gameover') {
960
+ drawGameOverScreen();
961
+ return;
962
+ }
963
+
964
+ drawHUD();
965
+ }
966
+
967
+ function drawStartScreen() {
968
+ cls(rgba8(2, 4, 16, 255));
969
+ drawGradient(0, 0, 640, 360, rgba8(2, 6, 20, 255), rgba8(1, 2, 8, 255), 'v');
970
+
971
+ // Intense Nebula glow
972
+ drawRadialGradient(320, 90, 280, rgba8(0, 100, 255, 50), rgba8(0, 0, 0, 0));
973
+ drawRadialGradient(320, 90, 140, rgba8(0, 220, 255, 40), rgba8(0, 0, 0, 0));
974
+
975
+ drawNoise(0, 0, 640, 360, 12, Math.floor(gameTime * 5));
976
+
977
+ const sp = Math.sin(gameTime * 3.5) * 0.5 + 0.5;
978
+ drawStarburst(40, 35, 16, 6, 8, rgba8(0, 200, 255, Math.floor(sp * 200)), true);
979
+ drawStarburst(600, 35, 16, 6, 8, rgba8(0, 200, 255, Math.floor(sp * 200)), true);
980
+
981
+ drawWave(0, 180, 640, 6, 0.03, gameTime * 3.2, rgba8(0, 150, 255, 100), 2);
982
+ drawWave(0, 184, 640, 4, 0.045, gameTime * 3.8 + 1, rgba8(0, 255, 200, 70), 1);
983
+
984
+ const bob = Math.sin(gameTime * 2.0) * 8;
985
+ drawGlowTextCentered(
986
+ 'STAR FOX',
987
+ 320,
988
+ 46 + bob,
989
+ rgba8(0, 220, 255, 255),
990
+ rgba8(0, 80, 255, 200),
991
+ 3
992
+ );
993
+ drawGlowTextCentered(
994
+ 'NOVA 64',
995
+ 320,
996
+ 100 + bob,
997
+ rgba8(140, 200, 255, 255),
998
+ rgba8(20, 60, 180, 180),
999
+ 2
1000
+ );
1001
+
1002
+ setFont('large');
1003
+ setTextAlign('center');
1004
+ const subPulse = Math.sin(gameTime * 4) * 0.25 + 0.75;
1005
+ drawText('ULTIMATE SPACE COMBAT', 320, 148, rgba8(0, 255, 200, Math.floor(subPulse * 255)), 1);
1006
+
1007
+ const panel = createPanel(centerX(440), 190, 440, 92, {
1008
+ bgColor: rgba8(4, 10, 30, 230),
1009
+ borderColor: rgba8(0, 180, 255, 255),
1010
+ borderWidth: 2,
1011
+ shadow: true,
1012
+ });
1013
+ drawPanel(panel);
1014
+
1015
+ setFont('small');
1016
+ drawText('◆ Blast enemy forces and avoid asteroids', 320, 208, uiColors.light, 1);
1017
+ drawText('◆ Collect rings to heal and points', 320, 223, uiColors.light, 1);
1018
+ drawText('◆ Press Q or E to BARREL ROLL! (Invincibility)', 320, 240, rgba8(255, 220, 50, 255), 1);
1019
+
1020
+ drawAllButtons();
1021
+
1022
+ setFont('tiny');
1023
+ setTextAlign('center');
1024
+ drawText('WASD / Arrows: Steer ◆ Space: Fire', 320, 316, uiColors.secondary, 1);
1025
+ const alpha = Math.floor((Math.sin(gameTime * 6) * 0.5 + 0.5) * 255);
1026
+ drawText('◆ PRESS SPACE TO LAUNCH ◆', 320, 335, rgba8(0, 220, 255, alpha), 1);
1027
+
1028
+ drawScanlines(45, 3);
1029
+ }
1030
+
1031
+ function drawGameOverScreen() {
1032
+ rect(0, 0, 640, 360, rgba8(100, 0, 0, 180), true);
1033
+ drawNoise(0, 0, 640, 360, 20, Math.floor(gameTime * 8));
1034
+
1035
+ setFont('huge');
1036
+ setTextAlign('center');
1037
+ drawTextShadow('MISSION FAILED', 320, 100, rgba8(255, 50, 50, 255), rgba8(0, 0, 0, 255), 4, 1);
1038
+
1039
+ setFont('large');
1040
+ drawText('FINAL SCORE', 320, 160, rgba8(180, 200, 255, 200), 1);
1041
+
1042
+ setFont('huge');
1043
+ drawText(Math.floor(game.score).toString(), 320, 195, rgba8(0, 255, 200, 255), 1);
1044
+
1045
+ setFont('normal');
1046
+ drawText('ENEMIES DESTROYED: ' + game.kills, 320, 235, rgba8(255, 180, 80, 220), 1);
1047
+ drawText('WAVE REACHED: ' + game.wave, 320, 255, rgba8(160, 200, 255, 200), 1);
1048
+
1049
+ drawAllButtons();
1050
+ drawScanlines(40, 2);
1051
+ }
1052
+
1053
+ function drawHUD() {
1054
+ setFont('normal');
1055
+ setTextAlign('left');
1056
+
1057
+ // Top info bar background
1058
+ rect(0, 0, 640, 28, rgba8(0, 4, 16, 180), true);
1059
+ line(0, 28, 640, 28, rgba8(0, 160, 255, 100));
1060
+
1061
+ drawTextShadow(
1062
+ 'SCORE ' + Math.floor(game.score),
1063
+ 16,
1064
+ 8,
1065
+ rgba8(0, 255, 200, 255),
1066
+ rgba8(0, 0, 0, 200),
1067
+ 2,
1068
+ 1
1069
+ );
1070
+ drawText('WAVE ' + game.wave, 180, 8, rgba8(180, 200, 255, 200), 1);
1071
+ drawText('KILLS ' + game.kills, 300, 8, rgba8(255, 180, 80, 180), 1);
1072
+
1073
+ setTextAlign('right');
1074
+ drawText('SPD ' + Math.floor(game.speed), 624, 8, rgba8(100, 200, 255, 180), 1);
1075
+
1076
+ // Modern angled health bar
1077
+ const barX = 420,
1078
+ barY = 330,
1079
+ barW = 200,
1080
+ barH = 16;
1081
+ // Frame
1082
+ rect(barX - 2, barY - 2, barW + 4, barH + 4, rgba8(0, 150, 255, 150), false);
1083
+ rect(barX, barY, barW, barH, rgba8(20, 0, 0, 200), true);
1084
+
1085
+ const hp = Math.max(0, game.player.health);
1086
+ const hpW = (hp / 100) * barW;
1087
+ const hpColor =
1088
+ hp > 50 ? rgba8(0, 255, 100, 255) : hp > 25 ? rgba8(255, 200, 0, 255) : rgba8(255, 50, 50, 255);
1089
+ rect(barX, barY, hpW, barH, hpColor, true);
1090
+
1091
+ setTextAlign('right');
1092
+ setFont('small');
1093
+ drawText('SHIELD', barX - 10, barY + 3, rgba8(180, 200, 255, 200), 1);
1094
+
1095
+ // Cool Crosshair
1096
+ const cx = 320,
1097
+ cy = 180;
1098
+ const retColor = rgba8(0, 255, 200, 180);
1099
+ line(cx - 15, cy, cx - 5, cy, retColor);
1100
+ line(cx + 5, cy, cx + 15, cy, retColor);
1101
+ line(cx, cy - 15, cx, cy - 5, retColor);
1102
+ line(cx, cy + 5, cx, cy + 15, retColor);
1103
+ // Center dot
1104
+ rect(cx - 1, cy - 1, 3, 3, rgba8(255, 0, 0, 200), true);
1105
+
1106
+ // Invuln flash / barrel roll glow
1107
+ if (isInvulnerable(playerHit)) {
1108
+ if (game.player.isBarrelRolling) {
1109
+ const rollAlpha = Math.floor(Math.sin(gameTime * 40) * 30 + 30);
1110
+ rect(0, 0, 640, 360, rgba8(0, 180, 255, rollAlpha), true);
1111
+ } else {
1112
+ const flashAlpha = Math.floor(Math.sin(gameTime * 30) * 40 + 40);
1113
+ rect(0, 0, 640, 360, rgba8(255, 0, 0, flashAlpha), true);
1114
+ }
1115
+ }
1116
+ }