awacke1 commited on
Commit
af7d9fa
·
verified ·
1 Parent(s): 6612fe6

Update index.html

Browse files
Files changed (1) hide show
  1. index.html +481 -1069
index.html CHANGED
@@ -1,1145 +1,557 @@
1
  <!DOCTYPE html>
2
  <html lang="en">
3
  <head>
4
- <!-- Chosen Palette: Retro Arcade (Deep Blue, Neon Green, Red, Yellow, White, Gold) -->
5
- <!-- Application Structure Plan: A single-page application with a clear state-driven flow: MENU -> DEMO -> PLAYING -> PAUSED -> GAME_OVER -> HIGH_SCORE_ENTRY -> HIGH_SCORES. This structure ensures a focused, arcade-like experience. UI overlays manage menu, pause, game over, and high score interactions. Two-player mode is managed through alternating turns on life loss. The user flow is linear through game states, with high scores being an end-game display. -->
6
- <!-- Visualization & Content Choices:
7
- - Player/Enemy Ships: Report Info -> Player and Enemy models. Goal -> Represent game agents with more visual variety. Viz/Method -> 3D compound objects (THREE.Group) with more detailed geometries (e.g., Cylinders, Spheres, Cones, Boxes combined). Interaction -> Player control, AI movement, collision. Justification -> Enhances visual fidelity and differentiation. Library/Method -> Three.js.
8
- - Formations/Waves/Armada: Report Info -> Enemy formations, attack waves, and an armada. Goal -> Create structured challenges with escalating difficulty. Viz/Method -> Positional data within JS objects driving enemy AI, complex formation logic for armada. Interaction -> Player must defeat waves to progress. Justification -> Central to Galaga's gameplay, armada adds unique challenge. Library/Method -> JavaScript logic.
9
- - Tractor Beam: Report Info -> Boss Galaga capture mechanic. Goal -> Introduce risk/reward for dual fighter. Viz/Method -> Animated, transparent THREE.Mesh (Cone). Interaction -> Captures player, enabling dual-fighter retrieval. Justification -> Iconic and requested feature. Library/Method -> Three.js.
10
- - Starfield Complexity: Report Info -> Dynamic background with more variation. Goal -> Simulate forward motion and enhance immersion. Viz/Method -> Multiple THREE.Points groups with varying sizes, colors, and speeds. Interaction -> None (ambient). Justification -> Visually richer background. Library/Method -> Three.js.
11
- - HUD (Score/Lives/Wave): Report Info -> UI requirements. Goal -> Provide critical game state info for both players. Viz/Method -> HTML overlays with Tailwind CSS. Interaction -> Read-only. Justification -> Standard, non-intrusive method for displaying game data. Library/Method -> HTML/CSS.
12
- - High Score Board: Report Info -> Persistent high score. Goal -> Provide a sense of progression and competition. Viz/Method -> HTML overlay with form for initials, dynamically updated list. Interaction -> User input for initials, display only. Justification -> Standard arcade feature. Library/Method -> JavaScript (localStorage for persistence).
13
- - Auto Shield: Report Info -> Easier auto-shield. Goal -> Provide a brief period of invincibility after being hit. Viz/Method -> Player ship flashing, temporary state variable. Interaction -> Automatic on hit. Justification -> Improves player survivability and reduces frustration. Library/Method -> JavaScript logic.
14
- - Heat-Seeking Missiles: Report Info -> Player missiles home-in. Goal -> Increase player's hit rate. Viz/Method -> Player projectile velocity adjustment towards nearest enemy. Interaction -> Automatic. Justification -> Enhances player experience, makes game feel more forgiving. Library/Method -> JavaScript logic.
15
- - Shot Power-Up: Report Info -> Blue capsules for more missiles. Goal -> Reward player, add progression. Viz/Method -> Small 3D cylinder/capsule, moves towards player. Interaction -> Collectible to increase missile output. Justification -> Adds depth to power-up system. Library/Method -> Three.js (for model), JavaScript logic.
16
- - Multi-Fighter Fleet: Report Info -> Acquire more ships. Goal -> Enhance offensive capabilities, visual progression. Viz/Method -> Multiple player ship models grouped under one main control. Interaction -> Dynamic visual arrangement. Justification -> Core Galaga '88 mechanic. Library/Method -> Three.js (for grouping), JavaScript logic.
17
- - Sound Effects: Report Info -> Game audio. Goal -> Enhance immersion and provide feedback. Viz/Method -> Tone.js for music and sound effects. Interaction -> Automatic playback based on game events. Justification -> Crucial for arcade feel. Library/Method -> Tone.js.
18
- - Mouse Control: Report Info -> Additional input. Goal -> Provide alternative control method. Viz/Method -> Mouse position mapping to ship X-coordinate. Interaction -> Direct player control. Justification -> Improves accessibility for different input preferences. Library/Method -> JavaScript DOM events.
19
- -->
20
- <!-- CONFIRMATION: NO SVG graphics used. NO Mermaid JS used. -->
21
- <meta charset="UTF-8">
22
- <meta name="viewport" content="width=device-width, initial-scale=1.0, user-scalable=no">
23
- <title>Galaga '88 3D</title>
24
- <script src="https://cdn.tailwindcss.com"></script>
25
- <script src="https://cdnjs.cloudflare.com/ajax/libs/three.js/r128/three.min.js"></script>
26
- <script src="https://unpkg.com/[email protected]/build/Tone.js"></script>
27
  <style>
