nova64 0.2.5 → 0.2.7

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (185) hide show
  1. package/README.md +25 -8
  2. package/bin/nova64.js +165 -0
  3. package/dist/assets/console-CY_kygm3.js +14 -0
  4. package/dist/assets/console-CY_kygm3.js.map +1 -0
  5. package/dist/assets/main-l0sNRNKZ.js.map +1 -0
  6. package/dist/assets/sky/studio/nx.png +0 -0
  7. package/dist/assets/sky/studio/ny.png +0 -0
  8. package/dist/assets/sky/studio/nz.png +0 -0
  9. package/dist/assets/sky/studio/px.png +0 -0
  10. package/dist/assets/sky/studio/py.png +0 -0
  11. package/dist/assets/sky/studio/pz.png +0 -0
  12. package/dist/assets/vanilla-Dcuy32gi.js +2 -0
  13. package/dist/assets/vanilla-Dcuy32gi.js.map +1 -0
  14. package/dist/console.html +899 -0
  15. package/dist/docs/BENCHMARK.md +77 -0
  16. package/dist/docs/CHEATSHEET.md +255 -0
  17. package/dist/docs/EFFECTS_API_GUIDE.md +577 -0
  18. package/dist/docs/EFFECTS_QUICK_REFERENCE.md +331 -0
  19. package/dist/docs/FONT_CHARACTER_REFERENCE.md +219 -0
  20. package/dist/docs/FREE_GLB_ASSETS.md +330 -0
  21. package/dist/docs/FULLSCREEN_BUTTON_FEATURE.md +296 -0
  22. package/dist/docs/GAMEPAD_SUPPORT.md +348 -0
  23. package/dist/docs/GAME_IMPROVEMENTS.md +278 -0
  24. package/dist/docs/GAME_QUALITY_STATUS.md +300 -0
  25. package/dist/docs/MIGRATION_GUIDE.md +553 -0
  26. package/dist/docs/NOVA64_3D_API.md +356 -0
  27. package/dist/docs/NOVA64_API_REFERENCE.md +1406 -0
  28. package/dist/docs/NOVA64_UI_API.md +503 -0
  29. package/dist/docs/UI_SYSTEM_SUMMARY.md +445 -0
  30. package/dist/docs/VOXEL_ENGINE_GUIDE.md +662 -0
  31. package/dist/docs/VOXEL_QUICK_REFERENCE.md +386 -0
  32. package/dist/docs/api-3d.html +750 -0
  33. package/dist/docs/api-effects.html +385 -0
  34. package/dist/docs/api-improvements.md +121 -0
  35. package/dist/docs/api-skybox.html +407 -0
  36. package/dist/docs/api-sprites.html +321 -0
  37. package/dist/docs/api-voxel.html +337 -0
  38. package/dist/docs/api.html +543 -0
  39. package/dist/docs/assets.html +306 -0
  40. package/dist/docs/audio.html +340 -0
  41. package/dist/docs/blogs.html +286 -0
  42. package/dist/docs/collision.html +316 -0
  43. package/dist/docs/console.html +247 -0
  44. package/dist/docs/editor.html +297 -0
  45. package/dist/docs/font.html +247 -0
  46. package/dist/docs/framebuffer.html +247 -0
  47. package/dist/docs/fullscreen-button.html +297 -0
  48. package/dist/docs/gpu-systems.html +247 -0
  49. package/dist/docs/index.html +580 -0
  50. package/dist/docs/input.html +491 -0
  51. package/dist/docs/physics.html +311 -0
  52. package/dist/docs/screens.html +311 -0
  53. package/dist/docs/storage.html +311 -0
  54. package/dist/docs/textinput.html +332 -0
  55. package/dist/docs/ui.html +488 -0
  56. package/dist/examples/3d-advanced/code.js +695 -0
  57. package/dist/examples/adventure-comic-3d/code.js +342 -0
  58. package/dist/examples/audio-lab/code.js +150 -0
  59. package/dist/examples/boids-flocking/code.js +270 -0
  60. package/dist/examples/crystal-cathedral-3d/code.js +706 -0
  61. package/dist/examples/cyberpunk-city-3d/code.js +1383 -0
  62. package/dist/examples/demoscene/README.md +192 -0
  63. package/dist/examples/demoscene/code.js +1081 -0
  64. package/dist/examples/demoscene/meta.json +21 -0
  65. package/dist/examples/dungeon-crawler-3d/code.js +1117 -0
  66. package/dist/examples/f-zero-nova-3d/code.js +865 -0
  67. package/dist/examples/f-zero-nova-3d/code_old.js +1555 -0
  68. package/dist/examples/fps-demo-3d/code.js +744 -0
  69. package/dist/examples/game-of-life-3d/code.js +338 -0
  70. package/dist/examples/generative-art/code.js +632 -0
  71. package/dist/examples/hello-3d/code.js +325 -0
  72. package/dist/examples/hello-skybox/code.js +183 -0
  73. package/dist/examples/hello-world/code.js +19 -0
  74. package/dist/examples/input-showcase/code.js +109 -0
  75. package/dist/examples/instancing-demo/code.js +315 -0
  76. package/dist/examples/minecraft-demo/code.js +387 -0
  77. package/dist/examples/model-viewer-3d/code.js +114 -0
  78. package/dist/examples/mystical-realm-3d/code.js +1203 -0
  79. package/dist/examples/nature-explorer-3d/code.js +1318 -0
  80. package/dist/examples/particles-demo/code.js +522 -0
  81. package/dist/examples/pbr-showcase/code.js +140 -0
  82. package/dist/examples/physics-demo-3d/code.js +948 -0
  83. package/dist/examples/screen-demo/code.js +267 -0
  84. package/dist/examples/shooter-demo-3d/code.js +1286 -0
  85. package/dist/examples/space-combat-3d/IMPLEMENTATION_SUMMARY.md +109 -0
  86. package/dist/examples/space-combat-3d/README.md +135 -0
  87. package/dist/examples/space-combat-3d/code.js +1332 -0
  88. package/dist/examples/space-harrier-3d/code.js +923 -0
  89. package/dist/examples/star-fox-nova-3d/code.js +1116 -0
  90. package/dist/examples/star-fox-nova-3d/code_backup.js +410 -0
  91. package/dist/examples/star-fox-nova-3d/code_broken.js +1821 -0
  92. package/dist/examples/storage-quest/code.js +209 -0
  93. package/dist/examples/strider-demo-3d/IMPROVEMENT_OPTIONS.md +285 -0
  94. package/dist/examples/strider-demo-3d/cache-test.html +132 -0
  95. package/dist/examples/strider-demo-3d/code-fixed.js +582 -0
  96. package/dist/examples/strider-demo-3d/code-old.js +1537 -0
  97. package/dist/examples/strider-demo-3d/code.js +1462 -0
  98. package/dist/examples/strider-demo-3d/code.js.bak2 +1169 -0
  99. package/dist/examples/strider-demo-3d/fix-game.sh +53 -0
  100. package/dist/examples/super-plumber-64/README.md +128 -0
  101. package/dist/examples/super-plumber-64/code.js +1185 -0
  102. package/dist/examples/super-plumber-64/index.html +88 -0
  103. package/dist/examples/test-2d-overlay/code.js +32 -0
  104. package/dist/examples/test-font/code.js +51 -0
  105. package/dist/examples/test-minimal/code.js +21 -0
  106. package/dist/examples/ui-demo/code.js +306 -0
  107. package/dist/examples/wing-commander-space/README.md +180 -0
  108. package/dist/examples/wing-commander-space/code.js +1285 -0
  109. package/dist/examples/wizardry-3d/CHANGELOG.md +366 -0
  110. package/dist/examples/wizardry-3d/code.js +3928 -0
  111. package/dist/index.html +666 -0
  112. package/dist/os9-shell/assets/index-DIHfrTaW.css +1 -0
  113. package/dist/os9-shell/assets/index-KchE_ngx.js +483 -0
  114. package/dist/os9-shell/assets/index-KchE_ngx.js.map +1 -0
  115. package/dist/os9-shell/index.html +23 -0
  116. package/dist/os9-shell/nova-icon.svg +12 -0
  117. package/dist/runtime/api-2d.js +1158 -0
  118. package/dist/runtime/api-3d/camera.js +73 -0
  119. package/dist/runtime/api-3d/instancing.js +180 -0
  120. package/dist/runtime/api-3d/lights.js +51 -0
  121. package/dist/runtime/api-3d/materials.js +47 -0
  122. package/dist/runtime/api-3d/models.js +84 -0
  123. package/dist/runtime/api-3d/particles.js +296 -0
  124. package/dist/runtime/api-3d/pbr.js +113 -0
  125. package/dist/runtime/api-3d/primitives.js +304 -0
  126. package/dist/runtime/api-3d/scene.js +169 -0
  127. package/dist/runtime/api-3d/transforms.js +161 -0
  128. package/dist/runtime/api-3d.js +166 -0
  129. package/dist/runtime/api-effects.js +840 -0
  130. package/dist/runtime/api-gameutils.js +476 -0
  131. package/dist/runtime/api-generative.js +610 -0
  132. package/dist/runtime/api-presets.js +85 -0
  133. package/dist/runtime/api-skybox.js +232 -0
  134. package/dist/runtime/api-sprites.js +100 -0
  135. package/dist/runtime/api-voxel.js +712 -0
  136. package/dist/runtime/api.js +201 -0
  137. package/dist/runtime/assets.js +27 -0
  138. package/dist/runtime/audio.js +114 -0
  139. package/dist/runtime/collision.js +47 -0
  140. package/dist/runtime/console.js +101 -0
  141. package/dist/runtime/editor.js +233 -0
  142. package/dist/runtime/font.js +233 -0
  143. package/dist/runtime/framebuffer.js +28 -0
  144. package/dist/runtime/fullscreen-button.js +185 -0
  145. package/dist/runtime/gpu-canvas2d.js +47 -0
  146. package/dist/runtime/gpu-threejs.js +643 -0
  147. package/dist/runtime/gpu-webgl2.js +310 -0
  148. package/dist/runtime/index.d.ts +682 -0
  149. package/dist/runtime/index.js +22 -0
  150. package/dist/runtime/input.js +225 -0
  151. package/dist/runtime/logger.js +60 -0
  152. package/dist/runtime/physics.js +101 -0
  153. package/dist/runtime/screens.js +213 -0
  154. package/dist/runtime/storage.js +38 -0
  155. package/dist/runtime/store.js +151 -0
  156. package/dist/runtime/textinput.js +68 -0
  157. package/dist/runtime/ui/buttons.js +124 -0
  158. package/dist/runtime/ui/panels.js +105 -0
  159. package/dist/runtime/ui/text.js +86 -0
  160. package/dist/runtime/ui/widgets.js +141 -0
  161. package/dist/runtime/ui.js +111 -0
  162. package/index.html +6 -1
  163. package/package.json +9 -2
  164. package/public/assets/sky/studio/nx.png +0 -0
  165. package/public/assets/sky/studio/ny.png +0 -0
  166. package/public/assets/sky/studio/nz.png +0 -0
  167. package/public/assets/sky/studio/px.png +0 -0
  168. package/public/assets/sky/studio/py.png +0 -0
  169. package/public/assets/sky/studio/pz.png +0 -0
  170. package/public/os9-shell/assets/index-KchE_ngx.js +483 -0
  171. package/public/os9-shell/assets/index-KchE_ngx.js.map +1 -0
  172. package/public/os9-shell/index.html +10 -1
  173. package/runtime/api-2d.js +301 -21
  174. package/runtime/api-3d/pbr.js +45 -1
  175. package/runtime/api-3d.js +1 -0
  176. package/runtime/api-effects.js +90 -3
  177. package/runtime/api-gameutils.js +476 -0
  178. package/runtime/api-generative.js +610 -0
  179. package/runtime/api-skybox.js +54 -0
  180. package/runtime/api-voxel.js +139 -28
  181. package/runtime/gpu-threejs.js +13 -9
  182. package/runtime/ui.js +2 -2
  183. package/src/main.js +20 -0
  184. package/public/os9-shell/assets/index-B1Uvacma.js +0 -32825
  185. package/public/os9-shell/assets/index-B1Uvacma.js.map +0 -1
