awacke1 commited on
Commit
c88bb73
·
verified ·
1 Parent(s): 54c89ff

Update index.html

Browse files
Files changed (1) hide show
  1. index.html +881 -18
index.html CHANGED
@@ -1,19 +1,882 @@
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 Combat Game - Auto Features</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 Auto Combat!</h1>
199
+ <p>P1 (Teal): <kbd>W</kbd><kbd>S</kbd> Fwd/Back | <kbd>A</kbd><kbd>D</kbd> Turn</p>
200
+ <p>P2 (Orange): <kbd>I</kbd><kbd>K</kbd> Fwd/Back | <kbd>J</kbd><kbd>L</kbd> Turn</p>
201
+ <p><kbd>SPACEBAR</kbd> to 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.15;
209
+ const PLAYER_RADIUS = 0.5;
210
+ const PROJECTILE_SIZE = 0.15;
211
+ const PROJECTILE_SPEED = 0.4;
212
+ const PLAYER_MAX_HEALTH = 10; // Increased max health for longer gameplay
213
+ const INVADER_RADIUS = 0.6;
214
+ const PARATROOPER_RADIUS = 0.4;
215
+ const INVADER_FIRE_COOLDOWN = 1800;
216
+ const PARATROOPER_FIRE_COOLDOWN = 2200;
217
+ const PLAYER_AUTO_FIRE_COOLDOWN = 400; // Auto-fire rate
218
+ const SHIELD_DURATION = 7000; // 7 seconds
219
+ const SHIELD_COOLDOWN = 15000; // 15 seconds
220
+ const AUTO_SHIELD_HEALTH_THRESHOLD = 3; // Activate shield if health <= this
221
+ const AUTO_SHIELD_CONSIDER_INTERVAL = 3000; // ms, how often to consider auto-shield if not critical
222
+
223
+ const GAME_PLANE_WIDTH = 30; // Increased for full screen feel
224
+ const GAME_PLANE_HEIGHT = 20; // Increased for full screen feel
225
+ const DIVIDING_LINE_POS_X = 0;
226
+ const PARATROOPER_SPAWN_Y = 15; // Spawn higher
227
+ const PARATROOPER_DROP_SPEED = 0.05;
228
+ const PARATROOPER_SPAWN_INTERVAL = 4500;
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;
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();
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(60, aspect, 0.1, 1000); // Wider FOV
285
+ camera.position.set(0, GAME_PLANE_WIDTH * 0.9, GAME_PLANE_HEIGHT * 0.7); // Adjusted for larger plane
286
+ camera.lookAt(0, 0, 0);
287
+ }
288
+
289
+ function setupLights() {
290
+ ambientLight = new THREE.AmbientLight(0xffffff, 0.7);
291
+ scene.add(ambientLight);
292
+ directionalLight = new THREE.DirectionalLight(0xffffff, 0.9);
293
+ directionalLight.position.set(GAME_PLANE_WIDTH * 0.5, 30, GAME_PLANE_HEIGHT * 0.5); // Higher, more spread
294
+ directionalLight.castShadow = true;
295
+ directionalLight.shadow.mapSize.width = 2048; // Better shadow quality
296
+ directionalLight.shadow.mapSize.height = 2048;
297
+ directionalLight.shadow.camera.near = 0.5;
298
+ directionalLight.shadow.camera.far = 80;
299
+ directionalLight.shadow.camera.left = -GAME_PLANE_WIDTH;
300
+ directionalLight.shadow.camera.right = GAME_PLANE_WIDTH;
301
+ directionalLight.shadow.camera.top = GAME_PLANE_HEIGHT;
302
+ directionalLight.shadow.camera.bottom = -GAME_PLANE_HEIGHT;
303
+ scene.add(directionalLight);
304
+ }
305
+
306
+ function createGround() {
307
+ const groundGeometry = new THREE.PlaneGeometry(GAME_PLANE_WIDTH, GAME_PLANE_HEIGHT);
308
+ const groundMaterial = new THREE.MeshStandardMaterial({ color: 0x2d3748, side: THREE.DoubleSide }); // Darker gray
309
+ groundPlane = new THREE.Mesh(groundGeometry, groundMaterial);
310
+ groundPlane.rotation.x = -Math.PI / 2;
311
+ groundPlane.receiveShadow = true;
312
+ scene.add(groundPlane);
313
+ }
314
+
315
+ function createDividingLine() {
316
+ const lineMaterial = new THREE.LineBasicMaterial({ color: 0x6b7280, linewidth: 3 }); // Tailwind gray-500, thicker
317
+ const points = [];
318
+ points.push(new THREE.Vector3(DIVIDING_LINE_POS_X, 0.02, -GAME_PLANE_HEIGHT / 2)); // Slightly above ground
319
+ points.push(new THREE.Vector3(DIVIDING_LINE_POS_X, 0.02, GAME_PLANE_HEIGHT / 2));
320
+ const lineGeometry = new THREE.BufferGeometry().setFromPoints(points);
321
+ dividingLineMesh = new THREE.Line(lineGeometry, lineMaterial);
322
+ scene.add(dividingLineMesh);
323
+ }
324
+
325
+ function resetGame() {
326
+ gameOver = false;
327
+ gameOverMessageEl.style.display = 'none';
328
+ keysPressed = {};
329
+
330
+ projectiles.forEach(p => scene.remove(p)); projectiles = [];
331
+ invaders.forEach(i => scene.remove(i.meshGroup)); invaders = [];
332
+ paratroopers.forEach(pt => scene.remove(pt.meshGroup)); paratroopers = [];
333
+ if (player1) scene.remove(player1.meshGroup);
334
+ if (player2) scene.remove(player2.meshGroup);
335
+
336
+ createPlayers();
337
+ createInitialInvaders();
338
+ lastParatrooperSpawnTime = Date.now();
339
+
340
+ updateUI(); // Initial UI update including health bars
341
+ }
342
+
343
+ function createPlayerModel(color) {
344
+ const group = new THREE.Group();
345
+ const bodyRadius = PLAYER_RADIUS * 0.6;
346
+ const bodyHeight = PLAYER_RADIUS * 1.2;
347
+ const bodyCylinderGeom = new THREE.CylinderGeometry(bodyRadius, bodyRadius, bodyHeight, 16);
348
+ const bodyMaterial = new THREE.MeshStandardMaterial({ color: color, metalness: 0.3, roughness: 0.6 });
349
+ const bodyCylinder = new THREE.Mesh(bodyCylinderGeom, bodyMaterial);
350
+ bodyCylinder.castShadow = true;
351
+ bodyCylinder.name = "body"; // For hit flash targeting
352
+ group.add(bodyCylinder);
353
+
354
+ const sphereGeom = new THREE.SphereGeometry(bodyRadius, 16, 8);
355
+ const topSphere = new THREE.Mesh(sphereGeom, bodyMaterial);
356
+ topSphere.position.y = bodyHeight / 2;
357
+ topSphere.castShadow = true;
358
+ group.add(topSphere);
359
+
360
+ const bottomSphere = new THREE.Mesh(sphereGeom, bodyMaterial);
361
+ bottomSphere.position.y = -bodyHeight / 2;
362
+ bottomSphere.castShadow = true;
363
+ group.add(bottomSphere);
364
+
365
+ const barrelLength = PLAYER_RADIUS * 0.8;
366
+ const barrelRadius = PLAYER_RADIUS * 0.15;
367
+ const barrelGeom = new THREE.CylinderGeometry(barrelRadius, barrelRadius, barrelLength, 8);
368
+ const barrelMaterial = new THREE.MeshStandardMaterial({ color: 0x666666, metalness: 0.5, roughness: 0.4 });
369
+ const barrel = new THREE.Mesh(barrelGeom, barrelMaterial);
370
+ barrel.rotation.z = Math.PI / 2;
371
+ barrel.position.x = bodyRadius + barrelLength / 2 - 0.1;
372
+ barrel.position.y = 0;
373
+ barrel.castShadow = true;
374
+ group.add(barrel);
375
+
376
+ group.position.y = bodyHeight/2;
377
+ return group;
378
+ }
379
+
380
+ function createInvaderModel(color) {
381
+ const group = new THREE.Group();
382
+ const mainBodySize = INVADER_RADIUS * 0.8;
383
+ const bodyGeom = new THREE.BoxGeometry(mainBodySize, mainBodySize, mainBodySize);
384
+ const bodyMaterial = new THREE.MeshStandardMaterial({ color: color, metalness: 0.2, roughness: 0.7 });
385
+ const body = new THREE.Mesh(bodyGeom, bodyMaterial);
386
+ body.castShadow = true;
387
+ body.name = "body";
388
+ group.add(body);
389
+
390
+ const eyeRadius = mainBodySize * 0.15;
391
+ const eyeGeom = new THREE.SphereGeometry(eyeRadius, 8, 8);
392
+ const eyeMaterial = new THREE.MeshStandardMaterial({ color: 0xffff00, emissive: 0xffff00, emissiveIntensity: 0.5 });
393
+
394
+ const eye1 = new THREE.Mesh(eyeGeom, eyeMaterial);
395
+ eye1.position.set(mainBodySize * 0.25, mainBodySize * 0.2, mainBodySize * 0.51);
396
+ group.add(eye1);
397
+ const eye2 = new THREE.Mesh(eyeGeom, eyeMaterial);
398
+ eye2.position.set(-mainBodySize * 0.25, mainBodySize * 0.2, mainBodySize * 0.51);
399
+ group.add(eye2);
400
+
401
+ group.position.y = mainBodySize / 2;
402
+ return group;
403
+ }
404
+
405
+ function createParatrooperModel(color) {
406
+ const group = new THREE.Group();
407
+ const bodyRadius = PARATROOPER_RADIUS * 0.7;
408
+ const bodyHeight = PARATROOPER_RADIUS * 1.5;
409
+
410
+ const bodyGeom = new THREE.CylinderGeometry(bodyRadius*0.7, bodyRadius, bodyHeight, 12);
411
+ const bodyMaterial = new THREE.MeshStandardMaterial({ color: color, metalness: 0.3, roughness: 0.6 });
412
+ const body = new THREE.Mesh(bodyGeom, bodyMaterial);
413
+ body.castShadow = true;
414
+ body.name = "body";
415
+ group.add(body);
416
+
417
+ const canopyRadius = PARATROOPER_RADIUS * 1.5;
418
+ const canopyGeom = new THREE.SphereGeometry(canopyRadius, 16, 8, 0, Math.PI * 2, 0, Math.PI / 2);
419
+ const canopyMaterial = new THREE.MeshStandardMaterial({ color: 0xf0f0f0, transparent: true, opacity: 0.75, side: THREE.DoubleSide });
420
+ const canopy = new THREE.Mesh(canopyGeom, canopyMaterial);
421
+ canopy.position.y = bodyHeight / 2 + canopyRadius * 0.3;
422
+ canopy.castShadow = false;
423
+ group.add(canopy);
424
+ return group;
425
+ }
426
+
427
+ function createPlayers() {
428
+ player1 = { meshGroup: createPlayerModel(0x38b2ac),
429
+ health: PLAYER_MAX_HEALTH, score: 0, lastShotTime: 0,
430
+ shieldActive: false, shieldEndTime: 0, shieldCooldownEndTime: 0,
431
+ lastAutoShieldConsiderTime: 0,
432
+ id: 'player1', radius: PLAYER_RADIUS
433
+ };
434
+ player1.meshGroup.position.set(-GAME_PLANE_WIDTH / 4, player1.meshGroup.position.y, 0);
435
+ scene.add(player1.meshGroup);
436
+
437
+ player2 = { meshGroup: createPlayerModel(0xed8936),
438
+ health: PLAYER_MAX_HEALTH, score: 0, lastShotTime: 0,
439
+ shieldActive: false, shieldEndTime: 0, shieldCooldownEndTime: 0,
440
+ lastAutoShieldConsiderTime: 0,
441
+ id: 'player2', radius: PLAYER_RADIUS
442
+ };
443
+ player2.meshGroup.position.set(GAME_PLANE_WIDTH / 4, player2.meshGroup.position.y, 0);
444
+ player2.meshGroup.rotation.y = Math.PI;
445
+ scene.add(player2.meshGroup);
446
+ }
447
+
448
+ function createInitialInvaders() {
449
+ const invaderPositions = [
450
+ new THREE.Vector3(-GAME_PLANE_WIDTH / 3, 0, GAME_PLANE_HEIGHT / 3.5),
451
+ new THREE.Vector3(GAME_PLANE_WIDTH / 3, 0, -GAME_PLANE_HEIGHT / 3.5),
452
+ new THREE.Vector3(-GAME_PLANE_WIDTH / 3.5, 0, -GAME_PLANE_HEIGHT / 4),
453
+ new THREE.Vector3(GAME_PLANE_WIDTH / 3.5, 0, GAME_PLANE_HEIGHT / 4),
454
+ ];
455
+ invaderPositions.forEach((pos, index) => {
456
+ const invaderMeshGroup = createInvaderModel(0x9f7aea);
457
+ invaderMeshGroup.position.set(pos.x, invaderMeshGroup.position.y, pos.z);
458
+ if (pos.x < 0) invaderMeshGroup.rotation.y = Math.PI / 2;
459
+ else invaderMeshGroup.rotation.y = -Math.PI / 2;
460
+
461
+ const invader = {
462
+ meshGroup: invaderMeshGroup, health: 1, id: `invader${index}`,
463
+ lastShotTime: 0, radius: INVADER_RADIUS, originalZ: pos.z, oscillationTime: Math.random() * Math.PI * 2
464
+ };
465
+ scene.add(invader.meshGroup);
466
+ invaders.push(invader);
467
+ });
468
+ }
469
+
470
+ function spawnParatrooper() {
471
+ const spawnX = (Math.random() - 0.5) * (GAME_PLANE_WIDTH * 0.95);
472
+ const spawnZ = (Math.random() - 0.5) * (GAME_PLANE_HEIGHT * 0.95);
473
+
474
+ const paratrooperMeshGroup = createParatrooperModel(0xdd6b20);
475
+ paratrooperMeshGroup.position.set(spawnX, PARATROOPER_SPAWN_Y, spawnZ);
476
+
477
+ const bodyHeight = PARATROOPER_RADIUS * 1.5;
478
+ const paratrooper = {
479
+ meshGroup: paratrooperMeshGroup, health: 1, id: `paratrooper${paratroopers.length}`,
480
+ lastShotTime: 0, radius: PARATROOPER_RADIUS,
481
+ targetY: bodyHeight / 2, landed: false
482
+ };
483
+ scene.add(paratrooper.meshGroup);
484
+ paratroopers.push(paratrooper);
485
+ lastParatrooperSpawnTime = Date.now();
486
+ }
487
+
488
+ function createProjectile(shooter) {
489
+ if (!shooter || shooter.health <= 0) return;
490
+ // Auto-fire cooldown is handled by handleAutoShooting, so direct call implies readiness
491
+ // However, for enemies, we still need the cooldown check here if they are not part of handleAutoShooting
492
+ if (shooter.id.includes('invader') || shooter.id.includes('paratrooper')) {
493
+ const now = Date.now();
494
+ const fireCooldown = shooter.id.includes('invader') ? INVADER_FIRE_COOLDOWN : PARATROOPER_FIRE_COOLDOWN;
495
+ if (now - shooter.lastShotTime < fireCooldown) return;
496
+ shooter.lastShotTime = now;
497
+ }
498
+
499
+
500
+ const projectileGeom = new THREE.SphereGeometry(PROJECTILE_SIZE, 8, 8);
501
+ let projectileColor;
502
+ let velocity = new THREE.Vector3();
503
+ const startPos = shooter.meshGroup.position.clone();
504
+
505
+ if(shooter.id.includes('player')) startPos.y += PLAYER_RADIUS * 0.8;
506
+ else startPos.y += INVADER_RADIUS * 0.4;
507
+
508
+ const localForward = new THREE.Vector3(1, 0, 0);
509
+ const worldForward = localForward.clone().applyQuaternion(shooter.meshGroup.quaternion);
510
+
511
+ if (shooter.id.includes('player')) {
512
+ projectileColor = shooter.id === 'player1' ? 0x81e6d9 : 0xfbd38d;
513
+ velocity.copy(worldForward).multiplyScalar(PROJECTILE_SPEED);
514
+ } else if (shooter.id.includes('invader') || shooter.id.includes('paratrooper')) {
515
+ projectileColor = shooter.id.includes('invader') ? 0xc4b5fd : 0xffa07a;
516
+ const targetPlayer = (player1.health > 0 && player2.health > 0) ? (Math.random() < 0.5 ? player1 : player2) : (player1.health > 0 ? player1 : (player2.health > 0 ? player2 : null));
517
+ if (targetPlayer) {
518
+ velocity.subVectors(targetPlayer.meshGroup.position, shooter.meshGroup.position).normalize().multiplyScalar(PROJECTILE_SPEED * 0.8);
519
+ } else { return; }
520
+ } else { return; }
521
+
522
+ const projectileMaterial = new THREE.MeshStandardMaterial({ color: projectileColor, emissive: projectileColor, emissiveIntensity: 0.7 });
523
+ const projectile = new THREE.Mesh(projectileGeom, projectileMaterial);
524
+ projectile.castShadow = true;
525
+
526
+ const offset = worldForward.clone().multiplyScalar(shooter.radius * 1.1);
527
+ startPos.add(offset);
528
+ projectile.position.copy(startPos);
529
+
530
+ projectile.userData = { ownerId: shooter.id, velocity: velocity, creationTime: Date.now() };
531
+ scene.add(projectile);
532
+ projectiles.push(projectile);
533
+ }
534
+
535
+ // --- Event Handlers ---
536
+ function onKeyDown(event) {
537
+ if (gameOver && event.key.toLowerCase() !== " ") return;
538
+ keysPressed[event.key.toLowerCase()] = true;
539
+ const key = event.key.toLowerCase();
540
+
541
+ if (key === ' ') { // Spacebar Heal
542
+ if (player1) player1.health = PLAYER_MAX_HEALTH;
543
+ if (player2) player2.health = PLAYER_MAX_HEALTH;
544
+ if (gameOver) {
545
+ gameOver = false;
546
+ gameOverMessageEl.style.display = 'none';
547
+ // Optionally, could call resetGame() for a full reset, or just continue
548
+ }
549
+ updateUI();
550
+ event.preventDefault();
551
+ }
552
+ // Manual fire and shield keys are removed for auto-features
553
+ }
554
+ function onKeyUp(event) {
555
+ keysPressed[event.key.toLowerCase()] = false;
556
+ }
557
+ function onWindowResize() {
558
+ renderer.setSize(window.innerWidth, window.innerHeight);
559
+ camera.aspect = window.innerWidth / window.innerHeight;
560
+ camera.updateProjectionMatrix();
561
+ }
562
+
563
+ function activateShield(player) { // Now called by auto-shield logic
564
+ const now = Date.now();
565
+ if (player && !player.shieldActive && now > player.shieldCooldownEndTime) {
566
+ player.shieldActive = true;
567
+ player.shieldEndTime = now + SHIELD_DURATION;
568
+ player.shieldCooldownEndTime = player.shieldEndTime + SHIELD_COOLDOWN;
569
+ if (!player.shieldMesh) {
570
+ const shieldGeom = new THREE.SphereGeometry(player.radius * 1.6, 16, 16); // Slightly larger shield
571
+ const shieldMat = new THREE.MeshStandardMaterial({ color: 0x00ddff, transparent: true, opacity: 0.30, emissive: 0x00ccee, emissiveIntensity: 0.25 });
572
+ player.shieldMesh = new THREE.Mesh(shieldGeom, shieldMat);
573
+ player.meshGroup.add(player.shieldMesh);
574
+ }
575
+ player.shieldMesh.visible = true;
576
+ updateUI();
577
+ }
578
+ }
579
+
580
+ function updateShields() { // Checks for shield expiration
581
+ const now = Date.now();
582
+ [player1, player2].forEach(player => {
583
+ if (player && player.shieldActive && now > player.shieldEndTime) {
584
+ player.shieldActive = false;
585
+ if (player.shieldMesh) player.shieldMesh.visible = false;
586
+ updateUI();
587
+ }
588
+ });
589
+ }
590
+
591
+ function handleAutoShooting() {
592
+ const now = Date.now();
593
+ [player1, player2].forEach(player => {
594
+ if (player && player.health > 0 && !player.shieldActive) { // Don't shoot if shielded
595
+ if (now - player.lastShotTime > PLAYER_AUTO_FIRE_COOLDOWN) {
596
+ createProjectile(player);
597
+ player.lastShotTime = now; // Update lastShotTime here after successful fire
598
+ }
599
+ }
600
+ });
601
+ }
602
+
603
+ function handleAutoShielding() {
604
+ const now = Date.now();
605
+ [player1, player2].forEach(player => {
606
+ if (player && player.health > 0 && !player.shieldActive && now > player.shieldCooldownEndTime) {
607
+ // Critical health trigger
608
+ if (player.health <= AUTO_SHIELD_HEALTH_THRESHOLD) {
609
+ activateShield(player);
610
+ player.lastAutoShieldConsiderTime = now; // Reset consider timer
611
+ return; // Shield activated
612
+ }
613
+ // Periodic consideration if not full health
614
+ if (player.health < PLAYER_MAX_HEALTH && (now - player.lastAutoShieldConsiderTime > AUTO_SHIELD_CONSIDER_INTERVAL)) {
615
+ if (Math.random() < 0.3) { // 30% chance to auto-shield if conditions met
616
+ activateShield(player);
617
+ }
618
+ player.lastAutoShieldConsiderTime = now;
619
+ }
620
+ }
621
+ });
622
+ }
623
+
624
+
625
+ function handlePlayerMovement(player, forwardKey, backwardKey, turnLeftKey, turnRightKey) {
626
+ if (!player || player.health <= 0) return;
627
+
628
+ let speed = 0;
629
+ if (keysPressed[forwardKey]) speed = PLAYER_SPEED;
630
+ if (keysPressed[backwardKey]) speed = -PLAYER_SPEED * 0.7;
631
+
632
+ if (keysPressed[turnLeftKey]) player.meshGroup.rotation.y += 0.05;
633
+ if (keysPressed[turnRightKey]) player.meshGroup.rotation.y -= 0.05;
634
+
635
+ if (speed !== 0) {
636
+ const moveDirection = new THREE.Vector3(1, 0, 0);
637
+ moveDirection.applyQuaternion(player.meshGroup.quaternion);
638
+ player.meshGroup.position.add(moveDirection.multiplyScalar(speed));
639
+ }
640
+
641
+ const halfWorldWidth = GAME_PLANE_WIDTH / 2 - player.radius;
642
+ const halfWorldDepth = GAME_PLANE_HEIGHT / 2 - player.radius;
643
+ player.meshGroup.position.z = Math.max(-halfWorldDepth, Math.min(halfWorldDepth, player.meshGroup.position.z));
644
+
645
+ if (player.id === 'player1') {
646
+ player.meshGroup.position.x = Math.max(-halfWorldWidth, Math.min(DIVIDING_LINE_POS_X - player.radius, player.meshGroup.position.x));
647
+ } else {
648
+ player.meshGroup.position.x = Math.max(DIVIDING_LINE_POS_X + player.radius, Math.min(halfWorldWidth, player.meshGroup.position.x));
649
+ }
650
+
651
+ const otherPlayer = player.id === 'player1' ? player2 : player1;
652
+ if (otherPlayer && otherPlayer.health > 0) {
653
+ const distSq = player.meshGroup.position.distanceToSquared(otherPlayer.meshGroup.position);
654
+ if (distSq < (player.radius + otherPlayer.radius) ** 2) {
655
+ const delta = player.meshGroup.position.clone().sub(otherPlayer.meshGroup.position).normalize();
656
+ const overlap = (player.radius + otherPlayer.radius) - Math.sqrt(distSq);
657
+ player.meshGroup.position.add(delta.multiplyScalar(overlap / 2 + 0.01));
658
+ }
659
+ }
660
+ }
661
+
662
+ function updateInvaderBehavior() {
663
+ invaders.forEach(invader => {
664
+ if (invader.health <= 0) return;
665
+ invader.oscillationTime += 0.025;
666
+ let targetZ = invader.originalZ + Math.sin(invader.oscillationTime) * (GAME_PLANE_HEIGHT * 0.15);
667
+ invader.meshGroup.position.z = THREE.MathUtils.lerp(invader.meshGroup.position.z, targetZ, 0.1);
668
+
669
+ if (Date.now() - invader.lastShotTime > INVADER_FIRE_COOLDOWN) { // Enemies still use their own fire logic
670
+ if (Math.random() < 0.4) createProjectile(invader);
671
+ }
672
+ });
673
+ }
674
+
675
+ function updateParatroopers() {
676
+ for (let i = paratroopers.length - 1; i >= 0; i--) {
677
+ const pt = paratroopers[i];
678
+ if (pt.health <= 0) continue;
679
+
680
+ if (pt.meshGroup.position.y > pt.targetY) {
681
+ pt.meshGroup.position.y -= PARATROOPER_DROP_SPEED;
682
+ } else {
683
+ pt.meshGroup.position.y = pt.targetY;
684
+ if(!pt.landed) {
685
+ const targetPlayer = (player1.health > 0 && player2.health > 0) ? (Math.random() < 0.5 ? player1 : player2) : (player1.health > 0 ? player1 : (player2.health > 0 ? player2 : null));
686
+ if(targetPlayer) {
687
+ // Make paratrooper look at target on XZ plane
688
+ const targetPosXZ = new THREE.Vector3(targetPlayer.meshGroup.position.x, pt.meshGroup.position.y, targetPlayer.meshGroup.position.z);
689
+ pt.meshGroup.lookAt(targetPosXZ);
690
+ }
691
+ pt.landed = true;
692
+ }
693
+ }
694
+ if (Date.now() - pt.lastShotTime > PARATROOPER_FIRE_COOLDOWN) { // Enemies still use their own fire logic
695
+ if (Math.random() < 0.35) createProjectile(pt);
696
+ }
697
+ }
698
+ if (Date.now() - lastParatrooperSpawnTime > PARATROOPER_SPAWN_INTERVAL && paratroopers.length < 8) { // Max 8 paratroopers
699
+ spawnParatrooper();
700
+ }
701
+ }
702
+
703
+ function updateProjectiles() {
704
+ for (let i = projectiles.length - 1; i >= 0; i--) {
705
+ const p = projectiles[i];
706
+ p.position.add(p.userData.velocity);
707
+
708
+ if (Date.now() - p.userData.creationTime > 4000 ||
709
+ Math.abs(p.position.x) > GAME_PLANE_WIDTH / 2 + 5 ||
710
+ Math.abs(p.position.z) > GAME_PLANE_HEIGHT / 2 + 5 ||
711
+ p.position.y < -2 || p.position.y > PARATROOPER_SPAWN_Y + 5) {
712
+ scene.remove(p);
713
+ projectiles.splice(i, 1);
714
+ continue;
715
+ }
716
+ checkProjectileHit(p, i);
717
+ }
718
+ }
719
+
720
+ function getHitFlashMaterial(meshGroup) {
721
+ if (meshGroup && meshGroup.children) {
722
+ let bodyMesh = meshGroup.children.find(child => child.name === 'body' && child.material && child.material.isMeshStandardMaterial);
723
+ if (bodyMesh) return bodyMesh.material;
724
+
725
+ // Fallback to first standard material if no 'body'
726
+ for(let child of meshGroup.children){
727
+ if(child.isMesh && child.material && child.material.isMeshStandardMaterial){
728
+ return child.material;
729
+ }
730
+ }
731
+ }
732
+ return null;
733
+ }
734
+
735
+ function checkProjectileHit(projectile, projectileIndex) {
736
+ const pPos = projectile.position;
737
+ const ownerId = projectile.userData.ownerId;
738
+
739
+ [player1, player2].forEach(player => {
740
+ if (!player || player.health <= 0 || player.id === ownerId || player.shieldActive) return;
741
+ const distSq = pPos.distanceToSquared(player.meshGroup.position);
742
+ if (distSq < (player.radius + PROJECTILE_SIZE) ** 2) {
743
+ player.health--;
744
+ scene.remove(projectile); projectiles.splice(projectileIndex, 1);
745
+ if (!ownerId.includes('invader') && !ownerId.includes('paratrooper')) {
746
+ const shooter = ownerId === 'player1' ? player1 : player2;
747
+ if(shooter) shooter.score++;
748
+ }
749
+ const hitMaterial = getHitFlashMaterial(player.meshGroup);
750
+ if (hitMaterial) {
751
+ const originalColor = hitMaterial.color.clone();
752
+ const originalEmissive = hitMaterial.emissive.clone();
753
+ const originalEmissiveIntensity = hitMaterial.emissiveIntensity;
754
+
755
+ hitMaterial.color.setHex(0xff0000);
756
+ hitMaterial.emissive.setHex(0xff0000); hitMaterial.emissiveIntensity = 0.8;
757
+ setTimeout(() => {
758
+ if(hitMaterial) {
759
+ hitMaterial.color.copy(originalColor);
760
+ hitMaterial.emissive.copy(originalEmissive);
761
+ hitMaterial.emissiveIntensity = originalEmissiveIntensity;
762
+ }
763
+ }, 120);
764
+ }
765
+ updateUI(); checkWinCondition(); return;
766
+ }
767
+ });
768
+ if (projectiles.indexOf(projectile) === -1) return;
769
+
770
+ const enemyTypes = [invaders, paratroopers];
771
+ for (const enemyList of enemyTypes) {
772
+ for (let j = enemyList.length - 1; j >= 0; j--) {
773
+ const enemy = enemyList[j];
774
+ if (enemy.health <= 0 || ownerId.includes(enemy.id.replace(/\d/g, ''))) continue;
775
+
776
+ const distSq = pPos.distanceToSquared(enemy.meshGroup.position);
777
+ if (distSq < (enemy.radius + PROJECTILE_SIZE) ** 2) {
778
+ enemy.health--;
779
+ scene.remove(projectile); projectiles.splice(projectileIndex, 1);
780
+ if (ownerId === 'player1' && player1) player1.score++;
781
+ else if (ownerId === 'player2' && player2) player2.score++;
782
+
783
+ if (enemy.health <= 0) {
784
+ scene.remove(enemy.meshGroup);
785
+ enemyList.splice(j, 1);
786
+ } else {
787
+ const hitMaterial = getHitFlashMaterial(enemy.meshGroup);
788
+ if (hitMaterial) {
789
+ const originalColor = hitMaterial.color.clone();
790
+ const originalEmissive = hitMaterial.emissive.clone();
791
+ const originalEmissiveIntensity = hitMaterial.emissiveIntensity;
792
+
793
+ hitMaterial.color.setHex(0xff0000);
794
+ hitMaterial.emissive.setHex(0xff0000); hitMaterial.emissiveIntensity = 0.8;
795
+ setTimeout(() => {
796
+ if(hitMaterial) {
797
+ hitMaterial.color.copy(originalColor);
798
+ hitMaterial.emissive.copy(originalEmissive);
799
+ hitMaterial.emissiveIntensity = originalEmissiveIntensity;
800
+ }
801
+ }, 120);
802
+ }
803
+ }
804
+ updateUI(); return;
805
+ }
806
+ }
807
+ if (projectiles.indexOf(projectile) === -1) return;
808
+ }
809
+ }
810
+
811
+ function updateUI() {
812
+ if (player1) {
813
+ player1ScoreEl.textContent = player1.score;
814
+ const p1HealthPercent = Math.max(0, (player1.health / PLAYER_MAX_HEALTH) * 100);
815
+ player1HealthBarFillEl.style.width = `${p1HealthPercent}%`;
816
+ // Change health bar color based on health
817
+ if (p1HealthPercent <= 30) player1HealthBarFillEl.style.backgroundColor = '#e53e3e'; // Red
818
+ else if (p1HealthPercent <= 60) player1HealthBarFillEl.style.backgroundColor = '#dd6b20'; // Orange-ish
819
+ else player1HealthBarFillEl.style.backgroundColor = '#38b2ac'; // Teal (normal)
820
+
821
+
822
+ const now = Date.now();
823
+ player1ShieldStatusEl.textContent = player1.shieldActive ? `ON (${Math.ceil((player1.shieldEndTime - now)/1000)}s)` : (now < player1.shieldCooldownEndTime ? `CD (${Math.ceil((player1.shieldCooldownEndTime - now)/1000)}s)`: 'READY');
824
+ }
825
+ if (player2) {
826
+ player2ScoreEl.textContent = player2.score;
827
+ const p2HealthPercent = Math.max(0, (player2.health / PLAYER_MAX_HEALTH) * 100);
828
+ player2HealthBarFillEl.style.width = `${p2HealthPercent}%`;
829
+ if (p2HealthPercent <= 30) player2HealthBarFillEl.style.backgroundColor = '#e53e3e'; // Red
830
+ else if (p2HealthPercent <= 60) player2HealthBarFillEl.style.backgroundColor = '#dd6b20'; // Orange-ish (darker orange)
831
+ else player2HealthBarFillEl.style.backgroundColor = '#ed8936'; // Orange (normal)
832
+
833
+ const now = Date.now();
834
+ player2ShieldStatusEl.textContent = player2.shieldActive ? `ON (${Math.ceil((player2.shieldEndTime - now)/1000)}s)` : (now < player2.shieldCooldownEndTime ? `CD (${Math.ceil((player2.shieldCooldownEndTime - now)/1000)}s)`: 'READY');
835
+ }
836
+ }
837
+
838
+ function checkWinCondition() {
839
+ if (gameOver) return;
840
+ let winner = null;
841
+ const p1Exists = !!player1;
842
+ const p2Exists = !!player2;
843
+ const p1Health = p1Exists ? player1.health : 1;
844
+ const p2Health = p2Exists ? player2.health : 1;
845
+
846
+ if (p1Exists && p2Exists && p1Health <= 0 && p2Health <=0) winner = "It's a Draw!";
847
+ else if (p1Exists && p1Health <= 0) winner = "Player 2 Wins!";
848
+ else if (p2Exists && p2Health <= 0) winner = "Player 1 Wins!";
849
+
850
+ if (winner) {
851
+ gameOver = true;
852
+ gameOverMessageEl.textContent = winner;
853
+ gameOverMessageEl.style.display = 'block';
854
+ }
855
+ }
856
+
857
+ function animate() {
858
+ requestAnimationFrame(animate);
859
+
860
+ if (!gameOver) {
861
+ if (player1) handlePlayerMovement(player1, 'w', 's', 'a', 'd');
862
+ if (player2) handlePlayerMovement(player2, 'i', 'k', 'j', 'l');
863
+ handleAutoShooting();
864
+ handleAutoShielding();
865
+ updateInvaderBehavior();
866
+ updateParatroopers();
867
+ updateShields(); // For expiration
868
+ }
869
+ updateProjectiles();
870
+ updateUI();
871
+
872
+ renderer.render(scene, camera);
873
+ }
874
+
875
+ if (document.readyState === 'loading') {
876
+ document.addEventListener('DOMContentLoaded', init);
877
+ } else {
878
+ init();
879
+ }
880
+ </script>
881
+ </body>
882
  </html>