awacke1 commited on
Commit
fb169a2
·
verified ·
1 Parent(s): d836555

Update index.html

Browse files
Files changed (1) hide show
  1. index.html +760 -18
index.html CHANGED
@@ -1,19 +1,761 @@
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 Isometric 3D Combat Game</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; /* Tailwind gray-900 */
15
+ color: #e2e8f0; /* Tailwind slate-200 */
16
+ display: flex;
17
+ flex-direction: column; /* Allow UI stacking */
18
+ align-items: center;
19
+ justify-content: center; /* Center game area */
20
+ height: 100vh;
21
+ position: relative; /* For absolute positioning of UI elements */
22
+ }
23
+ #game-canvas-wrapper {
24
+ /* Wrapper for the canvas, helps in centering or specific sizing */
25
+ /* width: 80vw; */ /* Example: Use viewport units for responsiveness */
26
+ /* height: 60vh; */
27
+ /* max-width: 1000px; */ /* Max size */
28
+ /* aspect-ratio: 16 / 9; */ /* Maintain aspect ratio */
29
+ border: 2px solid #4a5568; /* Tailwind gray-600 */
30
+ border-radius: 0.5rem; /* Tailwind rounded-lg */
31
+ position: relative; /* For game over message */
32
+ }
33
+ canvas {
34
+ display: block; /* Remove extra space below canvas */
35
+ width: 100%; /* Canvas fills its wrapper */
36
+ height: 100%;
37
+ }
38
+
39
+ .score-board {
40
+ position: absolute;
41
+ top: 20px;
42
+ padding: 10px 15px;
43
+ font-size: 1.2rem; /* md:text-lg */
44
+ font-weight: bold;
45
+ color: #1a202c; /* Tailwind gray-900 for text on colored bg */
46
+ border-radius: 0.375rem; /* rounded-md */
47
+ box-shadow: 0 2px 4px rgba(0,0,0,0.2);
48
+ z-index: 10;
49
+ }
50
+ #player1-ui {
51
+ left: 20px;
52
+ background-color: #38b2ac; /* Teal */
53
+ }
54
+ #player2-ui {
55
+ right: 20px;
56
+ background-color: #ed8936; /* Orange */
57
+ }
58
+ .shield-timer {
59
+ font-size: 0.9rem;
60
+ margin-top: 5px;
61
+ font-weight: normal;
62
+ }
63
+
64
+ .controls-and-reset {
65
+ position: absolute;
66
+ bottom: 10px; /* Position at the bottom */
67
+ left: 50%;
68
+ transform: translateX(-50%);
69
+ display: flex;
70
+ flex-direction: column;
71
+ align-items: center;
72
+ width: 100%;
73
+ max-width: 700px; /* Adjust width as needed */
74
+ z-index: 10;
75
+ }
76
+ .instructions {
77
+ background-color: rgba(45, 55, 72, 0.9); /* Tailwind gray-700 with more opacity */
78
+ padding: 0.75rem 1.25rem;
79
+ border-radius: 0.5rem;
80
+ box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
81
+ text-align: center;
82
+ margin-bottom: 10px;
83
+ }
84
+ .instructions h1 { font-size: 1.2rem; margin-bottom: 0.3rem; }
85
+ .instructions p { font-size: 0.85rem; margin-bottom: 0.2rem; }
86
+ kbd {
87
+ display: inline-block;
88
+ padding: 0.25rem 0.5rem;
89
+ font-size: 0.75rem;
90
+ font-weight: 600;
91
+ color: #1f2937;
92
+ background-color: #f3f4f6;
93
+ border: 1px solid #d1d5db;
94
+ border-radius: 0.25rem;
95
+ margin: 0 0.1rem;
96
+ }
97
+ #reset-button {
98
+ padding: 0.7rem 1.5rem;
99
+ font-size: 1rem;
100
+ font-weight: bold;
101
+ color: white;
102
+ background-color: #c53030; /* Tailwind red-700 */
103
+ border: none;
104
+ border-radius: 0.375rem;
105
+ cursor: pointer;
106
+ transition: background-color 0.2s;
107
+ }
108
+ #reset-button:hover {
109
+ background-color: #9b2c2c; /* Tailwind red-800 */
110
+ }
111
+ #game-over-message {
112
+ position: absolute;
113
+ top: 50%;
114
+ left: 50%;
115
+ transform: translate(-50%, -50%);
116
+ background-color: rgba(0, 0, 0, 0.9);
117
+ color: white;
118
+ padding: 25px 35px;
119
+ border-radius: 10px;
120
+ font-size: 2rem;
121
+ text-align: center;
122
+ z-index: 20;
123
+ display: none;
124
+ border: 3px solid #e53e3e; /* red-600 */
125
+ }
126
+ </style>
127
+ </head>
128
+ <body>
129
+ <div id="player1-ui" class="score-board">
130
+ <div>P1 Score: <span id="player1-score">0</span></div>
131
+ <div>P1 Health: <span id="player1-health">3</span></div>
132
+ <div class="shield-timer">Shield: <span id="player1-shield-status">OFF</span></div>
133
+ </div>
134
+ <div id="player2-ui" class="score-board">
135
+ <div>P2 Score: <span id="player2-score">0</span></div>
136
+ <div>P2 Health: <span id="player2-health">3</span></div>
137
+ <div class="shield-timer">Shield: <span id="player2-shield-status">OFF</span></div>
138
+ </div>
139
+
140
+ <div id="game-canvas-wrapper">
141
+ <div id="game-over-message">Game Over!</div>
142
+ </div>
143
+
144
+ <div class="controls-and-reset">
145
+ <div class="instructions">
146
+ <h1>Isometric Combat!</h1>
147
+ <p>P1 (Teal): <kbd>W</kbd><kbd>A</kbd><kbd>S</kbd><kbd>D</kbd> Move | <kbd>L SHIFT</kbd> Shoot | <kbd>TAB</kbd> Shield</p>
148
+ <p>P2 (Orange): <kbd>I</kbd><kbd>J</kbd><kbd>K</kbd><kbd>L</kbd> Move | <kbd>R SHIFT</kbd> Shoot | <kbd>\</kbd> Shield</p>
149
+ </div>
150
+ <button id="reset-button">Reset Game</button>
151
+ </div>
152
+
153
+ <script>
154
+ // --- Game Constants ---
155
+ const PLAYER_SPEED = 0.15; // Adjusted for 3D
156
+ const PLAYER_RADIUS = 0.5; // For collision, visual size might differ
157
+ const PROJECTILE_SIZE = 0.15;
158
+ const PROJECTILE_SPEED = 0.4;
159
+ const PLAYER_MAX_HEALTH = 3;
160
+ const INVADER_RADIUS = 0.6;
161
+ const PARATROOPER_RADIUS = 0.4;
162
+ const INVADER_FIRE_COOLDOWN = 1800;
163
+ const PARATROOPER_FIRE_COOLDOWN = 2200;
164
+ const PLAYER_FIRE_COOLDOWN = 300;
165
+ const SHIELD_DURATION = 10000; // 10 seconds
166
+ const SHIELD_COOLDOWN = 20000; // 20 seconds after shield ends
167
+
168
+ const GAME_PLANE_WIDTH = 20;
169
+ const GAME_PLANE_HEIGHT = 12; // This is depth (Z-axis)
170
+ const DIVIDING_LINE_POS_X = 0;
171
+ const PARATROOPER_SPAWN_Y = 10;
172
+ const PARATROOPER_DROP_SPEED = 0.05;
173
+ const PARATROOPER_SPAWN_INTERVAL = 5000; // ms
174
+
175
+ // --- Global Variables ---
176
+ let scene, camera, renderer;
177
+ let player1, player2;
178
+ let projectiles = [];
179
+ let invaders = [];
180
+ let paratroopers = [];
181
+ let keysPressed = {};
182
+ let gameOver = false;
183
+ let lastParatrooperSpawnTime = 0;
184
+ let ambientLight, directionalLight;
185
+ let groundPlane, dividingLineMesh;
186
+
187
+ // DOM Elements
188
+ let player1ScoreEl, player1HealthEl, player1ShieldStatusEl;
189
+ let player2ScoreEl, player2HealthEl, player2ShieldStatusEl;
190
+ let resetButtonEl, gameOverMessageEl, gameCanvasWrapperEl;
191
+
192
+ // --- Initialization ---
193
+ function init() {
194
+ gameCanvasWrapperEl = document.getElementById('game-canvas-wrapper');
195
+ player1ScoreEl = document.getElementById('player1-score');
196
+ player1HealthEl = document.getElementById('player1-health');
197
+ player1ShieldStatusEl = document.getElementById('player1-shield-status');
198
+ player2ScoreEl = document.getElementById('player2-score');
199
+ player2HealthEl = document.getElementById('player2-health');
200
+ player2ShieldStatusEl = document.getElementById('player2-shield-status');
201
+ resetButtonEl = document.getElementById('reset-button');
202
+ gameOverMessageEl = document.getElementById('game-over-message');
203
+
204
+ scene = new THREE.Scene();
205
+ scene.background = new THREE.Color(0x1a202c);
206
+
207
+ setupCamera();
208
+ setupLights();
209
+
210
+ renderer = new THREE.WebGLRenderer({ antialias: true });
211
+ renderer.setSize(gameCanvasWrapperEl.clientWidth || 800, (gameCanvasWrapperEl.clientWidth || 800) * (9/16) ); // Initial size
212
+ renderer.shadowMap.enabled = true; // Enable shadows
213
+ gameCanvasWrapperEl.appendChild(renderer.domElement);
214
+
215
+ createGround();
216
+ createDividingLine();
217
+
218
+ resetButtonEl.addEventListener('click', resetGame);
219
+ document.addEventListener('keydown', onKeyDown);
220
+ document.addEventListener('keyup', onKeyUp);
221
+ window.addEventListener('resize', onWindowResize, false);
222
+
223
+ resetGame();
224
+ animate();
225
+ }
226
+
227
+ function setupCamera() {
228
+ const aspect = (gameCanvasWrapperEl.clientWidth || 800) / ((gameCanvasWrapperEl.clientWidth || 800) * (9/16));
229
+ camera = new THREE.PerspectiveCamera(50, aspect, 0.1, 1000);
230
+ // Isometric-like position
231
+ camera.position.set(GAME_PLANE_WIDTH * 0.7, GAME_PLANE_WIDTH * 0.8, GAME_PLANE_HEIGHT * 0.7); // Adjust for good view
232
+ camera.lookAt(0, 0, 0); // Look at the center of the scene
233
+ }
234
+
235
+ function setupLights() {
236
+ ambientLight = new THREE.AmbientLight(0xffffff, 0.6);
237
+ scene.add(ambientLight);
238
+
239
+ directionalLight = new THREE.DirectionalLight(0xffffff, 0.8);
240
+ directionalLight.position.set(10, 15, 10);
241
+ directionalLight.castShadow = true;
242
+ directionalLight.shadow.mapSize.width = 1024;
243
+ directionalLight.shadow.mapSize.height = 1024;
244
+ directionalLight.shadow.camera.near = 0.5;
245
+ directionalLight.shadow.camera.far = 50;
246
+ // Define shadow camera frustum to cover play area
247
+ directionalLight.shadow.camera.left = -GAME_PLANE_WIDTH;
248
+ directionalLight.shadow.camera.right = GAME_PLANE_WIDTH;
249
+ directionalLight.shadow.camera.top = GAME_PLANE_HEIGHT;
250
+ directionalLight.shadow.camera.bottom = -GAME_PLANE_HEIGHT;
251
+ scene.add(directionalLight);
252
+ }
253
+
254
+ function createGround() {
255
+ const groundGeometry = new THREE.PlaneGeometry(GAME_PLANE_WIDTH, GAME_PLANE_HEIGHT);
256
+ const groundMaterial = new THREE.MeshStandardMaterial({ color: 0x4a5568, side: THREE.DoubleSide }); // Tailwind gray-600
257
+ groundPlane = new THREE.Mesh(groundGeometry, groundMaterial);
258
+ groundPlane.rotation.x = -Math.PI / 2; // Rotate to be flat
259
+ groundPlane.receiveShadow = true;
260
+ scene.add(groundPlane);
261
+ }
262
+
263
+ function createDividingLine() {
264
+ const lineMaterial = new THREE.LineBasicMaterial({ color: 0xffffff, linewidth: 2 });
265
+ const points = [];
266
+ points.push(new THREE.Vector3(DIVIDING_LINE_POS_X, 0.01, -GAME_PLANE_HEIGHT / 2));
267
+ points.push(new THREE.Vector3(DIVIDING_LINE_POS_X, 0.01, GAME_PLANE_HEIGHT / 2));
268
+ const lineGeometry = new THREE.BufferGeometry().setFromPoints(points);
269
+ dividingLineMesh = new THREE.Line(lineGeometry, lineMaterial);
270
+ scene.add(dividingLineMesh);
271
+ }
272
+
273
+ function resetGame() {
274
+ gameOver = false;
275
+ gameOverMessageEl.style.display = 'none';
276
+ keysPressed = {};
277
+
278
+ projectiles.forEach(p => scene.remove(p)); projectiles = [];
279
+ invaders.forEach(i => scene.remove(i.meshGroup)); invaders = []; // Remove group
280
+ paratroopers.forEach(pt => scene.remove(pt.meshGroup)); paratroopers = []; // Remove group
281
+ if (player1) scene.remove(player1.meshGroup);
282
+ if (player2) scene.remove(player2.meshGroup);
283
+
284
+ createPlayers();
285
+ createInitialInvaders();
286
+ lastParatrooperSpawnTime = Date.now();
287
+
288
+ updateUI();
289
+ }
290
+
291
+ // --- Create 3D Assembled Game Elements ---
292
+ function createPlayerModel(color) {
293
+ const group = new THREE.Group();
294
+
295
+ // Body (capsule-like: cylinder + two half-spheres)
296
+ const bodyRadius = PLAYER_RADIUS * 0.6;
297
+ const bodyHeight = PLAYER_RADIUS * 1.2;
298
+ const bodyCylinderGeom = new THREE.CylinderGeometry(bodyRadius, bodyRadius, bodyHeight, 16);
299
+ const bodyMaterial = new THREE.MeshStandardMaterial({ color: color });
300
+ const bodyCylinder = new THREE.Mesh(bodyCylinderGeom, bodyMaterial);
301
+ bodyCylinder.castShadow = true;
302
+ group.add(bodyCylinder);
303
+
304
+ const sphereGeom = new THREE.SphereGeometry(bodyRadius, 16, 8);
305
+ const topSphere = new THREE.Mesh(sphereGeom, bodyMaterial);
306
+ topSphere.position.y = bodyHeight / 2;
307
+ topSphere.castShadow = true;
308
+ group.add(topSphere);
309
+
310
+ const bottomSphere = new THREE.Mesh(sphereGeom, bodyMaterial);
311
+ bottomSphere.position.y = -bodyHeight / 2;
312
+ bottomSphere.castShadow = true;
313
+ group.add(bottomSphere);
314
+
315
+ // "Gun" barrel
316
+ const barrelLength = PLAYER_RADIUS * 0.8;
317
+ const barrelRadius = PLAYER_RADIUS * 0.15;
318
+ const barrelGeom = new THREE.CylinderGeometry(barrelRadius, barrelRadius, barrelLength, 8);
319
+ const barrelMaterial = new THREE.MeshStandardMaterial({ color: 0x666666 });
320
+ const barrel = new THREE.Mesh(barrelGeom, barrelMaterial);
321
+ barrel.rotation.z = Math.PI / 2; // Point forward along X
322
+ barrel.position.x = bodyRadius + barrelLength / 2 - 0.1; // Position in front of body
323
+ barrel.position.y = 0; // Centered vertically on body
324
+ barrel.castShadow = true;
325
+ group.add(barrel);
326
+
327
+ group.position.y = PLAYER_RADIUS * 0.6 + bodyHeight/2; // Sit on ground plane
328
+ return group;
329
+ }
330
+
331
+ function createInvaderModel(color) {
332
+ const group = new THREE.Group();
333
+ const mainBodySize = INVADER_RADIUS * 0.8;
334
+
335
+ // Main body (Box)
336
+ const bodyGeom = new THREE.BoxGeometry(mainBodySize, mainBodySize, mainBodySize);
337
+ const bodyMaterial = new THREE.MeshStandardMaterial({ color: color });
338
+ const body = new THREE.Mesh(bodyGeom, bodyMaterial);
339
+ body.castShadow = true;
340
+ group.add(body);
341
+
342
+ // "Eyes" or "Sensors" (small spheres)
343
+ const eyeRadius = mainBodySize * 0.15;
344
+ const eyeGeom = new THREE.SphereGeometry(eyeRadius, 8, 8);
345
+ const eyeMaterial = new THREE.MeshStandardMaterial({ color: 0xffff00 });
346
+
347
+ const eye1 = new THREE.Mesh(eyeGeom, eyeMaterial);
348
+ eye1.position.set(mainBodySize * 0.25, mainBodySize * 0.2, mainBodySize * 0.51);
349
+ group.add(eye1);
350
+ const eye2 = new THREE.Mesh(eyeGeom, eyeMaterial);
351
+ eye2.position.set(-mainBodySize * 0.25, mainBodySize * 0.2, mainBodySize * 0.51);
352
+ group.add(eye2);
353
+
354
+ group.position.y = mainBodySize / 2; // Sit on ground plane
355
+ return group;
356
+ }
357
+
358
+ function createParatrooperModel(color) {
359
+ const group = new THREE.Group();
360
+ const bodyRadius = PARATROOPER_RADIUS * 0.7;
361
+ const bodyHeight = PARATROOPER_RADIUS * 1.5;
362
+
363
+ // Body (Cylinder)
364
+ const bodyGeom = new THREE.CylinderGeometry(bodyRadius*0.7, bodyRadius, bodyHeight, 12);
365
+ const bodyMaterial = new THREE.MeshStandardMaterial({ color: color });
366
+ const body = new THREE.Mesh(bodyGeom, bodyMaterial);
367
+ body.castShadow = true;
368
+ group.add(body);
369
+
370
+ // "Canopy" (half-sphere)
371
+ const canopyRadius = PARATROOPER_RADIUS * 1.5;
372
+ const canopyGeom = new THREE.SphereGeometry(canopyRadius, 16, 8, 0, Math.PI * 2, 0, Math.PI / 2);
373
+ const canopyMaterial = new THREE.MeshStandardMaterial({ color: 0xffffff, transparent: true, opacity: 0.7 });
374
+ const canopy = new THREE.Mesh(canopyGeom, canopyMaterial);
375
+ canopy.position.y = bodyHeight / 2 + canopyRadius * 0.5;
376
+ canopy.castShadow = true; // May not look great with transparency
377
+ group.add(canopy);
378
+
379
+ // No specific ground adjustment here as it drops
380
+ return group;
381
+ }
382
+
383
+
384
+ function createPlayers() {
385
+ player1 = { meshGroup: createPlayerModel(0x38b2ac), // Teal
386
+ health: PLAYER_MAX_HEALTH, score: 0, lastShotTime: 0,
387
+ shieldActive: false, shieldEndTime: 0, shieldCooldownEndTime: 0,
388
+ id: 'player1', radius: PLAYER_RADIUS
389
+ };
390
+ player1.meshGroup.position.set(-GAME_PLANE_WIDTH / 4, player1.meshGroup.position.y, 0);
391
+ scene.add(player1.meshGroup);
392
+
393
+ player2 = { meshGroup: createPlayerModel(0xed8936), // Orange
394
+ health: PLAYER_MAX_HEALTH, score: 0, lastShotTime: 0,
395
+ shieldActive: false, shieldEndTime: 0, shieldCooldownEndTime: 0,
396
+ id: 'player2', radius: PLAYER_RADIUS
397
+ };
398
+ player2.meshGroup.position.set(GAME_PLANE_WIDTH / 4, player2.meshGroup.position.y, 0);
399
+ // Rotate P2 to face P1
400
+ player2.meshGroup.rotation.y = Math.PI;
401
+ scene.add(player2.meshGroup);
402
+ }
403
+
404
+ function createInitialInvaders() {
405
+ const invaderPositions = [
406
+ new THREE.Vector3(0, 0, GAME_PLANE_HEIGHT / 4),
407
+ new THREE.Vector3(0, 0, -GAME_PLANE_HEIGHT / 4),
408
+ ];
409
+ invaderPositions.forEach((pos, index) => {
410
+ const invaderMeshGroup = createInvaderModel(0x9f7aea); // Purple
411
+ invaderMeshGroup.position.set(pos.x, invaderMeshGroup.position.y, pos.z);
412
+ const invader = {
413
+ meshGroup: invaderMeshGroup, health: 1, id: `invader${index}`,
414
+ lastShotTime: 0, radius: INVADER_RADIUS, originalZ: pos.z, oscillationTime: Math.random() * Math.PI * 2
415
+ };
416
+ scene.add(invader.meshGroup);
417
+ invaders.push(invader);
418
+ });
419
+ }
420
+
421
+ function spawnParatrooper() {
422
+ const spawnX = (Math.random() - 0.5) * (GAME_PLANE_WIDTH * 0.8); // Random X within most of the width
423
+ const spawnZ = (Math.random() - 0.5) * (GAME_PLANE_HEIGHT * 0.8); // Random Z within most of the depth
424
+
425
+ const paratrooperMeshGroup = createParatrooperModel(0xdd6b20); // Darker Orange
426
+ paratrooperMeshGroup.position.set(spawnX, PARATROOPER_SPAWN_Y, spawnZ);
427
+
428
+ const paratrooper = {
429
+ meshGroup: paratrooperMeshGroup, health: 1, id: `paratrooper${paratroopers.length}`,
430
+ lastShotTime: 0, radius: PARATROOPER_RADIUS, targetY: paratrooperMeshGroup.position.y / 2 + PARATROOPER_RADIUS // Land on its feet
431
+ };
432
+ scene.add(paratrooper.meshGroup);
433
+ paratroopers.push(paratrooper);
434
+ lastParatrooperSpawnTime = Date.now();
435
+ }
436
+
437
+
438
+ function createProjectile(shooter) {
439
+ if (!shooter || shooter.health <= 0) return;
440
+ const now = Date.now();
441
+ const fireCooldown = (shooter.id.includes('invader') ? INVADER_FIRE_COOLDOWN :
442
+ (shooter.id.includes('paratrooper') ? PARATROOPER_FIRE_COOLDOWN : PLAYER_FIRE_COOLDOWN));
443
+ if (now - shooter.lastShotTime < fireCooldown) return;
444
+ shooter.lastShotTime = now;
445
+
446
+ const projectileGeom = new THREE.SphereGeometry(PROJECTILE_SIZE, 8, 8);
447
+ let projectileMaterial, projectileColor;
448
+ let velocity = new THREE.Vector3();
449
+ const startPos = shooter.meshGroup.position.clone();
450
+ startPos.y += PLAYER_RADIUS * 0.5; // Fire from mid-body height
451
+
452
+ // Determine direction based on shooter's orientation
453
+ const direction = new THREE.Vector3();
454
+ shooter.meshGroup.getWorldDirection(direction); // Gets the local -Z direction
455
+
456
+ if (shooter.id === 'player1') {
457
+ projectileColor = 0x81e6d9; // Lighter Teal
458
+ velocity.copy(direction).multiplyScalar(-PROJECTILE_SPEED); // Player 1 model faces -Z by default
459
+ } else if (shooter.id === 'player2') {
460
+ projectileColor = 0xfbd38d; // Lighter Orange
461
+ velocity.copy(direction).multiplyScalar(-PROJECTILE_SPEED); // Player 2 model is rotated PI, so its -Z is forward
462
+ } else if (shooter.id.includes('invader') || shooter.id.includes('paratrooper')) {
463
+ projectileColor = shooter.id.includes('invader') ? 0xc4b5fd : 0xffa07a; // Light purple or light salmon
464
+ const targetPlayer = (player1.health > 0 && player2.health > 0) ? (Math.random() < 0.5 ? player1 : player2) : (player1.health > 0 ? player1 : (player2.health > 0 ? player2 : null));
465
+ if (targetPlayer) {
466
+ velocity.subVectors(targetPlayer.meshGroup.position, shooter.meshGroup.position).normalize().multiplyScalar(PROJECTILE_SPEED * 0.8);
467
+ } else { return; } // No valid target
468
+ } else { return; }
469
+
470
+ projectileMaterial = new THREE.MeshStandardMaterial({ color: projectileColor, emissive: projectileColor, emissiveIntensity: 0.5 });
471
+ const projectile = new THREE.Mesh(projectileGeom, projectileMaterial);
472
+ projectile.castShadow = true;
473
+
474
+ // Adjust start position slightly in front of shooter based on their facing direction
475
+ const offset = direction.clone().multiplyScalar(-shooter.radius * 1.2); // Negative because getWorldDirection gives -Z
476
+ startPos.add(offset);
477
+ projectile.position.copy(startPos);
478
+
479
+ projectile.userData = { ownerId: shooter.id, velocity: velocity, creationTime: Date.now() };
480
+ scene.add(projectile);
481
+ projectiles.push(projectile);
482
+ }
483
+
484
+ // --- Event Handlers ---
485
+ function onKeyDown(event) {
486
+ if (gameOver && event.key !== "Escape") return; // Allow Esc for potential menu later
487
+ keysPressed[event.key.toLowerCase()] = true;
488
+ const key = event.key.toLowerCase();
489
+
490
+ // Player 1 Controls
491
+ if (player1.health > 0) {
492
+ if (key === 'shift' && event.location === KeyboardEvent.DOM_KEY_LOCATION_LEFT) {
493
+ createProjectile(player1); event.preventDefault();
494
+ }
495
+ if (key === 'tab') {
496
+ activateShield(player1); event.preventDefault();
497
+ }
498
+ }
499
+ // Player 2 Controls
500
+ if (player2.health > 0) {
501
+ if (key === 'shift' && event.location === KeyboardEvent.DOM_KEY_LOCATION_RIGHT) {
502
+ createProjectile(player2); event.preventDefault();
503
+ }
504
+ if (key === '\\') {
505
+ activateShield(player2); event.preventDefault();
506
+ }
507
+ }
508
+ }
509
+ function onKeyUp(event) {
510
+ keysPressed[event.key.toLowerCase()] = false;
511
+ }
512
+ function onWindowResize() {
513
+ const w = gameCanvasWrapperEl.clientWidth || 800;
514
+ const h = (gameCanvasWrapperEl.clientWidth || 800) * (9/16);
515
+ renderer.setSize(w, h);
516
+ camera.aspect = w / h;
517
+ camera.updateProjectionMatrix();
518
+ }
519
+
520
+ // --- Game Logic ---
521
+ function activateShield(player) {
522
+ const now = Date.now();
523
+ if (!player.shieldActive && now > player.shieldCooldownEndTime) {
524
+ player.shieldActive = true;
525
+ player.shieldEndTime = now + SHIELD_DURATION;
526
+ player.shieldCooldownEndTime = player.shieldEndTime + SHIELD_COOLDOWN; // Cooldown starts after shield ends
527
+ // Visual feedback for shield
528
+ if (!player.shieldMesh) {
529
+ const shieldGeom = new THREE.SphereGeometry(player.radius * 1.5, 16, 16);
530
+ const shieldMat = new THREE.MeshStandardMaterial({ color: 0x00ffff, transparent: true, opacity: 0.3 });
531
+ player.shieldMesh = new THREE.Mesh(shieldGeom, shieldMat);
532
+ player.meshGroup.add(player.shieldMesh); // Add to player's group
533
+ }
534
+ player.shieldMesh.visible = true;
535
+ updateUI();
536
+ }
537
+ }
538
+
539
+ function updateShields() {
540
+ const now = Date.now();
541
+ [player1, player2].forEach(player => {
542
+ if (player.shieldActive && now > player.shieldEndTime) {
543
+ player.shieldActive = false;
544
+ if (player.shieldMesh) player.shieldMesh.visible = false;
545
+ updateUI();
546
+ }
547
+ });
548
+ }
549
+
550
+ function handlePlayerMovement(player, up, down, left, right) {
551
+ if (!player || player.health <= 0) return;
552
+ const moveDirection = new THREE.Vector3(0, 0, 0);
553
+ if (keysPressed[left]) moveDirection.x -= 1;
554
+ if (keysPressed[right]) moveDirection.x += 1;
555
+ if (keysPressed[up]) moveDirection.z -= 1; // Forward in local Z
556
+ if (keysPressed[down]) moveDirection.z += 1; // Backward in local Z
557
+
558
+ if (moveDirection.lengthSq() > 0) {
559
+ moveDirection.normalize().multiplyScalar(PLAYER_SPEED);
560
+
561
+ // Apply rotation for turning, then move
562
+ if (keysPressed[left]) player.meshGroup.rotation.y += 0.05;
563
+ if (keysPressed[right]) player.meshGroup.rotation.y -= 0.05;
564
+
565
+ // Transform movement to world space based on player's orientation
566
+ const worldMove = moveDirection.clone().applyQuaternion(player.meshGroup.quaternion);
567
+ player.meshGroup.position.add(worldMove);
568
+ }
569
+
570
+ // Boundary and dividing line checks
571
+ const halfWidth = GAME_PLANE_WIDTH / 2 - player.radius;
572
+ const halfDepth = GAME_PLANE_HEIGHT / 2 - player.radius;
573
+ player.meshGroup.position.z = Math.max(-halfDepth, Math.min(halfDepth, player.meshGroup.position.z));
574
+
575
+ if (player.id === 'player1') { // Left player
576
+ player.meshGroup.position.x = Math.max(-halfWidth, Math.min(DIVIDING_LINE_POS_X - player.radius, player.meshGroup.position.x));
577
+ } else { // player2, Right player
578
+ player.meshGroup.position.x = Math.max(DIVIDING_LINE_POS_X + player.radius, Math.min(halfWidth, player.meshGroup.position.x));
579
+ }
580
+ // Collision with other player (simple sphere check)
581
+ const otherPlayer = player.id === 'player1' ? player2 : player1;
582
+ if (otherPlayer.health > 0) {
583
+ const distSq = player.meshGroup.position.distanceToSquared(otherPlayer.meshGroup.position);
584
+ if (distSq < (player.radius + otherPlayer.radius) ** 2) {
585
+ // Basic push-apart (can be jittery, more complex physics needed for smooth)
586
+ const delta = player.meshGroup.position.clone().sub(otherPlayer.meshGroup.position).normalize();
587
+ const overlap = (player.radius + otherPlayer.radius) - Math.sqrt(distSq);
588
+ player.meshGroup.position.add(delta.multiplyScalar(overlap / 2));
589
+ // otherPlayer.meshGroup.position.sub(delta.multiplyScalar(overlap / 2)); // Not strictly needed if only one moves
590
+ }
591
+ }
592
+ }
593
+
594
+ function updateInvaderBehavior() {
595
+ invaders.forEach(invader => {
596
+ if (invader.health <= 0) return;
597
+ // Simple oscillation on Z for invaders
598
+ invader.oscillationTime += 0.02;
599
+ invader.meshGroup.position.z = invader.originalZ + Math.sin(invader.oscillationTime) * (GAME_PLANE_HEIGHT * 0.1);
600
+
601
+ // Aim and fire
602
+ if (Date.now() - invader.lastShotTime > INVADER_FIRE_COOLDOWN) {
603
+ if (Math.random() < 0.5) createProjectile(invader);
604
+ }
605
+ });
606
+ }
607
+
608
+ function updateParatroopers() {
609
+ for (let i = paratroopers.length - 1; i >= 0; i--) {
610
+ const pt = paratroopers[i];
611
+ if (pt.health <= 0) continue;
612
+
613
+ // Drop until they reach their target Y (ground level)
614
+ if (pt.meshGroup.position.y > pt.targetY) {
615
+ pt.meshGroup.position.y -= PARATROOPER_DROP_SPEED;
616
+ } else {
617
+ pt.meshGroup.position.y = pt.targetY; // Landed
618
+ // Basic movement on ground (e.g., towards center or a player)
619
+ // For now, they just stay put and fire
620
+ }
621
+ // Fire
622
+ if (Date.now() - pt.lastShotTime > PARATROOPER_FIRE_COOLDOWN) {
623
+ if (Math.random() < 0.4) createProjectile(pt);
624
+ }
625
+ }
626
+ // Spawn new paratroopers
627
+ if (Date.now() - lastParatrooperSpawnTime > PARATROOPER_SPAWN_INTERVAL && paratroopers.length < 5) {
628
+ spawnParatrooper();
629
+ }
630
+ }
631
+
632
+ function updateProjectiles() {
633
+ for (let i = projectiles.length - 1; i >= 0; i--) {
634
+ const p = projectiles[i];
635
+ p.position.add(p.userData.velocity);
636
+
637
+ if (Date.now() - p.userData.creationTime > 5000 || // Lifespan
638
+ Math.abs(p.position.x) > GAME_PLANE_WIDTH / 2 + 2 ||
639
+ Math.abs(p.position.z) > GAME_PLANE_HEIGHT / 2 + 2 ||
640
+ p.position.y < -1 || p.position.y > PARATROOPER_SPAWN_Y + 2) {
641
+ scene.remove(p);
642
+ projectiles.splice(i, 1);
643
+ continue;
644
+ }
645
+ checkProjectileHit(p, i);
646
+ }
647
+ }
648
+
649
+ function checkProjectileHit(projectile, projectileIndex) {
650
+ const pPos = projectile.position;
651
+ const ownerId = projectile.userData.ownerId;
652
+
653
+ // Check players
654
+ [player1, player2].forEach(player => {
655
+ if (player.health <= 0 || player.id === ownerId || player.shieldActive) return;
656
+ const distSq = pPos.distanceToSquared(player.meshGroup.position);
657
+ if (distSq < (player.radius + PROJECTILE_SIZE) ** 2) {
658
+ player.health--;
659
+ scene.remove(projectile); projectiles.splice(projectileIndex, 1);
660
+ if (!ownerId.includes('invader') && !ownerId.includes('paratrooper')) { // Player hit player
661
+ const shooter = ownerId === 'player1' ? player1 : player2;
662
+ shooter.score++;
663
+ }
664
+ // Hit flash (can be improved)
665
+ const originalColor = player.id === 'player1' ? 0x38b2ac : 0xed8936;
666
+ player.meshGroup.children[0].material.color.setHex(0xff0000);
667
+ setTimeout(() => { if(player.meshGroup.children[0]) player.meshGroup.children[0].material.color.setHex(originalColor); }, 100);
668
+ updateUI(); checkWinCondition(); return;
669
+ }
670
+ });
671
+ if (projectiles.indexOf(projectile) === -1) return; // Hit a player
672
+
673
+ // Check invaders
674
+ for (let j = invaders.length - 1; j >= 0; j--) {
675
+ const inv = invaders[j];
676
+ if (inv.health <= 0 || ownerId.includes('invader')) continue;
677
+ const distSq = pPos.distanceToSquared(inv.meshGroup.position);
678
+ if (distSq < (inv.radius + PROJECTILE_SIZE) ** 2) {
679
+ inv.health--;
680
+ scene.remove(projectile); projectiles.splice(projectileIndex, 1);
681
+ if (ownerId === 'player1') player1.score++; else if (ownerId === 'player2') player2.score++;
682
+ if (inv.health <= 0) { scene.remove(inv.meshGroup); invaders.splice(j, 1); }
683
+ else { // Hit flash
684
+ inv.meshGroup.children[0].material.color.setHex(0xff0000);
685
+ setTimeout(() => { if(inv.meshGroup.children[0]) inv.meshGroup.children[0].material.color.setHex(0x9f7aea); }, 100);
686
+ }
687
+ updateUI(); return;
688
+ }
689
+ }
690
+ if (projectiles.indexOf(projectile) === -1) return;
691
+
692
+ // Check paratroopers
693
+ for (let k = paratroopers.length - 1; k >= 0; k--) {
694
+ const pt = paratroopers[k];
695
+ if (pt.health <= 0 || ownerId.includes('paratrooper')) continue;
696
+ const distSq = pPos.distanceToSquared(pt.meshGroup.position);
697
+ if (distSq < (pt.radius + PROJECTILE_SIZE) ** 2) {
698
+ pt.health--;
699
+ scene.remove(projectile); projectiles.splice(projectileIndex, 1);
700
+ if (ownerId === 'player1') player1.score++; else if (ownerId === 'player2') player2.score++;
701
+ if (pt.health <= 0) { scene.remove(pt.meshGroup); paratroopers.splice(k, 1); }
702
+ else { // Hit flash
703
+ pt.meshGroup.children[0].material.color.setHex(0xff0000);
704
+ setTimeout(() => { if(pt.meshGroup.children[0]) pt.meshGroup.children[0].material.color.setHex(0xdd6b20); }, 100);
705
+ }
706
+ updateUI(); return;
707
+ }
708
+ }
709
+ }
710
+
711
+ function updateUI() {
712
+ player1ScoreEl.textContent = player1.score;
713
+ player1HealthEl.textContent = Math.max(0, player1.health);
714
+ player2ScoreEl.textContent = player2.score;
715
+ player2HealthEl.textContent = Math.max(0, player2.health);
716
+
717
+ const now = Date.now();
718
+ player1ShieldStatusEl.textContent = player1.shieldActive ? `ON (${Math.ceil((player1.shieldEndTime - now)/1000)}s)` : (now < player1.shieldCooldownEndTime ? `CD (${Math.ceil((player1.shieldCooldownEndTime - now)/1000)}s)`: 'OFF');
719
+ player2ShieldStatusEl.textContent = player2.shieldActive ? `ON (${Math.ceil((player2.shieldEndTime - now)/1000)}s)` : (now < player2.shieldCooldownEndTime ? `CD (${Math.ceil((player2.shieldCooldownEndTime - now)/1000)}s)`: 'OFF');
720
+ }
721
+
722
+ function checkWinCondition() {
723
+ if (gameOver) return;
724
+ let winner = null;
725
+ if (player1.health <= 0 && player2.health <=0) winner = "It's a Draw!";
726
+ else if (player1.health <= 0) winner = "Player 2 Wins!";
727
+ else if (player2.health <= 0) winner = "Player 1 Wins!";
728
+
729
+ if (winner) {
730
+ gameOver = true;
731
+ gameOverMessageEl.textContent = winner;
732
+ gameOverMessageEl.style.display = 'block';
733
+ }
734
+ }
735
+
736
+ // --- Animation Loop ---
737
+ function animate() {
738
+ requestAnimationFrame(animate);
739
+
740
+ if (!gameOver) {
741
+ handlePlayerMovement(player1, 'w', 's', 'a', 'd');
742
+ handlePlayerMovement(player2, 'i', 'k', 'j', 'l');
743
+ updateInvaderBehavior();
744
+ updateParatroopers();
745
+ updateShields();
746
+ }
747
+ updateProjectiles();
748
+ updateUI(); // Continuously update UI for timers
749
+
750
+ renderer.render(scene, camera);
751
+ }
752
+
753
+ // --- Start the game ---
754
+ if (document.readyState === 'loading') {
755
+ document.addEventListener('DOMContentLoaded', init);
756
+ } else {
757
+ init();
758
+ }
759
+ </script>
760
+ </body>
761
  </html>