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