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,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
|
+
}
|