28
- body { margin: 0; overflow: hidden; background-color: #000; font-family: 'Courier New', Courier, monospace; }
29
- canvas { display: block; }
30
- .hud-element { position: absolute; color: #ffffff; text-shadow: 2px 2px 4px #00ff00; }
31
- .overlay { position: absolute; top: 0; left: 0; width: 100%; height: 100%; display: flex; flex-direction: column; justify-content: center; align-items: center; background-color: rgba(0, 0, 0, 0.7); z-index: 1000; }
32
- .overlay-box { background-color: rgba(0, 15, 30, 0.8); border: 2px solid #00ff00; padding: 2rem 4rem; text-align: center; border-radius: 0.5rem;}
33
- .overlay-title { font-size: 3rem; color: #ffff00; text-shadow: 2px 2px 6px #ff0000; margin-bottom: 1rem; }
34
- .overlay-text { font-size: 1.2rem; color: #ffffff; margin-bottom: 2rem; }
35
- .overlay-button { background-color: #ffff00; color: #000000; border: none; padding: 1rem 2rem; font-size: 1.5rem; cursor: pointer; transition: all 0.2s; border-radius: 0.3rem;}
36
- .overlay-button:hover { background-color: #ffffff; color: #000000; transform: scale(1.05); }
37
- .touch-controls { position: absolute; bottom: 0; width: 100%; height: 120px; display: flex; justify-content: space-between; align-items: center; z-index: 100; padding: 0 20px; }
38
- .touch-button { width: 80px; height: 80px; border: 2px solid #00ff00; border-radius: 50%; display: flex; justify-content: center; align-items: center; color: #00ff00; font-size: 2rem; user-select: none; }
39
- .touch-button.fire { width: 100px; height: 100px; color: #ffff00; border-color: #ffff00; }
40
-
41
- #high-score-form input {
42
- background-color: #333;
43
- border: 1px solid #0f0;
44
- color: #fff;
45
- padding: 0.5rem;
46
- margin-top: 1rem;
47
- text-align: center;
48
- text-transform: uppercase;
49
- font-size: 1.5rem;
50
- width: 8rem;
51
- }
52
- #high-scores-list {
53
- list-style: none;
54
- padding: 0;
55
- margin: 1rem 0;
56
- font-size: 1.2rem;
57
- color: #fff;
58
- }
59
- #high-scores-list li {
60
- padding: 0.2rem 0;
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
61
  display: flex;
62
  justify-content: space-between;
 
 
 
63
  }
64
- #high-scores-list li span:first-child {
65
- color: #ffff00;
66
- }
67
- #high-scores-list li span:last-child {
68
- color: #00ff00;
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
69
  }
70
  </style>
71
  </head>
72
  <body>
73
- <div id="score-p1-display" class="hud-element top-4 left-4 text-2xl">P1 SCORE: 0</div>
74
- <div id="score-p2-display" class="hud-element top-4 left-4 ml-48 text-2xl" style="display: none;">P2 SCORE: 0</div>
75
- <div id="high-score-current-display" class="hud-element top-4 left-1/2 -translate-x-1/2 text-2xl text-yellow-400">HIGH SCORE: 0</div>
76
- <div id="current-player-display" class="hud-element bottom-4 right-4 text-2xl text-yellow-400">P1 TURN</div>
77
- <div id="wave-display" class="hud-element top-4 right-4 text-2xl">WAVE: 1</div>
78
- <div id="lives-display" class="hud-element bottom-4 left-4 text-2xl flex items-center gap-2">LIVES: </div>
79
-
80
- <div id="menu-overlay" class="overlay">
81
- <div class="overlay-box">
82
- <h1 class="overlay-title">GALAGA '88 3D</h1>
83
- <p class="overlay-text">Use [A]/[D] or [←]/[→] to move. [SPACE] to fire.<br>
84
- [P] to Pause. Collect captured ships for Double Fighter!</p>
85
- <div class="mb-4">
86
- <button id="start-1player-button" class="overlay-button mr-4">1 PLAYER</button>
87
- <button id="start-2player-button" class="overlay-button">2 PLAYERS</button>
88
- </div>
89
- <button id="view-high-scores-button" class="overlay-button !bg-gray-700 !text-white !border !border-white mt-4">HIGH SCORES</button>
90
- </div>
91
  </div>
92
- <div id="pause-overlay" class="overlay" style="display: none;">
93
- <div class="overlay-box">
94
- <h1 class="overlay-title">PAUSED</h1>
95
- <button id="resume-button" class="overlay-button">RESUME</button>
96
- </div>
97
- </div>
98
- <div id="game-over-overlay" class="overlay" style="display: none;">
99
- <div class="overlay-box">
100
- <h1 class="overlay-title">GAME OVER</h1>
101
- <p id="final-score" class="overlay-text text-2xl"></p>
102
- <form id="high-score-form" class="hidden">
103
- <p class="text-white text-lg">Enter Initials (3 letters):</p>
104
- <input type="text" id="initials-input" maxlength="3" pattern="[A-Z]{3}" class="uppercase" required>
105
- <button type="submit" class="overlay-button mt-4">SAVE SCORE</button>
106
- </form>
107
- <button id="restart-button" class="overlay-button mt-4">PLAY AGAIN</button>
108
  </div>
109
- </div>
110
- <div id="high-scores-overlay" class="overlay" style="display: none;">
111
- <div class="overlay-box">
112
- <h1 class="overlay-title">HIGH SCORES</h1>
113
- <ul id="high-scores-list"></ul>
114
- <button id="back-to-menu-button" class="overlay-button mt-4">BACK TO MENU</button>
115
  </div>
116
  </div>
117
-
118
- <div class="touch-controls">
119
- <div id="left-touch" class="touch-button"><span>&#x2190;</span></div>
120
- <div id="fire-touch" class="touch-button fire"><span>&#x25CE;</span></div>
121
- <div id="right-touch" class="touch-button"><span>&#x2192;</span></div>
122
- </div>
123
-
124
- <script>
125
- const GAME = {
126
- state: 'MENU', // MENU, DEMO, PLAYING, PAUSED, GAME_OVER, HIGH_SCORE_ENTRY, HIGH_SCORES
127
- players: {
128
- p1: { score: 0, lives: 5, fighterCount: 1, missileCount: 1 },
129
- p2: { score: 0, lives: 5, fighterCount: 1, missileCount: 1 }
130
- },
131
- currentPlayer: 'p1',
132
- numPlayers: 1, // 1 or 2
133
- playerFleet: [], // Array to hold individual player ship meshes
134
- playerShipGroup: null, // The main THREE.Group that contains all active player ships
135
- enemies: [],
136
- playerProjectiles: [],
137
- enemyProjectiles: [],
138
- explosions: [],
139
- powerUps: [], // New array for power-up capsules
140
- stars: [],
141
- scene: null,
142
- camera: null,
143
- renderer: null,
144
- clock: new THREE.Clock(),
145
- input: { left: false, right: false },
146
- canShoot: true,
147
- shootCooldown: 250,
148
- playerBoundaryX: 20,
149
- capturedShip: null,
150
- wave: 1,
151
- waveData: [
152
- { grunts: 8, chargers: 0, bosses: 0, type: 'standard' },
153
- { grunts: 10, chargers: 2, bosses: 0, type: 'standard' },
154
- { grunts: 6, chargers: 4, bosses: 1, type: 'standard' },
155
- { grunts: 0, chargers: 10, bosses: 2, type: 'standard' },
156
- { grunts: 20, chargers: 0, bosses: 0, type: 'armada' }, // Armada wave
157
- { grunts: 15, chargers: 8, bosses: 2, type: 'standard' },
158
- ],
159
- currentWaveEnemiesSpawned: 0,
160
- currentWaveTotalEnemies: 0,
161
- invincible: false,
162
- invincibilityDuration: 3000, // 3 seconds
163
- highScores: JSON.parse(localStorage.getItem('galagaHighScores')) || [],
164
- powerUpDropChance: 0.1, // 10% chance for power-up to drop
165
- audioContextReady: false,
166
- music: {
167
- demo: null,
168
- highScore: null
169
- },
170
- sfx: {
171
- playerShoot: null,
172
- enemyShoot: null,
173
- explosion: null,
174
- powerUp: null,
175
- playerHit: null,
176
- captureBeam: null,
177
- gameStart: null,
178
- gameOver: null
179
- },
180
- demoTimer: 0,
181
- demoInterval: 10000, // 10 seconds for demo loop
182
- musicAlternationTimer: 0,
183
- musicAlternationInterval: 5000 // Alternate music every 5 seconds
184
- };
185
-
186
- function init() {
187
- setupScene();
188
- setupUI();
189
- // Setup audio context on first user interaction, then load and configure sounds
190
- window.addEventListener('resize', onWindowResize, false);
191
- document.body.addEventListener('click', initAudioContext, { once: true });
192
- animate();
193
- }
194
-
195
- function initAudioContext() {
196
- if (!GAME.audioContextReady) {
197
- Tone.start().then(() => {
198
- GAME.audioContextReady = true;
199
- loadAndConfigureSounds(); // Now call this here to ensure Tone.js is ready
200
- startDemoLoop();
201
- }).catch(e => console.error("Failed to start audio context:", e));
202
- }
203
- }
204
-
205
- function loadAndConfigureSounds() {
206
- // Instantiate all synths and sequences
207
- GAME.sfx.playerShoot = new Tone.PolySynth(Tone.Synth, {
208
- oscillator: { type: "square" }
209
- }).toDestination();
210
- GAME.sfx.playerShoot.volume.value = -10;
211
-
212
- GAME.sfx.enemyShoot = new Tone.PolySynth(Tone.Synth, {
213
- oscillator: { type: "sawtooth" }
214
- }).toDestination();
215
- GAME.sfx.enemyShoot.volume.value = -15;
216
-
217
- GAME.sfx.explosion = new Tone.NoiseSynth({
218
- noise: { type: "white" },
219
- envelope: { attack: 0.01, decay: 0.2, sustain: 0, release: 0.1 }
220
- }).toDestination();
221
- GAME.sfx.explosion.volume.value = -10;
222
-
223
- GAME.sfx.powerUp = new Tone.PluckSynth().toDestination();
224
- GAME.sfx.powerUp.volume.value = -5;
225
-
226
- GAME.sfx.playerHit = new Tone.MembraneSynth().toDestination();
227
- GAME.sfx.playerHit.volume.value = -5;
228
-
229
- GAME.sfx.captureBeam = new Tone.AMSynth({
230
- harmonicity: 0.5,
231
- oscillator: { type: "sine" },
232
- envelope: { attack: 0.1, decay: 0.2, sustain: 0.5, release: 0.5 },
233
- modulation: { type: "square" },
234
- modulationEnvelope: { attack: 0.2, decay: 0.5 }
235
- }).toDestination();
236
- GAME.sfx.captureBeam.volume.value = -10;
237
-
238
- GAME.sfx.gameStart = new Tone.Synth().toDestination();
239
- GAME.sfx.gameStart.volume.value = -5;
240
-
241
- GAME.sfx.gameOver = new Tone.FMSynth().toDestination();
242
- GAME.sfx.gameOver.volume.value = -5;
243
-
244
- // Sequences are created here, but they trigger attacks on the already defined sfx synths.
245
- GAME.music.demo = new Tone.Sequence((time, note) => {
246
- if(GAME.sfx.playerShoot) GAME.sfx.playerShoot.triggerAttackRelease(note, "8n", time);
247
- }, ["C4", "E4", "G4", "C5"]);
248
- GAME.music.demo.loop = true;
249
- GAME.music.demo.bpm.value = 120;
250
- GAME.music.demo.volume.value = -15;
251
-
252
- GAME.music.highScore = new Tone.Sequence((time, note) => {
253
- if(GAME.sfx.enemyShoot) GAME.sfx.enemyShoot.triggerAttackRelease(note, "8n", time);
254
- }, ["G3", "D4", "B3", "E4"]);
255
- GAME.music.highScore.loop = true;
256
- GAME.music.highScore.bpm.value = 100;
257
- GAME.music.highScore.volume.value = -15;
258
- }
259
-
260
- function startDemoLoop() {
261
- if (GAME.state === 'MENU' || GAME.state === 'HIGH_SCORES') {
262
- if (GAME.music.demo) GAME.music.demo.start();
263
- if (GAME.music.highScore) GAME.music.highScore.stop();
264
- GAME.state = 'DEMO';
265
- GAME.demoTimer = 0;
266
- }
267
- }
268
-
269
- function stopAllMusic() {
270
- if (GAME.music.demo && GAME.music.demo.state === 'started') GAME.music.demo.stop();
271
- if (GAME.music.highScore && GAME.music.highScore.state === 'started') GAME.music.highScore.stop();
272
- }
273
-
274
- function setupScene() {
275
- GAME.scene = new THREE.Scene();
276
- GAME.camera = new THREE.PerspectiveCamera(75, window.innerWidth / window.innerHeight, 0.1, 1000);
277
- GAME.camera.position.set(0, 10, 30);
278
- GAME.camera.lookAt(0, 0, 0);
279
-
280
- GAME.renderer = new THREE.WebGLRenderer({ antialias: true });
281
- GAME.renderer.setSize(window.innerWidth, window.innerHeight);
282
- document.body.appendChild(GAME.renderer.domElement);
283
-
284
- const ambientLight = new THREE.AmbientLight(0x606060);
285
- GAME.scene.add(ambientLight);
286
- const dirLight = new THREE.DirectionalLight(0xffffff, 0.8);
287
- dirLight.position.set(0, 1, 1);
288
- GAME.scene.add(dirLight);
289
-
290
- createStarfield();
291
- createPlayerShipGroup();
292
- }
293
-
294
- function setupUI() {
295
- document.getElementById('start-1player-button').addEventListener('click', () => { GAME.numPlayers = 1; startGame(); });
296
- document.getElementById('start-2player-button').addEventListener('click', () => { GAME.numPlayers = 2; startGame(); });
297
- document.getElementById('restart-button').addEventListener('click', startGame);
298
- document.getElementById('resume-button').addEventListener('click', togglePause);
299
- document.getElementById('view-high-scores-button').addEventListener('click', showHighScores);
300
- document.getElementById('back-to-menu-button').addEventListener('click', showMenu);
301
- document.getElementById('high-score-form').addEventListener('submit', handleHighScoreSubmit);
302
-
303
- document.addEventListener('keydown', e => {
304
- if (e.key === 'a' || e.key === 'ArrowLeft') GAME.input.left = true;
305
- if (e.key === 'd' || e.key === 'ArrowRight') GAME.input.right = true;
306
- if (e.key === ' ' && GAME.state === 'PLAYING') firePlayerProjectile();
307
- if (e.key === 'p' && (GAME.state === 'PLAYING' || GAME.state === 'PAUSED')) togglePause();
308
- });
309
- document.addEventListener('keyup', e => {
310
- if (e.key === 'a' || e.key === 'ArrowLeft') GAME.input.left = false;
311
- if (e.key === 'd' || e.key === 'ArrowRight') GAME.input.right = false;
312
- });
313
-
314
- const leftBtn = document.getElementById('left-touch');
315
- const rightBtn = document.getElementById('right-touch');
316
- const fireBtn = document.getElementById('fire-touch');
317
- leftBtn.addEventListener('touchstart', (e) => { e.preventDefault(); GAME.input.left = true; });
318
- leftBtn.addEventListener('touchend', (e) => { e.preventDefault(); GAME.input.left = false; });
319
- rightBtn.addEventListener('touchstart', (e) => { e.preventDefault(); GAME.input.right = true; });
320
- rightBtn.addEventListener('touchend', (e) => { e.preventDefault(); GAME.input.right = false; });
321
- fireBtn.addEventListener('touchstart', (e) => { e.preventDefault(); if (GAME.state === 'PLAYING') firePlayerProjectile(); });
322
-
323
- GAME.renderer.domElement.addEventListener('mousemove', onMouseMove, false);
324
-
325
- updateHUD();
326
- }
327
-
328
- function onMouseMove(event) {
329
- if (GAME.state !== 'PLAYING' || !GAME.playerShipGroup) return;
330
- const mouseX = (event.clientX / window.innerWidth) * 2 - 1;
331
- const vector = new THREE.Vector3(mouseX, 0, 0.5);
332
- vector.unproject(GAME.camera);
333
-
334
- const dir = vector.sub(GAME.camera.position).normalize();
335
- const distance = (GAME.playerShipGroup.position.z - GAME.camera.position.z) / dir.z;
336
- const pos = GAME.camera.position.clone().add(dir.multiplyScalar(distance));
337
-
338
- GAME.playerShipGroup.position.x = Math.max(-GAME.playerBoundaryX, Math.min(GAME.playerBoundaryX, pos.x));
339
- }
340
 
341
- function showMenu() {
342
- document.getElementById('menu-overlay').style.display = 'flex';
343
- document.getElementById('high-scores-overlay').style.display = 'none';
344
- GAME.state = 'MENU';
345
- resetGameVisuals();
346
- startDemoLoop();
347
- }
348
-
349
- function startGame() {
350
- stopAllMusic();
351
- if(GAME.audioContextReady && GAME.sfx.gameStart) GAME.sfx.gameStart.triggerAttackRelease("C5", "0.2");
352
- resetGame();
353
- GAME.state = 'PLAYING';
354
- document.getElementById('menu-overlay').style.display = 'none';
355
- document.getElementById('game-over-overlay').style.display = 'none';
356
- document.getElementById('high-scores-overlay').style.display = 'none';
357
- document.getElementById('score-p2-display').style.display = GAME.numPlayers === 2 ? 'block' : 'none';
358
-
359
- startWave();
360
- }
361
-
362
- function resetGame() {
363
- GAME.players.p1.score = 0;
364
- GAME.players.p1.lives = 5;
365
- GAME.players.p1.fighterCount = 1;
366
- GAME.players.p1.missileCount = 1;
367
-
368
- GAME.players.p2.score = 0;
369
- GAME.players.p2.lives = 5;
370
- GAME.players.p2.fighterCount = 1;
371
- GAME.players.p2.missileCount = 1;
372
-
373
- GAME.currentPlayer = 'p1';
374
- GAME.wave = 1;
375
-
376
- resetGameVisuals();
377
-
378
- GAME.capturedShip = null;
379
- GAME.invincible = false;
380
- GAME.canShoot = true;
381
-
382
- updateHUD();
383
- }
384
-
385
- function resetGameVisuals() {
386
- if(GAME.playerShipGroup && GAME.playerShipGroup.parent) GAME.scene.remove(GAME.playerShipGroup);
387
- GAME.playerFleet = [];
388
- createPlayerShipGroup();
389
-
390
- GAME.enemies.forEach(e => GAME.scene.remove(e.mesh));
391
- GAME.playerProjectiles.forEach(p => GAME.scene.remove(p.mesh));
392
- GAME.enemyProjectiles.forEach(p => GAME.scene.remove(p));
393
- GAME.explosions.forEach(e => GAME.scene.remove(e));
394
- GAME.powerUps.forEach(p => GAME.scene.remove(p));
395
- if (GAME.capturedShip && GAME.capturedShip.mesh.parent) GAME.scene.remove(GAME.capturedShip.mesh);
396
-
397
- GAME.enemies = [];
398
- GAME.playerProjectiles = [];
399
- GAME.enemyProjectiles = [];
400
- GAME.explosions = [];
401
- GAME.powerUps = [];
402
- }
403
-
404
- function createIndividualShipMesh(playerIdentifier) {
405
- const primaryColor = playerIdentifier === 'p1' ? 0x00ff00 : 0x00aaff;
406
- const accentColor = playerIdentifier === 'p1' ? 0x00aa00 : 0x0077bb;
407
- const cockpitColor = 0xffffff;
408
-
409
- const ship = new THREE.Group();
410
-
411
- const bodyGeo = new THREE.ConeGeometry(1, 2.5, 8);
412
- const bodyMat = new THREE.MeshPhongMaterial({ color: primaryColor });
413
- const body = new THREE.Mesh(bodyGeo, bodyMat);
414
- body.rotation.x = Math.PI / 2;
415
- ship.add(body);
416
-
417
- const wingGeo = new THREE.BoxGeometry(3.5, 0.2, 1.2);
418
- const wingMat = new THREE.MeshPhongMaterial({ color: accentColor });
419
- const wing = new THREE.Mesh(wingGeo, wingMat);
420
- wing.position.set(0, 0, 0.5);
421
- ship.add(wing);
422
-
423
- const cockpitGeo = new THREE.SphereGeometry(0.5, 16, 8);
424
- const cockpitMat = new THREE.MeshPhongMaterial({ color: cockpitColor, emissive: 0x5555ff, shininess: 100 });
425
- const cockpit = new THREE.Mesh(cockpitGeo, cockpitMat);
426
- cockpit.position.set(0, 0.3, -0.8);
427
- ship.add(cockpit);
428
-
429
- const thrusterGeo = new THREE.CylinderGeometry(0.3, 0.5, 1, 8);
430
- const thrusterMat = new THREE.MeshPhongMaterial({ color: 0xff8800, emissive: 0xff8800 });
431
- const leftThruster = new THREE.Mesh(thrusterGeo, thrusterMat);
432
- leftThruster.position.set(-1.2, -0.1, 1.5);
433
- ship.add(leftThruster);
434
- const rightThruster = new THREE.Mesh(thrusterGeo, thrusterMat);
435
- rightThruster.position.set(1.2, -0.1, 1.5);
436
- ship.add(rightThruster);
437
-
438
- ship.scale.set(0.7,0.7,0.7);
439
- return ship;
440
- }
441
-
442
- function createPlayerShipGroup() {
443
- if (GAME.playerShipGroup) {
444
- GAME.scene.remove(GAME.playerShipGroup);
445
- GAME.playerShipGroup = null;
446
- }
447
-
448
- GAME.playerShipGroup = new THREE.Group();
449
- GAME.playerFleet = [];
450
-
451
- const fighterCount = GAME.players[GAME.currentPlayer].fighterCount;
452
- const spacing = 3;
453
-
454
- for (let i = 0; i < fighterCount; i++) {
455
- const individualShip = createIndividualShipMesh(GAME.currentPlayer);
456
- individualShip.position.x = (i - (fighterCount - 1) / 2) * spacing;
457
- GAME.playerFleet.push(individualShip);
458
- GAME.playerShipGroup.add(individualShip);
459
  }
 
460
 
461
- GAME.playerShipGroup.position.set(0, 0, 15);
462
- GAME.playerShipGroup.velocity = new THREE.Vector3();
463
- GAME.scene.add(GAME.playerShipGroup);
464
- }
465
-
466
- function createGrunt(pos) {
467
- const mat = new THREE.MeshPhongMaterial({ color: 0xff0000 });
468
- const mesh = new THREE.Group();
469
- mesh.add(new THREE.Mesh(new THREE.TetrahedronGeometry(1), mat));
470
- const wing1 = new THREE.Mesh(new THREE.BoxGeometry(0.2, 0.2, 1.5), mat);
471
- wing1.position.set(0.8, 0, 0);
472
- wing1.rotation.y = Math.PI / 4;
473
- mesh.add(wing1);
474
- const wing2 = wing1.clone();
475
- wing2.position.set(-0.8, 0, 0);
476
- wing2.rotation.y = -Math.PI / 4;
477
- mesh.add(wing2);
478
- mesh.scale.set(0.8, 0.8, 0.8);
479
- return createEnemyObject(mesh, pos, 100, 'grunt');
480
- }
481
-
482
- function createCharger(pos) {
483
- const mat = new THREE.MeshPhongMaterial({ color: 0xffff00 });
484
- const mesh = new THREE.Group();
485
- mesh.add(new THREE.Mesh(new THREE.OctahedronGeometry(1), mat));
486
- const cannon = new THREE.Mesh(new THREE.CylinderGeometry(0.1, 0.1, 1, 8), new THREE.MeshPhongMaterial({ color: 0x888800 }));
487
- cannon.position.set(0, 0, -0.8);
488
- mesh.add(cannon);
489
- mesh.scale.set(0.9, 0.9, 0.9);
490
- return createEnemyObject(mesh, pos, 150, 'charger');
491
- }
492
 
493
- function createBoss(pos) {
494
- const mat = new THREE.MeshPhongMaterial({ color: 0x00ffff });
495
- const mesh = new THREE.Group();
496
- mesh.add(new THREE.Mesh(new THREE.IcosahedronGeometry(1.2), mat));
497
- const eye = new THREE.Mesh(new THREE.SphereGeometry(0.3, 16, 8), new THREE.MeshPhongMaterial({ color: 0xff0000, emissive: 0xff0000 }));
498
- eye.position.set(0, 0.5, -0.5);
499
- mesh.add(eye);
500
- const antenna1 = new THREE.Mesh(new THREE.CylinderGeometry(0.1, 0.1, 1, 8), new THREE.MeshPhongMaterial({ color: 0xaaaaaa }));
501
- antenna1.position.set(-0.7, 0.7, 0);
502
- antenna1.rotation.x = Math.PI / 4;
503
- mesh.add(antenna1);
504
- const antenna2 = antenna1.clone();
505
- antenna2.position.set(0.7, 0.7, 0);
506
- antenna2.rotation.x = -Math.PI / 4;
507
- mesh.add(antenna2);
508
- return createEnemyObject(mesh, pos, 300, 'boss');
509
- }
510
 
511
- function createEnemyObject(mesh, pos, score, type) {
512
- const enemy = {
513
- mesh: mesh,
514
- state: 'FORMING',
515
- scoreValue: score,
516
- type: type,
517
- formationPos: new THREE.Vector3().copy(pos),
518
- diveTimer: Math.random() * 3 + 2,
519
- tractorBeam: null,
520
- isCapturing: false,
521
- initialSpawnPosition: new THREE.Vector3()
522
  };
523
- const spawnX = (Math.random() - 0.5) * 80;
524
- const spawnZ = -50 - Math.random() * 20;
525
- enemy.mesh.position.set(spawnX, 0, spawnZ);
526
- enemy.initialSpawnPosition.copy(enemy.mesh.position);
527
- GAME.enemies.push(enemy);
528
- GAME.scene.add(enemy.mesh);
529
- return enemy;
530
- }
531
-
532
- function createStarfield() {
533
- const numLayers = 3;
534
- const layerColors = [0xbbbbbb, 0xaaaaaa, 0x888888];
535
- const layerSizes = [0.4, 0.7, 1.0];
536
- const layerSpeeds = [10, 20, 30];
537
-
538
- for (let i = 0; i < numLayers; i++) {
539
- const starGeometry = new THREE.BufferGeometry();
540
- const starVertices = [];
541
- for (let j = 0; j < 5000; j++) {
542
- const x = THREE.MathUtils.randFloatSpread(1000);
543
- const y = THREE.MathUtils.randFloatSpread(1000);
544
- const z = THREE.MathUtils.randFloatSpread(1000);
545
- starVertices.push(x, y, z);
546
  }
547
- starGeometry.setAttribute('position', new THREE.Float32BufferAttribute(starVertices, 3));
548
- const starMaterial = new THREE.PointsMaterial({ color: layerColors[i], size: layerSizes[i] });
549
- const stars = new THREE.Points(starGeometry, starMaterial);
550
- stars.userData.speed = layerSpeeds[i];
551
- GAME.stars.push(stars);
552
- GAME.scene.add(stars);
553
- }
554
- }
555
-
556
- function startWave() {
557
- GAME.currentWaveEnemiesSpawned = 0;
558
- const waveInfo = GAME.waveData[(GAME.wave - 1) % GAME.waveData.length];
559
- GAME.currentWaveTotalEnemies = waveInfo.grunts + waveInfo.chargers + waveInfo.bosses;
560
-
561
- if (waveInfo.type === 'armada') {
562
- createArmada(waveInfo.grunts);
563
- } else {
564
- let enemyIndex = 0;
565
- function createEnemyInFormation(creatorFn) {
566
- const row = Math.floor(enemyIndex / 8);
567
- const col = enemyIndex % 8;
568
- const pos = new THREE.Vector3((col - 3.5) * 4, 0, -15 - row * 4);
569
- creatorFn(pos);
570
- enemyIndex++;
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
571
  }
572
-
573
- for (let i = 0; i < waveInfo.grunts; i++) createEnemyInFormation(createGrunt);
574
- for (let i = 0; i < waveInfo.chargers; i++) createEnemyInFormation(createCharger);
575
- for (let i = 0; i < waveInfo.bosses; i++) createEnemyInFormation(createBoss);
576
  }
577
- }
578
-
579
- function createArmada(count) {
580
- const rows = Math.ceil(Math.sqrt(count));
581
- const cols = Math.ceil(count / rows);
582
- const spacing = 3;
583
- let enemyCount = 0;
584
- for (let r = 0; r < rows; r++) {
585
- for (let c = 0; c < cols; c++) {
586
- if (enemyCount >= count) break;
587
- const posX = (c - cols / 2 + 0.5) * spacing;
588
- const posZ = -30 - (r * spacing);
589
- createGrunt(new THREE.Vector3(posX, 0, posZ));
590
- enemyCount++;
591
  }
592
- if (enemyCount >= count) break;
593
- }
594
- }
595
 
596
- function firePlayerProjectile() {
597
- if (!GAME.canShoot || GAME.state !== 'PLAYING' || !GAME.playerShipGroup) return;
598
- // Use Tone.now() to ensure distinct trigger times for rapidly fired sounds
599
- if(GAME.audioContextReady && GAME.sfx.playerShoot) GAME.sfx.playerShoot.triggerAttackRelease("C4", "32n", Tone.now() + Math.random() * 0.0001);
 
 
600
 
601
- GAME.canShoot = false;
602
- setTimeout(() => { GAME.canShoot = true; }, GAME.shootCooldown);
 
 
 
603
 
604
- const currentPlayerData = GAME.players[GAME.currentPlayer];
605
- const numShotsPerFighter = currentPlayerData.missileCount;
606
 
607
- GAME.playerFleet.forEach(fighter => {
608
- for (let i = 0; i < numShotsPerFighter; i++) {
609
- const geo = new THREE.CylinderGeometry(0.1, 0.1, 1, 8);
610
- const mat = new THREE.MeshBasicMaterial({ color: 0x00ffff });
611
- const p = new THREE.Mesh(geo, mat);
612
-
613
- const shotOffset = (i - (numShotsPerFighter - 1) / 2) * 0.5;
614
- p.position.copy(fighter.getWorldPosition(new THREE.Vector3())).add(new THREE.Vector3(shotOffset, 0, -1));
615
- p.rotation.x = Math.PI / 2;
616
-
617
- GAME.playerProjectiles.push({ mesh: p, velocity: new THREE.Vector3(0, 0, -1.5) });
618
- GAME.scene.add(p);
619
  }
620
- });
621
- }
622
-
623
- function fireEnemyProjectile(enemy) {
624
- if(GAME.audioContextReady && GAME.sfx.enemyShoot) GAME.sfx.enemyShoot.triggerAttackRelease("A3", "32n", Tone.now() + Math.random() * 0.0001);
625
- const geo = new THREE.SphereGeometry(0.3, 8, 8);
626
- const mat = new THREE.MeshBasicMaterial({ color: 0xffa500 });
627
- const p = new THREE.Mesh(geo, mat);
628
- p.position.copy(enemy.mesh.position);
629
- p.velocity = new THREE.Vector3().subVectors(GAME.playerShipGroup.position, enemy.mesh.position).normalize().multiplyScalar(0.3);
630
- GAME.enemyProjectiles.push(p);
631
- GAME.scene.add(p);
632
- }
633
-
634
- function createExplosion(position) {
635
- if(GAME.audioContextReady && GAME.sfx.explosion) GAME.sfx.explosion.triggerAttackRelease("8n", Tone.now() + Math.random() * 0.0001);
636
- const geo = new THREE.IcosahedronGeometry(1, 1);
637
- const mat = new THREE.MeshBasicMaterial({ color: 0xffff00, transparent: true });
638
- const explosion = new THREE.Mesh(geo, mat);
639
- explosion.position.copy(position);
640
- explosion.scale.set(0.1, 0.1, 0.1);
641
- explosion.life = 0.5;
642
- GAME.explosions.push(explosion);
643
- GAME.scene.add(explosion);
644
- }
645
-
646
- function createPowerUp(position) {
647
- if(GAME.audioContextReady && GAME.sfx.powerUp) GAME.sfx.powerUp.triggerAttackRelease("C6", Tone.now() + Math.random() * 0.0001);
648
- const geo = new THREE.CylinderGeometry(0.5, 0.5, 1, 12);
649
- const mat = new THREE.MeshPhongMaterial({ color: 0x0000ff });
650
- const powerUp = new THREE.Mesh(geo, mat);
651
- powerUp.position.copy(position);
652
- powerUp.rotation.x = Math.PI / 2;
653
- powerUp.velocity = new THREE.Vector3(0, 0, 0.05);
654
- GAME.powerUps.push(powerUp);
655
- GAME.scene.add(powerUp);
656
- }
657
-
658
- function updatePlayer(delta) {
659
- const acceleration = 0.05;
660
- const damping = 0.9;
661
-
662
- if (GAME.input.left) GAME.playerShipGroup.velocity.x -= acceleration;
663
- if (GAME.input.right) GAME.playerShipGroup.velocity.x += acceleration;
664
-
665
- GAME.playerShipGroup.position.x += GAME.playerShipGroup.velocity.x;
666
- GAME.playerShipGroup.velocity.x *= damping;
667
-
668
- GAME.playerShipGroup.position.x = Math.max(-GAME.playerBoundaryX, Math.min(GAME.playerBoundaryX, GAME.playerShipGroup.position.x));
669
- GAME.playerShipGroup.rotation.z = -GAME.playerShipGroup.velocity.x * 2;
670
-
671
- if (GAME.invincible) {
672
- GAME.playerShipGroup.visible = (Math.floor(GAME.clock.elapsedTime * 10) % 2 === 0);
673
- } else {
674
- GAME.playerShipGroup.visible = true;
675
- }
676
- }
677
-
678
- function updateEnemies(delta) {
679
- for (let i = GAME.enemies.length - 1; i >= 0; i--) {
680
- const enemy = GAME.enemies[i];
681
 
682
- if (enemy.state === 'FORMING') {
683
- enemy.mesh.position.lerp(enemy.formationPos, delta * 2);
684
- if (enemy.mesh.position.distanceTo(enemy.formationPos) < 0.1) {
685
- enemy.state = 'IN_FORMATION';
686
- }
687
- } else if (enemy.state === 'IN_FORMATION') {
688
- enemy.diveTimer -= delta;
689
- if (enemy.diveTimer <= 0) {
690
- enemy.state = 'DIVING';
691
- enemy.diveTarget = new THREE.Vector3(GAME.playerShipGroup.position.x + (Math.random()-0.5)*10, 0, 25);
692
- }
693
-
694
- if (enemy.type === 'boss' && !enemy.isCapturing && GAME.players[GAME.currentPlayer].fighterCount < 2 && Math.random() < 0.005) {
695
- startTractorBeam(enemy);
696
- }
697
-
698
- if(Math.random() < 0.0005) {
699
- fireEnemyProjectile(enemy);
700
- }
701
- } else if (enemy.state === 'DIVING') {
702
- enemy.mesh.position.lerp(enemy.diveTarget, delta * 1.5);
703
- if (enemy.mesh.position.z > 20) {
704
- enemy.mesh.position.z = -30;
705
- enemy.state = 'FORMING';
706
- enemy.diveTimer = Math.random() * 5 + 5;
707
- }
708
  }
709
- if(enemy.isCapturing) updateTractorBeam(enemy, delta);
710
- }
711
- }
712
-
713
- function startTractorBeam(boss) {
714
- if(GAME.capturedShip || GAME.players[GAME.currentPlayer].fighterCount > 1) return;
715
- if(GAME.audioContextReady && GAME.sfx.captureBeam) GAME.sfx.captureBeam.triggerAttackRelease("G2", "2s", Tone.now());
716
- boss.isCapturing = true;
717
- boss.captureTimer = 1.5;
718
- const beamGeo = new THREE.ConeGeometry(2, 20, 16, 1, true);
719
- const beamMat = new THREE.MeshBasicMaterial({ color: 0x00ffff, transparent: true, opacity: 0.5, side: THREE.DoubleSide });
720
- boss.tractorBeam = new THREE.Mesh(beamGeo, beamMat);
721
- boss.tractorBeam.position.set(0, -10, 0);
722
- boss.mesh.add(boss.tractorBeam);
723
- }
724
 
725
- function updateTractorBeam(boss, delta) {
726
- boss.captureTimer -= delta;
727
-
728
- const beamWorldPos = new THREE.Vector3();
729
- boss.tractorBeam.getWorldPosition(beamWorldPos);
730
- const playerShipWorldPos = new THREE.Vector3();
731
- GAME.playerShipGroup.getWorldPosition(playerShipWorldPos);
732
-
733
- if (playerShipWorldPos.distanceTo(beamWorldPos) < 5 && Math.abs(playerShipWorldPos.x - boss.mesh.position.x) < 3 && !GAME.invincible) {
734
- GAME.state = 'CAPTURED';
735
- GAME.capturedShip = {
736
- mesh: GAME.playerShipGroup,
737
- boss: boss,
738
- isFree: false
739
- };
740
- GAME.playerShipGroup.velocity.set(0,0,0);
741
- GAME.scene.remove(GAME.playerShipGroup);
742
- boss.mesh.add(GAME.playerShipGroup);
743
- GAME.playerShipGroup.position.set(0, 0, 3);
744
- }
745
-
746
- if (boss.captureTimer <= 0 || GAME.state === 'CAPTURED') {
747
- boss.isCapturing = false;
748
- if (boss.tractorBeam && boss.mesh) {
749
- boss.mesh.remove(boss.tractorBeam);
750
- }
751
- boss.tractorBeam = null;
752
- if(GAME.state === 'CAPTURED') {
753
- if (!GAME.capturedShip.isFree) {
754
- playerLostLife(true);
755
  }
 
756
  }
757
- }
758
- }
759
-
760
- function updateProjectiles(delta) {
761
- GAME.playerProjectiles.forEach((pObj, i) => {
762
- const p = pObj.mesh;
763
- const pVel = pObj.velocity;
764
 
765
- if (GAME.enemies.length > 0) {
766
- let closestEnemy = null;
767
- let minDist = Infinity;
768
- GAME.enemies.forEach(enemy => {
769
- const dist = p.position.distanceTo(enemy.mesh.position);
770
- if (dist < minDist) {
771
- minDist = dist;
772
- closestEnemy = enemy;
 
 
 
 
773
  }
774
- });
775
-
776
- if (closestEnemy) {
777
- const targetDirection = new THREE.Vector3().subVectors(closestEnemy.mesh.position, p.position).normalize();
778
- pVel.lerp(targetDirection.multiplyScalar(1.5), 0.05);
779
  }
 
 
 
 
 
 
 
780
  }
781
 
782
- p.position.add(pVel);
783
-
784
- if (p.position.z < -40) {
785
- GAME.scene.remove(p);
786
- GAME.playerProjectiles.splice(i, 1);
787
- }
788
- });
789
- GAME.enemyProjectiles.forEach((p, i) => {
790
- p.position.add(p.velocity);
791
- if (p.position.z > 40) {
792
- GAME.scene.remove(p);
793
- GAME.enemyProjectiles.splice(i, 1);
794
- }
795
- });
796
- }
797
-
798
- function updateExplosions(delta) {
799
- GAME.explosions.forEach((ex, i) => {
800
- ex.life -= delta;
801
- ex.scale.multiplyScalar(1 + delta * 5);
802
- ex.material.opacity = ex.life * 2;
803
- if(ex.life <= 0) {
804
- GAME.scene.remove(ex);
805
- GAME.explosions.splice(i,1);
806
- }
807
- });
808
- }
809
-
810
- function updatePowerUps(delta) {
811
- GAME.powerUps.forEach((p, i) => {
812
- p.position.z += p.velocity.z;
813
- if (p.position.z > 20) {
814
- GAME.scene.remove(p);
815
- GAME.powerUps.splice(i, 1);
816
  }
817
- });
818
- }
819
 
820
- function checkCollisions() {
821
- if (!GAME.playerShipGroup || GAME.state !== 'PLAYING') return;
822
-
823
- const playerBox = new THREE.Box3().setFromObject(GAME.playerShipGroup);
824
-
825
- for (let i = GAME.playerProjectiles.length - 1; i >= 0; i--) {
826
- const pObj = GAME.playerProjectiles[i];
827
- const p = pObj.mesh;
828
- const pBox = new THREE.Box3().setFromObject(p);
829
- for (let j = GAME.enemies.length - 1; j >= 0; j--) {
830
- const enemy = GAME.enemies[j];
831
- const eBox = new THREE.Box3().setFromObject(enemy.mesh);
832
- if (pBox.intersectsBox(eBox)) {
833
- GAME.scene.remove(p);
834
- GAME.playerProjectiles.splice(i, 1);
835
-
836
- hitEnemy(enemy, j);
837
- break;
838
  }
839
- }
840
- }
841
-
842
- if (!GAME.invincible) {
843
- for (let i = GAME.enemies.length - 1; i >= 0; i--) {
844
- const enemy = GAME.enemies[i];
845
- const eBox = new THREE.Box3().setFromObject(enemy.mesh);
846
- if (playerBox.intersectsBox(eBox)) {
847
- hitEnemy(enemy, i);
848
- playerLostLife();
849
- break;
850
  }
 
851
  }
852
  }
853
 
854
- if (!GAME.invincible) {
855
- for (let i = GAME.enemyProjectiles.length - 1; i >= 0; i--) {
856
- const p = GAME.enemyProjectiles[i];
857
- const pBox = new THREE.Box3().setFromObject(p);
858
- if(playerBox.intersectsBox(pBox)) {
859
- GAME.scene.remove(p);
860
- GAME.enemyProjectiles.splice(i,1);
861
- playerLostLife();
862
- break;
 
 
 
 
 
 
 
 
863
  }
864
  }
865
- }
866
-
867
- if(GAME.capturedShip && GAME.capturedShip.isFree) {
868
- const cBox = new THREE.Box3().setFromObject(GAME.capturedShip.mesh);
869
- if(playerBox.intersectsBox(cBox)) {
870
- GAME.players[GAME.currentPlayer].fighterCount++;
871
- GAME.scene.remove(GAME.playerShipGroup);
872
- createPlayerShipGroup();
873
- GAME.scene.remove(GAME.capturedShip.mesh);
874
- GAME.capturedShip = null;
875
- startInvincibility();
876
- }
877
- }
878
 
879
- for (let i = GAME.powerUps.length - 1; i >= 0; i--) {
880
- const p = GAME.powerUps[i];
881
- const pBox = new THREE.Box3().setFromObject(p);
882
- if (playerBox.intersectsBox(pBox)) {
883
- GAME.players[GAME.currentPlayer].missileCount++;
884
- GAME.scene.remove(p);
885
- GAME.powerUps.splice(i, 1);
886
- break;
887
- }
888
- }
889
- }
890
-
891
- function hitEnemy(enemy, index) {
892
- createExplosion(enemy.mesh.position);
893
- GAME.players[GAME.currentPlayer].score += enemy.scoreValue;
894
-
895
- if (Math.random() < GAME.powerUpDropChance) {
896
- createPowerUp(enemy.mesh.position);
897
- }
898
-
899
- if (GAME.capturedShip && GAME.capturedShip.boss === enemy) {
900
- const captured = GAME.capturedShip;
901
- captured.isFree = true;
902
- if (captured.boss.mesh && captured.mesh && captured.mesh.parent === captured.boss.mesh) {
903
- captured.boss.mesh.remove(captured.mesh);
904
  }
905
- GAME.scene.add(captured.mesh);
906
- captured.mesh.position.copy(enemy.mesh.position);
907
- captured.velocity = new THREE.Vector3(0, 0, 0.2);
908
- GAME.state = 'PLAYING';
909
  }
910
-
911
- if (enemy.mesh.parent) {
912
- GAME.scene.remove(enemy.mesh);
913
- }
914
- GAME.enemies.splice(index, 1);
915
 
916
- if(GAME.enemies.length === 0) {
917
- GAME.wave++;
918
- setTimeout(startWave, 2000);
919
- }
920
- }
921
-
922
- function playerLostLife(isCapturedLoss = false) {
923
- if(GAME.state === 'GAME_OVER') return;
924
- if(GAME.audioContextReady && GAME.sfx.playerHit) GAME.sfx.playerHit.triggerAttackRelease("C2", Tone.now() + Math.random() * 0.0001);
925
-
926
- const currentPlayerData = GAME.players[GAME.currentPlayer];
927
-
928
- if (isCapturedLoss && GAME.capturedShip) {
929
- if (GAME.capturedShip.mesh.parent) {
930
- GAME.scene.remove(GAME.capturedShip.mesh);
931
- }
932
- GAME.capturedShip = null;
933
- }
934
-
935
- if(currentPlayerData.fighterCount > 1) {
936
- currentPlayerData.fighterCount--;
937
- createExplosion(GAME.playerShipGroup.position);
938
- GAME.scene.remove(GAME.playerShipGroup);
939
- createPlayerShipGroup();
940
- startInvincibility();
941
- return;
942
- }
943
 
944
- currentPlayerData.lives--;
945
- createExplosion(GAME.playerShipGroup.position);
946
- GAME.scene.remove(GAME.playerShipGroup);
947
-
948
- if (currentPlayerData.lives < 0) {
949
- let allPlayersOut = true;
950
- if (GAME.numPlayers === 2) {
951
- const otherPlayer = GAME.currentPlayer === 'p1' ? 'p2' : 'p1';
952
- if (GAME.players[otherPlayer].lives >= 0) {
953
- GAME.currentPlayer = otherPlayer;
954
- allPlayersOut = false;
955
- document.getElementById('current-player-display').textContent = `${otherPlayer.toUpperCase()} TURN`;
956
- GAME.players[GAME.currentPlayer].fighterCount = 1;
957
- GAME.players[GAME.currentPlayer].missileCount = 1;
958
- createPlayerShipGroup();
959
- startInvincibility();
960
- GAME.state = 'PLAYING';
961
- updateHUD();
962
- return;
963
- }
964
- }
965
- if (allPlayersOut) {
966
- gameOver();
967
- }
968
- } else {
969
- setTimeout(() => {
970
- if (GAME.state !== 'GAME_OVER') {
971
- createPlayerShipGroup();
972
- startInvincibility();
973
- }
974
- }, 1000);
975
- }
976
- }
 
 
 
 
 
 
 
 
 
977
 
978
- function startInvincibility() {
979
- GAME.invincible = true;
980
- setTimeout(() => {
981
- GAME.invincible = false;
982
- if (GAME.playerShipGroup) GAME.playerShipGroup.visible = true;
983
- }, GAME.invincibilityDuration);
984
- }
985
 
986
- function gameOver() {
987
- if(GAME.audioContextReady) {
988
- stopAllMusic();
989
- if(GAME.sfx.gameOver) GAME.sfx.gameOver.triggerAttackRelease("C2", "2s", Tone.now());
990
- }
991
- GAME.state = 'GAME_OVER';
992
- const finalScore = GAME.players.p1.score + (GAME.numPlayers === 2 ? GAME.players.p2.score : 0);
993
- document.getElementById('final-score').textContent = `TOTAL SCORE: ${finalScore}`;
994
- document.getElementById('game-over-overlay').style.display = 'flex';
995
 
996
- const lowestHighScore = GAME.highScores.length > 0 ? GAME.highScores[GAME.highScores.length - 1].score : 0;
997
- if (finalScore > lowestHighScore || GAME.highScores.length < 10) {
998
- document.getElementById('high-score-form').classList.remove('hidden');
999
- } else {
1000
- document.getElementById('high-score-form').classList.add('hidden');
1001
  }
1002
- }
1003
-
1004
- function handleHighScoreSubmit(e) {
1005
- e.preventDefault();
1006
- const initials = document.getElementById('initials-input').value.toUpperCase();
1007
- const finalScore = GAME.players.p1.score + (GAME.numPlayers === 2 ? GAME.players.p2.score : 0);
1008
 
1009
- GAME.highScores.push({ initials: initials, score: finalScore });
1010
- GAME.highScores.sort((a, b) => b.score - a.score);
1011
- GAME.highScores = GAME.highScores.slice(0, 10);
 
 
 
 
 
 
1012
 
1013
- localStorage.setItem('galagaHighScores', JSON.stringify(GAME.highScores));
1014
-
1015
- document.getElementById('high-score-form').classList.add('hidden');
1016
- document.getElementById('initials-input').value = '';
1017
- showHighScores();
1018
- }
1019
 
1020
- function showHighScores() {
1021
- stopAllMusic();
1022
- if(GAME.audioContextReady && GAME.music.highScore) GAME.music.highScore.start(Tone.now());
1023
- document.getElementById('menu-overlay').style.display = 'none';
1024
- document.getElementById('game-over-overlay').style.display = 'none';
1025
- document.getElementById('high-scores-overlay').style.display = 'flex';
1026
 
1027
- const list = document.getElementById('high-scores-list');
1028
- list.innerHTML = '';
1029
- if (GAME.highScores.length === 0) {
1030
- list.innerHTML = '<li>No high scores yet!</li>';
1031
- } else {
1032
- GAME.highScores.forEach((entry, index) => {
1033
- const li = document.createElement('li');
1034
- li.innerHTML = `<span>${index + 1}. ${entry.initials}</span> <span>${entry.score}</span>`;
1035
- list.appendChild(li);
1036
- });
 
 
 
 
 
1037
  }
1038
- GAME.state = 'HIGH_SCORES';
1039
- }
1040
 
1041
- function togglePause() {
1042
- if (GAME.state === 'PLAYING') {
1043
- GAME.state = 'PAUSED';
1044
- stopAllMusic();
1045
- document.getElementById('pause-overlay').style.display = 'flex';
1046
- } else if (GAME.state === 'PAUSED') {
1047
- GAME.state = 'PLAYING';
1048
- document.getElementById('pause-overlay').style.display = 'none';
 
 
 
 
 
 
 
 
 
 
 
1049
  }
1050
- }
1051
-
1052
- function updateHUD() {
1053
- document.getElementById('score-p1-display').textContent = `P1 SCORE: ${GAME.players.p1.score}`;
1054
- document.getElementById('score-p2-display').textContent = `P2 SCORE: ${GAME.players.p2.score}`;
1055
- document.getElementById('wave-display').textContent = `WAVE: ${GAME.wave}`;
1056
- document.getElementById('high-score-current-display').textContent = `HIGH SCORE: ${GAME.highScores.length > 0 ? GAME.highScores[0].score : 0}`;
1057
- document.getElementById('current-player-display').textContent = `${GAME.currentPlayer.toUpperCase()} TURN`;
1058
 
1059
- const livesContainer = document.getElementById('lives-display');
1060
- livesContainer.innerHTML = `LIVES: `;
1061
- const lifeIcon = '<span>&#x1F6F8;</span>';
1062
- for (let i = 0; i < GAME.players[GAME.currentPlayer].lives; i++) {
1063
- livesContainer.innerHTML += lifeIcon;
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1064
  }
1065
- }
1066
 
1067
- function onWindowResize() {
1068
- GAME.camera.aspect = window.innerWidth / window.innerHeight;
1069
- GAME.camera.updateProjectionMatrix();
1070
- GAME.renderer.setSize(window.innerWidth, window.innerHeight);
1071
- }
1072
-
1073
- function animate() {
1074
- requestAnimationFrame(animate);
1075
- const delta = GAME.clock.getDelta();
1076
 
1077
- GAME.stars.forEach(starLayer => {
1078
- starLayer.position.z += delta * starLayer.userData.speed;
1079
- if (starLayer.position.z > 500) starLayer.position.z -= 1000;
1080
- });
1081
-
1082
- if (GAME.state === 'DEMO') {
1083
- GAME.demoTimer += delta;
1084
- GAME.musicAlternationTimer += delta;
1085
-
1086
- if (GAME.musicAlternationTimer >= GAME.musicAlternationInterval) {
1087
- if (GAME.music.demo && GAME.music.demo.state === 'started') {
1088
- if (GAME.music.demo) GAME.music.demo.stop();
1089
- if(GAME.highScores.length > 0 && GAME.music.highScore) {
1090
- GAME.music.highScore.start(Tone.now()); // Start high score music
1091
- }
1092
- } else if (GAME.highScores.length > 0 && GAME.music.highScore && GAME.music.highScore.state === 'started') {
1093
- if (GAME.music.highScore) GAME.music.highScore.stop();
1094
- if(GAME.music.demo) GAME.music.demo.start(Tone.now()); // Start demo music
1095
- } else if (GAME.highScores.length === 0 && GAME.music.demo && GAME.music.demo.state !== 'started') {
1096
- if(GAME.music.demo) GAME.music.demo.start(Tone.now()); // Only demo music if no high scores
1097
- }
1098
- GAME.musicAlternationTimer = 0;
1099
  }
1100
 
1101
- GAME.enemies.forEach(enemy => {
1102
- enemy.mesh.position.z += delta * 0.5;
1103
- enemy.mesh.rotation.y += delta * 0.5;
1104
- if (enemy.mesh.position.z > 10) {
1105
- enemy.mesh.position.z = -50;
1106
- }
1107
- });
1108
- updateEnemyProjectilesInDemo(delta);
1109
- updateExplosions(delta);
1110
- } else if (GAME.state === 'PLAYING') {
1111
- updatePlayer(delta);
1112
- updateEnemies(delta);
1113
- updateProjectiles(delta);
1114
- updatePowerUps(delta);
1115
- checkCollisions();
1116
- updateHUD();
1117
-
1118
- if (GAME.capturedShip && GAME.capturedShip.isFree) {
1119
- GAME.capturedShip.mesh.position.add(GAME.capturedShip.velocity);
1120
- if (GAME.capturedShip.mesh.position.z > 25) {
1121
- GAME.scene.remove(GAME.capturedShip.mesh);
1122
- GAME.capturedShip = null;
1123
- }
1124
- }
1125
  }
1126
-
1127
- updateExplosions(delta);
1128
-
1129
- GAME.renderer.render(GAME.scene, GAME.camera);
1130
- }
1131
-
1132
- function updateEnemyProjectilesInDemo(delta) {
1133
- GAME.enemyProjectiles.forEach((p, i) => {
1134
- p.position.add(p.velocity);
1135
- if (p.position.z > 40) {
1136
- GAME.scene.remove(p);
1137
- GAME.enemyProjectiles.splice(i, 1);
1138
- }
1139
- });
1140
- }
1141
 
1142
- window.onload = init;
1143
  </script>
1144
  </body>
1145
  </html>
 
1
  <!DOCTYPE html>
2
  <html lang="en">
3
  <head>
4
+ <title>Mermaid of Minnetonka</title>
5
+ <meta charset="utf-8">
6
+ <meta name="viewport" content="width=device-width, user-scalable=no, minimum-scale=1.0, maximum-scale=1.0">
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
7
  <style>
8
+ body {
9
+ margin: 0;
10
+ overflow: hidden;
11
+ background-color: #000;
12
+ font-family: 'Georgia', serif;
13
+ }
14
+ #blocker {
15
+ position: absolute;
16
+ width: 100%;
17
+ height: 100%;
18
+ background-color: rgba(0,0,0,0.7);
19
+ display: flex;
20
+ justify-content: center;
21
+ align-items: center;
22
+ z-index: 100;
23
+ }
24
+ #start-button {
25
+ padding: 20px 40px;
26
+ font-size: 24px;
27
+ background: #1a3a5a;
28
+ color: #cceeff;
29
+ border: 2px solid #cceeff;
30
+ border-radius: 10px;
31
+ cursor: pointer;
32
+ text-shadow: 0 0 10px #cceeff;
33
+ box-shadow: 0 0 20px rgba(135, 206, 250, 0.5);
34
+ }
35
+ #top-bar {
36
+ position: absolute;
37
+ top: 0;
38
+ left: 0;
39
+ width: 100%;
40
+ padding: 10px;
41
+ background: linear-gradient(to bottom, rgba(0, 15, 30, 0.8), rgba(0, 15, 30, 0));
42
+ box-sizing: border-box;
43
+ color: #e0f7ff;
44
+ text-shadow: 0 0 5px #66aaff;
45
+ z-index: 10;
46
+ }
47
+ #lyric-ticker-container {
48
+ width: 100%;
49
+ overflow: hidden;
50
+ white-space: nowrap;
51
+ }
52
+ #lyric-ticker-text {
53
+ display: inline-block;
54
+ padding-left: 100%;
55
+ animation: scrollText linear infinite;
56
+ }
57
+ @keyframes scrollText {
58
+ from { transform: translateX(0%); }
59
+ to { transform: translateX(-100%); }
60
+ }
61
+ #controls-ui {
62
  display: flex;