@@ -0,0 +1,1332 @@
1
+ // WING COMMANDER SPACE COMBAT
2
+ // First-person asteroid field combat with enemy ships
3
+ // Inspired by Wing Commander, Star Fox, and classic space shooters
4
+
5
+ // ============================================
6
+ // GAME CONFIGURATION
7
+ // ============================================
8
+ const CONFIG = {
9
+ // Player ship
10
+ SHIP_SPEED: 25,
11
+ SHIP_TURN_SPEED: 2.5,
12
+ SHIP_BOOST_SPEED: 45,
13
+ SHIP_STRAFE_SPEED: 15,
14
+
15
+ // Combat
16
+ LASER_SPEED: 80,
17
+ LASER_COOLDOWN: 0.15,
18
+ MISSILE_SPEED: 40,
19
+ MISSILE_COOLDOWN: 1.5,
20
+ MISSILE_TURN_RATE: 3,
21
+
22
+ // Enemies
23
+ ENEMY_SPAWN_DISTANCE: 150,
24
+ ENEMY_SPEED: 20,
25
+ ENEMY_HEALTH: 3,
26
+
27
+ // Asteroids
28
+ ASTEROID_COUNT: 50,
29
+ ASTEROID_SPAWN_RANGE: 200,
30
+
31
+ // Visuals
32
+ COCKPIT_OVERLAY: true,
33
+ CROSSHAIR: true,
34
+ RADAR_RANGE: 100,
35
+ };
36
+
37
+ // ============================================
38
+ // GAME STATE
39
+ // ============================================
40
+ let gameState = 'start'; // 'start', 'playing', 'paused', 'gameover', 'waveclear'
41
+ let gameTime = 0;
42
+ let score = 0;
43
+ let kills = 0;
44
+ let wave = 1;
45
+ let waveClearTimer = 0;
46
+ let bossAlive = false;
47
+ let pickups = [];
48
+ let totalWaveEnemies = 0;
49
+
50
+ // Player ship state
51
+ let player = {
52
+ pos: { x: 0, y: 0, z: 0 },
53
+ vel: { x: 0, y: 0, z: 0 },
54
+ pitch: 0, // Up/down rotation
55
+ yaw: 0, // Left/right rotation
56
+ roll: 0, // Barrel roll rotation
57
+ health: 100,
58
+ maxHealth: 100,
59
+ shields: 100,
60
+ maxShields: 100,
61
+ energy: 100,
62
+ maxEnergy: 100,
63
+ boosting: false,
64
+ missiles: 10,
65
+ laserCooldown: 0,
66
+ missileCooldown: 0,
67
+ targetLocked: null,
68
+ };
69
+
70
+ // Camera (first-person from cockpit)
71
+ let camera = {
72
+ pos: { x: 0, y: 0, z: 0 },
73
+ pitch: 0,
74
+ yaw: 0,
75
+ roll: 0,
76
+ shake: 0,
77
+ };
78
+
79
+ // Game objects
80
+ let asteroids = [];
81
+ let enemies = [];
82
+ let projectiles = [];
83
+ let explosions = [];
84
+ let stars = [];
85
+ let cockpitMesh = null;
86
+ let crosshairMesh = null;
87
+ let shake;
88
+ let cooldowns;
89
+
90
+ // ============================================
91
+ // INITIALIZATION
92
+ // ============================================
93
+ export async function init() {
94
+ // Reset game state
95
+ gameState = 'start';
96
+ gameTime = 0;
97
+ score = 0;
98
+ kills = 0;
99
+ wave = 1;
100
+
101
+ // Reset player
102
+ player = {
103
+ pos: { x: 0, y: 0, z: 0 },
104
+ vel: { x: 0, y: 0, z: 0 },
105
+ pitch: 0,
106
+ yaw: 0,
107
+ roll: 0,
108
+ health: 100,
109
+ maxHealth: 100,
110
+ shields: 100,
111
+ maxShields: 100,
112
+ energy: 100,
113
+ maxEnergy: 100,
114
+ boosting: false,
115
+ missiles: 10,
116
+ laserCooldown: 0,
117
+ missileCooldown: 0,
118
+ targetLocked: null,
119
+ };
120
+
121
+ // Reset camera
122
+ camera = {
123
+ pos: { x: 0, y: 0, z: 0 },
124
+ pitch: 0,
125
+ yaw: 0,
126
+ roll: 0,
127
+ };
128
+
129
+ shake = createShake({ decay: 3 });
130
+ cooldowns = createCooldownSet({ laser: CONFIG.LASER_COOLDOWN, missile: CONFIG.MISSILE_COOLDOWN });
131
+
132
+ // Clear arrays
133
+ asteroids = [];
134
+ enemies = [];
135
+ projectiles = [];
136
+ explosions = [];
137
+ stars = [];
138
+ pickups = [];
139
+ bossAlive = false;
140
+ totalWaveEnemies = 0;
141
+ waveClearTimer = 0;
142
+
143
+ // Create starfield
144
+ createStarfield();
145
+
146
+ // Create asteroid field
147
+ createAsteroidField();
148
+
149
+ // Create cockpit overlay
150
+ if (CONFIG.COCKPIT_OVERLAY) {
151
+ createCockpit();
152
+ }
153
+
154
+ // Create crosshair
155
+ if (CONFIG.CROSSHAIR) {
156
+ createCrosshair();
157
+ }
158
+
159
+ // Spawn initial enemies
160
+ spawnWave(wave);
161
+
162
+ // Setup lighting
163
+ setAmbientLight(0x333333);
164
+ setLightColor(0xffffff);
165
+ setLightDirection(0.5, -0.5, 0.5);
166
+
167
+ // Post-processing: cinematic space look
168
+ enableBloom(0.8, 0.4, 0.45);
169
+ enableFXAA();
170
+ enableVignette(1.3, 0.9);
171
+
172
+ // Setup camera
173
+ updateCamera(0);
174
+ }
175
+
176
+ // ============================================
177
+ // SCENE CREATION
178
+ // ============================================
179
+ function createStarfield() {
180
+ // Create distant stars
181
+ for (let i = 0; i < 300; i++) {
182
+ const distance = 400 + Math.random() * 400;
183
+ const angle1 = Math.random() * Math.PI * 2;
184
+ const angle2 = (Math.random() - 0.5) * Math.PI;
185
+
186
+ const star = {
187
+ x: Math.cos(angle1) * Math.cos(angle2) * distance,
188
+ y: Math.sin(angle2) * distance,
189
+ z: Math.sin(angle1) * Math.cos(angle2) * distance,
190
+ brightness: 0.3 + Math.random() * 0.7,
191
+ mesh: createSphere(0.3 + Math.random() * 0.5, 0xffffff, [0, 0, 0]),
192
+ };
193
+
194
+ setPosition(star.mesh, star.x, star.y, star.z);
195
+ stars.push(star);
196
+ }
197
+ }
198
+
199
+ function createAsteroidField() {
200
+ for (let i = 0; i < CONFIG.ASTEROID_COUNT; i++) {
201
+ spawnAsteroid();
202
+ }
203
+ }
204
+
205
+ function spawnAsteroid() {
206
+ const range = CONFIG.ASTEROID_SPAWN_RANGE;
207
+ const size = 2 + Math.random() * 6;
208
+ const x = (Math.random() - 0.5) * range * 2;
209
+ const y = (Math.random() - 0.5) * range;
210
+ const z = (Math.random() - 0.5) * range * 2;
211
+
212
+ const asteroid = {
213
+ pos: { x, y, z },
214
+ vel: {
215
+ x: (Math.random() - 0.5) * 2,
216
+ y: (Math.random() - 0.5) * 2,
217
+ z: (Math.random() - 0.5) * 2,
218
+ },
219
+ rotation: {
220
+ x: Math.random() * Math.PI * 2,
221
+ y: Math.random() * Math.PI * 2,
222
+ z: Math.random() * Math.PI * 2,
223
+ },
224
+ rotationSpeed: {
225
+ x: (Math.random() - 0.5) * 0.5,
226
+ y: (Math.random() - 0.5) * 0.5,
227
+ z: (Math.random() - 0.5) * 0.5,
228
+ },
229
+ size,
230
+ health: Math.ceil(size / 2),
231
+ mesh: createCube(size, 0x666666, [x, y, z]),
232
+ };
233
+
234
+ asteroids.push(asteroid);
235
+ }
236
+
237
+ function createCockpit() {
238
+ // Create semi-transparent cockpit frame
239
+ // Left frame
240
+ const leftFrame = createCube(0.5, 0x333333, [-8, 0, -5]);
241
+ setScale(leftFrame, 0.2, 8, 0.2);
242
+
243
+ // Right frame
244
+ const rightFrame = createCube(0.5, 0x333333, [8, 0, -5]);
245
+ setScale(rightFrame, 0.2, 8, 0.2);
246
+
247
+ // Top frame
248
+ const topFrame = createCube(0.5, 0x333333, [0, 6, -5]);
249
+ setScale(topFrame, 16, 0.2, 0.2);
250
+
251
+ // Bottom frame (with HUD console)
252
+ const bottomFrame = createCube(0.5, 0x222222, [0, -5, -5]);
253
+ setScale(bottomFrame, 16, 1.5, 0.2);
254
+
255
+ cockpitMesh = { leftFrame, rightFrame, topFrame, bottomFrame };
256
+ }
257
+
258
+ function createCrosshair() {
259
+ // Simple crosshair in center of view
260
+ const size = 0.3;
261
+ const distance = 15;
262
+
263
+ // Horizontal line
264
+ const horizontal = createCube(size, 0x00ff00, [0, 0, -distance]);
265
+ setScale(horizontal, 2, 0.05, 0.05);
266
+
267
+ // Vertical line
268
+ const vertical = createCube(size, 0x00ff00, [0, 0, -distance]);
269
+ setScale(vertical, 0.05, 2, 0.05);
270
+
271
+ // Center dot
272
+ const center = createSphere(0.05, 0xff0000, [0, 0, -distance]);
273
+
274
+ crosshairMesh = { horizontal, vertical, center };
275
+ }
276
+
277
+ function spawnWave(waveNum) {
278
+ const baseCount = 3 + waveNum * 2;
279
+ let types = [];
280
+
281
+ // Boss wave every 5 waves
282
+ if (waveNum % 5 === 0) {
283
+ types.push('boss');
284
+ bossAlive = true;
285
+ }
286
+
287
+ for (let i = 0; i < baseCount; i++) {
288
+ let type = 'fighter';
289
+ if (waveNum >= 3 && Math.random() < 0.3) type = 'bomber';
290
+ if (waveNum >= 5 && Math.random() < 0.2) type = 'ace';
291
+ types.push(type);
292
+ }
293
+
294
+ totalWaveEnemies = types.length;
295
+ for (let i = 0; i < types.length; i++) {
296
+ const t = types[i];
297
+ setTimeout(() => spawnEnemy(t), i * 800);
298
+ }
299
+ }
300
+
301
+ function spawnEnemy(type) {
302
+ const distance = CONFIG.ENEMY_SPAWN_DISTANCE;
303
+ const angle = Math.random() * Math.PI * 2;
304
+ let hp, spd, color, size, score_val;
305
+
306
+ switch (type) {
307
+ case 'bomber':
308
+ hp = 6 + wave;
309
+ spd = 12;
310
+ color = 0xff8800;
311
+ size = 4;
312
+ score_val = 250;
313
+ break;
314
+ case 'ace':
315
+ hp = 4 + wave;
316
+ spd = 30;
317
+ color = 0x00ccff;
318
+ size = 2.5;
319
+ score_val = 400;
320
+ break;
321
+ case 'boss':
322
+ hp = 30 + wave * 5;
323
+ spd = 10;
324
+ color = 0xffcc00;
325
+ size = 8;
326
+ score_val = 2000;
327
+ break;
328
+ default: // fighter
329
+ hp = CONFIG.ENEMY_HEALTH + Math.floor(wave / 3);
330
+ spd = CONFIG.ENEMY_SPEED;
331
+ color = 0xff3333;
332
+ size = 2;
333
+ score_val = 100;
334
+ break;
335
+ }
336
+
337
+ const body = createCube(size, color, [0, 0, 0], {
338
+ material: 'emissive',
339
+ color,
340
+ intensity: type === 'boss' ? 2.5 : 1.5,
341
+ });
342
+ setScale(
343
+ body,
344
+ type === 'boss' ? 1.5 : 1.5,
345
+ type === 'boss' ? 0.6 : 0.5,
346
+ type === 'boss' ? 1.5 : 1.5
347
+ );
348
+
349
+ const enemy = {
350
+ pos: {
351
+ x: player.pos.x + Math.cos(angle) * distance,
352
+ y: player.pos.y + (Math.random() - 0.5) * 50,
353
+ z: player.pos.z + Math.sin(angle) * distance,
354
+ },
355
+ vel: { x: 0, y: 0, z: 0 },
356
+ yaw: 0,
357
+ pitch: 0,
358
+ health: hp,
359
+ maxHealth: hp,
360
+ shootCooldown: Math.random() * 2,
361
+ aiState: 'approach',
362
+ mesh: body,
363
+ type,
364
+ speed: spd,
365
+ size,
366
+ score_val,
367
+ };
368
+
369
+ enemies.push(enemy);
370
+ }
371
+
372
+ // ============================================
373
+ // UPDATE LOOP
374
+ // ============================================
375
+ export function update() {
376
+ const dt = 1 / 60;
377
+
378
+ if (gameState === 'start') {
379
+ if (isKeyDown('Enter') || isKeyDown('Space') || isKeyDown(' ')) {
380
+ gameState = 'playing';
381
+ }
382
+ return;
383
+ }
384
+
385
+ // Wave clear transition
386
+ if (gameState === 'waveclear') {
387
+ waveClearTimer -= dt;
388
+ updateCamera(dt);
389
+ updateExplosions(dt);
390
+ updatePickups(dt);
391
+ if (waveClearTimer <= 0) {
392
+ wave++;
393
+ gameState = 'playing';
394
+ spawnWave(wave);
395
+ }
396
+ return;
397
+ }
398
+
399
+ if (gameState === 'paused') {
400
+ // Check for unpause
401
+ if (isKeyPressed('Escape')) {
402
+ gameState = 'playing';
403
+ }
404
+ return;
405
+ }
406
+
407
+ if (gameState === 'gameover') return;
408
+
409
+ if (gameState !== 'playing') return;
410
+
411
+ gameTime += dt;
412
+
413
+ // Update game
414
+ updateInput(dt);
415
+ updatePlayer(dt);
416
+ updateEnemies(dt);
417
+ updateProjectiles(dt);
418
+ updateAsteroids(dt);
419
+ updateExplosions(dt);
420
+ updateCamera(dt);
421
+ updateTargeting();
422
+
423
+ // Regenerate shields and energy
424
+ if (player.shields < player.maxShields) {
425
+ player.shields = Math.min(player.maxShields, player.shields + 5 * dt);
426
+ }
427
+ if (player.energy < player.maxEnergy) {
428
+ player.energy = Math.min(player.maxEnergy, player.energy + 20 * dt);
429
+ }
430
+
431
+ // Cooldowns
432
+ updateCooldowns(cooldowns, dt);
433
+
434
+ // Update pickups
435
+ updatePickups(dt);
436
+
437
+ // Check for wave completion
438
+ if (enemies.length === 0 && gameState === 'playing') {
439
+ gameState = 'waveclear';
440
+ waveClearTimer = 3.0;
441
+ score += 1000 * wave;
442
+ player.missiles = Math.min(player.missiles + 3, 20);
443
+ sfx('powerup');
444
+ }
445
+
446
+ // Check game over
447
+ if (player.health <= 0) {
448
+ gameState = 'gameover';
449
+ sfx('death');
450
+ }
451
+ }
452
+
453
+ // ============================================
454
+ // INPUT HANDLING
455
+ // ============================================
456
+ function updateInput(dt) {
457
+ const turnSpeed = CONFIG.SHIP_TURN_SPEED * dt;
458
+ const speed = player.boosting ? CONFIG.SHIP_BOOST_SPEED : CONFIG.SHIP_SPEED;
459
+
460
+ // Mouse control for pitch/yaw (optional - for now keyboard)
461
+
462
+ // Pitch (up/down) - W/S or Arrow Up/Down
463
+ if (isKeyDown('KeyW') || isKeyDown('ArrowUp')) {
464
+ player.pitch -= turnSpeed;
465
+ }
466
+ if (isKeyDown('KeyS') || isKeyDown('ArrowDown')) {
467
+ player.pitch += turnSpeed;
468
+ }
469
+
470
+ // Yaw (left/right) - A/D or Arrow Left/Right
471
+ if (isKeyDown('KeyA') || isKeyDown('ArrowLeft')) {
472
+ player.yaw -= turnSpeed;
473
+ player.roll = Math.max(-0.5, player.roll - dt * 2);
474
+ } else if (isKeyDown('KeyD') || isKeyDown('ArrowRight')) {
475
+ player.yaw += turnSpeed;
476
+ player.roll = Math.min(0.5, player.roll + dt * 2);
477
+ } else {
478
+ // Return to level
479
+ player.roll *= 0.9;
480
+ }
481
+
482
+ // Boost - Shift
483
+ player.boosting = isKeyDown('ShiftLeft') || isKeyDown('ShiftRight');
484
+
485
+ // Strafe - Q/E
486
+ const strafeSpeed = CONFIG.SHIP_STRAFE_SPEED * dt;
487
+ if (isKeyDown('KeyQ')) {
488
+ player.vel.x -= Math.cos(player.yaw + Math.PI / 2) * strafeSpeed;
489
+ player.vel.z -= Math.sin(player.yaw + Math.PI / 2) * strafeSpeed;
490
+ }
491
+ if (isKeyDown('KeyE')) {
492
+ player.vel.x += Math.cos(player.yaw + Math.PI / 2) * strafeSpeed;
493
+ player.vel.z += Math.sin(player.yaw + Math.PI / 2) * strafeSpeed;
494
+ }
495
+
496
+ // Fire lasers - Space or Left Click
497
+ if (
498
+ (isKeyDown('Space') || isKeyDown(' ')) &&
499
+ useCooldown(cooldowns.laser) &&
500
+ player.energy >= 5
501
+ ) {
502
+ fireLasers();
503
+ player.energy -= 5;
504
+ sfx('laser');
505
+ }
506
+
507
+ // Fire missile - M or Right Click
508
+ if (isKeyPressed('KeyM') && useCooldown(cooldowns.missile) && player.missiles > 0) {
509
+ fireMissile();
510
+ player.missiles--;
511
+ sfx('explosion');
512
+ }
513
+
514
+ // Target lock - T
515
+ if (isKeyPressed('KeyT')) {
516
+ lockTarget();
517
+ }
518
+
519
+ // Pause - Escape
520
+ if (isKeyPressed('Escape')) {
521
+ gameState = 'paused';
522
+ }
523
+
524
+ // Apply movement in direction ship is facing
525
+ const forward = {
526
+ x: Math.sin(player.yaw) * Math.cos(player.pitch),
527
+ y: Math.sin(player.pitch),
528
+ z: -Math.cos(player.yaw) * Math.cos(player.pitch),
529
+ };
530
+
531
+ player.vel.x += forward.x * speed * dt;
532
+ player.vel.y += forward.y * speed * dt;
533
+ player.vel.z += forward.z * speed * dt;
534
+
535
+ // Apply drag
536
+ player.vel.x *= 0.98;
537
+ player.vel.y *= 0.98;
538
+ player.vel.z *= 0.98;
539
+ }
540
+
541
+ // ============================================
542
+ // PLAYER UPDATE
543
+ // ============================================
544
+ function updatePlayer(dt) {
545
+ // Update position
546
+ player.pos.x += player.vel.x * dt;
547
+ player.pos.y += player.vel.y * dt;
548
+ player.pos.z += player.vel.z * dt;
549
+
550
+ // Clamp pitch to prevent over-rotation
551
+ player.pitch = Math.max(-Math.PI / 2, Math.min(Math.PI / 2, player.pitch));
552
+
553
+ // Wrap yaw
554
+ if (player.yaw > Math.PI) player.yaw -= Math.PI * 2;
555
+ if (player.yaw < -Math.PI) player.yaw += Math.PI * 2;
556
+ }
557
+
558
+ // ============================================
559
+ // COMBAT SYSTEM
560
+ // ============================================
561
+ function fireLasers() {
562
+ // Fire dual lasers from ship wings
563
+ const forward = {
564
+ x: Math.sin(player.yaw) * Math.cos(player.pitch),
565
+ y: Math.sin(player.pitch),
566
+ z: -Math.cos(player.yaw) * Math.cos(player.pitch),
567
+ };
568
+
569
+ const right = {
570
+ x: Math.cos(player.yaw),
571
+ y: 0,
572
+ z: Math.sin(player.yaw),
573
+ };
574
+
575
+ // Left laser
576
+ createLaser(
577
+ player.pos.x - right.x * 2,
578
+ player.pos.y - right.y * 2,
579
+ player.pos.z - right.z * 2,
580
+ forward.x,
581
+ forward.y,
582
+ forward.z,
583
+ 'player'
584
+ );
585
+
586
+ // Right laser
587
+ createLaser(
588
+ player.pos.x + right.x * 2,
589
+ player.pos.y + right.y * 2,
590
+ player.pos.z + right.z * 2,
591
+ forward.x,
592
+ forward.y,
593
+ forward.z,
594
+ 'player'
595
+ );
596
+
597
+ // Camera shake
598
+ triggerShake(shake, 0.1);
599
+ }
600
+
601
+ function createLaser(x, y, z, dx, dy, dz, owner) {
602
+ const color = owner === 'player' ? 0x00ff00 : 0xff0000;
603
+ const mesh = createCube(0.3, color, [x, y, z]);
604
+ setScale(mesh, 0.2, 0.2, 2);
605
+
606
+ const projectile = {
607
+ pos: { x, y, z },
608
+ vel: {
609
+ x: dx * CONFIG.LASER_SPEED,
610
+ y: dy * CONFIG.LASER_SPEED,
611
+ z: dz * CONFIG.LASER_SPEED,
612
+ },
613
+ damage: 1,
614
+ owner,
615
+ life: 3,
616
+ mesh,
617
+ };
618
+
619
+ projectiles.push(projectile);
620
+ }
621
+
622
+ function fireMissile() {
623
+ const forward = {
624
+ x: Math.sin(player.yaw) * Math.cos(player.pitch),
625
+ y: Math.sin(player.pitch),
626
+ z: -Math.cos(player.yaw) * Math.cos(player.pitch),
627
+ };
628
+
629
+ const mesh = createCube(0.5, 0xffff00, [player.pos.x, player.pos.y, player.pos.z]);
630
+ setScale(mesh, 0.3, 0.3, 1.5);
631
+
632
+ const missile = {
633
+ pos: { ...player.pos },
634
+ vel: {
635
+ x: forward.x * CONFIG.MISSILE_SPEED,
636
+ y: forward.y * CONFIG.MISSILE_SPEED,
637
+ z: forward.z * CONFIG.MISSILE_SPEED,
638
+ },
639
+ damage: 5,
640
+ owner: 'player',
641
+ life: 10,
642
+ mesh,
643
+ target: player.targetLocked,
644
+ isMissile: true,
645
+ };
646
+
647
+ projectiles.push(missile);
648
+ }
649
+
650
+ function lockTarget() {
651
+ // Find closest enemy in front of player
652
+ let closest = null;
653
+ let minDist = 100;
654
+
655
+ const forward = {
656
+ x: Math.sin(player.yaw) * Math.cos(player.pitch),
657
+ y: Math.sin(player.pitch),
658
+ z: -Math.cos(player.yaw) * Math.cos(player.pitch),
659
+ };
660
+
661
+ enemies.forEach(enemy => {
662
+ const toEnemy = {
663
+ x: enemy.pos.x - player.pos.x,
664
+ y: enemy.pos.y - player.pos.y,
665
+ z: enemy.pos.z - player.pos.z,
666
+ };
667
+
668
+ const dist = Math.sqrt(toEnemy.x ** 2 + toEnemy.y ** 2 + toEnemy.z ** 2);
669
+ const len = Math.sqrt(forward.x ** 2 + forward.y ** 2 + forward.z ** 2);
670
+ const dot =
671
+ (toEnemy.x * forward.x + toEnemy.y * forward.y + toEnemy.z * forward.z) / (dist * len);
672
+
673
+ if (dot > 0.9 && dist < minDist) {
674
+ closest = enemy;
675
+ minDist = dist;
676
+ }
677
+ });
678
+
679
+ player.targetLocked = closest;
680
+ }
681
+
682
+ function updateTargeting() {
683
+ // Clear lock if target destroyed
684
+ if (player.targetLocked && !enemies.includes(player.targetLocked)) {
685
+ player.targetLocked = null;
686
+ }
687
+ }
688
+
689
+ // ============================================
690
+ // ENEMY AI
691
+ // ============================================
692
+ function updateEnemies(dt) {
693
+ enemies.forEach(enemy => {
694
+ const toPlayer = {
695
+ x: player.pos.x - enemy.pos.x,
696
+ y: player.pos.y - enemy.pos.y,
697
+ z: player.pos.z - enemy.pos.z,
698
+ };
699
+
700
+ const dist = Math.sqrt(toPlayer.x ** 2 + toPlayer.y ** 2 + toPlayer.z ** 2);
701
+ const spd = enemy.speed * dt;
702
+
703
+ // Type-specific AI
704
+ if (enemy.type === 'ace') {
705
+ // Ace: fast evasive strafing
706
+ if (dist > 60) {
707
+ enemy.vel.x = (toPlayer.x / dist) * spd;
708
+ enemy.vel.y = (toPlayer.y / dist) * spd;
709
+ enemy.vel.z = (toPlayer.z / dist) * spd;
710
+ } else {
711
+ const perpendicular = { x: -toPlayer.z, y: 0, z: toPlayer.x };
712
+ const perpLen = Math.sqrt(perpendicular.x ** 2 + perpendicular.z ** 2) || 1;
713
+ let weave = Math.sin(gameTime * 3 + enemies.indexOf(enemy)) * 0.5;
714
+ enemy.vel.x = (perpendicular.x / perpLen) * spd + weave * spd * 0.3;
715
+ enemy.vel.z = (perpendicular.z / perpLen) * spd + weave * spd * 0.3;
716
+ enemy.vel.y = Math.sin(gameTime * 2) * spd * 0.2;
717
+ }
718
+ } else if (enemy.type === 'bomber') {
719
+ // Bomber: slow steady approach, stays at range
720
+ if (dist > 50) {
721
+ enemy.vel.x = (toPlayer.x / dist) * spd;
722
+ enemy.vel.y = (toPlayer.y / dist) * spd;
723
+ enemy.vel.z = (toPlayer.z / dist) * spd;
724
+ } else {
725
+ enemy.vel.x *= 0.95;
726
+ enemy.vel.y *= 0.95;
727
+ enemy.vel.z *= 0.95;
728
+ }
729
+ } else if (enemy.type === 'boss') {
730
+ // Boss: circles slowly, always faces player
731
+ if (dist > 40) {
732
+ enemy.vel.x = (toPlayer.x / dist) * spd;
733
+ enemy.vel.y = (toPlayer.y / dist) * spd;
734
+ enemy.vel.z = (toPlayer.z / dist) * spd;
735
+ } else {
736
+ const perpendicular = { x: -toPlayer.z, y: 0, z: toPlayer.x };
737
+ const perpLen = Math.sqrt(perpendicular.x ** 2 + perpendicular.z ** 2) || 1;
738
+ enemy.vel.x = (perpendicular.x / perpLen) * spd;
739
+ enemy.vel.z = (perpendicular.z / perpLen) * spd;
740
+ }
741
+ } else {
742
+ // Fighter: default approach/circle/retreat
743
+ if (dist > 80) {
744
+ enemy.vel.x = (toPlayer.x / dist) * spd;
745
+ enemy.vel.y = (toPlayer.y / dist) * spd;
746
+ enemy.vel.z = (toPlayer.z / dist) * spd;
747
+ } else if (dist > 30) {
748
+ const perpendicular = { x: -toPlayer.z, y: 0, z: toPlayer.x };
749
+ const perpLen = Math.sqrt(perpendicular.x ** 2 + perpendicular.z ** 2) || 1;
750
+ enemy.vel.x = (perpendicular.x / perpLen) * spd;
751
+ enemy.vel.z = (perpendicular.z / perpLen) * spd;
752
+ } else {
753
+ enemy.vel.x = -(toPlayer.x / dist) * spd;
754
+ enemy.vel.y = -(toPlayer.y / dist) * spd;
755
+ enemy.vel.z = -(toPlayer.z / dist) * spd;
756
+ }
757
+ }
758
+
759
+ enemy.pos.x += enemy.vel.x;
760
+ enemy.pos.y += enemy.vel.y;
761
+ enemy.pos.z += enemy.vel.z;
762
+
763
+ setPosition(enemy.mesh, enemy.pos.x, enemy.pos.y, enemy.pos.z);
764
+
765
+ // Shoot at player - different fire rates per type
766
+ enemy.shootCooldown -= dt;
767
+ let fireRate =
768
+ enemy.type === 'boss'
769
+ ? 1.0
770
+ : enemy.type === 'ace'
771
+ ? 1.5
772
+ : enemy.type === 'bomber'
773
+ ? 2.5
774
+ : 2 + Math.random();
775
+ if (enemy.shootCooldown <= 0 && dist < 120) {
776
+ enemyShoot(enemy, toPlayer, dist);
777
+ enemy.shootCooldown = fireRate;
778
+ }
779
+ });
780
+ }
781
+
782
+ function enemyShoot(enemy, toPlayer, dist) {
783
+ if (enemy.type === 'boss') {
784
+ // Boss fires spread of 3
785
+ for (let a = -0.1; a <= 0.1; a += 0.1) {
786
+ let dx = toPlayer.x / dist + a;
787
+ let dy = toPlayer.y / dist;
788
+ let dz = toPlayer.z / dist + a;
789
+ createLaser(enemy.pos.x, enemy.pos.y, enemy.pos.z, dx, dy, dz, 'enemy');
790
+ }
791
+ } else {
792
+ createLaser(
793
+ enemy.pos.x,
794
+ enemy.pos.y,
795
+ enemy.pos.z,
796
+ toPlayer.x / dist,
797
+ toPlayer.y / dist,
798
+ toPlayer.z / dist,
799
+ 'enemy'
800
+ );
801
+ }
802
+ }
803
+
804
+ function spawnPickup(x, y, z) {
805
+ let r = Math.random();
806
+ let type, color;
807
+ if (r < 0.4) {
808
+ type = 'missile';
809
+ color = 0xffff00;
810
+ } else if (r < 0.7) {
811
+ type = 'shield';
812
+ color = 0x0088ff;
813
+ } else {
814
+ type = 'energy';
815
+ color = 0x00ff88;
816
+ }
817
+
818
+ let mesh = createSphere(1.5, color, [x, y, z], { material: 'emissive', color, intensity: 3 });
819
+ pickups.push({ pos: { x, y, z }, mesh, type, life: 15 });
820
+ }
821
+
822
+ function updatePickups(dt) {
823
+ for (let i = pickups.length - 1; i >= 0; i--) {
824
+ let p = pickups[i];
825
+ p.life -= dt;
826
+ // Spin pickup
827
+ setRotation(p.mesh, 0, gameTime * 2, 0);
828
+ let py = p.pos.y + Math.sin(gameTime * 3 + i) * 0.5;
829
+ setPosition(p.mesh, p.pos.x, py, p.pos.z);
830
+
831
+ // Check player proximity
832
+ let dist = Math.sqrt(
833
+ (p.pos.x - player.pos.x) ** 2 + (p.pos.y - player.pos.y) ** 2 + (p.pos.z - player.pos.z) ** 2
834
+ );
835
+ if (dist < 8) {
836
+ if (p.type === 'missile') {
837
+ player.missiles = Math.min(player.missiles + 3, 20);
838
+ sfx('coin');
839
+ } else if (p.type === 'shield') {
840
+ player.shields = Math.min(player.maxShields, player.shields + 30);
841
+ sfx('powerup');
842
+ } else if (p.type === 'energy') {
843
+ player.energy = Math.min(player.maxEnergy, player.energy + 40);
844
+ sfx('powerup');
845
+ }
846
+ destroyMesh(p.mesh);
847
+ pickups.splice(i, 1);
848
+ continue;
849
+ }
850
+ if (p.life <= 0) {
851
+ destroyMesh(p.mesh);
852
+ pickups.splice(i, 1);
853
+ }
854
+ }
855
+ }
856
+
857
+ // ============================================
858
+ // PROJECTILE UPDATE
859
+ // ============================================
860
+ function updateProjectiles(dt) {
861
+ for (let i = projectiles.length - 1; i >= 0; i--) {
862
+ const proj = projectiles[i];
863
+
864
+ // Missile homing
865
+ if (proj.isMissile && proj.target && enemies.includes(proj.target)) {
866
+ const toTarget = {
867
+ x: proj.target.pos.x - proj.pos.x,
868
+ y: proj.target.pos.y - proj.pos.y,
869
+ z: proj.target.pos.z - proj.pos.z,
870
+ };
871
+
872
+ const dist = Math.sqrt(toTarget.x ** 2 + toTarget.y ** 2 + toTarget.z ** 2);
873
+ const turnRate = CONFIG.MISSILE_TURN_RATE * dt;
874
+
875
+ proj.vel.x += (toTarget.x / dist) * turnRate;
876
+ proj.vel.y += (toTarget.y / dist) * turnRate;
877
+ proj.vel.z += (toTarget.z / dist) * turnRate;
878
+ }
879
+
880
+ // Update position
881
+ proj.pos.x += proj.vel.x * dt;
882
+ proj.pos.y += proj.vel.y * dt;
883
+ proj.pos.z += proj.vel.z * dt;
884
+
885
+ setPosition(proj.mesh, proj.pos.x, proj.pos.y, proj.pos.z);
886
+
887
+ // Lifetime
888
+ proj.life -= dt;
889
+ if (proj.life <= 0) {
890
+ destroyMesh(proj.mesh);
891
+ projectiles.splice(i, 1);
892
+ continue;
893
+ }
894
+
895
+ // Check collisions
896
+ let hit = false;
897
+
898
+ // Hit enemies
899
+ if (proj.owner === 'player') {
900
+ enemies.forEach((enemy, ei) => {
901
+ const dist = Math.sqrt(
902
+ (proj.pos.x - enemy.pos.x) ** 2 +
903
+ (proj.pos.y - enemy.pos.y) ** 2 +
904
+ (proj.pos.z - enemy.pos.z) ** 2
905
+ );
906
+
907
+ if (dist < enemy.size + 1) {
908
+ enemy.health -= proj.damage;
909
+ hit = true;
910
+ sfx('hit');
911
+
912
+ if (enemy.health <= 0) {
913
+ let expSize = enemy.type === 'boss' ? 12 : enemy.type === 'bomber' ? 7 : 5;
914
+ createExplosion(enemy.pos.x, enemy.pos.y, enemy.pos.z, expSize);
915
+ destroyMesh(enemy.mesh);
916
+ enemies.splice(ei, 1);
917
+ score += enemy.score_val;
918
+ kills++;
919
+ sfx('explosion');
920
+ if (enemy.type === 'boss') bossAlive = false;
921
+ // Drop pickup (40% chance, 80% for boss)
922
+ if (Math.random() < (enemy.type === 'boss' ? 0.8 : 0.4)) {
923
+ spawnPickup(enemy.pos.x, enemy.pos.y, enemy.pos.z);
924
+ }
925
+ }
926
+ }
927
+ });
928
+ }
929
+
930
+ // Hit player
931
+ if (proj.owner === 'enemy') {
932
+ const dist = Math.sqrt(
933
+ (proj.pos.x - player.pos.x) ** 2 +
934
+ (proj.pos.y - player.pos.y) ** 2 +
935
+ (proj.pos.z - player.pos.z) ** 2
936
+ );
937
+
938
+ if (dist < 3) {
939
+ if (player.shields > 0) {
940
+ player.shields -= proj.damage * 10;
941
+ if (player.shields < 0) {
942
+ player.health += player.shields;
943
+ player.shields = 0;
944
+ }
945
+ } else {
946
+ player.health -= proj.damage * 10;
947
+ }
948
+ hit = true;
949
+ triggerShake(shake, 0.3);
950
+ sfx('hit');
951
+ }
952
+ }
953
+
954
+ // Hit asteroids
955
+ asteroids.forEach((asteroid, ai) => {
956
+ const dist = Math.sqrt(
957
+ (proj.pos.x - asteroid.pos.x) ** 2 +
958
+ (proj.pos.y - asteroid.pos.y) ** 2 +
959
+ (proj.pos.z - asteroid.pos.z) ** 2
960
+ );
961
+
962
+ if (dist < asteroid.size) {
963
+ asteroid.health -= proj.damage;
964
+ hit = true;
965
+
966
+ if (asteroid.health <= 0) {
967
+ createExplosion(asteroid.pos.x, asteroid.pos.y, asteroid.pos.z, asteroid.size);
968
+ destroyMesh(asteroid.mesh);
969
+ asteroids.splice(ai, 1);
970
+ score += 10;
971
+
972
+ // Respawn asteroid elsewhere
973
+ setTimeout(() => spawnAsteroid(), 5000);
974
+ }
975
+ }
976
+ });
977
+
978
+ if (hit) {
979
+ destroyMesh(proj.mesh);
980
+ projectiles.splice(i, 1);
981
+ }
982
+ }
983
+ }
984
+
985
+ // ============================================
986
+ // ASTEROID UPDATE
987
+ // ============================================
988
+ function updateAsteroids(dt) {
989
+ asteroids.forEach(asteroid => {
990
+ // Update position
991
+ asteroid.pos.x += asteroid.vel.x * dt;
992
+ asteroid.pos.y += asteroid.vel.y * dt;
993
+ asteroid.pos.z += asteroid.vel.z * dt;
994
+
995
+ // Update rotation
996
+ asteroid.rotation.x += asteroid.rotationSpeed.x * dt;
997
+ asteroid.rotation.y += asteroid.rotationSpeed.y * dt;
998
+ asteroid.rotation.z += asteroid.rotationSpeed.z * dt;
999
+
1000
+ // Wrap around play area
1001
+ const range = CONFIG.ASTEROID_SPAWN_RANGE;
1002
+ if (Math.abs(asteroid.pos.x - player.pos.x) > range) {
1003
+ asteroid.pos.x = player.pos.x + (Math.random() - 0.5) * range;
1004
+ }
1005
+ if (Math.abs(asteroid.pos.z - player.pos.z) > range) {
1006
+ asteroid.pos.z = player.pos.z + (Math.random() - 0.5) * range;
1007
+ }
1008
+
1009
+ // Update mesh
1010
+ setPosition(asteroid.mesh, asteroid.pos.x, asteroid.pos.y, asteroid.pos.z);
1011
+ setRotation(asteroid.mesh, asteroid.rotation.x, asteroid.rotation.y, asteroid.rotation.z);
1012
+ });
1013
+ }
1014
+
1015
+ // ============================================
1016
+ // EXPLOSIONS
1017
+ // ============================================
1018
+ function createExplosion(x, y, z, size) {
1019
+ for (let i = 0; i < size * 3; i++) {
1020
+ const particle = {
1021
+ pos: { x, y, z },
1022
+ vel: {
1023
+ x: (Math.random() - 0.5) * 20,
1024
+ y: (Math.random() - 0.5) * 20,
1025
+ z: (Math.random() - 0.5) * 20,
1026
+ },
1027
+ life: 0.5 + Math.random() * 0.5,
1028
+ mesh: createSphere(0.3, 0xff6600, [x, y, z]),
1029
+ };
1030
+ explosions.push(particle);
1031
+ }
1032
+ }
1033
+
1034
+ function updateExplosions(dt) {
1035
+ for (let i = explosions.length - 1; i >= 0; i--) {
1036
+ const exp = explosions[i];
1037
+
1038
+ exp.pos.x += exp.vel.x * dt;
1039
+ exp.pos.y += exp.vel.y * dt;
1040
+ exp.pos.z += exp.vel.z * dt;
1041
+
1042
+ setPosition(exp.mesh, exp.pos.x, exp.pos.y, exp.pos.z);
1043
+
1044
+ exp.life -= dt;
1045
+ const scale = exp.life;
1046
+ setScale(exp.mesh, scale, scale, scale);
1047
+
1048
+ if (exp.life <= 0) {
1049
+ destroyMesh(exp.mesh);
1050
+ explosions.splice(i, 1);
1051
+ }
1052
+ }
1053
+ }
1054
+
1055
+ // ============================================
1056
+ // CAMERA UPDATE
1057
+ // ============================================
1058
+ function updateCamera(dt) {
1059
+ // First-person camera - follows player ship orientation
1060
+ camera.pos.x = player.pos.x;
1061
+ camera.pos.y = player.pos.y;
1062
+ camera.pos.z = player.pos.z;
1063
+
1064
+ camera.pitch = player.pitch;
1065
+ camera.yaw = player.yaw;
1066
+ camera.roll = player.roll;
1067
+
1068
+ // Camera shake
1069
+ updateShake(shake, dt);
1070
+ const [shakeX, shakeY] = getShakeOffset(shake);
1071
+ camera.pos.x += shakeX;
1072
+ camera.pos.y += shakeY;
1073
+
1074
+ // Calculate look direction
1075
+ const forward = {
1076
+ x: Math.sin(camera.yaw) * Math.cos(camera.pitch),
1077
+ y: Math.sin(camera.pitch),
1078
+ z: -Math.cos(camera.yaw) * Math.cos(camera.pitch),
1079
+ };
1080
+
1081
+ // Set camera
1082
+ setCameraPosition(camera.pos.x, camera.pos.y, camera.pos.z);
1083
+ setCameraTarget(
1084
+ camera.pos.x + forward.x * 100,
1085
+ camera.pos.y + forward.y * 100,
1086
+ camera.pos.z + forward.z * 100
1087
+ );
1088
+
1089
+ // Update cockpit elements to follow camera
1090
+ if (crosshairMesh) {
1091
+ const dist = 15;
1092
+ setPosition(
1093
+ crosshairMesh.horizontal,
1094
+ camera.pos.x + forward.x * dist,
1095
+ camera.pos.y + forward.y * dist,
1096
+ camera.pos.z + forward.z * dist
1097
+ );
1098
+ setPosition(
1099
+ crosshairMesh.vertical,
1100
+ camera.pos.x + forward.x * dist,
1101
+ camera.pos.y + forward.y * dist,
1102
+ camera.pos.z + forward.z * dist
1103
+ );
1104
+ setPosition(
1105
+ crosshairMesh.center,
1106
+ camera.pos.x + forward.x * dist,
1107
+ camera.pos.y + forward.y * dist,
1108
+ camera.pos.z + forward.z * dist
1109
+ );
1110
+ }
1111
+ }
1112
+
1113
+ // ============================================
1114
+ // DRAW / UI
1115
+ // ============================================
1116
+ export function draw() {
1117
+ if (gameState === 'start') {
1118
+ drawStartScreen();
1119
+ return;
1120
+ }
1121
+
1122
+ if (gameState === 'playing') {
1123
+ drawHUD();
1124
+ } else if (gameState === 'waveclear') {
1125
+ drawHUD();
1126
+ rect(120, 100, 400, 60, rgba8(0, 0, 0, 200), true);
1127
+ print(`WAVE ${wave} CLEAR!`, 230, 110, rgba8(0, 255, 100, 255));
1128
+ print(`+${1000 * wave} BONUS +3 MISSILES`, 190, 135, rgba8(255, 200, 0, 255));
1129
+ } else if (gameState === 'paused') {
1130
+ drawHUD();
1131
+ drawPauseScreen();
1132
+ } else if (gameState === 'gameover') {
1133
+ drawGameOver();
1134
+ }
1135
+ }
1136
+
1137
+ function drawStartScreen() {
1138
+ // Background
1139
+ rect(0, 0, 640, 360, rgba8(0, 0, 0, 200), true);
1140
+
1141
+ // Title
1142
+ print('WING COMMANDER', 200, 80, rgba8(255, 255, 0, 255));
1143
+ print('SPACE COMBAT', 220, 110, rgba8(200, 200, 200, 255));
1144
+
1145
+ // Instructions
1146
+ const pulse = Math.sin(gameTime * 3) * 0.5 + 0.5;
1147
+ print('PRESS SPACE OR ENTER TO START', 170, 160, rgba8(255, 255, 255, Math.floor(pulse * 255)));
1148
+
1149
+ // Controls
1150
+ rect(120, 200, 400, 140, rgba8(20, 20, 40, 220), true);
1151
+ rect(120, 200, 400, 140, rgba8(100, 100, 255, 180), false);
1152
+
1153
+ print('CONTROLS:', 270, 215, rgba8(255, 255, 100, 255));
1154
+
1155
+ print('W/S or Arrows = Pitch Up/Down', 140, 240, rgba8(200, 200, 200, 255));
1156
+ print('A/D or Arrows = Yaw Left/Right', 140, 260, rgba8(200, 200, 200, 255));
1157
+ print('Q/E = Strafe Left/Right', 140, 280, rgba8(200, 200, 200, 255));
1158
+ print('SPACE = Fire Lasers', 140, 300, rgba8(200, 200, 200, 255));
1159
+ print('M = Fire Missile', 140, 320, rgba8(200, 200, 200, 255));
1160
+
1161
+ print('SHIFT = Boost', 370, 260, rgba8(200, 200, 200, 255));
1162
+ print('T = Target Lock', 370, 280, rgba8(200, 200, 200, 255));
1163
+ print('ESC = Pause', 370, 300, rgba8(200, 200, 200, 255));
1164
+ }
1165
+
1166
+ function drawHUD() {
1167
+ // Top HUD bar
1168
+ rect(0, 0, 640, 40, rgba8(0, 0, 0, 180), true);
1169
+ rect(0, 0, 640, 1, rgba8(0, 255, 255, 255), true);
1170
+
1171
+ // Score and stats
1172
+ print(`SCORE: ${score.toString().padStart(8, '0')}`, 10, 10, rgba8(0, 255, 255, 255));
1173
+ print(`KILLS: ${kills}`, 10, 25, rgba8(255, 100, 100, 255));
1174
+
1175
+ print(`WAVE: ${wave}`, 250, 10, rgba8(255, 255, 0, 255));
1176
+ print(`ENEMIES: ${enemies.length}`, 250, 25, rgba8(255, 100, 100, 255));
1177
+
1178
+ // Boss health bar
1179
+ if (bossAlive) {
1180
+ let boss = enemies.find(e => e.type === 'boss');
1181
+ if (boss) {
1182
+ print('BOSS', 220, 45, rgba8(255, 204, 0, 255));
1183
+ rect(260, 43, 120, 10, rgba8(50, 50, 0, 255), true);
1184
+ rect(
1185
+ 260,
1186
+ 43,
1187
+ Math.floor((boss.health / boss.maxHealth) * 120),
1188
+ 10,
1189
+ rgba8(255, 204, 0, 255),
1190
+ true
1191
+ );
1192
+ rect(260, 43, 120, 10, rgba8(255, 204, 0, 100), false);
1193
+ }
1194
+ }
1195
+
1196
+ // Health bar
1197
+ print('HULL:', 400, 10, rgba8(255, 255, 255, 255));
1198
+ rect(445, 8, 100, 10, rgba8(50, 0, 0, 255), true);
1199
+ rect(
1200
+ 445,
1201
+ 8,
1202
+ Math.floor((player.health / player.maxHealth) * 100),
1203
+ 10,
1204
+ rgba8(255, 0, 0, 255),
1205
+ true
1206
+ );
1207
+ rect(445, 8, 100, 10, rgba8(255, 0, 0, 100), false);
1208
+
1209
+ // Shield bar
1210
+ print('SHIELDS:', 400, 25, rgba8(255, 255, 255, 255));
1211
+ rect(465, 23, 80, 10, rgba8(0, 0, 50, 255), true);
1212
+ rect(
1213
+ 465,
1214
+ 23,
1215
+ Math.floor((player.shields / player.maxShields) * 80),
1216
+ 10,
1217
+ rgba8(0, 100, 255, 255),
1218
+ true
1219
+ );
1220
+ rect(465, 23, 80, 10, rgba8(0, 100, 255, 100), false);
1221
+
1222
+ // Bottom HUD
1223
+ rect(0, 320, 640, 40, rgba8(0, 0, 0, 180), true);
1224
+ rect(0, 359, 640, 1, rgba8(0, 255, 255, 255), true);
1225
+
1226
+ // Energy
1227
+ print('ENERGY:', 10, 330, rgba8(255, 255, 255, 255));
1228
+ rect(70, 328, 100, 10, rgba8(50, 50, 0, 255), true);
1229
+ rect(
1230
+ 70,
1231
+ 328,
1232
+ Math.floor((player.energy / player.maxEnergy) * 100),
1233
+ 10,
1234
+ rgba8(0, 255, 0, 255),
1235
+ true
1236
+ );
1237
+
1238
+ // Missiles
1239
+ print(`MISSILES: ${player.missiles}`, 10, 345, rgba8(255, 255, 0, 255));
1240
+
1241
+ // Speed indicator
1242
+ const speed = Math.sqrt(player.vel.x ** 2 + player.vel.y ** 2 + player.vel.z ** 2);
1243
+ print(`SPEED: ${Math.floor(speed)}`, 250, 330, rgba8(200, 200, 200, 255));
1244
+
1245
+ // Target lock indicator
1246
+ if (player.targetLocked) {
1247
+ print('TARGET LOCKED', 250, 345, rgba8(255, 0, 0, 255));
1248
+ }
1249
+
1250
+ // Boost indicator
1251
+ if (player.boosting) {
1252
+ print('BOOST', 450, 330, rgba8(255, 100, 0, 255));
1253
+ }
1254
+
1255
+ // Radar (simple)
1256
+ drawRadar();
1257
+ }
1258
+
1259
+ function drawRadar() {
1260
+ const radarX = 560;
1261
+ const radarY = 290;
1262
+ const radarSize = 60;
1263
+
1264
+ // Radar background
1265
+ rect(
1266
+ radarX - radarSize / 2,
1267
+ radarY - radarSize / 2,
1268
+ radarSize,
1269
+ radarSize,
1270
+ rgba8(0, 20, 0, 180),
1271
+ true
1272
+ );
1273
+ rect(
1274
+ radarX - radarSize / 2,
1275
+ radarY - radarSize / 2,
1276
+ radarSize,
1277
+ radarSize,
1278
+ rgba8(0, 255, 0, 100),
1279
+ false
1280
+ );
1281
+
1282
+ // Center (player)
1283
+ rect(radarX - 2, radarY - 2, 4, 4, rgba8(0, 255, 255, 255), true);
1284
+
1285
+ // Enemies on radar
1286
+ enemies.forEach(enemy => {
1287
+ const dx = enemy.pos.x - player.pos.x;
1288
+ const dz = enemy.pos.z - player.pos.z;
1289
+ const dist = Math.sqrt(dx ** 2 + dz ** 2);
1290
+
1291
+ if (dist < CONFIG.RADAR_RANGE) {
1292
+ const scale = radarSize / 2 / CONFIG.RADAR_RANGE;
1293
+ const x = radarX + dx * scale;
1294
+ const y = radarY + dz * scale;
1295
+
1296
+ let dotSize = enemy.type === 'boss' ? 4 : 2;
1297
+ const color =
1298
+ enemy === player.targetLocked
1299
+ ? rgba8(255, 0, 0, 255)
1300
+ : enemy.type === 'boss'
1301
+ ? rgba8(255, 204, 0, 255)
1302
+ : enemy.type === 'ace'
1303
+ ? rgba8(0, 204, 255, 255)
1304
+ : enemy.type === 'bomber'
1305
+ ? rgba8(255, 136, 0, 255)
1306
+ : rgba8(255, 100, 100, 255);
1307
+ rect(Math.floor(x) - dotSize / 2, Math.floor(y) - dotSize / 2, dotSize, dotSize, color, true);
1308
+ }
1309
+ });
1310
+ }
1311
+
1312
+ function drawPauseScreen() {
1313
+ rect(0, 0, 640, 360, rgba8(0, 0, 0, 150), true);
1314
+ print('PAUSED', 280, 160, rgba8(255, 255, 255, 255));
1315
+ print('Press ESC to resume', 230, 190, rgba8(200, 200, 200, 255));
1316
+ }
1317
+
1318
+ function drawGameOver() {
1319
+ rect(0, 0, 640, 360, rgba8(0, 0, 0, 200), true);
1320
+
1321
+ print('GAME OVER', 250, 120, rgba8(255, 50, 50, 255));
1322
+ print(`FINAL SCORE: ${score}`, 240, 160, rgba8(255, 255, 0, 255));
1323
+ print(`KILLS: ${kills}`, 280, 190, rgba8(255, 100, 100, 255));
1324
+ print(`WAVE REACHED: ${wave}`, 250, 220, rgba8(200, 200, 200, 255));
1325
+
1326
+ const pulse = Math.sin(gameTime * 3) * 0.5 + 0.5;
1327
+ print('Press SPACE or ENTER to restart', 200, 270, rgba8(255, 255, 255, Math.floor(pulse * 255)));
1328
+
1329
+ if (isKeyDown('KeyR') || isKeyDown('Enter') || isKeyDown('Space') || isKeyDown(' ')) {
1330
+ init();
1331
+ }
1332
+ }