awacke1 commited on
Commit
19a13cf
·
verified ·
1 Parent(s): 6494f2b

Update index.html

Browse files
Files changed (1) hide show
  1. index.html +914 -18
index.html CHANGED
@@ -1,19 +1,915 @@
1
- <!doctype html>
2
- <html>
3
- <head>
4
- <meta charset="utf-8" />
5
- <meta name="viewport" content="width=device-width" />
6
- <title>My static Space</title>
7
- <link rel="stylesheet" href="style.css" />
8
- </head>
9
- <body>
10
- <div class="card">
11
- <h1>Welcome to your static Space!</h1>
12
- <p>You can modify this app directly by editing <i>index.html</i> in the Files and versions tab.</p>
13
- <p>
14
- Also don't forget to check the
15
- <a href="https://huggingface.co/docs/hub/spaces" target="_blank">Spaces documentation</a>.
16
- </p>
17
- </div>
18
- </body>
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
19
  </html>
 
1
+ <!DOCTYPE html>
2
+ <html lang="en">
3
+ <head>
4
+ <meta charset="UTF-8">
5
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
6
+ <title>Three.js 3D Co-op Combat Game - Top Down</title>
7
+ <script src="https://cdnjs.cloudflare.com/ajax/libs/three.js/r128/three.min.js"></script>
8
+ <script src="https://cdn.tailwindcss.com"></script>
9
+ <style>
10
+ body {
11
+ margin: 0;
12
+ overflow: hidden;
13
+ font-family: 'Inter', sans-serif;
14
+ background-color: #1a202c;
15
+ color: #e2e8f0;
16
+ display: flex;
17
+ flex-direction: column;
18
+ align-items: center;
19
+ justify-content: center;
20
+ height: 100vh;
21
+ position: relative;
22
+ }
23
+ #game-canvas-wrapper {
24
+ width: 100vw;
25
+ height: 100vh;
26
+ border: none;
27
+ border-radius: 0;
28
+ position: relative;
29
+ }
30
+ canvas {
31
+ display: block;
32
+ width: 100%;
33
+ height: 100%;
34
+ }
35
+
36
+ /* Score and Shield Status UI (Sides) */
37
+ .player-side-ui {
38
+ position: absolute;
39
+ top: 20px;
40
+ padding: 10px 15px;
41
+ font-size: 1.1rem;
42
+ font-weight: bold;
43
+ color: #1a202c;
44
+ border-radius: 0.375rem;
45
+ box-shadow: 0 2px 4px rgba(0,0,0,0.2);
46
+ z-index: 10;
47
+ background-color: rgba(255,255,255,0.1); /* Fallback */
48
+ }
49
+ #player1-side-ui {
50
+ left: 20px;
51
+ background-color: #38b2ac; /* Teal */
52
+ }
53
+ #player2-side-ui {
54
+ right: 20px;
55
+ background-color: #ed8936; /* Orange */
56
+ }
57
+ .shield-timer {
58
+ font-size: 0.9rem;
59
+ margin-top: 5px;
60
+ font-weight: normal;
61
+ }
62
+
63
+ /* Health Bars Container (Top Center) */
64
+ #health-bars-container {
65
+ position: absolute;
66
+ top: 15px;
67
+ left: 50%;
68
+ transform: translateX(-50%);
69
+ display: flex;
70
+ gap: 20px; /* Space between health bars */
71
+ z-index: 11; /* Above side UIs */
72
+ align-items: center;
73
+ }
74
+ .health-bar-wrapper {
75
+ display: flex;
76
+ flex-direction: column;
77
+ align-items: center;
78
+ }
79
+ .health-bar-label {
80
+ font-size: 0.9rem;
81
+ font-weight: bold;
82
+ margin-bottom: 3px;
83
+ }
84
+ .health-bar {
85
+ width: 200px; /* Width of the health bar */
86
+ height: 20px; /* Height of the health bar */
87
+ background-color: #4a5568; /* Tailwind gray-600 (darker background) */
88
+ border-radius: 5px;
89
+ border: 2px solid #718096; /* Tailwind gray-500 (border) */
90
+ overflow: hidden; /* To contain the fill */
91
+ box-shadow: inset 0 1px 3px rgba(0,0,0,0.2);
92
+ }
93
+ .health-bar-fill {
94
+ height: 100%;
95
+ width: 100%; /* Start full */
96
+ border-radius: 3px; /* Slightly smaller radius for fill */
97
+ transition: width 0.3s ease-out, background-color 0.3s ease;
98
+ }
99
+ #player1-health-bar-fill { background-color: #38b2ac; } /* Teal */
100
+ #player2-health-bar-fill { background-color: #ed8936; } /* Orange */
101
+
102
+
103
+ .controls-and-reset {
104
+ position: absolute;
105
+ bottom: 10px;
106
+ left: 50%;
107
+ transform: translateX(-50%);
108
+ display: flex;
109
+ flex-direction: column;
110
+ align-items: center;
111
+ width: 100%;
112
+ max-width: 700px;
113
+ z-index: 10;
114
+ }
115
+ .instructions {
116
+ background-color: rgba(45, 55, 72, 0.9);
117
+ padding: 0.75rem 1.25rem;
118
+ border-radius: 0.5rem;
119
+ box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
120
+ text-align: center;
121
+ margin-bottom: 10px;
122
+ }
123
+ .instructions h1 { font-size: 1.2rem; margin-bottom: 0.3rem; }
124
+ .instructions p { font-size: 0.85rem; margin-bottom: 0.2rem; }
125
+ kbd {
126
+ display: inline-block;
127
+ padding: 0.25rem 0.5rem;
128
+ font-size: 0.75rem;
129
+ font-weight: 600;
130
+ color: #1f2937;
131
+ background-color: #f3f4f6;
132
+ border: 1px solid #d1d5db;
133
+ border-radius: 0.25rem;
134
+ margin: 0 0.1rem;
135
+ }
136
+ #reset-button {
137
+ padding: 0.7rem 1.5rem;
138
+ font-size: 1rem;
139
+ font-weight: bold;
140
+ color: white;
141
+ background-color: #c53030;
142
+ border: none;
143
+ border-radius: 0.375rem;
144
+ cursor: pointer;
145
+ transition: background-color 0.2s;
146
+ }
147
+ #reset-button:hover {
148
+ background-color: #9b2c2c;
149
+ }
150
+ #game-over-message {
151
+ position: absolute;
152
+ top: 50%;
153
+ left: 50%;
154
+ transform: translate(-50%, -50%);
155
+ background-color: rgba(0, 0, 0, 0.9);
156
+ color: white;
157
+ padding: 25px 35px;
158
+ border-radius: 10px;
159
+ font-size: 2rem;
160
+ text-align: center;
161
+ z-index: 20;
162
+ display: none;
163
+ border: 3px solid #e53e3e;
164
+ }
165
+ </style>
166
+ </head>
167
+ <body>
168
+ <div id="player1-side-ui" class="player-side-ui">
169
+ <div>P1 Score: <span id="player1-score-text">0</span></div>
170
+ <div class="shield-timer">Shield: <span id="player1-shield-status">READY</span></div>
171
+ </div>
172
+ <div id="player2-side-ui" class="player-side-ui">
173
+ <div>P2 Score: <span id="player2-score-text">0</span></div>
174
+ <div class="shield-timer">Shield: <span id="player2-shield-status">READY</span></div>
175
+ </div>
176
+
177
+ <div id="health-bars-container">
178
+ <div class="health-bar-wrapper">
179
+ <div class="health-bar-label" style="color: #38b2ac;">Player 1 Health</div>
180
+ <div id="player1-health-bar" class="health-bar">
181
+ <div id="player1-health-bar-fill" class="health-bar-fill"></div>
182
+ </div>
183
+ </div>
184
+ <div class="health-bar-wrapper">
185
+ <div class="health-bar-label" style="color: #ed8936;">Player 2 Health</div>
186
+ <div id="player2-health-bar" class="health-bar">
187
+ <div id="player2-health-bar-fill" class="health-bar-fill"></div>
188
+ </div>
189
+ </div>
190
+ </div>
191
+
192
+ <div id="game-canvas-wrapper">
193
+ <div id="game-over-message">Game Over!</div>
194
+ </div>
195
+
196
+ <div class="controls-and-reset">
197
+ <div class="instructions">
198
+ <h1>3D Co-op Combat! (Top Down)</h1>
199
+ <p>P1 (Teal): <kbd>W</kbd> Up, <kbd>S</kbd> Down, <kbd>A</kbd> Left, <kbd>D</kbd> Right</p>
200
+ <p>P2 (Orange): <kbd>I</kbd> Up, <kbd>K</kbd> Down, <kbd>J</kbd> Left, <kbd>L</kbd> Right</p>
201
+ <p><kbd>SPACEBAR</kbd> to Fire (Both Players) | Heal (Both Players)</p>
202
+ </div>
203
+ <button id="reset-button">Reset Game</button>
204
+ </div>
205
+
206
+ <script>
207
+ // --- Game Constants ---
208
+ const PLAYER_SPEED = 0.18; // Slightly increased speed for larger area
209
+ const PLAYER_RADIUS = 0.5;
210
+ const PROJECTILE_SIZE = 0.15;
211
+ const PROJECTILE_SPEED = 0.45;
212
+ const PLAYER_MAX_HEALTH = 10;
213
+ const INVADER_RADIUS = 0.6;
214
+ const PARATROOPER_RADIUS = 0.4;
215
+ const INVADER_FIRE_COOLDOWN = 1700;
216
+ const PARATROOPER_FIRE_COOLDOWN = 2100;
217
+ const PLAYER_MANUAL_FIRE_COOLDOWN = 350;
218
+ const SHIELD_DURATION = 7000;
219
+ const SHIELD_COOLDOWN = 15000;
220
+ const AUTO_SHIELD_HEALTH_THRESHOLD = 3;
221
+ const AUTO_SHIELD_CONSIDER_INTERVAL = 3000;
222
+
223
+ const GAME_PLANE_WIDTH = 32; // Wider plane
224
+ const GAME_PLANE_HEIGHT = 22; // Deeper plane
225
+ // const DIVIDING_LINE_POS_X = 0; // No longer used
226
+ const PARATROOPER_SPAWN_Y = 15;
227
+ const PARATROOPER_DROP_SPEED = 0.05;
228
+ const PARATROOPER_SPAWN_INTERVAL = 4000;
229
+
230
+ // --- Global Variables ---
231
+ let scene, camera, renderer;
232
+ let player1, player2;
233
+ let projectiles = [];
234
+ let invaders = [];
235
+ let paratroopers = [];
236
+ let keysPressed = {};
237
+ let gameOver = false;
238
+ let lastParatrooperSpawnTime = 0;
239
+ let ambientLight, directionalLight;
240
+ let groundPlane; // dividingLineMesh removed
241
+
242
+ // DOM Elements
243
+ let player1ScoreEl, player1ShieldStatusEl, player1HealthBarFillEl;
244
+ let player2ScoreEl, player2ShieldStatusEl, player2HealthBarFillEl;
245
+ let resetButtonEl, gameOverMessageEl, gameCanvasWrapperEl;
246
+
247
+ // --- Initialization ---
248
+ function init() {
249
+ gameCanvasWrapperEl = document.getElementById('game-canvas-wrapper');
250
+ player1ScoreEl = document.getElementById('player1-score-text');
251
+ player1ShieldStatusEl = document.getElementById('player1-shield-status');
252
+ player1HealthBarFillEl = document.getElementById('player1-health-bar-fill');
253
+ player2ScoreEl = document.getElementById('player2-score-text');
254
+ player2ShieldStatusEl = document.getElementById('player2-shield-status');
255
+ player2HealthBarFillEl = document.getElementById('player2-health-bar-fill');
256
+ resetButtonEl = document.getElementById('reset-button');
257
+ gameOverMessageEl = document.getElementById('game-over-message');
258
+
259
+ scene = new THREE.Scene();
260
+ scene.background = new THREE.Color(0x1a202c);
261
+
262
+ setupCamera();
263
+ setupLights();
264
+
265
+ renderer = new THREE.WebGLRenderer({ antialias: true });
266
+ renderer.setSize(window.innerWidth, window.innerHeight);
267
+ renderer.shadowMap.enabled = true;
268
+ gameCanvasWrapperEl.appendChild(renderer.domElement);
269
+
270
+ createGround();
271
+ // createDividingLine(); // Removed
272
+
273
+ resetButtonEl.addEventListener('click', resetGame);
274
+ document.addEventListener('keydown', onKeyDown);
275
+ document.addEventListener('keyup', onKeyUp);
276
+ window.addEventListener('resize', onWindowResize, false);
277
+
278
+ resetGame();
279
+ animate();
280
+ }
281
+
282
+ function setupCamera() {
283
+ const aspect = window.innerWidth / window.innerHeight;
284
+ camera = new THREE.PerspectiveCamera(55, aspect, 0.1, 1000); // FOV can be adjusted
285
+ // True Top-Down View: Position camera directly above, looking straight down.
286
+ // Adjust Y based on desired visible area of the plane.
287
+ // For a plane of 32x22, a Y of ~25-30 with FOV 55-60 should work well.
288
+ camera.position.set(0, 28, 0);
289
+ camera.lookAt(0, 0, 0);
290
+ // Ensure camera's 'up' is correct for top-down if issues arise (usually (0,0,-1) or (0,0,1) if Y is depth)
291
+ // For Y-up world, default up (0,1,0) for camera is fine when looking at (0,0,0) from (0,Y,0)
292
+ // However, for the models to appear upright from top-down, their 'up' should be world Y.
293
+ // And camera's 'up' should be world Z (or -Z) to orient the view correctly.
294
+ // Let's try setting camera.up to make world +Z appear as "up" on screen.
295
+ camera.up.set(0, 0, -1); // This makes world -Z "up" on screen.
296
+ // If world +Z should be "up", use (0,0,1)
297
+ // For typical top-down where -Z world is "up" on screen:
298
+ camera.up.set(0,0,-1); // This orients the view so positive Z world is "down" on screen.
299
+ }
300
+
301
+ function setupLights() {
302
+ ambientLight = new THREE.AmbientLight(0xffffff, 0.7);
303
+ scene.add(ambientLight);
304
+ directionalLight = new THREE.DirectionalLight(0xffffff, 0.9);
305
+ directionalLight.position.set(10, 30, 10); // Light from an angle
306
+ directionalLight.castShadow = true;
307
+ directionalLight.shadow.mapSize.width = 2048;
308
+ directionalLight.shadow.mapSize.height = 2048;
309
+ directionalLight.shadow.camera.near = 0.5;
310
+ directionalLight.shadow.camera.far = 100; // Increased far plane for larger view
311
+ directionalLight.shadow.camera.left = -GAME_PLANE_WIDTH / 1.5; // Adjust shadow camera for larger plane
312
+ directionalLight.shadow.camera.right = GAME_PLANE_WIDTH / 1.5;
313
+ directionalLight.shadow.camera.top = GAME_PLANE_HEIGHT / 1.5;
314
+ directionalLight.shadow.camera.bottom = -GAME_PLANE_HEIGHT / 1.5;
315
+ scene.add(directionalLight);
316
+ }
317
+
318
+ function createGround() {
319
+ const groundGeometry = new THREE.PlaneGeometry(GAME_PLANE_WIDTH, GAME_PLANE_HEIGHT);
320
+ const groundMaterial = new THREE.MeshStandardMaterial({ color: 0x2d3748, side: THREE.DoubleSide });
321
+ groundPlane = new THREE.Mesh(groundGeometry, groundMaterial);
322
+ groundPlane.rotation.x = -Math.PI / 2;
323
+ groundPlane.receiveShadow = true;
324
+ scene.add(groundPlane);
325
+ }
326
+
327
+ // createDividingLine function is removed
328
+
329
+ function resetGame() {
330
+ gameOver = false;
331
+ gameOverMessageEl.style.display = 'none';
332
+ keysPressed = {};
333
+
334
+ projectiles.forEach(p => scene.remove(p)); projectiles = [];
335
+ invaders.forEach(i => scene.remove(i.meshGroup)); invaders = [];
336
+ paratroopers.forEach(pt => scene.remove(pt.meshGroup)); paratroopers = [];
337
+ if (player1) scene.remove(player1.meshGroup);
338
+ if (player2) scene.remove(player2.meshGroup);
339
+
340
+ createPlayers();
341
+ createInitialInvaders();
342
+ lastParatrooperSpawnTime = Date.now();
343
+
344
+ updateUI();
345
+ }
346
+
347
+ function createPlayerModel(color) {
348
+ const group = new THREE.Group();
349
+ const bodyRadius = PLAYER_RADIUS * 0.6;
350
+ const bodyHeight = PLAYER_RADIUS * 1.2;
351
+ const bodyCylinderGeom = new THREE.CylinderGeometry(bodyRadius, bodyRadius, bodyHeight, 16);
352
+ const bodyMaterial = new THREE.MeshStandardMaterial({ color: color, metalness: 0.3, roughness: 0.6 });
353
+ const bodyCylinder = new THREE.Mesh(bodyCylinderGeom, bodyMaterial);
354
+ bodyCylinder.castShadow = true;
355
+ bodyCylinder.name = "body";
356
+ group.add(bodyCylinder);
357
+
358
+ const sphereGeom = new THREE.SphereGeometry(bodyRadius, 16, 8);
359
+ const topSphere = new THREE.Mesh(sphereGeom, bodyMaterial);
360
+ topSphere.position.y = bodyHeight / 2;
361
+ topSphere.castShadow = true;
362
+ group.add(topSphere);
363
+
364
+ const bottomSphere = new THREE.Mesh(sphereGeom, bodyMaterial);
365
+ bottomSphere.position.y = -bodyHeight / 2;
366
+ bottomSphere.castShadow = true;
367
+ group.add(bottomSphere);
368
+
369
+ const barrelLength = PLAYER_RADIUS * 0.8;
370
+ const barrelRadius = PLAYER_RADIUS * 0.15;
371
+ const barrelGeom = new THREE.CylinderGeometry(barrelRadius, barrelRadius, barrelLength, 8);
372
+ const barrelMaterial = new THREE.MeshStandardMaterial({ color: 0x666666, metalness: 0.5, roughness: 0.4 });
373
+ const barrel = new THREE.Mesh(barrelGeom, barrelMaterial);
374
+ // Barrel points along the group's local +Z axis for top-down view (if model is upright)
375
+ barrel.rotation.x = Math.PI / 2;
376
+ barrel.position.z = bodyRadius + barrelLength / 2 - 0.1; // Position in "front" (local +Z)
377
+ barrel.position.y = 0;
378
+ barrel.castShadow = true;
379
+ group.add(barrel);
380
+
381
+ // For top-down, models are typically upright on Y, and rotate around Y.
382
+ // The group itself will be positioned with its base on y=0.
383
+ group.position.y = bodyHeight/2;
384
+ return group;
385
+ }
386
+
387
+ function createInvaderModel(color) {
388
+ const group = new THREE.Group();
389
+ const mainBodySize = INVADER_RADIUS * 0.8;
390
+ const bodyGeom = new THREE.BoxGeometry(mainBodySize, mainBodySize, mainBodySize);
391
+ const bodyMaterial = new THREE.MeshStandardMaterial({ color: color, metalness: 0.2, roughness: 0.7 });
392
+ const body = new THREE.Mesh(bodyGeom, bodyMaterial);
393
+ body.castShadow = true;
394
+ body.name = "body";
395
+ group.add(body);
396
+
397
+ const eyeRadius = mainBodySize * 0.15;
398
+ const eyeGeom = new THREE.SphereGeometry(eyeRadius, 8, 8);
399
+ const eyeMaterial = new THREE.MeshStandardMaterial({ color: 0xffff00, emissive: 0xffff00, emissiveIntensity: 0.5 });
400
+
401
+ // Eyes on +Z face, assuming model's "front" is +Z for top-down
402
+ const eye1 = new THREE.Mesh(eyeGeom, eyeMaterial);
403
+ eye1.position.set(mainBodySize * 0.25, mainBodySize * 0.2, mainBodySize * 0.51);
404
+ group.add(eye1);
405
+ const eye2 = new THREE.Mesh(eyeGeom, eyeMaterial);
406
+ eye2.position.set(-mainBodySize * 0.25, mainBodySize * 0.2, mainBodySize * 0.51);
407
+ group.add(eye2);
408
+
409
+ group.position.y = mainBodySize / 2;
410
+ return group;
411
+ }
412
+
413
+ function createParatrooperModel(color) {
414
+ const group = new THREE.Group();
415
+ const bodyRadius = PARATROOPER_RADIUS * 0.7;
416
+ const bodyHeight = PARATROOPER_RADIUS * 1.5;
417
+
418
+ const bodyGeom = new THREE.CylinderGeometry(bodyRadius*0.7, bodyRadius, bodyHeight, 12);
419
+ const bodyMaterial = new THREE.MeshStandardMaterial({ color: color, metalness: 0.3, roughness: 0.6 });
420
+ const body = new THREE.Mesh(bodyGeom, bodyMaterial);
421
+ body.castShadow = true;
422
+ body.name = "body";
423
+ group.add(body);
424
+
425
+ const canopyRadius = PARATROOPER_RADIUS * 1.5;
426
+ const canopyGeom = new THREE.SphereGeometry(canopyRadius, 16, 8, 0, Math.PI * 2, 0, Math.PI / 2);
427
+ const canopyMaterial = new THREE.MeshStandardMaterial({ color: 0xf0f0f0, transparent: true, opacity: 0.75, side: THREE.DoubleSide });
428
+ const canopy = new THREE.Mesh(canopyGeom, canopyMaterial);
429
+ canopy.position.y = bodyHeight / 2 + canopyRadius * 0.3;
430
+ canopy.castShadow = false;
431
+ group.add(canopy);
432
+ return group;
433
+ }
434
+
435
+ function createPlayers() {
436
+ player1 = { meshGroup: createPlayerModel(0x38b2ac),
437
+ health: PLAYER_MAX_HEALTH, score: 0, lastShotTime: 0,
438
+ shieldActive: false, shieldEndTime: 0, shieldCooldownEndTime: 0,
439
+ lastAutoShieldConsiderTime: 0,
440
+ id: 'player1', radius: PLAYER_RADIUS
441
+ };
442
+ player1.meshGroup.position.set(-GAME_PLANE_WIDTH / 4, player1.meshGroup.position.y, 0);
443
+ scene.add(player1.meshGroup);
444
+
445
+ player2 = { meshGroup: createPlayerModel(0xed8936),
446
+ health: PLAYER_MAX_HEALTH, score: 0, lastShotTime: 0,
447
+ shieldActive: false, shieldEndTime: 0, shieldCooldownEndTime: 0,
448
+ lastAutoShieldConsiderTime: 0,
449
+ id: 'player2', radius: PLAYER_RADIUS
450
+ };
451
+ player2.meshGroup.position.set(GAME_PLANE_WIDTH / 4, player2.meshGroup.position.y, 0);
452
+ scene.add(player2.meshGroup);
453
+ }
454
+
455
+ function createInitialInvaders() {
456
+ const invaderPositions = [
457
+ new THREE.Vector3(-GAME_PLANE_WIDTH / 2.5, 0, GAME_PLANE_HEIGHT / 3),
458
+ new THREE.Vector3(GAME_PLANE_WIDTH / 2.5, 0, -GAME_PLANE_HEIGHT / 3),
459
+ new THREE.Vector3(-GAME_PLANE_WIDTH / 3, 0, -GAME_PLANE_HEIGHT / 3.5),
460
+ new THREE.Vector3(GAME_PLANE_WIDTH / 3, 0, GAME_PLANE_HEIGHT / 3.5),
461
+ new THREE.Vector3(0, 0, GAME_PLANE_HEIGHT / 2.2),
462
+ new THREE.Vector3(0, 0, -GAME_PLANE_HEIGHT / 2.2),
463
+ new THREE.Vector3(GAME_PLANE_WIDTH / 4, 0, 0), // More central spawns
464
+ new THREE.Vector3(-GAME_PLANE_WIDTH / 4, 0, 0),
465
+ ];
466
+ invaderPositions.forEach((pos, index) => {
467
+ const invaderMeshGroup = createInvaderModel(0x9f7aea);
468
+ invaderMeshGroup.position.set(pos.x, invaderMeshGroup.position.y, pos.z);
469
+ const invader = {
470
+ meshGroup: invaderMeshGroup, health: 1, id: `invader${index}`,
471
+ lastShotTime: 0, radius: INVADER_RADIUS, originalZ: pos.z, oscillationTime: Math.random() * Math.PI * 2
472
+ };
473
+ scene.add(invader.meshGroup);
474
+ invaders.push(invader);
475
+ });
476
+ }
477
+
478
+ function spawnParatrooper() {
479
+ const spawnX = (Math.random() - 0.5) * (GAME_PLANE_WIDTH * 0.95);
480
+ const spawnZ = (Math.random() - 0.5) * (GAME_PLANE_HEIGHT * 0.95);
481
+
482
+ const paratrooperMeshGroup = createParatrooperModel(0xdd6b20);
483
+ paratrooperMeshGroup.position.set(spawnX, PARATROOPER_SPAWN_Y, spawnZ);
484
+
485
+ const bodyHeight = PARATROOPER_RADIUS * 1.5;
486
+ const paratrooper = {
487
+ meshGroup: paratrooperMeshGroup, health: 1, id: `paratrooper${paratroopers.length}`,
488
+ lastShotTime: 0, radius: PARATROOPER_RADIUS,
489
+ targetY: bodyHeight / 2, landed: false
490
+ };
491
+ scene.add(paratrooper.meshGroup);
492
+ paratroopers.push(paratrooper);
493
+ lastParatrooperSpawnTime = Date.now();
494
+ }
495
+
496
+ function createProjectile(shooter) {
497
+ if (!shooter || shooter.health <= 0) return;
498
+ const now = Date.now();
499
+
500
+ if (shooter.id.includes('player')) {
501
+ if (now - shooter.lastShotTime < PLAYER_MANUAL_FIRE_COOLDOWN) return;
502
+ shooter.lastShotTime = now;
503
+ }
504
+ else if (shooter.id.includes('invader') || shooter.id.includes('paratrooper')) {
505
+ const fireCooldown = shooter.id.includes('invader') ? INVADER_FIRE_COOLDOWN : PARATROOPER_FIRE_COOLDOWN;
506
+ if (now - shooter.lastShotTime < fireCooldown) return;
507
+ shooter.lastShotTime = now;
508
+ }
509
+
510
+ const projectileGeom = new THREE.SphereGeometry(PROJECTILE_SIZE, 8, 8);
511
+ let projectileColor;
512
+ let velocity = new THREE.Vector3();
513
+ const startPos = shooter.meshGroup.position.clone();
514
+
515
+ // Adjust start Y for projectile origin from model center
516
+ const modelHeight = shooter.id.includes('player') ? (PLAYER_RADIUS * 1.2 + PLAYER_RADIUS * 0.6) : // Approx height of player model body
517
+ (shooter.id.includes('paratrooper') ? PARATROOPER_RADIUS * 1.5 : INVADER_RADIUS * 0.8); // Approx height of enemy body
518
+ startPos.y = shooter.meshGroup.position.y; // Model base is already at its Y position.
519
+ // For top-down, this is fine, or adjust slightly if needed.
520
+ // Let's assume firing from model's current Y is okay for top-down.
521
+
522
+ // Projectiles fire in the direction the shooter's model is currently facing
523
+ const worldForward = new THREE.Vector3();
524
+ // Model's "front" is local +Z due to createPlayerModel barrel change for top-down
525
+ const localForward = new THREE.Vector3(0, 0, 1);
526
+ worldForward.copy(localForward).applyQuaternion(shooter.meshGroup.quaternion);
527
+
528
+
529
+ if (shooter.id.includes('player')) {
530
+ projectileColor = shooter.id === 'player1' ? 0x81e6d9 : 0xfbd38d;
531
+ velocity.copy(worldForward).multiplyScalar(PROJECTILE_SPEED);
532
+ } else if (shooter.id.includes('invader') || shooter.id.includes('paratrooper')) {
533
+ projectileColor = shooter.id.includes('invader') ? 0xc4b5fd : 0xffa07a;
534
+ velocity.copy(worldForward).multiplyScalar(PROJECTILE_SPEED * 0.8);
535
+ } else { return; }
536
+
537
+ const projectileMaterial = new THREE.MeshStandardMaterial({ color: projectileColor, emissive: projectileColor, emissiveIntensity: 0.7 });
538
+ const projectile = new THREE.Mesh(projectileGeom, projectileMaterial);
539
+ projectile.castShadow = true;
540
+
541
+ const offset = worldForward.clone().multiplyScalar(shooter.radius * 1.2);
542
+ startPos.add(offset);
543
+ projectile.position.copy(startPos);
544
+
545
+ projectile.userData = { ownerId: shooter.id, velocity: velocity, creationTime: Date.now() };
546
+ scene.add(projectile);
547
+ projectiles.push(projectile);
548
+ }
549
+
550
+ // --- Event Handlers ---
551
+ function onKeyDown(event) {
552
+ if (gameOver && event.key.toLowerCase() !== " ") return;
553
+ keysPressed[event.key.toLowerCase()] = true;
554
+ const key = event.key.toLowerCase();
555
+
556
+ if (key === ' ') {
557
+ if (player1) player1.health = PLAYER_MAX_HEALTH;
558
+ if (player2) player2.health = PLAYER_MAX_HEALTH;
559
+ if (gameOver) {
560
+ gameOver = false;
561
+ gameOverMessageEl.style.display = 'none';
562
+ }
563
+ if (player1 && player1.health > 0 && !player1.shieldActive) createProjectile(player1);
564
+ if (player2 && player2.health > 0 && !player2.shieldActive) createProjectile(player2);
565
+
566
+ updateUI();
567
+ event.preventDefault();
568
+ }
569
+ }
570
+ function onKeyUp(event) {
571
+ keysPressed[event.key.toLowerCase()] = false;
572
+ }
573
+ function onWindowResize() {
574
+ renderer.setSize(window.innerWidth, window.innerHeight);
575
+ camera.aspect = window.innerWidth / window.innerHeight;
576
+ camera.updateProjectionMatrix();
577
+ }
578
+
579
+ function activateShield(player) {
580
+ const now = Date.now();
581
+ if (player && !player.shieldActive && now > player.shieldCooldownEndTime) {
582
+ player.shieldActive = true;
583
+ player.shieldEndTime = now + SHIELD_DURATION;
584
+ player.shieldCooldownEndTime = player.shieldEndTime + SHIELD_COOLDOWN;
585
+ if (!player.shieldMesh) {
586
+ const shieldGeom = new THREE.SphereGeometry(player.radius * 1.6, 16, 16);
587
+ const shieldMat = new THREE.MeshStandardMaterial({ color: 0x00ddff, transparent: true, opacity: 0.30, emissive: 0x00ccee, emissiveIntensity: 0.25 });
588
+ player.shieldMesh = new THREE.Mesh(shieldGeom, shieldMat);
589
+ player.meshGroup.add(player.shieldMesh);
590
+ }
591
+ player.shieldMesh.visible = true;
592
+ updateUI();
593
+ }
594
+ }
595
+
596
+ function updateShields() {
597
+ const now = Date.now();
598
+ [player1, player2].forEach(player => {
599
+ if (player && player.shieldActive && now > player.shieldEndTime) {
600
+ player.shieldActive = false;
601
+ if (player.shieldMesh) player.shieldMesh.visible = false;
602
+ updateUI();
603
+ }
604
+ });
605
+ }
606
+
607
+ function handleAutoShielding() {
608
+ const now = Date.now();
609
+ [player1, player2].forEach(player => {
610
+ if (player && player.health > 0 && !player.shieldActive && now > player.shieldCooldownEndTime) {
611
+ if (player.health <= AUTO_SHIELD_HEALTH_THRESHOLD) {
612
+ activateShield(player);
613
+ player.lastAutoShieldConsiderTime = now;
614
+ return;
615
+ }
616
+ if (player.health < PLAYER_MAX_HEALTH && (now - player.lastAutoShieldConsiderTime > AUTO_SHIELD_CONSIDER_INTERVAL)) {
617
+ if (Math.random() < 0.3) {
618
+ activateShield(player);
619
+ }
620
+ player.lastAutoShieldConsiderTime = now;
621
+ }
622
+ }
623
+ });
624
+ }
625
+
626
+ function handleEntityAutoRotation(entity) { // Generic for players and enemies
627
+ if (!entity || entity.health <= 0) return;
628
+
629
+ let closestTarget = null;
630
+ let minDistanceSq = Infinity;
631
+ let targets = [];
632
+
633
+ if (entity.id.includes('player')) { // Player targets enemies
634
+ targets = [...invaders, ...paratroopers];
635
+ } else { // Enemy targets players
636
+ if (player1 && player1.health > 0) targets.push(player1);
637
+ if (player2 && player2.health > 0) targets.push(player2);
638
+ }
639
+
640
+ targets.forEach(target => {
641
+ if (target.health > 0) {
642
+ const distanceSq = entity.meshGroup.position.distanceToSquared(target.meshGroup.position);
643
+ if (distanceSq < minDistanceSq) {
644
+ minDistanceSq = distanceSq;
645
+ closestTarget = target;
646
+ }
647
+ }
648
+ });
649
+
650
+ if (closestTarget) {
651
+ const targetPosition = new THREE.Vector3();
652
+ targetPosition.copy(closestTarget.meshGroup.position);
653
+ // For top-down, we want to rotate around Y axis, looking at XZ plane projection of target
654
+ targetPosition.y = entity.meshGroup.position.y;
655
+ entity.meshGroup.lookAt(targetPosition);
656
+ }
657
+ }
658
+
659
+
660
+ function handlePlayerMovement(player, upKey, downKey, leftKey, rightKey) {
661
+ if (!player || player.health <= 0) return;
662
+
663
+ const moveDelta = new THREE.Vector3(0,0,0);
664
+
665
+ // W/I for "up" on screen (world -Z), S/K for "down" (world +Z)
666
+ // A/J for "left" on screen (world -X), D/L for "right" (world +X)
667
+ if (keysPressed[upKey]) moveDelta.z -= PLAYER_SPEED;
668
+ if (keysPressed[downKey]) moveDelta.z += PLAYER_SPEED;
669
+ if (keysPressed[leftKey]) moveDelta.x -= PLAYER_SPEED;
670
+ if (keysPressed[rightKey]) moveDelta.x += PLAYER_SPEED;
671
+
672
+ // Normalize for consistent speed if moving diagonally
673
+ if (moveDelta.x !== 0 && moveDelta.z !== 0) {
674
+ moveDelta.normalize().multiplyScalar(PLAYER_SPEED);
675
+ }
676
+
677
+ player.meshGroup.position.add(moveDelta);
678
+
679
+ // Boundary checks (no dividing line)
680
+ const halfWorldWidth = GAME_PLANE_WIDTH / 2 - player.radius;
681
+ const halfWorldDepth = GAME_PLANE_HEIGHT / 2 - player.radius;
682
+
683
+ player.meshGroup.position.x = Math.max(-halfWorldWidth, Math.min(halfWorldWidth, player.meshGroup.position.x));
684
+ player.meshGroup.position.z = Math.max(-halfWorldDepth, Math.min(halfWorldDepth, player.meshGroup.position.z));
685
+
686
+ const otherPlayer = player.id === 'player1' ? player2 : player1;
687
+ if (otherPlayer && otherPlayer.health > 0) {
688
+ const distSq = player.meshGroup.position.distanceToSquared(otherPlayer.meshGroup.position);
689
+ if (distSq < (player.radius + otherPlayer.radius) ** 2 && distSq > 0.001) {
690
+ const delta = player.meshGroup.position.clone().sub(otherPlayer.meshGroup.position).normalize();
691
+ const overlap = (player.radius + otherPlayer.radius) - Math.sqrt(distSq);
692
+ player.meshGroup.position.add(delta.multiplyScalar(overlap / 2 + 0.01));
693
+ }
694
+ }
695
+ }
696
+
697
+ function updateInvaderBehavior() {
698
+ invaders.forEach(invader => {
699
+ if (invader.health <= 0) return;
700
+ handleEntityAutoRotation(invader); // Invaders also auto-rotate
701
+
702
+ invader.oscillationTime += 0.025;
703
+ // Simple sidestep or hold position logic could be added here, for now they mostly rotate and fire
704
+ // let targetZ = invader.originalZ + Math.sin(invader.oscillationTime) * (GAME_PLANE_HEIGHT * 0.1);
705
+ // invader.meshGroup.position.z = THREE.MathUtils.lerp(invader.meshGroup.position.z, targetZ, 0.05);
706
+
707
+ if (Date.now() - invader.lastShotTime > INVADER_FIRE_COOLDOWN) {
708
+ if (Math.random() < 0.5) createProjectile(invader); // Increased fire chance
709
+ }
710
+ });
711
+ }
712
+
713
+ function updateParatroopers() {
714
+ for (let i = paratroopers.length - 1; i >= 0; i--) {
715
+ const pt = paratroopers[i];
716
+ if (pt.health <= 0) continue;
717
+
718
+ if (pt.meshGroup.position.y > pt.targetY) {
719
+ pt.meshGroup.position.y -= PARATROOPER_DROP_SPEED;
720
+ } else {
721
+ pt.meshGroup.position.y = pt.targetY;
722
+ if(!pt.landed) {
723
+ handleEntityAutoRotation(pt); // Auto-rotate once landed
724
+ pt.landed = true;
725
+ } else {
726
+ // Continuous auto-rotation for landed paratroopers
727
+ handleEntityAutoRotation(pt);
728
+ }
729
+ }
730
+ if (Date.now() - pt.lastShotTime > PARATROOPER_FIRE_COOLDOWN) {
731
+ if (Math.random() < 0.45) createProjectile(pt);
732
+ }
733
+ }
734
+ if (Date.now() - lastParatrooperSpawnTime > PARATROOPER_SPAWN_INTERVAL && paratroopers.length < 12) { // Max 12 paratroopers
735
+ spawnParatrooper();
736
+ }
737
+ }
738
+
739
+ function updateProjectiles() {
740
+ for (let i = projectiles.length - 1; i >= 0; i--) {
741
+ const p = projectiles[i];
742
+ p.position.add(p.userData.velocity);
743
+
744
+ if (Date.now() - p.userData.creationTime > 3500 ||
745
+ Math.abs(p.position.x) > GAME_PLANE_WIDTH / 2 + 5 ||
746
+ Math.abs(p.position.z) > GAME_PLANE_HEIGHT / 2 + 5 ||
747
+ p.position.y < -2 || p.position.y > PARATROOPER_SPAWN_Y + 5) {
748
+ scene.remove(p);
749
+ projectiles.splice(i, 1);
750
+ continue;
751
+ }
752
+ checkProjectileHit(p, i);
753
+ }
754
+ }
755
+
756
+ function getHitFlashMaterial(meshGroup) {
757
+ if (meshGroup && meshGroup.children) {
758
+ let bodyMesh = meshGroup.children.find(child => child.name === 'body' && child.material && child.material.isMeshStandardMaterial);
759
+ if (bodyMesh) return bodyMesh.material;
760
+ for(let child of meshGroup.children){
761
+ if(child.isMesh && child.material && child.material.isMeshStandardMaterial){
762
+ return child.material;
763
+ }
764
+ }
765
+ }
766
+ return null;
767
+ }
768
+
769
+ function checkProjectileHit(projectile, projectileIndex) {
770
+ const pPos = projectile.position;
771
+ const ownerId = projectile.userData.ownerId;
772
+
773
+ if (ownerId.includes('player')) {
774
+ // Player projectiles only hit enemies
775
+ } else { // Enemy projectile
776
+ [player1, player2].forEach(player => {
777
+ if (!player || player.health <= 0 || player.shieldActive) return;
778
+ const distSq = pPos.distanceToSquared(player.meshGroup.position);
779
+ if (distSq < (player.radius + PROJECTILE_SIZE) ** 2) {
780
+ player.health--;
781
+ scene.remove(projectile); projectiles.splice(projectileIndex, 1);
782
+ const hitMaterial = getHitFlashMaterial(player.meshGroup);
783
+ if (hitMaterial) {
784
+ const originalColor = hitMaterial.color.clone();
785
+ const originalEmissive = hitMaterial.emissive.clone();
786
+ const originalEmissiveIntensity = hitMaterial.emissiveIntensity;
787
+ hitMaterial.color.setHex(0xff0000);
788
+ hitMaterial.emissive.setHex(0xff0000); hitMaterial.emissiveIntensity = 0.8;
789
+ setTimeout(() => {
790
+ if(hitMaterial) {
791
+ hitMaterial.color.copy(originalColor);
792
+ hitMaterial.emissive.copy(originalEmissive);
793
+ hitMaterial.emissiveIntensity = originalEmissiveIntensity;
794
+ }
795
+ }, 120);
796
+ }
797
+ updateUI(); checkWinCondition(); return;
798
+ }
799
+ });
800
+ if (projectiles.indexOf(projectile) === -1) return;
801
+ }
802
+
803
+ const enemyTypes = [invaders, paratroopers];
804
+ for (const enemyList of enemyTypes) {
805
+ for (let j = enemyList.length - 1; j >= 0; j--) {
806
+ const enemy = enemyList[j];
807
+ if (enemy.health <= 0 || ownerId === enemy.id) continue;
808
+
809
+ const distSq = pPos.distanceToSquared(enemy.meshGroup.position);
810
+ if (distSq < (enemy.radius + PROJECTILE_SIZE) ** 2) {
811
+ enemy.health--;
812
+ scene.remove(projectile); projectiles.splice(projectileIndex, 1);
813
+
814
+ if (ownerId === 'player1' && player1) player1.score++;
815
+ else if (ownerId === 'player2' && player2) player2.score++;
816
+
817
+ if (enemy.health <= 0) {
818
+ scene.remove(enemy.meshGroup);
819
+ enemyList.splice(j, 1);
820
+ } else {
821
+ const hitMaterial = getHitFlashMaterial(enemy.meshGroup);
822
+ if (hitMaterial) {
823
+ const originalColor = hitMaterial.color.clone();
824
+ const originalEmissive = hitMaterial.emissive.clone();
825
+ const originalEmissiveIntensity = hitMaterial.emissiveIntensity;
826
+ hitMaterial.color.setHex(0xff0000);
827
+ hitMaterial.emissive.setHex(0xff0000); hitMaterial.emissiveIntensity = 0.8;
828
+ setTimeout(() => {
829
+ if(hitMaterial) {
830
+ hitMaterial.color.copy(originalColor);
831
+ hitMaterial.emissive.copy(originalEmissive);
832
+ hitMaterial.emissiveIntensity = originalEmissiveIntensity;
833
+ }
834
+ }, 120);
835
+ }
836
+ }
837
+ updateUI(); return;
838
+ }
839
+ }
840
+ if (projectiles.indexOf(projectile) === -1) return;
841
+ }
842
+ }
843
+
844
+ function updateUI() {
845
+ if (player1) {
846
+ player1ScoreEl.textContent = player1.score;
847
+ const p1HealthPercent = Math.max(0, (player1.health / PLAYER_MAX_HEALTH) * 100);
848
+ player1HealthBarFillEl.style.width = `${p1HealthPercent}%`;
849
+ if (p1HealthPercent <= 30) player1HealthBarFillEl.style.backgroundColor = '#e53e3e';
850
+ else if (p1HealthPercent <= 60) player1HealthBarFillEl.style.backgroundColor = '#dd6b20';
851
+ else player1HealthBarFillEl.style.backgroundColor = '#38b2ac';
852
+ const now = Date.now();
853
+ player1ShieldStatusEl.textContent = player1.shieldActive ? `ON (${Math.ceil((player1.shieldEndTime - now)/1000)}s)` : (now < player1.shieldCooldownEndTime ? `CD (${Math.ceil((player1.shieldCooldownEndTime - now)/1000)}s)`: 'READY');
854
+ }
855
+ if (player2) {
856
+ player2ScoreEl.textContent = player2.score;
857
+ const p2HealthPercent = Math.max(0, (player2.health / PLAYER_MAX_HEALTH) * 100);
858
+ player2HealthBarFillEl.style.width = `${p2HealthPercent}%`;
859
+ if (p2HealthPercent <= 30) player2HealthBarFillEl.style.backgroundColor = '#e53e3e';
860
+ else if (p2HealthPercent <= 60) player2HealthBarFillEl.style.backgroundColor = '#dd6b20';
861
+ else player2HealthBarFillEl.style.backgroundColor = '#ed8936';
862
+ const now = Date.now();
863
+ player2ShieldStatusEl.textContent = player2.shieldActive ? `ON (${Math.ceil((player2.shieldEndTime - now)/1000)}s)` : (now < player2.shieldCooldownEndTime ? `CD (${Math.ceil((player2.shieldCooldownEndTime - now)/1000)}s)`: 'READY');
864
+ }
865
+ }
866
+
867
+ function checkWinCondition() {
868
+ if (gameOver) return;
869
+ let message = null;
870
+ const p1Exists = !!player1;
871
+ const p2Exists = !!player2;
872
+ const p1Health = p1Exists ? player1.health : 1;
873
+ const p2Health = p2Exists ? player2.health : 1;
874
+
875
+ if (p1Exists && p1Health <= 0 && p2Exists && p2Health <=0) {
876
+ message = "Game Over! Both players defeated.";
877
+ }
878
+ if (message) {
879
+ gameOver = true;
880
+ gameOverMessageEl.textContent = message;
881
+ gameOverMessageEl.style.display = 'block';
882
+ }
883
+ }
884
+
885
+ function animate() {
886
+ requestAnimationFrame(animate);
887
+
888
+ if (!gameOver) {
889
+ if (player1) {
890
+ handlePlayerMovement(player1, 'w', 's', 'a', 'd'); // Up, Down, Left, Right
891
+ handleEntityAutoRotation(player1); // Renamed from handlePlayerAutoRotation
892
+ }
893
+ if (player2) {
894
+ handlePlayerMovement(player2, 'i', 'k', 'j', 'l'); // Up, Down, Left, Right
895
+ handleEntityAutoRotation(player2); // Renamed from handlePlayerAutoRotation
896
+ }
897
+ handleAutoShielding();
898
+ updateInvaderBehavior();
899
+ updateParatroopers();
900
+ updateShields();
901
+ }
902
+ updateProjectiles();
903
+ updateUI();
904
+
905
+ renderer.render(scene, camera);
906
+ }
907
+
908
+ if (document.readyState === 'loading') {
909
+ document.addEventListener('DOMContentLoaded', init);
910
+ } else {
911
+ init();
912
+ }
913
+ </script>
914
+ </body>
915
  </html>