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,1285 @@
1
+ // WING COMMANDER SPACE COMBAT - First Person View
2
+ // Asteroid field combat with cockpit view like Wing Commander
3
+ // VERSION: v001-INITIAL
4
+
5
+ console.log('🚀 Wing Commander Space Combat Loading...');
6
+
7
+ // Helper function for 3D vectors
8
+ function vec3(x, y, z) {
9
+ return { x: x || 0, y: y || 0, z: z || 0 };
10
+ }
11
+
12
+ // Game configuration
13
+ const CONFIG = {
14
+ // Ship controls
15
+ SHIP_SPEED: 20,
16
+ SHIP_TURN_SPEED: 2.5,
17
+ SHIP_BOOST_MULTIPLIER: 2,
18
+
19
+ // Combat
20
+ LASER_SPEED: 80,
21
+ LASER_COOLDOWN: 0.15,
22
+ MISSILE_SPEED: 40,
23
+ MISSILE_COOLDOWN: 1.0,
24
+
25
+ // Asteroids
26
+ ASTEROID_MIN_SPEED: 5,
27
+ ASTEROID_MAX_SPEED: 15,
28
+ ASTEROID_SPAWN_DISTANCE: 100,
29
+
30
+ // Camera
31
+ CAMERA_FOV: 85,
32
+ CAMERA_SHAKE_INTENSITY: 0.3,
33
+
34
+ // Visual
35
+ USE_COCKPIT_OVERLAY: true,
36
+ };
37
+
38
+ // Game state
39
+ let gameState = 'start'; // 'start', 'playing', 'waveclear', 'gameover'
40
+ let gameTime = 0;
41
+ let score = 0;
42
+ let kills = 0;
43
+
44
+ // Wave system
45
+ let wave = 0;
46
+ let waveEnemiesRemaining = 0;
47
+ let waveClearTimer = 0;
48
+ let bossActive = false;
49
+
50
+ // Player state
51
+ let player = {
52
+ pos: vec3(0, 0, 0),
53
+ vel: vec3(0, 0, 0),
54
+ rot: vec3(0, 0, 0), // pitch, yaw, roll
55
+ health: 100,
56
+ shield: 100,
57
+ energy: 100,
58
+ boosting: false,
59
+ laserCooldown: 0,
60
+ missileCooldown: 0,
61
+ missileCount: 20,
62
+ };
63
+
64
+ // Game objects
65
+ let asteroids = [];
66
+ let enemies = [];
67
+ let playerLasers = [];
68
+ let enemyLasers = [];
69
+ let missiles = [];
70
+ let explosions = [];
71
+ let particles = [];
72
+ let stars = [];
73
+ let pickups = [];
74
+
75
+ // Cockpit meshes
76
+ let cockpit = {
77
+ frame: null,
78
+ hud: null,
79
+ crosshair: null,
80
+ };
81
+
82
+ // Camera shake
83
+ let shake;
84
+ let cooldowns;
85
+
86
+ // UI
87
+ let uiButtons = [];
88
+
89
+ // ============================================
90
+ // INITIALIZATION
91
+ // ============================================
92
+ export async function init() {
93
+ console.log('🎮 Initializing Wing Commander Space Combat...');
94
+
95
+ // Reset game state
96
+ gameState = 'start';
97
+ gameTime = 0;
98
+ score = 0;
99
+ kills = 0;
100
+
101
+ // Reset player
102
+ player = {
103
+ pos: vec3(0, 0, 0),
104
+ vel: vec3(0, 0, 0),
105
+ rot: vec3(0, 0, 0),
106
+ health: 100,
107
+ shield: 100,
108
+ energy: 100,
109
+ boosting: false,
110
+ laserCooldown: 0,
111
+ missileCooldown: 0,
112
+ missileCount: 20,
113
+ };
114
+
115
+ shake = createShake({ decay: 3 });
116
+ cooldowns = createCooldownSet({ laser: CONFIG.LASER_COOLDOWN, missile: CONFIG.MISSILE_COOLDOWN });
117
+
118
+ // Reset wave state
119
+ wave = 0;
120
+ waveEnemiesRemaining = 0;
121
+ waveClearTimer = 0;
122
+ bossActive = false;
123
+
124
+ // Clear arrays
125
+ asteroids = [];
126
+ enemies = [];
127
+ playerLasers = [];
128
+ enemyLasers = [];
129
+ missiles = [];
130
+ explosions = [];
131
+ particles = [];
132
+ stars = [];
133
+ pickups = [];
134
+
135
+ // Setup 3D environment
136
+ setupCamera();
137
+ setupLighting();
138
+
139
+ // Create star field
140
+ createStarField();
141
+
142
+ // Create UI
143
+ createStartScreenUI();
144
+
145
+ // Focus canvas for keyboard input
146
+ const canvas = document.querySelector('canvas');
147
+ if (canvas) {
148
+ canvas.focus();
149
+ canvas.tabIndex = 1;
150
+ }
151
+
152
+ console.log('✅ Wing Commander Space Combat Ready!');
153
+ }
154
+
155
+ function setupCamera() {
156
+ // First person view from cockpit
157
+ setCameraPosition(0, 0, 0);
158
+ setCameraTarget(0, 0, -10);
159
+ setCameraFOV(CONFIG.CAMERA_FOV);
160
+ }
161
+
162
+ function setupLighting() {
163
+ // Space lighting - dim ambient with directional sun
164
+ setAmbientLight(0x222244);
165
+ setLightColor(0xffffee);
166
+ setLightDirection(0.3, -0.5, -0.8);
167
+ // Post-processing for cinematic cockpit feel
168
+ enableBloom(0.8, 0.4, 0.45); // Engine glow & weapon flash
169
+ enableFXAA(); // Smooth starfield
170
+ enableVignette(1.8, 0.85); // Cockpit-style dark border
171
+ enableChromaticAberration(0.0015); // Subtle lens dispersion
172
+ }
173
+
174
+ function createStarField() {
175
+ // Create distant stars
176
+ for (let i = 0; i < 500; i++) {
177
+ const angle1 = Math.random() * Math.PI * 2;
178
+ const angle2 = (Math.random() - 0.5) * Math.PI;
179
+ const distance = 200 + Math.random() * 300;
180
+
181
+ const x = Math.cos(angle1) * Math.cos(angle2) * distance;
182
+ const y = Math.sin(angle2) * distance;
183
+ const z = Math.sin(angle1) * Math.cos(angle2) * distance;
184
+
185
+ const brightness = 0.5 + Math.random() * 0.5;
186
+ const color = brightness > 0.8 ? 0xffffee : 0xaabbff;
187
+
188
+ const star = {
189
+ mesh: createSphere(0.3, color, [x, y, z]),
190
+ pos: vec3(x, y, z),
191
+ brightness: brightness,
192
+ };
193
+ stars.push(star);
194
+ }
195
+ }
196
+
197
+ function createStartScreenUI() {
198
+ uiButtons = [];
199
+
200
+ // Start button with working keyboard fallback
201
+ uiButtons.push(
202
+ createButton(
203
+ 200,
204
+ 200,
205
+ 240,
206
+ 60,
207
+ '🚀 LAUNCH FIGHTER',
208
+ () => {
209
+ console.log('🎯 LAUNCH FIGHTER clicked!');
210
+ startGame();
211
+ },
212
+ {
213
+ normalColor: rgba8(255, 100, 0, 255),
214
+ hoverColor: rgba8(255, 140, 40, 255),
215
+ pressedColor: rgba8(200, 60, 0, 255),
216
+ }
217
+ )
218
+ );
219
+ }
220
+
221
+ function startGame() {
222
+ console.log('🚀 Starting game...');
223
+ gameState = 'playing';
224
+ gameTime = 0;
225
+
226
+ // Spawn initial asteroids
227
+ for (let i = 0; i < 15; i++) {
228
+ spawnAsteroid();
229
+ }
230
+
231
+ // Start wave 1
232
+ spawnWave();
233
+ }
234
+
235
+ function spawnWave() {
236
+ wave++;
237
+ bossActive = false;
238
+
239
+ if (wave % 5 === 0) {
240
+ // Boss wave
241
+ spawnEnemy('boss');
242
+ bossActive = true;
243
+ // Add escort fighters
244
+ for (let i = 0; i < 2; i++) spawnEnemy('fighter');
245
+ waveEnemiesRemaining = 3;
246
+ } else {
247
+ const count = 3 + Math.floor(wave * 0.8);
248
+ waveEnemiesRemaining = count;
249
+ for (let i = 0; i < count; i++) {
250
+ let type = 'fighter';
251
+ if (wave >= 3 && Math.random() < 0.25) type = 'bomber';
252
+ if (wave >= 5 && Math.random() < 0.2) type = 'ace';
253
+ spawnEnemy(type);
254
+ }
255
+ }
256
+ }
257
+
258
+ // ============================================
259
+ // GAME OBJECTS
260
+ // ============================================
261
+ function spawnAsteroid() {
262
+ // Random position in front of player
263
+ const angle1 = (Math.random() - 0.5) * Math.PI * 0.5;
264
+ const angle2 = (Math.random() - 0.5) * Math.PI * 0.5;
265
+ const distance = CONFIG.ASTEROID_SPAWN_DISTANCE;
266
+
267
+ const x = Math.sin(angle1) * distance;
268
+ const y = Math.sin(angle2) * distance;
269
+ const z = -distance;
270
+
271
+ const size = 2 + Math.random() * 4;
272
+ const speed =
273
+ CONFIG.ASTEROID_MIN_SPEED +
274
+ Math.random() * (CONFIG.ASTEROID_MAX_SPEED - CONFIG.ASTEROID_MIN_SPEED);
275
+
276
+ // Gray/brown asteroid colors
277
+ const colors = [0x888888, 0x666666, 0x997755, 0x775544];
278
+ const color = colors[Math.floor(Math.random() * colors.length)];
279
+
280
+ const asteroid = {
281
+ mesh: createCube(size, color, [x, y, z]),
282
+ pos: vec3(x, y, z),
283
+ vel: vec3((Math.random() - 0.5) * speed, (Math.random() - 0.5) * speed, speed * 1.5),
284
+ rot: vec3(Math.random() * 0.5, Math.random() * 0.5, Math.random() * 0.5),
285
+ size: size,
286
+ health: Math.floor(size * 2),
287
+ };
288
+
289
+ setScale(asteroid.mesh, size, size, size);
290
+ asteroids.push(asteroid);
291
+ }
292
+
293
+ const ENEMY_TYPES = {
294
+ fighter: {
295
+ color: 0xff3333,
296
+ wingColor: 0xcc2222,
297
+ hp: 30,
298
+ speed: 7,
299
+ fireRate: 1.5,
300
+ score: 100,
301
+ size: 2,
302
+ damage: 5,
303
+ },
304
+ bomber: {
305
+ color: 0xff8800,
306
+ wingColor: 0xcc6600,
307
+ hp: 60,
308
+ speed: 4,
309
+ fireRate: 2.5,
310
+ score: 250,
311
+ size: 3,
312
+ damage: 10,
313
+ },
314
+ ace: {
315
+ color: 0x00ffff,
316
+ wingColor: 0x00bbbb,
317
+ hp: 40,
318
+ speed: 12,
319
+ fireRate: 0.8,
320
+ score: 400,
321
+ size: 1.8,
322
+ damage: 8,
323
+ },
324
+ boss: {
325
+ color: 0xffdd00,
326
+ wingColor: 0xcc9900,
327
+ hp: 200,
328
+ speed: 3,
329
+ fireRate: 0.6,
330
+ score: 2000,
331
+ size: 5,
332
+ damage: 12,
333
+ },
334
+ };
335
+
336
+ function spawnEnemy(type) {
337
+ type = type || 'fighter';
338
+ const cfg = ENEMY_TYPES[type];
339
+ const angle1 = (Math.random() - 0.5) * Math.PI * 0.3;
340
+ const angle2 = (Math.random() - 0.5) * Math.PI * 0.3;
341
+ const dist = 60 + Math.random() * 40;
342
+
343
+ const x = Math.sin(angle1) * dist;
344
+ const y = Math.sin(angle2) * dist;
345
+ const z = -dist;
346
+
347
+ const s = cfg.size;
348
+ const body = createCube(s, cfg.color, [x, y, z]);
349
+ setScale(body, s, s * 0.5, s * 1.5);
350
+
351
+ const wing1 = createCube(s * 2, cfg.wingColor, [x - s, y, z]);
352
+ setScale(wing1, s * 2, 0.2, s * 0.75);
353
+
354
+ const wing2 = createCube(s * 2, cfg.wingColor, [x + s, y, z]);
355
+ setScale(wing2, s * 2, 0.2, s * 0.75);
356
+
357
+ // Scale HP with wave
358
+ const hpScale = type === 'boss' ? cfg.hp + wave * 20 : cfg.hp + Math.floor(wave / 2) * 5;
359
+
360
+ const enemy = {
361
+ type: type,
362
+ body: body,
363
+ wings: [wing1, wing2],
364
+ pos: vec3(x, y, z),
365
+ vel: vec3(0, 0, cfg.speed + Math.random() * 3),
366
+ rot: vec3(0, Math.PI, 0),
367
+ health: hpScale,
368
+ maxHealth: hpScale,
369
+ fireCooldown: Math.random() * cfg.fireRate,
370
+ fireRate: cfg.fireRate,
371
+ attackPattern: Math.floor(Math.random() * 3),
372
+ phaseTime: 0,
373
+ };
374
+
375
+ enemies.push(enemy);
376
+ }
377
+
378
+ function firePlayerLaser() {
379
+ if (!useCooldown(cooldowns.laser)) return;
380
+
381
+ // Fire two lasers from wing positions
382
+ for (let i = -1; i <= 1; i += 2) {
383
+ const laser = {
384
+ mesh: createCube(0.2, 0x00ff00, [i * 1.5, -0.5, -2]),
385
+ pos: vec3(player.pos.x + i * 1.5, player.pos.y - 0.5, player.pos.z - 2),
386
+ vel: vec3(0, 0, -CONFIG.LASER_SPEED),
387
+ life: 3,
388
+ damage: 10,
389
+ };
390
+ setScale(laser.mesh, 0.2, 0.2, 2);
391
+ playerLasers.push(laser);
392
+ }
393
+
394
+ player.energy -= 2;
395
+ sfx('laser');
396
+ }
397
+
398
+ function fireMissile() {
399
+ if (player.missileCount <= 0 || !useCooldown(cooldowns.missile)) return;
400
+
401
+ const missile = {
402
+ mesh: createCube(0.3, 0xffaa00, [0, 0, -2]),
403
+ pos: vec3(player.pos.x, player.pos.y, player.pos.z - 2),
404
+ vel: vec3(0, 0, -CONFIG.MISSILE_SPEED),
405
+ rot: vec3(0, 0, 0),
406
+ life: 5,
407
+ damage: 50,
408
+ target: null,
409
+ trail: [],
410
+ };
411
+ setScale(missile.mesh, 0.3, 0.3, 1);
412
+ missiles.push(missile);
413
+
414
+ player.missileCount--;
415
+ sfx('explosion');
416
+ }
417
+
418
+ // ============================================
419
+ // UPDATE LOOP
420
+ // ============================================
421
+ export function update() {
422
+ const dt = 1 / 60;
423
+
424
+ if (gameState === 'start') {
425
+ updateStartScreen(dt);
426
+ return;
427
+ }
428
+
429
+ if (gameState === 'playing') {
430
+ gameTime += dt;
431
+
432
+ updateInput(dt);
433
+ updatePlayer(dt);
434
+ updateAsteroids(dt);
435
+ updateEnemies(dt);
436
+ updateLasers(dt);
437
+ updateMissiles(dt);
438
+ updatePickups(dt);
439
+ updateExplosions(dt);
440
+ updateParticles(dt);
441
+ updateCamera(dt);
442
+ checkCollisions(dt);
443
+
444
+ // Spawn more asteroids
445
+ if (asteroids.length < 15 && Math.random() < 0.02) {
446
+ spawnAsteroid();
447
+ }
448
+
449
+ // Wave clear check
450
+ if (waveEnemiesRemaining <= 0 && enemies.length === 0) {
451
+ gameState = 'waveclear';
452
+ waveClearTimer = 3.0;
453
+ score += wave * 500;
454
+ player.missileCount = Math.min(20, player.missileCount + 3);
455
+ sfx('powerup');
456
+ }
457
+ }
458
+
459
+ if (gameState === 'waveclear') {
460
+ gameTime += dt;
461
+ waveClearTimer -= dt;
462
+ updateExplosions(dt);
463
+ updateParticles(dt);
464
+ updatePickups(dt);
465
+ updateCamera(dt);
466
+ if (waveClearTimer <= 0) {
467
+ gameState = 'playing';
468
+ spawnWave();
469
+ }
470
+ }
471
+ }
472
+
473
+ function updateStartScreen(dt) {
474
+ gameTime += dt;
475
+
476
+ // Update buttons
477
+ updateAllButtons();
478
+
479
+ // KEYBOARD FALLBACK - Use isKeyDown for reliable detection
480
+ if (isKeyDown('Enter') || isKeyDown('Space') || isKeyDown(' ')) {
481
+ console.log('⌨️ Keyboard start detected!');
482
+ startGame();
483
+ }
484
+ }
485
+
486
+ function updateInput(dt) {
487
+ // Ship rotation with arrow keys (pitch and yaw)
488
+ if (isKeyDown('ArrowUp')) {
489
+ player.rot.x -= CONFIG.SHIP_TURN_SPEED * dt;
490
+ }
491
+ if (isKeyDown('ArrowDown')) {
492
+ player.rot.x += CONFIG.SHIP_TURN_SPEED * dt;
493
+ }
494
+ if (isKeyDown('ArrowLeft')) {
495
+ player.rot.y += CONFIG.SHIP_TURN_SPEED * dt;
496
+ }
497
+ if (isKeyDown('ArrowRight')) {
498
+ player.rot.y -= CONFIG.SHIP_TURN_SPEED * dt;
499
+ }
500
+
501
+ // Roll with Q/E
502
+ if (isKeyDown('KeyQ')) {
503
+ player.rot.z += CONFIG.SHIP_TURN_SPEED * dt;
504
+ }
505
+ if (isKeyDown('KeyE')) {
506
+ player.rot.z -= CONFIG.SHIP_TURN_SPEED * dt;
507
+ }
508
+
509
+ // Speed control with W/S
510
+ const speedMultiplier = isKeyDown('KeyW') ? 1 : isKeyDown('KeyS') ? -0.5 : 0.5;
511
+ player.boosting = isKeyDown('ShiftLeft') || isKeyDown('ShiftRight');
512
+
513
+ const finalSpeed =
514
+ CONFIG.SHIP_SPEED * speedMultiplier * (player.boosting ? CONFIG.SHIP_BOOST_MULTIPLIER : 1);
515
+
516
+ // Convert rotation to velocity (forward is negative Z)
517
+ const forward = {
518
+ x: -Math.sin(player.rot.y) * Math.cos(player.rot.x),
519
+ y: Math.sin(player.rot.x),
520
+ z: -Math.cos(player.rot.y) * Math.cos(player.rot.x),
521
+ };
522
+
523
+ player.vel.x = forward.x * finalSpeed;
524
+ player.vel.y = forward.y * finalSpeed;
525
+ player.vel.z = forward.z * finalSpeed;
526
+
527
+ // Weapons
528
+ if (isKeyDown('KeyZ') || isKeyDown('Space')) {
529
+ firePlayerLaser();
530
+ }
531
+
532
+ if (isKeyPressed('KeyX')) {
533
+ fireMissile();
534
+ }
535
+
536
+ // Cooldowns
537
+ updateCooldowns(cooldowns, dt);
538
+
539
+ // Energy regeneration
540
+ if (player.energy < 100 && !player.boosting) {
541
+ player.energy += 10 * dt;
542
+ }
543
+ if (player.boosting && player.energy > 0) {
544
+ player.energy -= 20 * dt;
545
+ }
546
+ }
547
+
548
+ function updatePlayer(dt) {
549
+ // Update position (but keep camera at origin)
550
+ // We move the world, not the player for first-person view
551
+ player.pos.x += player.vel.x * dt;
552
+ player.pos.y += player.vel.y * dt;
553
+ player.pos.z += player.vel.z * dt;
554
+
555
+ // Clamp rotation
556
+ player.rot.x = Math.max(-Math.PI / 2, Math.min(Math.PI / 2, player.rot.x));
557
+
558
+ // Shield regeneration
559
+ if (player.shield < 100) {
560
+ player.shield += 5 * dt;
561
+ }
562
+ }
563
+
564
+ function updateAsteroids(dt) {
565
+ for (let i = asteroids.length - 1; i >= 0; i--) {
566
+ const asteroid = asteroids[i];
567
+
568
+ // Move relative to player
569
+ asteroid.pos.x -= player.vel.x * dt;
570
+ asteroid.pos.y -= player.vel.y * dt;
571
+ asteroid.pos.z -= player.vel.z * dt;
572
+
573
+ // Add asteroid velocity
574
+ asteroid.pos.x += asteroid.vel.x * dt;
575
+ asteroid.pos.y += asteroid.vel.y * dt;
576
+ asteroid.pos.z += asteroid.vel.z * dt;
577
+
578
+ // Rotate asteroid
579
+ asteroid.rot.x += 0.5 * dt;
580
+ asteroid.rot.y += 0.3 * dt;
581
+ asteroid.rot.z += 0.2 * dt;
582
+
583
+ setPosition(asteroid.mesh, asteroid.pos.x, asteroid.pos.y, asteroid.pos.z);
584
+ setRotation(asteroid.mesh, asteroid.rot.x, asteroid.rot.y, asteroid.rot.z);
585
+
586
+ // Remove if too far behind
587
+ if (asteroid.pos.z > 50 || asteroid.health <= 0) {
588
+ destroyMesh(asteroid.mesh);
589
+ asteroids.splice(i, 1);
590
+
591
+ if (asteroid.health <= 0) {
592
+ createExplosion(asteroid.pos, asteroid.size);
593
+ score += 10;
594
+ }
595
+ }
596
+ }
597
+ }
598
+
599
+ function updateEnemies(dt) {
600
+ for (let i = enemies.length - 1; i >= 0; i--) {
601
+ const enemy = enemies[i];
602
+
603
+ // Move relative to player
604
+ enemy.pos.x -= player.vel.x * dt;
605
+ enemy.pos.y -= player.vel.y * dt;
606
+ enemy.pos.z -= player.vel.z * dt;
607
+
608
+ enemy.phaseTime += dt;
609
+ const cfg = ENEMY_TYPES[enemy.type] || ENEMY_TYPES.fighter;
610
+
611
+ // Type-specific AI
612
+ if (enemy.type === 'boss') {
613
+ // Boss circles slowly and holds range
614
+ enemy.vel.x = Math.cos(gameTime * 0.5) * 6;
615
+ enemy.vel.y = Math.sin(gameTime * 0.7) * 4;
616
+ const dz = -20 - enemy.pos.z;
617
+ enemy.vel.z = dz * 0.5;
618
+ } else if (enemy.type === 'ace') {
619
+ // Ace: fast evasive with direction changes
620
+ enemy.vel.x = Math.sin(gameTime * 3 + enemy.phaseTime) * 12;
621
+ enemy.vel.y = Math.cos(gameTime * 2.5 + i) * 6;
622
+ enemy.vel.z = cfg.speed + Math.sin(enemy.phaseTime * 2) * 4;
623
+ } else if (enemy.type === 'bomber') {
624
+ // Bomber: slow steady approach
625
+ enemy.vel.x = Math.sin(gameTime * 0.8 + i) * 2;
626
+ enemy.vel.z = cfg.speed;
627
+ } else {
628
+ // Fighter: original patterns
629
+ switch (enemy.attackPattern) {
630
+ case 0:
631
+ enemy.vel.z = cfg.speed + 3;
632
+ break;
633
+ case 1:
634
+ enemy.vel.x = Math.sin(gameTime * 2) * 5;
635
+ enemy.vel.z = cfg.speed;
636
+ break;
637
+ case 2:
638
+ enemy.vel.x = Math.cos(gameTime * 1.5) * 8;
639
+ enemy.vel.y = Math.sin(gameTime * 1.5) * 8;
640
+ enemy.vel.z = cfg.speed - 2;
641
+ break;
642
+ }
643
+ }
644
+
645
+ enemy.pos.x += enemy.vel.x * dt;
646
+ enemy.pos.y += enemy.vel.y * dt;
647
+ enemy.pos.z += enemy.vel.z * dt;
648
+
649
+ // Update meshes
650
+ setPosition(enemy.body, enemy.pos.x, enemy.pos.y, enemy.pos.z);
651
+ setRotation(enemy.body, enemy.rot.x, enemy.rot.y, enemy.rot.z);
652
+
653
+ enemy.wings.forEach((wing, idx) => {
654
+ const offset = idx === 0 ? -2 : 2;
655
+ setPosition(wing, enemy.pos.x + offset, enemy.pos.y, enemy.pos.z);
656
+ setRotation(wing, enemy.rot.x, enemy.rot.y, enemy.rot.z);
657
+ });
658
+
659
+ // Fire at player
660
+ enemy.fireCooldown -= dt;
661
+ if (enemy.fireCooldown <= 0 && enemy.pos.z < 20 && enemy.pos.z > -50) {
662
+ if (enemy.type === 'boss') {
663
+ // Boss fires spread of 3
664
+ for (let s = -1; s <= 1; s++) {
665
+ fireEnemyLaser(enemy, s * 3, cfg.damage);
666
+ }
667
+ } else {
668
+ fireEnemyLaser(enemy, 0, cfg.damage);
669
+ }
670
+ enemy.fireCooldown = enemy.fireRate + Math.random() * 0.5;
671
+ }
672
+
673
+ // Remove if too far or dead
674
+ if (enemy.pos.z > 60 || enemy.health <= 0) {
675
+ destroyMesh(enemy.body);
676
+ enemy.wings.forEach(w => destroyMesh(w));
677
+
678
+ if (enemy.health <= 0) {
679
+ const size = enemy.type === 'boss' ? 6 : 3;
680
+ createExplosion(enemy.pos, size);
681
+ if (enemy.type === 'boss') {
682
+ createExplosion({ x: enemy.pos.x - 3, y: enemy.pos.y, z: enemy.pos.z }, 3);
683
+ createExplosion({ x: enemy.pos.x + 3, y: enemy.pos.y, z: enemy.pos.z }, 3);
684
+ bossActive = false;
685
+ }
686
+ score += (ENEMY_TYPES[enemy.type] || ENEMY_TYPES.fighter).score;
687
+ kills++;
688
+ waveEnemiesRemaining--;
689
+ sfx('explosion');
690
+ // Drop pickup
691
+ const dropChance =
692
+ enemy.type === 'boss'
693
+ ? 1.0
694
+ : enemy.type === 'ace'
695
+ ? 0.4
696
+ : enemy.type === 'bomber'
697
+ ? 0.5
698
+ : 0.2;
699
+ if (Math.random() < dropChance) {
700
+ spawnPickup(enemy.pos);
701
+ }
702
+ } else {
703
+ waveEnemiesRemaining--;
704
+ }
705
+ enemies.splice(i, 1);
706
+ }
707
+ }
708
+ }
709
+
710
+ function fireEnemyLaser(enemy, xOffset, damage) {
711
+ const laser = {
712
+ mesh: createCube(0.15, enemy.type === 'boss' ? 0xffaa00 : 0xff0000, [
713
+ enemy.pos.x + (xOffset || 0),
714
+ enemy.pos.y,
715
+ enemy.pos.z,
716
+ ]),
717
+ pos: vec3(enemy.pos.x + (xOffset || 0), enemy.pos.y, enemy.pos.z),
718
+ vel: vec3(xOffset ? xOffset * 0.5 : 0, 0, 30),
719
+ life: 2,
720
+ damage: damage || 5,
721
+ };
722
+ setScale(laser.mesh, 0.15, 0.15, 1.5);
723
+ enemyLasers.push(laser);
724
+ }
725
+
726
+ // Pickup system
727
+ function spawnPickup(pos) {
728
+ const types = ['missile', 'health', 'shield', 'energy'];
729
+ const type = types[Math.floor(Math.random() * types.length)];
730
+ const colors = { missile: 0xffaa00, health: 0x00ff00, shield: 0x0088ff, energy: 0xff00ff };
731
+ const pickup = {
732
+ mesh: createCube(1, colors[type], [pos.x, pos.y, pos.z]),
733
+ pos: vec3(pos.x, pos.y, pos.z),
734
+ type: type,
735
+ life: 12,
736
+ rotY: 0,
737
+ };
738
+ pickups.push(pickup);
739
+ }
740
+
741
+ function updatePickups(dt) {
742
+ for (let i = pickups.length - 1; i >= 0; i--) {
743
+ const p = pickups[i];
744
+ p.life -= dt;
745
+ p.rotY += dt * 3;
746
+ // Move relative to player
747
+ p.pos.x -= player.vel.x * dt;
748
+ p.pos.y -= player.vel.y * dt;
749
+ p.pos.z -= player.vel.z * dt;
750
+ setPosition(p.mesh, p.pos.x, p.pos.y, p.pos.z);
751
+ setRotation(p.mesh, 0, p.rotY, 0);
752
+
753
+ // Collect
754
+ const dist = Math.sqrt(p.pos.x * p.pos.x + p.pos.y * p.pos.y + p.pos.z * p.pos.z);
755
+ if (dist < 5) {
756
+ switch (p.type) {
757
+ case 'missile':
758
+ player.missileCount = Math.min(20, player.missileCount + 3);
759
+ break;
760
+ case 'health':
761
+ player.health = Math.min(100, player.health + 25);
762
+ break;
763
+ case 'shield':
764
+ player.shield = Math.min(100, player.shield + 30);
765
+ break;
766
+ case 'energy':
767
+ player.energy = Math.min(100, player.energy + 40);
768
+ break;
769
+ }
770
+ sfx(p.type === 'missile' ? 'coin' : 'powerup');
771
+ destroyMesh(p.mesh);
772
+ pickups.splice(i, 1);
773
+ continue;
774
+ }
775
+
776
+ if (p.life <= 0 || p.pos.z > 50) {
777
+ destroyMesh(p.mesh);
778
+ pickups.splice(i, 1);
779
+ }
780
+ }
781
+ }
782
+
783
+ function updateLasers(dt) {
784
+ // Player lasers
785
+ for (let i = playerLasers.length - 1; i >= 0; i--) {
786
+ const laser = playerLasers[i];
787
+
788
+ // Move relative to player
789
+ laser.pos.x -= player.vel.x * dt;
790
+ laser.pos.y -= player.vel.y * dt;
791
+ laser.pos.z -= player.vel.z * dt;
792
+
793
+ // Add laser velocity
794
+ laser.pos.z += laser.vel.z * dt;
795
+
796
+ laser.life -= dt;
797
+
798
+ setPosition(laser.mesh, laser.pos.x, laser.pos.y, laser.pos.z);
799
+
800
+ if (laser.life <= 0) {
801
+ destroyMesh(laser.mesh);
802
+ playerLasers.splice(i, 1);
803
+ }
804
+ }
805
+
806
+ // Enemy lasers
807
+ for (let i = enemyLasers.length - 1; i >= 0; i--) {
808
+ const laser = enemyLasers[i];
809
+
810
+ // Move relative to player
811
+ laser.pos.x -= player.vel.x * dt;
812
+ laser.pos.y -= player.vel.y * dt;
813
+ laser.pos.z -= player.vel.z * dt;
814
+
815
+ laser.pos.z += laser.vel.z * dt;
816
+ laser.life -= dt;
817
+
818
+ setPosition(laser.mesh, laser.pos.x, laser.pos.y, laser.pos.z);
819
+
820
+ if (laser.life <= 0 || laser.pos.z > 10) {
821
+ destroyMesh(laser.mesh);
822
+ enemyLasers.splice(i, 1);
823
+ }
824
+ }
825
+ }
826
+
827
+ function updateMissiles(dt) {
828
+ for (let i = missiles.length - 1; i >= 0; i--) {
829
+ const missile = missiles[i];
830
+
831
+ // Find target if none
832
+ if (!missile.target && enemies.length > 0) {
833
+ missile.target = enemies[0];
834
+ }
835
+
836
+ // Home in on target
837
+ if (missile.target && missile.target.health > 0) {
838
+ const dx = missile.target.pos.x - missile.pos.x;
839
+ const dy = missile.target.pos.y - missile.pos.y;
840
+ const dz = missile.target.pos.z - missile.pos.z;
841
+ const dist = Math.sqrt(dx * dx + dy * dy + dz * dz);
842
+
843
+ if (dist > 0) {
844
+ const homingStrength = 10;
845
+ missile.vel.x += (dx / dist) * homingStrength * dt;
846
+ missile.vel.y += (dy / dist) * homingStrength * dt;
847
+ missile.vel.z += (dz / dist) * homingStrength * dt;
848
+ }
849
+ }
850
+
851
+ // Move relative to player
852
+ missile.pos.x -= player.vel.x * dt;
853
+ missile.pos.y -= player.vel.y * dt;
854
+ missile.pos.z -= player.vel.z * dt;
855
+
856
+ missile.pos.x += missile.vel.x * dt;
857
+ missile.pos.y += missile.vel.y * dt;
858
+ missile.pos.z += missile.vel.z * dt;
859
+
860
+ missile.life -= dt;
861
+
862
+ setPosition(missile.mesh, missile.pos.x, missile.pos.y, missile.pos.z);
863
+
864
+ // Trail particles
865
+ if (Math.random() < 0.5) {
866
+ createParticle(missile.pos, 0xff6600, 0.5);
867
+ }
868
+
869
+ if (missile.life <= 0) {
870
+ destroyMesh(missile.mesh);
871
+ missiles.splice(i, 1);
872
+ }
873
+ }
874
+ }
875
+
876
+ function updateExplosions(dt) {
877
+ for (let i = explosions.length - 1; i >= 0; i--) {
878
+ const explosion = explosions[i];
879
+
880
+ explosion.life -= dt;
881
+ explosion.scale += dt * 5;
882
+
883
+ // Move relative to player
884
+ explosion.pos.x -= player.vel.x * dt;
885
+ explosion.pos.y -= player.vel.y * dt;
886
+ explosion.pos.z -= player.vel.z * dt;
887
+
888
+ setPosition(explosion.mesh, explosion.pos.x, explosion.pos.y, explosion.pos.z);
889
+ setScale(explosion.mesh, explosion.scale, explosion.scale, explosion.scale);
890
+
891
+ // Fade out
892
+ const alpha = Math.max(0, explosion.life / 0.5);
893
+
894
+ if (explosion.life <= 0) {
895
+ destroyMesh(explosion.mesh);
896
+ explosions.splice(i, 1);
897
+ }
898
+ }
899
+ }
900
+
901
+ function updateParticles(dt) {
902
+ for (let i = particles.length - 1; i >= 0; i--) {
903
+ const particle = particles[i];
904
+
905
+ particle.life -= dt;
906
+
907
+ // Move relative to player
908
+ particle.pos.x -= player.vel.x * dt;
909
+ particle.pos.y -= player.vel.y * dt;
910
+ particle.pos.z -= player.vel.z * dt;
911
+
912
+ particle.pos.x += particle.vel.x * dt;
913
+ particle.pos.y += particle.vel.y * dt;
914
+ particle.pos.z += particle.vel.z * dt;
915
+
916
+ setPosition(particle.mesh, particle.pos.x, particle.pos.y, particle.pos.z);
917
+
918
+ const scale = particle.life / particle.maxLife;
919
+ setScale(particle.mesh, scale, scale, scale);
920
+
921
+ if (particle.life <= 0) {
922
+ destroyMesh(particle.mesh);
923
+ particles.splice(i, 1);
924
+ }
925
+ }
926
+ }
927
+
928
+ function updateCamera(dt) {
929
+ // First person camera - apply rotation but stay at origin
930
+ updateShake(shake, dt);
931
+ const [shakeX, shakeY] = getShakeOffset(shake);
932
+
933
+ // Set camera position with shake
934
+ setCameraPosition(shakeX, shakeY, 0);
935
+
936
+ // Look direction based on ship rotation
937
+ const lookDist = 10;
938
+ const lookX = -Math.sin(player.rot.y) * Math.cos(player.rot.x) * lookDist;
939
+ const lookY = Math.sin(player.rot.x) * lookDist;
940
+ const lookZ = -Math.cos(player.rot.y) * Math.cos(player.rot.x) * lookDist;
941
+
942
+ setCameraTarget(lookX, lookY, lookZ);
943
+
944
+ // Apply roll by rotating camera
945
+ // Note: setCameraRotation might not be available, this is visual only
946
+ }
947
+
948
+ function checkCollisions(dt) {
949
+ // Player lasers vs asteroids
950
+ for (let i = playerLasers.length - 1; i >= 0; i--) {
951
+ const laser = playerLasers[i];
952
+
953
+ for (let j = asteroids.length - 1; j >= 0; j--) {
954
+ const asteroid = asteroids[j];
955
+ const dist = distance(laser.pos, asteroid.pos);
956
+
957
+ if (dist < asteroid.size) {
958
+ asteroid.health -= laser.damage;
959
+ destroyMesh(laser.mesh);
960
+ playerLasers.splice(i, 1);
961
+ sfx('hit');
962
+
963
+ createParticle(laser.pos, 0xffaa00, 0.3);
964
+ triggerShake(shake, 0.2);
965
+ break;
966
+ }
967
+ }
968
+ }
969
+
970
+ // Player lasers vs enemies
971
+ for (let i = playerLasers.length - 1; i >= 0; i--) {
972
+ const laser = playerLasers[i];
973
+
974
+ for (let j = enemies.length - 1; j >= 0; j--) {
975
+ const enemy = enemies[j];
976
+ const dist = distance(laser.pos, enemy.pos);
977
+
978
+ if (dist < 3) {
979
+ enemy.health -= laser.damage;
980
+ destroyMesh(laser.mesh);
981
+ playerLasers.splice(i, 1);
982
+ sfx('hit');
983
+
984
+ createParticle(laser.pos, 0xff0000, 0.3);
985
+ triggerShake(shake, 0.3);
986
+ break;
987
+ }
988
+ }
989
+ }
990
+
991
+ // Missiles vs enemies
992
+ for (let i = missiles.length - 1; i >= 0; i--) {
993
+ const missile = missiles[i];
994
+
995
+ for (let j = enemies.length - 1; j >= 0; j--) {
996
+ const enemy = enemies[j];
997
+ const dist = distance(missile.pos, enemy.pos);
998
+
999
+ if (dist < 4) {
1000
+ enemy.health -= missile.damage;
1001
+ createExplosion(missile.pos, 2);
1002
+ destroyMesh(missile.mesh);
1003
+ missiles.splice(i, 1);
1004
+
1005
+ triggerShake(shake, 0.8);
1006
+ break;
1007
+ }
1008
+ }
1009
+ }
1010
+
1011
+ // Enemy lasers vs player
1012
+ for (let i = enemyLasers.length - 1; i >= 0; i--) {
1013
+ const laser = enemyLasers[i];
1014
+ const dist = Math.sqrt(
1015
+ laser.pos.x * laser.pos.x + laser.pos.y * laser.pos.y + laser.pos.z * laser.pos.z
1016
+ );
1017
+
1018
+ if (dist < 2) {
1019
+ if (player.shield > 0) {
1020
+ player.shield -= laser.damage;
1021
+ } else {
1022
+ player.health -= laser.damage;
1023
+ }
1024
+ sfx('hit');
1025
+
1026
+ destroyMesh(laser.mesh);
1027
+ enemyLasers.splice(i, 1);
1028
+ triggerShake(shake, 0.5);
1029
+ }
1030
+ }
1031
+
1032
+ // Asteroids vs player
1033
+ for (let i = asteroids.length - 1; i >= 0; i--) {
1034
+ const asteroid = asteroids[i];
1035
+ const dist = Math.sqrt(
1036
+ asteroid.pos.x * asteroid.pos.x +
1037
+ asteroid.pos.y * asteroid.pos.y +
1038
+ asteroid.pos.z * asteroid.pos.z
1039
+ );
1040
+
1041
+ if (dist < asteroid.size + 2) {
1042
+ if (player.shield > 0) {
1043
+ player.shield -= 20;
1044
+ } else {
1045
+ player.health -= 20;
1046
+ }
1047
+ sfx('hit');
1048
+
1049
+ createExplosion(asteroid.pos, asteroid.size);
1050
+ destroyMesh(asteroid.mesh);
1051
+ asteroids.splice(i, 1);
1052
+ triggerShake(shake, 1.0);
1053
+ }
1054
+ }
1055
+
1056
+ // Check game over
1057
+ if (player.health <= 0) {
1058
+ gameState = 'gameover';
1059
+ sfx('death');
1060
+ }
1061
+ }
1062
+
1063
+ // ============================================
1064
+ // HELPER FUNCTIONS
1065
+ // ============================================
1066
+ function distance(p1, p2) {
1067
+ const dx = p1.x - p2.x;
1068
+ const dy = p1.y - p2.y;
1069
+ const dz = p1.z - p2.z;
1070
+ return Math.sqrt(dx * dx + dy * dy + dz * dz);
1071
+ }
1072
+
1073
+ function createExplosion(pos, size) {
1074
+ const explosion = {
1075
+ mesh: createSphere(size, 0xff6600, [pos.x, pos.y, pos.z]),
1076
+ pos: vec3(pos.x, pos.y, pos.z),
1077
+ scale: size,
1078
+ life: 0.5,
1079
+ };
1080
+ explosions.push(explosion);
1081
+
1082
+ // Spawn particles
1083
+ for (let i = 0; i < 10; i++) {
1084
+ createParticle(pos, 0xff6600, 0.8);
1085
+ }
1086
+ }
1087
+
1088
+ function createParticle(pos, color, life) {
1089
+ const particle = {
1090
+ mesh: createSphere(0.2, color, [pos.x, pos.y, pos.z]),
1091
+ pos: vec3(pos.x, pos.y, pos.z),
1092
+ vel: vec3((Math.random() - 0.5) * 10, (Math.random() - 0.5) * 10, (Math.random() - 0.5) * 10),
1093
+ life: life,
1094
+ maxLife: life,
1095
+ };
1096
+ particles.push(particle);
1097
+ }
1098
+
1099
+ // ============================================
1100
+ // DRAW FUNCTIONS
1101
+ // ============================================
1102
+ export function draw() {
1103
+ if (gameState === 'start') {
1104
+ drawStartScreen();
1105
+ return;
1106
+ }
1107
+
1108
+ if (gameState === 'playing' || gameState === 'waveclear') {
1109
+ drawHUD();
1110
+ drawCrosshair();
1111
+ }
1112
+
1113
+ if (gameState === 'waveclear') {
1114
+ const alpha = Math.floor(Math.min(1, waveClearTimer) * 255);
1115
+ printCentered(`WAVE ${wave} CLEAR!`, 320, 140, rgba8(0, 255, 100, alpha), 2);
1116
+ printCentered(`+${wave * 500} BONUS`, 320, 170, rgba8(255, 255, 0, alpha));
1117
+ printCentered('+3 MISSILES', 320, 195, rgba8(255, 180, 0, alpha));
1118
+ }
1119
+
1120
+ if (gameState === 'gameover') {
1121
+ drawGameOver();
1122
+ }
1123
+ }
1124
+
1125
+ function drawStartScreen() {
1126
+ // Space background
1127
+ rect(0, 0, 640, 360, rgba8(0, 0, 20, 255), true);
1128
+
1129
+ // Title
1130
+ print('WING COMMANDER', 180, 80, rgba8(255, 200, 0, 255));
1131
+ print('SPACE COMBAT', 200, 110, rgba8(0, 200, 255, 255));
1132
+
1133
+ // Pulsing start prompt
1134
+ const pulse = Math.sin(gameTime * 3) * 0.5 + 0.5;
1135
+ print('PRESS ENTER OR SPACE TO START', 170, 150, rgba8(255, 255, 100, Math.floor(pulse * 255)));
1136
+
1137
+ // Controls
1138
+ rect(150, 180, 340, 150, rgba8(10, 10, 40, 220), true);
1139
+ rect(150, 180, 340, 150, rgba8(100, 100, 255, 180), false);
1140
+
1141
+ print('CONTROLS:', 260, 195, rgba8(255, 255, 255, 255));
1142
+ print('ARROWS - Pitch/Yaw', 180, 220, rgba8(200, 200, 255, 255));
1143
+ print('Q/E - Roll', 180, 240, rgba8(200, 200, 255, 255));
1144
+ print('W/S - Speed Up/Down', 180, 260, rgba8(200, 200, 255, 255));
1145
+ print('Z/SPACE - Fire Lasers', 180, 280, rgba8(200, 200, 255, 255));
1146
+ print('X - Fire Missile', 180, 300, rgba8(200, 200, 255, 255));
1147
+ print('SHIFT - Boost', 180, 320, rgba8(200, 200, 255, 255));
1148
+
1149
+ // Draw buttons
1150
+ drawAllButtons();
1151
+ }
1152
+
1153
+ function drawHUD() {
1154
+ // HUD background panel
1155
+ rect(10, 10, 300, 100, rgba8(0, 0, 0, 180), true);
1156
+ rect(10, 10, 300, 100, rgba8(0, 255, 255, 100), false);
1157
+
1158
+ // Stats
1159
+ print(`SCORE: ${score}`, 20, 25, rgba8(255, 255, 0, 255));
1160
+ print(`WAVE: ${wave} KILLS: ${kills}`, 20, 45, rgba8(255, 100, 100, 255));
1161
+
1162
+ // Health bar
1163
+ print('HULL:', 20, 65, rgba8(255, 255, 255, 255));
1164
+ rect(70, 63, 100, 12, rgba8(50, 0, 0, 255), true);
1165
+ rect(70, 63, Math.floor(player.health), 12, rgba8(255, 0, 0, 255), true);
1166
+ rect(70, 63, 100, 12, rgba8(255, 0, 0, 100), false);
1167
+
1168
+ // Shield bar
1169
+ print('SHIELD:', 20, 85, rgba8(255, 255, 255, 255));
1170
+ rect(85, 83, 100, 12, rgba8(0, 20, 50, 255), true);
1171
+ rect(85, 83, Math.floor(player.shield), 12, rgba8(0, 150, 255, 255), true);
1172
+ rect(85, 83, 100, 12, rgba8(0, 150, 255, 100), false);
1173
+
1174
+ // Energy bar (top right)
1175
+ rect(530, 10, 100, 25, rgba8(0, 0, 0, 180), true);
1176
+ print('ENERGY', 540, 18, rgba8(255, 255, 255, 255));
1177
+ rect(535, 28, 95, 5, rgba8(0, 50, 0, 255), true);
1178
+ rect(535, 28, Math.floor(player.energy * 0.95), 5, rgba8(0, 255, 0, 255), true);
1179
+
1180
+ // Weapon status
1181
+ rect(330, 10, 180, 50, rgba8(0, 0, 0, 180), true);
1182
+ print('WEAPONS', 370, 20, rgba8(255, 255, 255, 255));
1183
+
1184
+ const laserColor = !cooldownReady(cooldowns.laser)
1185
+ ? rgba8(100, 100, 100, 255)
1186
+ : rgba8(0, 255, 0, 255);
1187
+ print(`LASER: READY`, 340, 35, laserColor);
1188
+
1189
+ const missileColor = !cooldownReady(cooldowns.missile)
1190
+ ? rgba8(100, 100, 100, 255)
1191
+ : rgba8(255, 150, 0, 255);
1192
+ print(`MISSILE: ${player.missileCount}`, 340, 50, missileColor);
1193
+
1194
+ // Speed indicator
1195
+ const speed = Math.sqrt(
1196
+ player.vel.x * player.vel.x + player.vel.y * player.vel.y + player.vel.z * player.vel.z
1197
+ );
1198
+ print(`SPEED: ${Math.floor(speed)}`, 530, 45, rgba8(200, 200, 255, 255));
1199
+
1200
+ // Target info (bottom center)
1201
+ if (enemies.length > 0) {
1202
+ const target = enemies[0];
1203
+ const targetDist = distance(vec3(0, 0, 0), target.pos);
1204
+
1205
+ rect(220, 320, 200, 30, rgba8(0, 0, 0, 180), true);
1206
+ rect(220, 320, 200, 30, rgba8(255, 0, 0, 100), false);
1207
+ const typeLabel = (target.type || 'fighter').toUpperCase();
1208
+ print(`${typeLabel}: ${Math.floor(targetDist)}m`, 240, 330, rgba8(255, 0, 0, 255));
1209
+ }
1210
+
1211
+ // Boss health bar
1212
+ if (bossActive) {
1213
+ const boss = enemies.find(e => e.type === 'boss');
1214
+ if (boss) {
1215
+ const bw = 300;
1216
+ const bx = (640 - bw) / 2;
1217
+ print('BOSS', bx, 118, rgba8(255, 50, 50, 255));
1218
+ rect(bx + 40, 116, bw - 40, 12, rgba8(80, 0, 0, 255), true);
1219
+ const hp = Math.max(0, boss.health / boss.maxHealth);
1220
+ rect(bx + 40, 116, Math.floor(hp * (bw - 40)), 12, rgba8(255, 0, 0, 255), true);
1221
+ rect(bx + 40, 116, bw - 40, 12, rgba8(200, 100, 100), false);
1222
+ }
1223
+ }
1224
+
1225
+ // Wave warning at start of new wave
1226
+ if (
1227
+ wave > 0 &&
1228
+ gameState === 'playing' &&
1229
+ waveEnemiesRemaining === enemies.length &&
1230
+ enemies.length > 0
1231
+ ) {
1232
+ const wt = gameTime % 100;
1233
+ if (wt < 2) {
1234
+ const alpha = Math.floor(Math.min(1, 2 - wt) * 255);
1235
+ const warnText = bossActive ? 'WARNING: BOSS INCOMING!' : `WAVE ${wave}`;
1236
+ printCentered(warnText, 320, 180, rgba8(255, 100, 0, alpha), 2);
1237
+ }
1238
+ }
1239
+ }
1240
+
1241
+ function drawCrosshair() {
1242
+ const cx = 320;
1243
+ const cy = 180;
1244
+ const size = 15;
1245
+
1246
+ // Center dot
1247
+ rect(cx - 2, cy - 2, 4, 4, rgba8(0, 255, 0, 255), true);
1248
+
1249
+ // Cross lines
1250
+ rect(cx - size, cy - 1, size - 5, 2, rgba8(0, 255, 0, 200), true);
1251
+ rect(cx + 5, cy - 1, size - 5, 2, rgba8(0, 255, 0, 200), true);
1252
+ rect(cx - 1, cy - size, 2, size - 5, rgba8(0, 255, 0, 200), true);
1253
+ rect(cx - 1, cy + 5, 2, size - 5, rgba8(0, 255, 0, 200), true);
1254
+
1255
+ // Corner brackets
1256
+ const bracket = 30;
1257
+ // Top left
1258
+ rect(cx - bracket, cy - bracket, 10, 2, rgba8(0, 255, 0, 150), true);
1259
+ rect(cx - bracket, cy - bracket, 2, 10, rgba8(0, 255, 0, 150), true);
1260
+ // Top right
1261
+ rect(cx + bracket - 10, cy - bracket, 10, 2, rgba8(0, 255, 0, 150), true);
1262
+ rect(cx + bracket - 2, cy - bracket, 2, 10, rgba8(0, 255, 0, 150), true);
1263
+ // Bottom left
1264
+ rect(cx - bracket, cy + bracket - 2, 10, 2, rgba8(0, 255, 0, 150), true);
1265
+ rect(cx - bracket, cy + bracket - 10, 2, 10, rgba8(0, 255, 0, 150), true);
1266
+ // Bottom right
1267
+ rect(cx + bracket - 10, cy + bracket - 2, 10, 2, rgba8(0, 255, 0, 150), true);
1268
+ rect(cx + bracket - 2, cy + bracket - 10, 2, 10, rgba8(0, 255, 0, 150), true);
1269
+ }
1270
+
1271
+ function drawGameOver() {
1272
+ rect(0, 0, 640, 360, rgba8(20, 0, 0, 230), true);
1273
+
1274
+ print('MISSION FAILED', 220, 120, rgba8(255, 50, 50, 255));
1275
+ print(`FINAL SCORE: ${score}`, 230, 160, rgba8(255, 255, 255, 255));
1276
+ print(`ENEMIES DESTROYED: ${kills}`, 210, 190, rgba8(255, 255, 255, 255));
1277
+ print(`WAVES SURVIVED: ${wave}`, 220, 210, rgba8(200, 200, 255, 255));
1278
+
1279
+ const pulse = Math.sin(gameTime * 3) * 0.5 + 0.5;
1280
+ print('PRESS ENTER TO RESTART', 210, 240, rgba8(255, 255, 100, Math.floor(pulse * 255)));
1281
+
1282
+ if (isKeyDown('Enter') || isKeyDown('Space')) {
1283
+ init();
1284
+ }
1285
+ }