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,1286 @@
1
+ // STAR COMBAT 64 - True 3D Space Fighter
2
+ // Nintendo 64 / PlayStation style 3D space combat with full GPU acceleration
3
+
4
+ // Screen management
5
+ let gameState = 'start'; // 'start', 'playing', 'gameOver'
6
+ let startScreenTime = 0;
7
+ let uiButtons = [];
8
+
9
+ // Game data with screen management
10
+ let gameData = {
11
+ time: 0,
12
+ score: 0,
13
+ level: 1,
14
+ lives: 3,
15
+ playerShip: null,
16
+ playerBullets: [],
17
+ enemies: [],
18
+ enemyBullets: [],
19
+ powerups: [],
20
+ explosions: [],
21
+ stars: [],
22
+ player: {
23
+ x: 0,
24
+ y: 0,
25
+ z: -5,
26
+ health: 100,
27
+ shield: 100,
28
+ energy: 100,
29
+ fireCooldown: 0,
30
+ weaponLevel: 1,
31
+ },
32
+ inputState: {
33
+ left: false,
34
+ right: false,
35
+ up: false,
36
+ down: false,
37
+ fire: false,
38
+ charge: false,
39
+ },
40
+ };
41
+
42
+ export async function init() {
43
+ // Setup 3D scene with N64-style aesthetics
44
+ setCameraPosition(0, 2, 0);
45
+ setCameraTarget(0, 0, -10);
46
+ setCameraFOV(75);
47
+
48
+ // Setup dramatic space lighting
49
+ setLightDirection(-0.5, -1, -0.8);
50
+ setFog(0x000511, 20, 80);
51
+
52
+ // Enable retro effects + post-processing
53
+ enablePixelation(1);
54
+ enableDithering(true);
55
+ enableBloom(1.0, 0.4, 0.4); // Weapon fire & engine trail
56
+ enableFXAA();
57
+ enableVignette(1.4, 0.88);
58
+
59
+ // Initialize start screen
60
+ initStartScreen();
61
+
62
+ // Setup screen management
63
+ addScreen('title', {
64
+ draw: drawTitleScreen,
65
+ update: updateTitleScreen,
66
+ });
67
+
68
+ addScreen('game', {
69
+ draw: drawGameScreen,
70
+ update: updateGameScreen,
71
+ enter: enterGameScreen,
72
+ exit: exitGameScreen,
73
+ });
74
+
75
+ addScreen('gameover', {
76
+ draw: drawGameOverScreen,
77
+ update: updateGameOverScreen,
78
+ });
79
+
80
+ // Start with title screen
81
+ switchToScreen('title');
82
+ }
83
+
84
+ // === TITLE SCREEN ===
85
+ function drawTitleScreen() {
86
+ // Background gradient
87
+ drawGradientRect(0, 0, 640, 360, rgba8(0, 10, 40, 255), rgba8(0, 0, 20, 255), true);
88
+
89
+ // Title
90
+ setFont('huge');
91
+ setTextAlign('center');
92
+ drawTextShadow('STAR COMBAT 64', 320, 120, rgba8(255, 102, 0, 255), rgba8(0, 0, 0, 255), 4, 1);
93
+
94
+ // Subtitle
95
+ setFont('large');
96
+ drawText('3D Space Fighter', 320, 160, rgba8(0, 255, 255, 255), 1);
97
+
98
+ // Prompt
99
+ setFont('normal');
100
+ const pulse = Math.sin(Date.now() * 0.005) * 0.5 + 0.5;
101
+ drawText(
102
+ 'Press SPACE, ENTER, or A Button',
103
+ 320,
104
+ 200,
105
+ rgba8(255, 255, 0, Math.floor(pulse * 255)),
106
+ 1
107
+ );
108
+
109
+ // Controls
110
+ setFont('small');
111
+ drawText('ARROWS: Move • Z: Fire • X: Charge Shot', 320, 240, rgba8(255, 255, 255, 255), 1);
112
+
113
+ // Draw buttons if they exist
114
+ if (uiButtons && uiButtons.length > 0) {
115
+ drawAllButtons();
116
+ }
117
+ }
118
+
119
+ function updateTitleScreen() {
120
+ // Check for Space key, Enter, or gamepad button
121
+ if (isKeyPressed('Space') || isKeyPressed('Enter') || btnp(4) || btnp(12)) {
122
+ switchToScreen('game');
123
+ }
124
+
125
+ // Also update buttons if they exist
126
+ if (uiButtons && uiButtons.length > 0) {
127
+ updateAllButtons();
128
+ }
129
+ }
130
+
131
+ // === GAME SCREEN ===
132
+ async function enterGameScreen() {
133
+ // Reset game state
134
+ gameData.score = 0;
135
+ gameData.level = 1;
136
+ gameData.lives = 3;
137
+ gameData.time = 0;
138
+
139
+ // Reset player
140
+ gameData.player = {
141
+ x: 0,
142
+ y: 0,
143
+ z: -5,
144
+ health: 100,
145
+ shield: 100,
146
+ energy: 100,
147
+ fireCooldown: 0,
148
+ weaponLevel: 1,
149
+ };
150
+
151
+ gameData.flags = { bossActive: false, waveSpawning: false, bossPhase: 0 };
152
+ gameData.combo = 0;
153
+ gameData.comboTimer = 0;
154
+
155
+ // Clear arrays
156
+ gameData.playerBullets = [];
157
+ gameData.enemies = [];
158
+ gameData.enemyBullets = [];
159
+ gameData.powerups = [];
160
+ gameData.explosions = [];
161
+ gameData.stars = [];
162
+
163
+ // Create player ship - sleek fighter
164
+ gameData.playerShip = createPlayerShip();
165
+
166
+ // Create star field environment
167
+ await createStarField();
168
+
169
+ // Create space environment
170
+ await createSpaceEnvironment();
171
+
172
+ // Spawn initial wave
173
+ spawnEnemyWave();
174
+ }
175
+
176
+ function drawGameScreen() {
177
+ // 3D scene is automatically rendered by GPU backend
178
+ // Draw UI overlay using 2D API
179
+ drawUI();
180
+ }
181
+
182
+ function updateGameScreen(dt) {
183
+ gameData.time += dt;
184
+
185
+ updateInput(dt);
186
+ updatePlayer(dt);
187
+ updateBullets(dt);
188
+ updateEnemies(dt);
189
+ updatePowerups(dt);
190
+ updateExplosions(dt);
191
+ updateStarField(dt);
192
+ checkCollisions(dt);
193
+ updateGameLogic(dt);
194
+
195
+ updateCamera(dt);
196
+
197
+ // Check game over
198
+ if (gameData.lives <= 0 || gameData.player.health <= 0) {
199
+ switchToScreen('gameover');
200
+ }
201
+ }
202
+
203
+ function exitGameScreen() {
204
+ // Clean up 3D objects
205
+ if (gameData.playerShip) {
206
+ if (gameData.playerShip.body) destroyMesh(gameData.playerShip.body);
207
+ if (gameData.playerShip.leftWing) destroyMesh(gameData.playerShip.leftWing);
208
+ if (gameData.playerShip.rightWing) destroyMesh(gameData.playerShip.rightWing);
209
+ if (gameData.playerShip.cockpit) destroyMesh(gameData.playerShip.cockpit);
210
+ }
211
+
212
+ // Clean up all other 3D objects
213
+ [
214
+ ...gameData.playerBullets,
215
+ ...gameData.enemies,
216
+ ...gameData.enemyBullets,
217
+ ...gameData.powerups,
218
+ ...gameData.explosions,
219
+ ...gameData.stars,
220
+ ].forEach(obj => {
221
+ if (obj.mesh) destroyMesh(obj.mesh);
222
+ });
223
+ }
224
+
225
+ // === GAME OVER SCREEN ===
226
+ function drawGameOverScreen() {
227
+ // Dark red background
228
+ drawGradientRect(0, 0, 640, 360, rgba8(40, 0, 0, 255), rgba8(20, 0, 0, 255), true);
229
+
230
+ // Mission Failed
231
+ setFont('huge');
232
+ setTextAlign('center');
233
+ drawTextShadow('MISSION FAILED', 320, 120, rgba8(255, 0, 0, 255), rgba8(100, 0, 0, 255), 4, 1);
234
+
235
+ // Stats
236
+ setFont('large');
237
+ drawText(`Final Score: ${gameData.score}`, 320, 170, rgba8(255, 255, 255, 255), 1);
238
+ drawText(`Level Reached: ${gameData.level}`, 320, 200, rgba8(255, 255, 255, 255), 1);
239
+
240
+ // Prompts
241
+ setFont('normal');
242
+ const pulse = Math.sin(Date.now() * 0.005) * 0.5 + 0.5;
243
+ drawText('Press SPACE to try again', 320, 260, rgba8(0, 255, 255, Math.floor(pulse * 255)), 1);
244
+ drawText('Press ESC for title screen', 320, 290, rgba8(0, 255, 255, 200), 1);
245
+ }
246
+
247
+ function updateGameOverScreen() {
248
+ if (isKeyPressed(' ')) {
249
+ switchToScreen('game');
250
+ } else if (isKeyPressed('Escape')) {
251
+ switchToScreen('title');
252
+ }
253
+ }
254
+
255
+ function initStartScreen() {
256
+ uiButtons = [];
257
+
258
+ uiButtons.push(
259
+ createButton(
260
+ centerX(240),
261
+ 150,
262
+ 240,
263
+ 60,
264
+ '🚀 LAUNCH FIGHTER',
265
+ () => {
266
+ gameState = 'playing';
267
+ switchToScreen('game');
268
+ },
269
+ {
270
+ normalColor: rgba8(255, 100, 0, 255),
271
+ hoverColor: rgba8(255, 130, 30, 255),
272
+ pressedColor: rgba8(220, 70, 0, 255),
273
+ }
274
+ )
275
+ );
276
+
277
+ uiButtons.push(
278
+ createButton(
279
+ centerX(200),
280
+ 355,
281
+ 200,
282
+ 45,
283
+ '🎮 CONTROLS',
284
+ () => {
285
+ // Controls info shown on screen
286
+ },
287
+ {
288
+ normalColor: rgba8(0, 255, 255, 255),
289
+ hoverColor: rgba8(60, 255, 255, 255),
290
+ pressedColor: rgba8(0, 220, 220, 255),
291
+ }
292
+ )
293
+ );
294
+ }
295
+
296
+ export function update(dt) {
297
+ if (gameState === 'start') {
298
+ startScreenTime += dt;
299
+ updateAllButtons();
300
+ return;
301
+ }
302
+ // Screen management handles updates
303
+ }
304
+
305
+ export function draw() {
306
+ if (gameState === 'start') {
307
+ drawStartScreen();
308
+ return;
309
+ }
310
+ // Screen management handles drawing
311
+ }
312
+
313
+ function drawStartScreen() {
314
+ // Space gradient background
315
+ drawGradientRect(0, 0, 640, 360, rgba8(10, 5, 25, 235), rgba8(5, 2, 12, 250), true);
316
+
317
+ // Animated title
318
+ setFont('huge');
319
+ setTextAlign('center');
320
+ const pulse = Math.sin(startScreenTime * 4) * 0.3 + 0.7;
321
+ const fireColor = rgba8(255, Math.floor(pulse * 150), 0, 255);
322
+
323
+ const shake = Math.sin(startScreenTime * 15) * 2;
324
+ drawTextShadow('STAR', 320 + shake, 50, fireColor, rgba8(0, 0, 0, 255), 7, 1);
325
+ drawTextShadow('COMBAT 64', 320, 105, rgba8(0, 255, 255, 255), rgba8(0, 0, 0, 255), 7, 1);
326
+
327
+ // Subtitle
328
+ setFont('large');
329
+ const glow = Math.sin(startScreenTime * 5) * 0.2 + 0.8;
330
+ drawTextOutline(
331
+ '🚀 3D Space Fighter 🚀',
332
+ 320,
333
+ 165,
334
+ rgba8(255, 255, 0, Math.floor(glow * 255)),
335
+ rgba8(0, 0, 0, 255),
336
+ 1
337
+ );
338
+
339
+ // Info panel
340
+ const panel = createPanel(centerX(480), 210, 480, 190, {
341
+ bgColor: rgba8(15, 10, 30, 215),
342
+ borderColor: rgba8(255, 100, 0, 255),
343
+ borderWidth: 3,
344
+ shadow: true,
345
+ gradient: true,
346
+ gradientColor: rgba8(25, 15, 45, 215),
347
+ });
348
+ drawPanel(panel);
349
+
350
+ setFont('normal');
351
+ setTextAlign('center');
352
+ drawText('MISSION BRIEFING', 320, 230, rgba8(255, 100, 0, 255), 1);
353
+
354
+ setFont('small');
355
+ drawText('🚀 Pilot advanced fighter spacecraft', 320, 255, uiColors.light, 1);
356
+ drawText('🚀 Destroy enemy forces and collect powerups', 320, 270, uiColors.light, 1);
357
+ drawText('🚀 Use charge shots for devastating attacks', 320, 285, uiColors.light, 1);
358
+ drawText('🚀 Nintendo 64 / PlayStation style combat', 320, 300, uiColors.light, 1);
359
+
360
+ setFont('tiny');
361
+ drawText('ARROWS: Move | Z: Fire | X: Charge Shot', 320, 320, uiColors.secondary, 1);
362
+
363
+ // Draw buttons
364
+ drawAllButtons();
365
+
366
+ // Pulsing prompt
367
+ const alpha = Math.floor((Math.sin(startScreenTime * 6) * 0.5 + 0.5) * 255);
368
+ setFont('normal');
369
+ drawText('🚀 PREPARE FOR COMBAT 🚀', 320, 430, rgba8(255, 150, 0, alpha), 1);
370
+
371
+ // Info
372
+ setFont('tiny');
373
+ drawText('3D Space Combat Simulator', 320, 345, rgba8(150, 150, 200, 150), 1);
374
+ }
375
+
376
+ function createPlayerShip() {
377
+ // Create main body
378
+ const body = createCube(1.5, 0x4488ff, [0, 0, -5]);
379
+ setScale(body, 1.5, 0.6, 2.5);
380
+
381
+ // Create wings
382
+ const leftWing = createCube(1.8, 0x2266dd, [-1.2, 0, -5]);
383
+ setScale(leftWing, 1.8, 0.3, 1.2);
384
+
385
+ const rightWing = createCube(1.8, 0x2266dd, [1.2, 0, -5]);
386
+ setScale(rightWing, 1.8, 0.3, 1.2);
387
+
388
+ // Create cockpit
389
+ const cockpit = createSphere(0.4, 0x88ccff, [0, 0.3, -4.5]);
390
+ setScale(cockpit, 0.8, 0.6, 1.0);
391
+
392
+ return { body, leftWing, rightWing, cockpit };
393
+ }
394
+
395
+ async function createStarField() {
396
+ gameData.stars = [];
397
+ const stars = gameData.stars;
398
+
399
+ // Create 3D starfield
400
+ for (let i = 0; i < 200; i++) {
401
+ const star = createSphere(0.05, 0xffffff, [
402
+ (Math.random() - 0.5) * 100,
403
+ (Math.random() - 0.5) * 60,
404
+ -Math.random() * 100 - 10,
405
+ ]);
406
+
407
+ const brightness = Math.random();
408
+
409
+ // Vary star sizes and colors
410
+ setScale(star, brightness * 2 + 0.5);
411
+
412
+ stars.push({
413
+ mesh: star,
414
+ originalZ: getPosition(star)[2],
415
+ speed: 2 + Math.random() * 4,
416
+ twinkle: Math.random() * Math.PI * 2,
417
+ });
418
+ }
419
+ }
420
+
421
+ async function createSpaceEnvironment() {
422
+ // Create distant nebula planes
423
+ for (let i = 0; i < 5; i++) {
424
+ const nebula = createPlane(40, 25, 0x220033 + i * 0x001122, [
425
+ (Math.random() - 0.5) * 60,
426
+ (Math.random() - 0.5) * 30,
427
+ -60 - i * 10,
428
+ ]);
429
+ setRotation(nebula, Math.random() * 0.5, Math.random() * 0.5, Math.random() * 6.28);
430
+ }
431
+
432
+ // Create space station or planet in distance
433
+ createSphere(8, 0x664422, [30, -15, -70]);
434
+
435
+ // Add some space debris
436
+ for (let i = 0; i < 10; i++) {
437
+ const debris = createCube(0.3, 0x444444, [
438
+ (Math.random() - 0.5) * 80,
439
+ (Math.random() - 0.5) * 40,
440
+ -20 - Math.random() * 40,
441
+ ]);
442
+ setRotation(debris, Math.random() * 6.28, Math.random() * 6.28, Math.random() * 6.28);
443
+ }
444
+ }
445
+
446
+ function updateInput(_dt) {
447
+ const inputState = gameData.inputState;
448
+ inputState.left = btn(0);
449
+ inputState.right = btn(1);
450
+ inputState.up = btn(2);
451
+ inputState.down = btn(3);
452
+ inputState.charge = btn(4); // Z
453
+ inputState.fire = btn(5); // X
454
+ }
455
+
456
+ function updatePlayer(dt) {
457
+ // Update player position
458
+ const speed = 12 * dt;
459
+ const inputState = gameData.inputState;
460
+ const player = gameData.player;
461
+ const playerShip = gameData.playerShip;
462
+
463
+ if (inputState.left && player.x > -12) player.x -= speed;
464
+ if (inputState.right && player.x < 12) player.x += speed;
465
+ if (inputState.up && player.y < 8) player.y += speed;
466
+ if (inputState.down && player.y > -6) player.y -= speed;
467
+
468
+ // Update ship positions
469
+ setPosition(playerShip.body, player.x, player.y, player.z);
470
+ setPosition(playerShip.leftWing, player.x - 1.2, player.y, player.z);
471
+ setPosition(playerShip.rightWing, player.x + 1.2, player.y, player.z);
472
+ setPosition(playerShip.cockpit, player.x, player.y + 0.3, player.z + 0.5);
473
+
474
+ // Tilt ship based on movement
475
+ const tiltX = inputState.up ? 0.2 : inputState.down ? -0.2 : 0;
476
+ const tiltZ = inputState.left ? 0.3 : inputState.right ? -0.3 : 0;
477
+
478
+ setRotation(playerShip.body, tiltX, 0, tiltZ);
479
+ setRotation(playerShip.leftWing, tiltX, 0, tiltZ);
480
+ setRotation(playerShip.rightWing, tiltX, 0, tiltZ);
481
+
482
+ // Handle firing
483
+ player.fireCooldown -= dt;
484
+
485
+ if (inputState.fire && player.fireCooldown <= 0) {
486
+ fireBullet('normal');
487
+ player.fireCooldown = 0.15;
488
+ }
489
+
490
+ if (inputState.charge && player.energy >= 20 && player.fireCooldown <= 0) {
491
+ fireBullet('charged');
492
+ player.energy -= 20;
493
+ player.fireCooldown = 0.4;
494
+ }
495
+
496
+ // Regenerate energy and shield
497
+ if (player.energy < 100) player.energy += 30 * dt;
498
+ if (player.shield < 100) player.shield += 15 * dt;
499
+
500
+ // Engine glow effect - animate engine exhaust
501
+ const gameTime = gameData.time;
502
+ rotateMesh(playerShip.body, 0, 0, Math.sin(gameTime * 20) * 0.02);
503
+ }
504
+
505
+ function fireBullet(type) {
506
+ const player = gameData.player;
507
+ const playerBullets = gameData.playerBullets;
508
+
509
+ if (type === 'charged') {
510
+ const bullet = {
511
+ type: type,
512
+ damage: 3,
513
+ speed: 25,
514
+ life: 3.0,
515
+ mesh: createSphere(0.15, 0x00ffff, [player.x, player.y, player.z + 1]),
516
+ };
517
+ setScale(bullet.mesh, 1.5);
518
+ playerBullets.push(bullet);
519
+ sfx('explosion');
520
+ return;
521
+ }
522
+
523
+ const level = player.weaponLevel;
524
+ const positions = [];
525
+
526
+ if (level === 1) {
527
+ positions.push([player.x, player.y, player.z + 1]);
528
+ } else if (level === 2) {
529
+ positions.push([player.x - 0.5, player.y, player.z + 1]);
530
+ positions.push([player.x + 0.5, player.y, player.z + 1]);
531
+ } else {
532
+ positions.push([player.x, player.y, player.z + 1.2]);
533
+ positions.push([player.x - 0.8, player.y, player.z + 0.8]);
534
+ positions.push([player.x + 0.8, player.y, player.z + 0.8]);
535
+ }
536
+
537
+ positions.forEach(pos => {
538
+ const bullet = {
539
+ type: type,
540
+ damage: 1,
541
+ speed: 25,
542
+ life: 3.0,
543
+ mesh: createCube(0.1, 0xffff00, pos),
544
+ };
545
+ setScale(bullet.mesh, 0.3, 0.3, 1.0);
546
+ playerBullets.push(bullet);
547
+ });
548
+ sfx('laser');
549
+ }
550
+
551
+ function spawnEnemyWave() {
552
+ const level = gameData.level;
553
+
554
+ if (level > 0 && level % 5 === 0 && !gameData.flags.bossActive) {
555
+ // Boss wave!
556
+ spawnEnemy(0, 6, -35, 'boss');
557
+ gameData.flags.bossActive = true;
558
+ gameData.flags.bossPhase = 0;
559
+ return;
560
+ }
561
+ gameData.flags.bossActive = false;
562
+
563
+ const waveSize = 4 + level * 2;
564
+ const formations = ['line', 'V', 'diamond', 'circle'];
565
+ const formation = formations[level % formations.length];
566
+
567
+ for (let i = 0; i < waveSize; i++) {
568
+ let x, y, z;
569
+
570
+ switch (formation) {
571
+ case 'line':
572
+ x = (i - waveSize / 2) * 3;
573
+ y = 6;
574
+ z = -25;
575
+ break;
576
+ case 'V':
577
+ x = (i - waveSize / 2) * 2;
578
+ y = 6 - Math.abs(i - waveSize / 2) * 0.5;
579
+ z = -25;
580
+ break;
581
+ case 'diamond': {
582
+ const angle = (i / waveSize) * Math.PI * 2;
583
+ x = Math.cos(angle) * 8;
584
+ y = Math.sin(angle) * 4 + 6;
585
+ z = -25;
586
+ break;
587
+ }
588
+ case 'circle': {
589
+ const circleAngle = (i / waveSize) * Math.PI * 2;
590
+ x = Math.cos(circleAngle) * 6;
591
+ y = Math.sin(circleAngle) * 3 + 6;
592
+ z = -25;
593
+ break;
594
+ }
595
+ }
596
+
597
+ let typeToSpawn = formation;
598
+ if (level >= 2 && Math.random() < 0.25) {
599
+ typeToSpawn = 'fast';
600
+ } else if (level >= 3 && Math.random() < 0.15) {
601
+ typeToSpawn = 'tank';
602
+ }
603
+
604
+ spawnEnemy(x, y, z, typeToSpawn);
605
+ }
606
+ }
607
+
608
+ function spawnEnemy(x, y, z, type) {
609
+ const level = gameData.level;
610
+ const enemies = gameData.enemies;
611
+
612
+ let body,
613
+ engine,
614
+ parts = null;
615
+ let health = 2 + level;
616
+ let maxHealth = health;
617
+ let vz = 8;
618
+ let vy = 0;
619
+ let vx = 0;
620
+ let fireCooldown = Math.random() * 2;
621
+
622
+ if (type === 'boss') {
623
+ health = 50 + level * 10;
624
+ maxHealth = health;
625
+ vz = 2; // Slow incoming speed
626
+ body = createCube(3.0, 0xff0000, [x, y, z]);
627
+ const wingL = createCube(1.5, 0x333333, [x - 2, y, z]);
628
+ const wingR = createCube(1.5, 0x333333, [x + 2, y, z]);
629
+ parts = { body, wingL, wingR };
630
+ } else if (type === 'fast') {
631
+ health = 1 + Math.floor(level / 2);
632
+ vz = 14;
633
+ body = createCube(0.4, 0xffaa00, [x, y, z]);
634
+ engine = createSphere(0.2, 0xffffff, [x, y, z - 0.5]);
635
+ parts = { body, engine };
636
+ } else if (type === 'tank') {
637
+ health = 10 + level * 3;
638
+ vz = 4;
639
+ body = createCube(1.2, 0xff00ff, [x, y, z]);
640
+ engine = createSphere(0.5, 0x5500aa, [x, y, z - 0.8]);
641
+ parts = { body, engine };
642
+ } else {
643
+ // Normal / formations
644
+ body = createCube(0.6, 0xff4444, [x, y, z]);
645
+ engine = createSphere(0.3, 0xff8800, [x, y, z - 0.5]);
646
+ parts = { body, engine };
647
+ }
648
+
649
+ const enemy = {
650
+ type: type,
651
+ health: health,
652
+ mesh: parts,
653
+ x: x,
654
+ y: y,
655
+ z: z,
656
+ vx: vx,
657
+ vy: vy,
658
+ vz: vz,
659
+ fireCooldown: fireCooldown,
660
+ behavior: type,
661
+ alive: true,
662
+ timer: 0,
663
+ hitFlash: 0,
664
+ maxHealth: maxHealth,
665
+ };
666
+
667
+ enemies.push(enemy);
668
+ }
669
+
670
+ function updateBullets(dt) {
671
+ const playerBullets = gameData.playerBullets;
672
+ const enemyBullets = gameData.enemyBullets;
673
+ const gameTime = gameData.time;
674
+
675
+ // Update player bullets
676
+ for (let i = playerBullets.length - 1; i >= 0; i--) {
677
+ const bullet = playerBullets[i];
678
+ bullet.life -= dt;
679
+
680
+ const pos = getPosition(bullet.mesh);
681
+ pos[2] -= bullet.speed * dt;
682
+ setPosition(bullet.mesh, pos[0], pos[1], pos[2]);
683
+
684
+ // Add bullet glow animation
685
+ if (bullet.type === 'charged') {
686
+ const glow = 1.2 + Math.sin(gameTime * 10) * 0.3;
687
+ setScale(bullet.mesh, glow);
688
+ }
689
+
690
+ if (bullet.life <= 0 || pos[2] < -50) {
691
+ destroyMesh(bullet.mesh);
692
+ playerBullets.splice(i, 1);
693
+ }
694
+ }
695
+
696
+ // Update enemy bullets
697
+ for (let i = enemyBullets.length - 1; i >= 0; i--) {
698
+ const bullet = enemyBullets[i];
699
+ bullet.life -= dt;
700
+
701
+ const pos = getPosition(bullet.mesh);
702
+ pos[2] += bullet.speed * dt;
703
+ pos[0] += (bullet.vx || 0) * bullet.speed * dt;
704
+ setPosition(bullet.mesh, pos[0], pos[1], pos[2]);
705
+
706
+ if (bullet.life <= 0 || pos[2] > 5) {
707
+ destroyMesh(bullet.mesh);
708
+ enemyBullets.splice(i, 1);
709
+ }
710
+ }
711
+ }
712
+
713
+ function updateEnemies(dt) {
714
+ const enemies = gameData.enemies;
715
+ const gameTime = gameData.time;
716
+ const player = gameData.player;
717
+ let lives = gameData.lives;
718
+
719
+ for (let i = enemies.length - 1; i >= 0; i--) {
720
+ const enemy = enemies[i];
721
+ if (!enemy.alive) continue;
722
+
723
+ enemy.timer += dt;
724
+ enemy.fireCooldown -= dt;
725
+
726
+ // Hit flash decay
727
+ if (enemy.hitFlash > 0) enemy.hitFlash -= dt * 4;
728
+
729
+ if (enemy.type === 'boss') {
730
+ _updateBoss(enemy, dt, player, gameTime);
731
+ } else {
732
+ // Advance toward player
733
+ enemy.z += enemy.vz * dt;
734
+
735
+ // Smart AI behaviors based on type
736
+ if (enemy.type === 'fast') {
737
+ // Fast enemies: strafe toward player, then dodge away
738
+ const dx = player.x - enemy.x;
739
+ const approach = enemy.z > -18 ? 1 : 0;
740
+ enemy.vx += (dx * 0.8 + Math.sin(gameTime * 6 + i * 3) * 4) * dt * 3;
741
+ enemy.vx *= 0.95; // Damping
742
+ enemy.vy = Math.sin(gameTime * 4 + i) * 2 * approach;
743
+ } else if (enemy.type === 'tank') {
744
+ // Tank enemies: slow, steady advance, aim at player, fire often
745
+ const dx = player.x - enemy.x;
746
+ enemy.vx = dx * 0.3;
747
+ enemy.vy = Math.sin(gameTime * 0.5 + i) * 0.5;
748
+ } else {
749
+ // Formation enemies: follow formation but drift toward player
750
+ const dx = player.x - enemy.x;
751
+ switch (enemy.behavior) {
752
+ case 'line':
753
+ enemy.vx = Math.sin(gameTime + i) * 2 + dx * 0.1;
754
+ break;
755
+ case 'V':
756
+ enemy.vx = Math.sin(gameTime * 2 + i) * 3 + dx * 0.15;
757
+ enemy.vy = Math.cos(gameTime + i) * 1;
758
+ break;
759
+ case 'circle':
760
+ enemy.vx = Math.cos(gameTime + i * 2) * 4;
761
+ enemy.vy = Math.sin(gameTime + i * 2) * 2;
762
+ break;
763
+ case 'diamond':
764
+ enemy.vx = Math.sin(gameTime * 1.5 + i) * 3 + dx * 0.2;
765
+ enemy.vy = Math.cos(gameTime * 0.8 + i) * 1.5;
766
+ break;
767
+ }
768
+ }
769
+
770
+ enemy.x += enemy.vx * dt;
771
+ enemy.y += enemy.vy * dt;
772
+
773
+ // Clamp to play area
774
+ enemy.x = Math.max(-14, Math.min(14, enemy.x));
775
+ enemy.y = Math.max(-8, Math.min(10, enemy.y));
776
+
777
+ // Update mesh positions
778
+ if (enemy.mesh.body) setPosition(enemy.mesh.body, enemy.x, enemy.y, enemy.z);
779
+ if (enemy.mesh.engine) setPosition(enemy.mesh.engine, enemy.x, enemy.y, enemy.z - 0.5);
780
+
781
+ // Rotate enemy ships
782
+ if (enemy.type === 'fast') {
783
+ if (enemy.mesh.body) rotateMesh(enemy.mesh.body, 0, dt * 5, dt * 5);
784
+ } else {
785
+ if (enemy.mesh.body) rotateMesh(enemy.mesh.body, 0, dt * 2, 0);
786
+ }
787
+ if (enemy.mesh.engine) rotateMesh(enemy.mesh.engine, 0, dt * 4, 0);
788
+
789
+ // Enemy firing — smarter targeting
790
+ if (enemy.fireCooldown <= 0 && enemy.z > -20) {
791
+ const fireChance = enemy.type === 'tank' ? 0.8 : 0.3;
792
+ if (Math.random() < fireChance * dt) {
793
+ // Aim toward player with some spread
794
+ const spread = enemy.type === 'tank' ? 0.5 : 1.5;
795
+ fireEnemyBullet(enemy.x, enemy.y, enemy.z, player.x + (Math.random() - 0.5) * spread);
796
+ enemy.fireCooldown =
797
+ enemy.type === 'tank' ? 0.8 + Math.random() * 0.5 : 1.5 + Math.random();
798
+ }
799
+ }
800
+ }
801
+
802
+ // Remove enemies that passed player
803
+ if (enemy.z > 5) {
804
+ if (enemy.mesh.body) destroyMesh(enemy.mesh.body);
805
+ if (enemy.mesh.engine) destroyMesh(enemy.mesh.engine);
806
+ if (enemy.mesh.wingL) destroyMesh(enemy.mesh.wingL);
807
+ if (enemy.mesh.wingR) destroyMesh(enemy.mesh.wingR);
808
+
809
+ enemies.splice(i, 1);
810
+
811
+ if (enemy.type === 'boss') {
812
+ gameData.flags.bossActive = false;
813
+ } else {
814
+ lives--;
815
+ }
816
+ }
817
+ }
818
+
819
+ gameData.lives = lives;
820
+ }
821
+
822
+ // Boss with multi-phase attack patterns
823
+ function _updateBoss(boss, dt, player, gameTime) {
824
+ boss.z += boss.vz * dt;
825
+
826
+ if (boss.z > -15) {
827
+ boss.vz = 0;
828
+
829
+ // Phase transitions based on health percentage
830
+ const healthPct = boss.health / boss.maxHealth;
831
+ let phase = 0;
832
+ if (healthPct < 0.33) phase = 2;
833
+ else if (healthPct < 0.66) phase = 1;
834
+ gameData.flags.bossPhase = phase;
835
+
836
+ switch (phase) {
837
+ case 0: // Phase 1: Slow strafe, triple shot
838
+ boss.x = Math.sin(boss.timer * 0.5) * 8;
839
+ boss.y = 6 + Math.sin(boss.timer * 0.3) * 2;
840
+ if (boss.fireCooldown <= 0) {
841
+ for (let a = -1; a <= 1; a++) {
842
+ fireEnemyBullet(boss.x + a * 2, boss.y, boss.z + 2, player.x);
843
+ }
844
+ boss.fireCooldown = 1.0;
845
+ }
846
+ break;
847
+
848
+ case 1: // Phase 2: Fast strafe, spread shot + aimed shot
849
+ boss.x = Math.sin(boss.timer * 1.2) * 10;
850
+ boss.y = 6 + Math.cos(boss.timer * 0.8) * 3;
851
+ if (boss.fireCooldown <= 0) {
852
+ // Spread fan
853
+ for (let a = -2; a <= 2; a++) {
854
+ fireEnemyBullet(boss.x + a * 1.5, boss.y, boss.z + 2, player.x + a * 3);
855
+ }
856
+ // Aimed shot
857
+ fireEnemyBullet(boss.x, boss.y - 1, boss.z + 2, player.x);
858
+ boss.fireCooldown = 0.7;
859
+ }
860
+ break;
861
+
862
+ case 2: // Phase 3: Erratic movement, rapid fire + sweep
863
+ boss.x = Math.sin(boss.timer * 2) * 8 + Math.cos(boss.timer * 3.7) * 3;
864
+ boss.y = 5 + Math.sin(boss.timer * 1.5) * 4;
865
+ if (boss.fireCooldown <= 0) {
866
+ // Sweep pattern
867
+ const sweepAngle = boss.timer * 4;
868
+ for (let a = 0; a < 3; a++) {
869
+ const sx = boss.x + Math.cos(sweepAngle + a * 2) * 4;
870
+ fireEnemyBullet(sx, boss.y, boss.z + 2, player.x);
871
+ }
872
+ boss.fireCooldown = 0.4;
873
+ }
874
+ break;
875
+ }
876
+ }
877
+
878
+ setPosition(boss.mesh.body, boss.x, boss.y, boss.z);
879
+ setPosition(boss.mesh.wingL, boss.x - 2, boss.y, boss.z);
880
+ setPosition(boss.mesh.wingR, boss.x + 2, boss.y, boss.z);
881
+ setRotation(boss.mesh.body, boss.timer * 0.5, boss.timer, 0);
882
+ }
883
+
884
+ function fireEnemyBullet(x, y, z, targetX) {
885
+ const enemyBullets = gameData.enemyBullets;
886
+
887
+ // Slight horizontal tracking toward target
888
+ const dx = targetX !== undefined ? (targetX - x) * 0.05 : 0;
889
+
890
+ const bullet = {
891
+ speed: 25 * 0.7,
892
+ vx: dx,
893
+ mesh: createCube(0.08, 0xff4444, [x, y, z]),
894
+ life: 2.0,
895
+ };
896
+
897
+ setScale(bullet.mesh, 0.2, 0.2, 0.8);
898
+ enemyBullets.push(bullet);
899
+ }
900
+
901
+ function updatePowerups(dt) {
902
+ const powerups = gameData.powerups;
903
+
904
+ // Spawn random powerups
905
+ if (Math.random() < 0.01 * dt && powerups.length < 3) {
906
+ spawnPowerup();
907
+ }
908
+
909
+ for (let i = powerups.length - 1; i >= 0; i--) {
910
+ const powerup = powerups[i];
911
+ powerup.z += powerup.speed * dt;
912
+ powerup.rotationY += dt * 3;
913
+
914
+ setPosition(powerup.mesh, powerup.x, powerup.y, powerup.z);
915
+ setRotation(powerup.mesh, 0, powerup.rotationY, 0);
916
+
917
+ if (powerup.z > 5) {
918
+ destroyMesh(powerup.mesh);
919
+ powerups.splice(i, 1);
920
+ }
921
+ }
922
+ }
923
+
924
+ const powerupColors = {
925
+ health: 0x00ff00,
926
+ shield: 0x0088ff,
927
+ weapon: 0xffff00,
928
+ energy: 0xff00ff,
929
+ };
930
+
931
+ function spawnPowerup() {
932
+ const types = ['health', 'shield', 'weapon', 'energy'];
933
+ const type = types[Math.floor(Math.random() * types.length)];
934
+ spawnPowerupAt((Math.random() - 0.5) * 20, (Math.random() - 0.5) * 12, -30, type);
935
+ }
936
+
937
+ function spawnPowerupAt(x, y, z, type) {
938
+ const powerups = gameData.powerups;
939
+ const powerup = {
940
+ type: type,
941
+ x: x,
942
+ y: y,
943
+ z: z,
944
+ speed: 8 * 0.5,
945
+ rotationY: 0,
946
+ mesh: createCube(0.8, powerupColors[type] || 0xffffff, [x, y, z]),
947
+ };
948
+ powerups.push(powerup);
949
+ }
950
+
951
+ function updateExplosions(dt) {
952
+ const explosions = gameData.explosions;
953
+
954
+ for (let i = explosions.length - 1; i >= 0; i--) {
955
+ const explosion = explosions[i];
956
+ explosion.life -= dt;
957
+ explosion.scale += dt * 3;
958
+
959
+ setScale(explosion.mesh, explosion.scale);
960
+
961
+ if (explosion.life <= 0) {
962
+ destroyMesh(explosion.mesh);
963
+ explosions.splice(i, 1);
964
+ }
965
+ }
966
+ }
967
+
968
+ function createExplosion(x, y, z) {
969
+ const explosions = gameData.explosions;
970
+
971
+ const explosion = {
972
+ mesh: createSphere(0.5, 0xff6600, [x, y, z]),
973
+ life: 0.5,
974
+ scale: 0.1,
975
+ };
976
+
977
+ explosions.push(explosion);
978
+ }
979
+
980
+ function updateStarField(dt) {
981
+ const stars = gameData.stars;
982
+
983
+ for (const star of stars) {
984
+ const pos = getPosition(star.mesh);
985
+ pos[2] += star.speed * dt;
986
+
987
+ // Twinkle effect
988
+ star.twinkle += dt * 5;
989
+ const brightness = 0.5 + Math.sin(star.twinkle) * 0.5;
990
+ setScale(star.mesh, brightness * 2 + 0.5);
991
+
992
+ if (pos[2] > 10) {
993
+ pos[2] = star.originalZ - 100;
994
+ }
995
+
996
+ setPosition(star.mesh, pos[0], pos[1], pos[2]);
997
+ }
998
+ }
999
+
1000
+ function checkCollisions(_dt) {
1001
+ const playerBullets = gameData.playerBullets;
1002
+ const enemyBullets = gameData.enemyBullets;
1003
+ const enemies = gameData.enemies;
1004
+ const player = gameData.player;
1005
+ let score = gameData.score;
1006
+ let lives = gameData.lives;
1007
+
1008
+ // Player bullets vs enemies
1009
+ for (let i = playerBullets.length - 1; i >= 0; i--) {
1010
+ const bullet = playerBullets[i];
1011
+ const bulletPos = getPosition(bullet.mesh);
1012
+
1013
+ for (let j = enemies.length - 1; j >= 0; j--) {
1014
+ const enemy = enemies[j];
1015
+ if (!enemy.alive) continue;
1016
+
1017
+ const distance = Math.sqrt(
1018
+ Math.pow(bulletPos[0] - enemy.x, 2) +
1019
+ Math.pow(bulletPos[1] - enemy.y, 2) +
1020
+ Math.pow(bulletPos[2] - enemy.z, 2)
1021
+ );
1022
+
1023
+ let collisionRadius = enemy.type === 'boss' ? 4.0 : 1.5;
1024
+ if (distance < collisionRadius) {
1025
+ // Hit!
1026
+ enemy.health -= bullet.damage;
1027
+ enemy.hitFlash = 1.0; // Flash white on hit
1028
+ sfx('hit');
1029
+ destroyMesh(bullet.mesh);
1030
+ playerBullets.splice(i, 1);
1031
+
1032
+ if (enemy.health <= 0) {
1033
+ gameData.combo++;
1034
+ gameData.comboTimer = 2.0;
1035
+ let multiplier = Math.min(gameData.combo, 10);
1036
+
1037
+ // Enemy destroyed
1038
+ sfx('explosion');
1039
+ if (enemy.type === 'boss') {
1040
+ createExplosion(enemy.x, enemy.y, enemy.z);
1041
+ createExplosion(enemy.x - 2, enemy.y, enemy.z);
1042
+ createExplosion(enemy.x + 2, enemy.y, enemy.z);
1043
+ createExplosion(enemy.x, enemy.y + 2, enemy.z);
1044
+ if (enemy.mesh.body) destroyMesh(enemy.mesh.body);
1045
+ if (enemy.mesh.wingL) destroyMesh(enemy.mesh.wingL);
1046
+ if (enemy.mesh.wingR) destroyMesh(enemy.mesh.wingR);
1047
+ score += 5000 * multiplier;
1048
+ gameData.flags.bossActive = false;
1049
+ // Boss always drops weapon upgrade
1050
+ spawnPowerupAt(enemy.x, enemy.y, enemy.z, 'weapon');
1051
+ } else {
1052
+ createExplosion(enemy.x, enemy.y, enemy.z);
1053
+ if (enemy.mesh.body) destroyMesh(enemy.mesh.body);
1054
+ if (enemy.mesh.engine) destroyMesh(enemy.mesh.engine);
1055
+ let baseScore = enemy.type === 'tank' ? 300 : enemy.type === 'fast' ? 200 : 100;
1056
+ score += baseScore * multiplier;
1057
+ // Chance to drop powerup on kill
1058
+ let dropChance = enemy.type === 'tank' ? 0.4 : enemy.type === 'fast' ? 0.25 : 0.15;
1059
+ if (Math.random() < dropChance) {
1060
+ const dropTypes = ['health', 'shield', 'weapon', 'energy'];
1061
+ spawnPowerupAt(
1062
+ enemy.x,
1063
+ enemy.y,
1064
+ enemy.z,
1065
+ dropTypes[Math.floor(Math.random() * dropTypes.length)]
1066
+ );
1067
+ }
1068
+ }
1069
+ enemies.splice(j, 1);
1070
+ }
1071
+ break;
1072
+ }
1073
+ }
1074
+ }
1075
+
1076
+ // Enemy bullets vs player
1077
+ for (let i = enemyBullets.length - 1; i >= 0; i--) {
1078
+ const bullet = enemyBullets[i];
1079
+ const bulletPos = getPosition(bullet.mesh);
1080
+
1081
+ const distance = Math.sqrt(
1082
+ Math.pow(bulletPos[0] - player.x, 2) +
1083
+ Math.pow(bulletPos[1] - player.y, 2) +
1084
+ Math.pow(bulletPos[2] - player.z, 2)
1085
+ );
1086
+
1087
+ if (distance < 2.0) {
1088
+ if (player.shield > 0) {
1089
+ player.shield -= 15;
1090
+ sfx('hit');
1091
+ } else {
1092
+ player.health -= 25;
1093
+ sfx('hit');
1094
+ }
1095
+
1096
+ destroyMesh(bullet.mesh);
1097
+ enemyBullets.splice(i, 1);
1098
+
1099
+ if (player.health <= 0) {
1100
+ lives--;
1101
+ player.health = 100;
1102
+ sfx('death');
1103
+ if (lives <= 0) {
1104
+ gameState = 'gameOver';
1105
+ }
1106
+ }
1107
+ }
1108
+ }
1109
+
1110
+ // Player vs powerups
1111
+ const powerups = gameData.powerups;
1112
+ for (let i = powerups.length - 1; i >= 0; i--) {
1113
+ const powerup = powerups[i];
1114
+ const distance = Math.sqrt(
1115
+ Math.pow(powerup.x - player.x, 2) +
1116
+ Math.pow(powerup.y - player.y, 2) +
1117
+ Math.pow(powerup.z - player.z, 2)
1118
+ );
1119
+
1120
+ if (distance < 2.0) {
1121
+ // Collect powerup
1122
+ switch (powerup.type) {
1123
+ case 'health':
1124
+ player.health = Math.min(100, player.health + 25);
1125
+ break;
1126
+ case 'shield':
1127
+ player.shield = Math.min(100, player.shield + 50);
1128
+ break;
1129
+ case 'weapon':
1130
+ player.weaponLevel = Math.min(3, player.weaponLevel + 1);
1131
+ break;
1132
+ case 'energy':
1133
+ player.energy = 100;
1134
+ break;
1135
+ }
1136
+
1137
+ destroyMesh(powerup.mesh);
1138
+ powerups.splice(i, 1);
1139
+ score += 50;
1140
+ sfx(powerup.type === 'weapon' ? 'powerup' : 'coin');
1141
+ }
1142
+ }
1143
+
1144
+ gameData.score = score;
1145
+ gameData.lives = lives;
1146
+ }
1147
+
1148
+ function updateGameLogic(dt) {
1149
+ const enemies = gameData.enemies;
1150
+ let level = gameData.level;
1151
+ const lives = gameData.lives;
1152
+
1153
+ if (gameData.comboTimer > 0) {
1154
+ gameData.comboTimer -= dt;
1155
+ if (gameData.comboTimer <= 0) {
1156
+ gameData.comboTimer = 0;
1157
+ gameData.combo = 0;
1158
+ }
1159
+ }
1160
+
1161
+ // Wave warning timer
1162
+ if (gameData.waveWarning === undefined) gameData.waveWarning = 0;
1163
+ if (gameData.waveWarning > 0) gameData.waveWarning -= dt;
1164
+
1165
+ // Spawn new wave when all enemies are cleared
1166
+ if (enemies.length === 0 && !gameData.waveClearPause) {
1167
+ gameData.waveClearPause = true;
1168
+ gameData.waveClearTimer = 2.0;
1169
+ // Wave clear bonus
1170
+ if (level > 0) {
1171
+ gameData.score += level * 500;
1172
+ sfx('powerup');
1173
+ }
1174
+ }
1175
+
1176
+ if (gameData.waveClearPause) {
1177
+ gameData.waveClearTimer -= dt;
1178
+ if (gameData.waveClearTimer <= 0) {
1179
+ gameData.waveClearPause = false;
1180
+ level++;
1181
+ gameData.level = level;
1182
+ gameData.waveWarning = 2.0;
1183
+ spawnEnemyWave();
1184
+ }
1185
+ return;
1186
+ }
1187
+
1188
+ // Game over check
1189
+ if (lives <= 0) {
1190
+ gameState = 'gameOver';
1191
+ }
1192
+ }
1193
+
1194
+ function updateCamera(_dt) {
1195
+ const player = gameData.player;
1196
+
1197
+ // Dynamic camera movement based on player position
1198
+ const targetX = player.x * 0.1;
1199
+ const targetY = 2 + player.y * 0.05;
1200
+
1201
+ setCameraPosition(targetX, targetY, 0);
1202
+ setCameraTarget(player.x * 0.3, player.y * 0.2, -10);
1203
+ }
1204
+
1205
+ function drawUI() {
1206
+ // HUD Background
1207
+ rect(16, 16, 400, 80, rgba8(0, 0, 0, 150), true);
1208
+ rect(16, 16, 400, 80, rgba8(0, 100, 200, 100), false);
1209
+
1210
+ // Score and Level
1211
+ print(`SCORE: ${gameData.score.toString().padStart(8, '0')}`, 24, 24, rgba8(255, 255, 0, 255));
1212
+ print(`LEVEL: ${gameData.level}`, 24, 40, rgba8(0, 255, 255, 255));
1213
+ print(`LIVES: ${gameData.lives}`, 24, 56, rgba8(255, 100, 100, 255));
1214
+
1215
+ if (gameData.combo > 1) {
1216
+ let c = Math.min(gameData.combo, 10);
1217
+ print(`COMBO x${c}`, 24, 72, rgba8(255, 150, 0, 255));
1218
+ }
1219
+
1220
+ // Health bar
1221
+ print('HULL:', 200, 24, rgba8(255, 255, 255, 255));
1222
+ rect(240, 22, 100, 8, rgba8(100, 0, 0, 255), true);
1223
+ rect(240, 22, Math.floor((gameData.player.health / 100) * 100), 8, rgba8(255, 0, 0, 255), true);
1224
+
1225
+ // Shield bar
1226
+ print('SHIELD:', 200, 40, rgba8(255, 255, 255, 255));
1227
+ rect(260, 38, 100, 8, rgba8(0, 0, 100, 255), true);
1228
+ rect(260, 38, Math.floor((gameData.player.shield / 100) * 100), 8, rgba8(0, 100, 255, 255), true);
1229
+
1230
+ // Energy bar
1231
+ print('ENERGY:', 200, 56, rgba8(255, 255, 255, 255));
1232
+ rect(260, 54, 100, 8, rgba8(100, 0, 100, 255), true);
1233
+ rect(260, 54, Math.floor((gameData.player.energy / 100) * 100), 8, rgba8(255, 0, 255, 255), true);
1234
+
1235
+ // 3D Stats
1236
+ const stats = get3DStats();
1237
+ if (stats) {
1238
+ print(`3D: ${stats.meshes || 0} meshes`, 450, 24, rgba8(150, 150, 150, 255));
1239
+ print(`GPU: ${stats.renderer || 'ThreeJS'}`, 450, 40, rgba8(150, 150, 150, 255));
1240
+ }
1241
+
1242
+ // Boss health bar
1243
+ if (gameData.flags.bossActive) {
1244
+ const boss = gameData.enemies.find(e => e.type === 'boss');
1245
+ if (boss) {
1246
+ const bw = 300;
1247
+ const bx = (640 - bw) / 2;
1248
+ const phaseName = ['PHASE 1', 'PHASE 2 - ENRAGED', 'PHASE 3 - DESPERATE'][
1249
+ gameData.flags.bossPhase || 0
1250
+ ];
1251
+ print('BOSS', bx, 96, rgba8(255, 50, 50));
1252
+ print(phaseName, bx + 50, 96, rgba8(255, 200, 50));
1253
+ rect(bx, 108, bw, 10, rgba8(80, 0, 0), true);
1254
+ const hp = Math.max(0, boss.health / boss.maxHealth);
1255
+ rect(bx, 108, Math.floor(hp * bw), 10, rgba8(255, 0, 0), true);
1256
+ rect(bx, 108, bw, 10, rgba8(200, 100, 100), false);
1257
+ }
1258
+ }
1259
+
1260
+ // Wave clear bonus display
1261
+ if (gameData.waveClearPause && gameData.level > 0) {
1262
+ const alpha = Math.floor(Math.min(1, gameData.waveClearTimer) * 255);
1263
+ printCentered(`WAVE ${gameData.level} CLEAR!`, 320, 160, rgba8(0, 255, 100, alpha), 2);
1264
+ printCentered(`+${gameData.level * 500} BONUS`, 320, 190, rgba8(255, 255, 0, alpha));
1265
+ }
1266
+
1267
+ // Wave warning
1268
+ if (gameData.waveWarning > 0) {
1269
+ const alpha = Math.floor(Math.min(1, gameData.waveWarning) * 255);
1270
+ const warnText =
1271
+ gameData.level % 5 === 0 ? 'WARNING: BOSS INCOMING!' : `WAVE ${gameData.level}`;
1272
+ printCentered(warnText, 320, 180, rgba8(255, 100, 0, alpha), 2);
1273
+ }
1274
+
1275
+ // Weapon level indicator
1276
+ const wpnColors = [rgba8(255, 255, 0), rgba8(0, 255, 255), rgba8(255, 100, 255)];
1277
+ print(
1278
+ `WPN LV${gameData.player.weaponLevel}`,
1279
+ 24,
1280
+ 80,
1281
+ wpnColors[gameData.player.weaponLevel - 1] || rgba8(255, 255, 255)
1282
+ );
1283
+
1284
+ // Controls
1285
+ print('ARROWS=MOVE X=FIRE Z=CHARGE', 24, 340, rgba8(150, 150, 150, 200));
1286
+ }