63
  justify-content: space-between;
64
+ align-items: center;
65
+ padding: 5px 20px;
66
+ font-size: 14px;
67
  }
68
+ #speed-control {
69
+ display: flex;
70
+ align-items: center;
71
+ gap: 10px;
72
+ }
73
+ input[type="range"] {
74
+ -webkit-appearance: none;
75
+ appearance: none;
76
+ width: 150px;
77
+ height: 5px;
78
+ background: rgba(135, 206, 250, 0.3);
79
+ border-radius: 5px;
80
+ outline: none;
81
+ }
82
+ input[type="range"]::-webkit-slider-thumb {
83
+ -webkit-appearance: none;
84
+ appearance: none;
85
+ width: 15px;
86
+ height: 15px;
87
+ background: #87cefa;
88
+ cursor: pointer;
89
+ border-radius: 50%;
90
+ border: 2px solid #e0f7ff;
91
  }
92
  </style>
93
  </head>
94
  <body>
95
+ <div id="blocker">
96
+ <button id="start-button">Start Experience</button>
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
97
  </div>
98
+
99
+ <div id="top-bar">
100
+ <div id="lyric-ticker-container">
101
+ <span id="lyric-ticker-text"></span>
 
 
 
 
 
 
 
 
 
 
 
 
102
  </div>
103
+ <div id="controls-ui">
104
+ <span>W/S: Fwd/Back | A/D: Turn | Space: Ascend | Shift: Descend</span>
105
+ <div id="speed-control">
106
+ <label for="speed-slider">Scroll Speed:</label>
107
+ <input type="range" id="speed-slider" min="1" max="100" value="50">
108
+ </div>
109
  </div>
110
  </div>
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
111
 
112
+ <audio id="song" loop>
113
+ <source src="YOUR_SONG_FILE.mp3" type="audio/mpeg">
114
+ Your browser does not support the audio element.
115
+ </audio>
116
+
117
+ <script type="importmap">
118
+ {
119
+ "imports": {
120
+ "three": "https://cdn.jsdelivr.net/npm/[email protected]/build/three.module.js",
121
+ "three/addons/": "https://cdn.jsdelivr.net/npm/[email protected]/examples/jsm/"
122
+ }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
123
  }
124
+ </script>
125
 
126
+ <script type="module">
127
+ import * as THREE from 'three';
128
+ import { Water } from 'three/addons/objects/Water.js';
129
+ import { Sky } from 'three/addons/objects/Sky.js';
130
+ import { SimplexNoise } from 'three/addons/math/SimplexNoise.js';
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
131
 
132
+ let scene, camera, renderer, mermaid, water, sky, terrain;
133
+ let controls = {};
134
+ const clock = new THREE.Clock();
135
+ const worldSize = 4000;
136
+ const waterLevel = 100;
 
 
 
 
 
 
 
 
 
 
 
 
137
 
138
+ let schools = [];
139
+ let animationId;
140
+
141
+ const mermaidVelocity = new THREE.Vector3();
142
+ const mermaidState = {
143
+ turnSpeed: 0,
144
+ forwardSpeed: 0,
145
+ tailSegments: [],
146
+ hairStrands: [],
147
+ leftEye: null,
148
+ rightEye: null
149
  };
150
+ const raycaster = new THREE.Raycaster();
151
+ const downVector = new THREE.Vector3(0, -1, 0);
152
+
153
+
154
+ const rhymingWords = [
155
+ 'gold', 'told', 'blue', 'through', 'see', 'be', 'stone', 'throne',
156
+ 'haze', 'ways', 'blue', 'new', 'remains', 'wanes',
157
+ 'art', 'apart', 'hymn', 'whim', 'before', 'shore', 'eulogy', 'monarchy',
158
+ 'historian', 'pre-diluvian', 'time', 'crime', 'rise', 'skies', 'feel', 'real',
159
+ 'haze', 'ways', 'blue', 'new', 'remains', 'wanes',
160
+ 'know', 'flow', 'keep', 'deep', 'drowned', 'Mound'
161
+ ].join(' ');
162
+
163
+ // --- L-SYSTEM ENGINE ---
164
+ function generateLSystem(axiom, rules, iterations) {
165
+ let currentString = axiom;
166
+ for (let i = 0; i < iterations; i++) {
167
+ let nextString = '';
168
+ for (const char of currentString) {
169
+ nextString += rules[char] || char;
170
+ }
171
+ currentString = nextString;
 
172
  }
173
+ return currentString;
174
+ }
175
+
176
+ function createLSystemGeometry(lsystemString, angle, length) {
177
+ const points = [];
178
+ const turtle = {
179
+ pos: new THREE.Vector3(0, 0, 0),
180
+ dir: new THREE.Vector3(0, 1, 0)
181
+ };
182
+ const stack = [];
183
+
184
+ points.push(turtle.pos.clone());
185
+
186
+ for (const char of lsystemString) {
187
+ switch (char) {
188
+ case 'F':
189
+ turtle.pos.addScaledVector(turtle.dir, length);
190
+ points.push(turtle.pos.clone());
191
+ break;
192
+ case '+':
193
+ turtle.dir.applyAxisAngle(new THREE.Vector3(0, 0, 1), angle);
194
+ break;
195
+ case '-':
196
+ turtle.dir.applyAxisAngle(new THREE.Vector3(0, 0, 1), -angle);
197
+ break;
198
+ case '&':
199
+ turtle.dir.applyAxisAngle(new THREE.Vector3(0, 1, 0), angle);
200
+ break;
201
+ case '^':
202
+ turtle.dir.applyAxisAngle(new THREE.Vector3(0, 1, 0), -angle);
203
+ break;
204
+ case '[':
205
+ stack.push({ pos: turtle.pos.clone(), dir: turtle.dir.clone() });
206
+ break;
207
+ case ']':
208
+ const popped = stack.pop();
209
+ turtle.pos = popped.pos;
210
+ turtle.dir = popped.dir;
211
+ points.push(turtle.pos.clone()); // Create a gap in the tube
212
+ points.push(turtle.pos.clone());
213
+ break;
214
+ }
215
  }
216
+ const curve = new THREE.CatmullRomCurve3(points);
217
+ return new THREE.TubeGeometry(curve, Math.round(points.length * 1.5), 0.2, 5, false);
 
 
218
  }
219
+
220
+ // --- BOIDS FLOCKING ENGINE ---
221
+ class Boid {
222
+ constructor(mesh) {
223
+ this.mesh = mesh;
224
+ this.velocity = new THREE.Vector3(Math.random() - 0.5, Math.random() - 0.5, Math.random() - 0.5).normalize();
225
+ this.maxSpeed = 10;
226
+ this.maxForce = 0.3;
 
 
 
 
 
 
227
  }
 
 
 
228
 
229
+ update(boids, school) {
230
+ const separation = this.separate(boids);
231
+ const alignment = this.align(boids);
232
+ const cohesion = this.cohere(boids);
233
+ const avoidance = this.avoid(mermaid.position);
234
+ const wander = this.wander(school.target);
235
 
236
+ separation.multiplyScalar(2.0);
237
+ alignment.multiplyScalar(1.0);
238
+ cohesion.multiplyScalar(1.0);
239
+ avoidance.multiplyScalar(3.0);
240
+ wander.multiplyScalar(0.5);
241
 
242
+ this.velocity.add(separation).add(alignment).add(cohesion).add(avoidance).add(wander);
243
+ this.velocity.clampLength(0, this.maxSpeed);
244
 
245
+ this.mesh.position.addScaledVector(this.velocity, clock.getDelta());
246
+ this.mesh.quaternion.setFromUnitVectors(new THREE.Vector3(0, 0, 1), this.velocity.clone().normalize());
 
 
 
 
 
 
 
 
 
 
247
  }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
248
 
249
+ wander(target) {
250
+ const desired = target.clone().sub(this.mesh.position);
251
+ desired.setLength(this.maxSpeed);
252
+ const steer = desired.sub(this.velocity);
253
+ steer.clampLength(0, this.maxForce);
254
+ return steer;
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
255
  }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
256
 
257
+ avoid(targetPos) {
258
+ const steer = new THREE.Vector3();
259
+ const distance = this.mesh.position.distanceTo(targetPos);
260
+ if (distance < 50) {
261
+ const desired = this.mesh.position.clone().sub(targetPos);
262
+ desired.setLength(this.maxSpeed);
263
+ steer.subVectors(desired, this.velocity).clampLength(0, this.maxForce * 2);
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
264
  }
265
+ return steer;
266
  }
 
 
 
 
 
 
 
267
 
268
+ separate(boids) {
269
+ const desiredSeparation = 10.0;
270
+ const steer = new THREE.Vector3();
271
+ let count = 0;
272
+ for (const other of boids) {
273
+ const d = this.mesh.position.distanceTo(other.mesh.position);
274
+ if ((d > 0) && (d < desiredSeparation)) {
275
+ const diff = new THREE.Vector3().subVectors(this.mesh.position, other.mesh.position);
276
+ diff.normalize();
277
+ diff.divideScalar(d);
278
+ steer.add(diff);
279
+ count++;
280
  }
 
 
 
 
 
281
  }
282
+ if (count > 0) steer.divideScalar(count);
283
+ if (steer.length() > 0) {
284
+ steer.setLength(this.maxSpeed);
285
+ steer.sub(this.velocity);
286
+ steer.clampLength(0, this.maxForce);
287
+ }
288
+ return steer;
289
  }
290
 
291
+ align(boids) {
292
+ const neighborDist = 50;
293
+ const sum = new THREE.Vector3();
294
+ let count = 0;
295
+ for (const other of boids) {
296
+ const d = this.mesh.position.distanceTo(other.mesh.position);
297
+ if ((d > 0) && (d < neighborDist)) {
298
+ sum.add(other.velocity);
299
+ count++;
300
+ }
301
+ }
302
+ if (count > 0) {
303
+ sum.divideScalar(count);
304
+ sum.setLength(this.maxSpeed);
305
+ const steer = sum.sub(this.velocity);
306
+ steer.clampLength(0, this.maxForce);
307
+ return steer;
308
+ }
309
+ return new THREE.Vector3();
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
310
  }
 
 
311
 
312
+ cohere(boids) {
313
+ const neighborDist = 50;
314
+ const sum = new THREE.Vector3();
315
+ let count = 0;
316
+ for (const other of boids) {
317
+ const d = this.mesh.position.distanceTo(other.mesh.position);
318
+ if ((d > 0) && (d < neighborDist)) {
319
+ sum.add(other.mesh.position);
320
+ count++;
321
+ }
 
 
 
 
 
 
 
 
322
  }
323
+ if (count > 0) {
324
+ sum.divideScalar(count);
325
+ const desired = sum.sub(this.mesh.position);
326
+ desired.setLength(this.maxSpeed);
327
+ const steer = desired.sub(this.velocity);
328
+ steer.clampLength(0, this.maxForce);
329
+ return steer;
 
 
 
 
330
  }
331
+ return new THREE.Vector3();
332
  }
333
  }
334
 
335
+ class School {
336
+ constructor(scene, count, modelFn, scale) {
337
+ this.boids = [];
338
+ this.target = new THREE.Vector3((Math.random() - 0.5) * worldSize, waterLevel - 50, (Math.random() - 0.5) * worldSize);
339
+
340
+ const model = modelFn();
341
+ model.scale.set(scale, scale, scale);
342
+
343
+ for(let i=0; i<count; i++){
344
+ const boidMesh = model.clone();
345
+ boidMesh.position.set(
346
+ (Math.random() - 0.5) * 200,
347
+ waterLevel - 50 + (Math.random() - 0.5) * 50,
348
+ (Math.random() - 0.5) * 200
349
+ );
350
+ scene.add(boidMesh);
351
+ this.boids.push(new Boid(boidMesh));
352
  }
353
  }
 
 
 
 
 
 
 
 
 
 
 
 
 
354
 
355
+ update() {
356
+ if(this.boids[0].mesh.position.distanceTo(this.target) < 200){
357
+ this.target.set((Math.random() - 0.5) * worldSize, waterLevel - 50 - Math.random() * 50, (Math.random() - 0.5) * worldSize);
358
+ }
359
+ for (const boid of this.boids) {
360
+ boid.update(this.boids, this);
361
+ }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
362
  }
 
 
 
 
363
  }
 
 
 
 
 
364
 
365
+ function init() {
366
+ scene = new THREE.Scene();
367
+ scene.fog = new THREE.FogExp2(0x0a1429, 0.0035);
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
368
 
369
+ camera = new THREE.PerspectiveCamera(75, window.innerWidth / window.innerHeight, 0.1, worldSize * 1.5);
370
+
371
+ renderer = new THREE.WebGLRenderer({ antialias: true });
372
+ renderer.setSize(window.innerWidth, window.innerHeight);
373
+ renderer.setPixelRatio(window.devicePixelRatio);
374
+ renderer.toneMapping = THREE.ACESFilmicToneMapping;
375
+ document.body.appendChild(renderer.domElement);
376
+
377
+ scene.add(new THREE.AmbientLight(0x6688aa, 2));
378
+ const sun = new THREE.DirectionalLight(0xffffff, 2.5);
379
+ sun.position.set(100, 200, 100);
380
+ scene.add(sun);
381
+ const godrayLight = new THREE.DirectionalLight(0x8eadd4, 1.5);
382
+ godrayLight.position.set(200, 300, 200);
383
+ scene.add(godrayLight);
384
+
385
+ // ... water and sky setup (no changes)
386
+ const waterGeometry = new THREE.PlaneGeometry(worldSize * 2, worldSize * 2);
387
+ water = new Water(waterGeometry, { textureWidth: 512, textureHeight: 512, waterNormals: new THREE.TextureLoader().load('https://cdn.jsdelivr.net/npm/[email protected]/examples/textures/waternormals.jpg', (t) => { t.wrapS = t.wrapT = THREE.RepeatWrapping; }), sunDirection: sun.position.clone().normalize(), sunColor: 0xffffff, waterColor: 0x001e0f, distortionScale: 3.7, fog: scene.fog !== undefined });
388
+ water.rotation.x = -Math.PI / 2;
389
+ water.position.y = waterLevel;
390
+ scene.add(water);
391
+ sky = new Sky();
392
+ sky.scale.setScalar(worldSize);
393
+ scene.add(sky);
394
+ const skyUniforms = sky.material.uniforms;
395
+ skyUniforms['turbidity'].value = 10; skyUniforms['rayleigh'].value = 2; skyUniforms['mieCoefficient'].value = 0.005; skyUniforms['mieDirectionalG'].value = 0.8;
396
+ const pmremGenerator = new THREE.PMREMGenerator(renderer);
397
+ const phi = THREE.MathUtils.degToRad(88); const theta = THREE.MathUtils.degToRad(170);
398
+ sun.position.setFromSphericalCoords(1, phi, theta);
399
+ sky.material.uniforms['sunPosition'].value.copy(sun.position);
400
+ scene.environment = pmremGenerator.fromScene(sky).texture;
401
+
402
+
403
+ createTerrain();
404
+ createMermaid();
405
+ camera.position.set(mermaid.position.x, mermaid.position.y, mermaid.position.z + 15);
406
+
407
+ createFlora(200);
408
+ createMermaidCity(-worldSize/2 + 500, -worldSize/2 + 500);
409
+ createSurfaceWildlife(50);
410
+ createLilyPads(100);
411
 
412
+ // Create fish schools
413
+ schools.push(new School(scene, 50, createPikeModel, 1.0));
414
+ schools.push(new School(scene, 80, createSunfishModel, 1.5));
415
+ schools.push(new School(scene, 60, createBullheadModel, 1.2));
 
 
 
416
 
417
+ setupUI();
418
+ window.addEventListener('resize', onWindowResize, false);
419
+ document.addEventListener('keydown', (e) => controls[e.code] = true);
420
+ document.addEventListener('keyup', (e) => controls[e.code] = false);
 
 
 
 
 
421
 
422
+ renderer.render(scene, camera);
 
 
 
 
423
  }
 
 
 
 
 
 
424
 
425
+ function setupUI() { /* ... no changes ... */ }
426
+ function createTerrain() { /* ... no changes ... */ }
427
+ function createMermaid() { /* ... no changes ... */ }
428
+ function onWindowResize() { /* ... no changes ... */ }
429
+ // Copying unchanged functions for brevity
430
+ setupUI = () => {const s=document.getElementById('start-button'),b=document.getElementById('blocker'),l=document.getElementById('lyric-ticker-text'),c=document.getElementById('speed-slider');l.textContent=rhymingWords;function u(){const n=20,t=200,i=t-((c.value-1)/99)*(t-n);l.style.animationDuration=`${i}s`}c.addEventListener('input',u);u();s.addEventListener('click',()=>{b.style.display='none';document.getElementById('song').play().catch(e=>console.error("Audio play failed:",e));animate()})};
431
+ createTerrain = () => {const s=worldSize,e=100,g=new THREE.PlaneGeometry(s,s,e,e);g.rotateX(-Math.PI/2);const n=new SimplexNoise(),p=g.attributes.position;for(let i=0;i<p.count;i++){const x=p.getX(i),z=p.getZ(i);let y=10*n.noise(x/500,z/500)-30*n.noise(x/800,z/800)-15*(Math.abs(n.noise(x/200,z/200))**2);p.setY(i,Math.max(y,-50))}g.computeVertexNormals();const m=new THREE.MeshStandardMaterial({color:0x3c322a,roughness:0.8,metalness:0.1});terrain=new THREE.Mesh(g,m);scene.add(terrain)};
432
+ createMermaid = () => {mermaid=new THREE.Group();const s=new THREE.MeshStandardMaterial({color:0x89CFF0,metalness:0.5,roughness:0.2}),t=new THREE.MeshStandardMaterial({color:0x008080,metalness:0.6,roughness:0.1,emissive:0x002222}),h=new THREE.MeshStandardMaterial({color:0xff4500,roughness:0.8});const o=new THREE.Mesh(new THREE.CapsuleGeometry(0.5,1.5,4,8),s);o.position.y=1.0;mermaid.add(o);const a=new THREE.Mesh(new THREE.SphereGeometry(0.7,16,12),s);a.position.y=2.5;mermaid.add(a);const e=new THREE.MeshBasicMaterial({color:0xffffff}),p=new THREE.MeshBasicMaterial({color:0x000000});mermaidState.leftEye=new THREE.Mesh(new THREE.SphereGeometry(0.15,8,8),e);mermaidState.rightEye=new THREE.Mesh(new THREE.SphereGeometry(0.15,8,8),e);const l=new THREE.Mesh(new THREE.SphereGeometry(0.08,8,8),p),r=new THREE.Mesh(new THREE.SphereGeometry(0.08,8,8),p);mermaidState.leftEye.add(l);mermaidState.rightEye.add(r);l.position.z=0.1;r.position.z=0.1;mermaidState.leftEye.position.set(-0.25,2.6,0.55);mermaidState.rightEye.position.set(0.25,2.6,0.55);mermaid.add(mermaidState.leftEye,mermaidState.rightEye);let c=mermaid;for(let i=0;i<8;i++){const g=new THREE.Mesh(new THREE.SphereGeometry(0.4-i*0.04,8,6),t);g.position.y=-0.4;c.add(g);mermaidState.tailSegments.push(g);c=g}const d=new THREE.ShapeGeometry(new THREE.Shape([new THREE.Vector2(0,0),new THREE.Vector2(1.5,0.5),new THREE.Vector2(1,1.5),new THREE.Vector2(0,1),new THREE.Vector2(-1,1.5),new THREE.Vector2(-1.5,0.5)]));const m=new THREE.Mesh(d,t);m.rotation.x=Math.PI/2;m.position.y=-0.5;c.add(m);for(let i=0;i<5;i++){const u=new THREE.CatmullRomCurve3([new THREE.Vector3(0,0,0),new THREE.Vector3(Math.random()-0.5,-1,Math.random()-0.5),new THREE.Vector3((Math.random()-0.5)*2,-3,(Math.random()-0.5)*2),new THREE.Vector3((Math.random()-0.5)*3,-5,(Math.random()-0.5)*3)]);const f=new THREE.TubeGeometry(u,20,0.05,5,false),w=new THREE.Mesh(f,h);w.position.y=2.8;mermaidState.hairStrands.push(w);mermaid.add(w)}mermaid.position.set(0,waterLevel-10,0);scene.add(mermaid)};
433
+ onWindowResize = () => {camera.aspect=window.innerWidth/window.innerHeight;camera.updateProjectionMatrix();renderer.setSize(window.innerWidth,window.innerHeight)};
434
 
435
+ // --- NEW/UPDATED WORLD CREATION ---
 
 
 
 
 
436
 
437
+ function createFlora(count) {
438
+ const plantRule = { 'F': 'FF+[+F-F-F]-[-F+F+F]' };
439
+ const plantAxiom = 'F';
440
+ const plantLSystem = generateLSystem(plantAxiom, plantRule, 3);
441
+ const plantGeom = createLSystemGeometry(plantLSystem, THREE.MathUtils.degToRad(25), 0.5);
442
+ plantGeom.scale(2,2,2);
443
 
444
+ const material = new THREE.MeshStandardMaterial({color: 0x228B22, emissive: 0x113311, side: THREE.DoubleSide});
445
+
446
+ for(let i=0; i < count; i++) {
447
+ const plantMesh = new THREE.Mesh(plantGeom, material);
448
+ const x = (Math.random() - 0.5) * worldSize;
449
+ const z = (Math.random() - 0.5) * worldSize;
450
+
451
+ raycaster.set(new THREE.Vector3(x, waterLevel, z), downVector);
452
+ const intersects = raycaster.intersectObject(terrain);
453
+ if(intersects.length > 0) {
454
+ plantMesh.position.copy(intersects[0].point);
455
+ plantMesh.rotation.set(0, Math.random() * Math.PI * 2, 0);
456
+ scene.add(plantMesh);
457
+ }
458
+ }
459
  }
 
 
460
 
461
+ function createMermaidCity(x_offset, z_offset) {
462
+ const cityRule = {'F': 'F[+FF][-FF]F[-F][+F]F'};
463
+ const cityAxiom = 'F';
464
+ const cityLSystem = generateLSystem(cityAxiom, cityRule, 4);
465
+ const cityGeom = createLSystemGeometry(cityLSystem, THREE.MathUtils.degToRad(20), 5);
466
+
467
+ const material = new THREE.MeshStandardMaterial({ color: 0x77aaff, emissive: 0x88ccff, emissiveIntensity: 0.8, roughness: 0.6 });
468
+
469
+ for(let i = 0; i < 20; i++){
470
+ const building = new THREE.Mesh(cityGeom, material);
471
+ building.position.set(
472
+ x_offset + (Math.random() - 0.5) * 800,
473
+ -50,
474
+ z_offset + (Math.random() - 0.5) * 800
475
+ );
476
+ building.scale.setScalar(1 + Math.random() * 2);
477
+ building.rotation.set(0, Math.random() * Math.PI * 2, 0);
478
+ scene.add(building);
479
+ }
480
  }
 
 
 
 
 
 
 
 
481
 
482
+ function createPikeModel() {
483
+ const group = new THREE.Group();
484
+ const bodyMat = new THREE.MeshStandardMaterial({ color: 0x90ee90, roughness: 0.5, metalness: 0.2});
485
+ const body = new THREE.Mesh(new THREE.CapsuleGeometry(0.3, 2.5, 4, 8), bodyMat);
486
+ body.rotation.z = Math.PI / 2;
487
+ const tail = new THREE.Mesh(new THREE.BoxGeometry(0.1, 0.8, 0.8), bodyMat);
488
+ tail.position.x = -1.3;
489
+ group.add(body, tail);
490
+ return group;
491
+ }
492
+
493
+ function createSunfishModel() {
494
+ const group = new THREE.Group();
495
+ const bodyMat = new THREE.MeshStandardMaterial({ color: 0x6495ed, roughness: 0.5, metalness: 0.2});
496
+ const body = new THREE.Mesh(new THREE.SphereGeometry(1, 8, 6), bodyMat);
497
+ body.scale.set(1.5, 1, 0.3);
498
+ const tail = new THREE.Mesh(new THREE.BoxGeometry(0.5, 0.6, 0.1), bodyMat);
499
+ tail.position.x = -1;
500
+ group.add(body, tail);
501
+ return group;
502
+ }
503
+
504
+ function createBullheadModel() {
505
+ const group = new THREE.Group();
506
+ const bodyMat = new THREE.MeshStandardMaterial({ color: 0x8b4513, roughness: 0.8});
507
+ const body = new THREE.Mesh(new THREE.BoxGeometry(1.5, 0.8, 1), bodyMat);
508
+ const tail = new THREE.Mesh(new THREE.ConeGeometry(0.5, 1, 4), bodyMat);
509
+ tail.rotation.z = -Math.PI / 2;
510
+ tail.position.x = -1;
511
+ group.add(body, tail);
512
+ return group;
513
+ }
514
+
515
+ function createSurfaceWildlife(count) {
516
+ const mesh = new THREE.InstancedMesh(new THREE.BoxGeometry(1, 0.5, 2), new THREE.MeshLambertMaterial({color: 0xeeeeee}), count);
517
+ const dummy = new THREE.Object3D();
518
+ for(let i=0; i<count; i++){
519
+ dummy.position.set((Math.random() - 0.5) * worldSize, waterLevel, (Math.random() - 0.5) * worldSize);
520
+ dummy.updateMatrix();
521
+ mesh.setMatrixAt(i, dummy.matrix);
522
+ }
523
+ scene.add(mesh);
524
+ }
525
+
526
+ function createLilyPads(count) {
527
+ const g = new THREE.CircleGeometry(1, 8);
528
+ g.rotateX(-Math.PI/2);
529
+ const mesh = new THREE.InstancedMesh(g, new THREE.MeshLambertMaterial({color: 0x006400}), count);
530
+ const dummy = new THREE.Object3D();
531
+ for(let i=0; i<count; i++){
532
+ dummy.position.set((Math.random() - 0.5) * worldSize, waterLevel + 0.1, (Math.random() - 0.5) * worldSize);
533
+ dummy.updateMatrix();
534
+ mesh.setMatrixAt(i, dummy.matrix);
535
+ }
536
+ scene.add(mesh);
537
  }
 
538
 
539
+ function updateMermaid() { /* ... no changes ... */ }
540
+ updateMermaid = () => {const d=clock.getDelta(),t=clock.getElapsedTime();const m=30.0,r=1.0;let a=0,v=0,f=0;if(controls['KeyW'])f=1.0;if(controls['KeyS'])f=-0.5;if(controls['KeyA'])a=r;if(controls['KeyD'])a=-r;if(controls['Space'])v=10.0;if(controls['ShiftLeft']||controls['ShiftRight'])v=-10.0;mermaidState.turnSpeed=THREE.MathUtils.lerp(mermaidState.turnSpeed,a,d*2.0);mermaid.rotation.y+=mermaidState.turnSpeed*d;mermaidState.forwardSpeed=THREE.MathUtils.lerp(mermaidState.forwardSpeed,m*f,d*1.5);const w=new THREE.Vector3(0,0,-1).applyQuaternion(mermaid.quaternion);mermaidVelocity.x=w.x*mermaidState.forwardSpeed;mermaidVelocity.z=w.z*mermaidState.forwardSpeed;const b=0.5;mermaidVelocity.y=THREE.MathUtils.lerp(mermaidVelocity.y,v||b,d*2.0);mermaid.position.addScaledVector(mermaidVelocity,d);raycaster.set(mermaid.position,downVector);const i=raycaster.intersectObject(terrain);if(i.length>0){const g=i[0].point.y;if(mermaid.position.y<g+3.0){mermaid.position.y=g+3.0;mermaidVelocity.y=Math.max(mermaidVelocity.y,2.0)}}mermaid.position.y=Math.min(mermaid.position.y,waterLevel-1);mermaid.rotation.z=THREE.MathUtils.lerp(mermaid.rotation.z,mermaidState.turnSpeed*-0.5,d*2.0);const p=-mermaidVelocity.y*0.05;mermaid.rotation.x=THREE.MathUtils.lerp(mermaid.rotation.x,p,d*2.5);const s=Math.abs(mermaidState.forwardSpeed/m);mermaidState.tailSegments.forEach((e,i)=>{const n=Math.sin(t*6.0-i*0.6)*(0.1+s*0.6);e.rotation.y=n;e.rotation.z=n*0.5});mermaidState.hairStrands.forEach((e,i)=>{e.rotation.x=Math.sin(t*1.5+i)*0.1-s*0.1;e.rotation.z=Math.sin(t*1.5+i)*0.1});const j=Math.sin(t*20)*0.02;mermaidState.leftEye.children[0].position.x=j;mermaidState.rightEye.children[0].position.x=-j;const o=new THREE.Vector3(0,4,18);o.applyQuaternion(mermaid.quaternion);camera.position.lerp(mermaid.position.clone().add(o),d*2.0);camera.lookAt(mermaid.position.clone().add(new THREE.Vector3(0,2,0)))};
 
 
 
 
 
 
 
541
 
542
+ function animate() {
543
+ animationId = requestAnimationFrame(animate);
544
+ updateMermaid();
545
+
546
+ for (const school of schools) {
547
+ school.update();
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
548
  }
549
 
550
+ water.material.uniforms['time'].value += 1.0 / 60.0;
551
+ renderer.render(scene, camera);
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
552
  }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
553
 
554
+ init();
555
  </script>
556
  </body>
557
  </html>