bebechien commited on
Commit
da4045e
·
verified ·
1 Parent(s): 1cd7978

Upload index.html

Browse files
Files changed (1) hide show
  1. index.html +742 -19
index.html CHANGED
@@ -1,19 +1,742 @@
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>Project Supernova</title>
7
+ <script src="https://cdn.tailwindcss.com"></script>
8
+ <style>
9
+ @import url('https://fonts.googleapis.com/css2?family=Orbitron:wght@400;700&display=swap');
10
+ body {
11
+ background-color: #000;
12
+ color: #0f0;
13
+ font-family: 'Orbitron', sans-serif;
14
+ overflow: hidden;
15
+ display: flex;
16
+ align-items: center;
17
+ justify-content: center;
18
+ height: 100vh;
19
+ margin: 0;
20
+ }
21
+ canvas {
22
+ background-color: #0a0a0a;
23
+ border: 2px solid #0ff;
24
+ box-shadow: 0 0 20px #0ff;
25
+ cursor: crosshair;
26
+ }
27
+ .ui-overlay {
28
+ position: absolute;
29
+ top: 0;
30
+ left: 0;
31
+ width: 100%;
32
+ height: 100%;
33
+ display: flex;
34
+ align-items: center;
35
+ justify-content: center;
36
+ flex-direction: column;
37
+ background-color: rgba(0, 0, 0, 0.7);
38
+ color: #0ff;
39
+ text-align: center;
40
+ }
41
+ .ui-overlay h1 {
42
+ font-size: 4rem;
43
+ text-shadow: 0 0 15px #0ff;
44
+ margin-bottom: 1rem;
45
+ }
46
+ .ui-overlay p {
47
+ font-size: 1.2rem;
48
+ margin-bottom: 2rem;
49
+ }
50
+ .ui-button {
51
+ background-color: transparent;
52
+ border: 2px solid #0ff;
53
+ color: #0ff;
54
+ padding: 1rem 2rem;
55
+ font-family: 'Orbitron', sans-serif;
56
+ font-size: 1.2rem;
57
+ text-transform: uppercase;
58
+ cursor: pointer;
59
+ transition: all 0.3s ease;
60
+ margin: 0.5rem;
61
+ box-shadow: 0 0 10px #0ff inset;
62
+ }
63
+ .ui-button:hover {
64
+ background-color: #0ff;
65
+ color: #000;
66
+ box-shadow: 0 0 20px #0ff;
67
+ }
68
+ #hud {
69
+ position: absolute;
70
+ top: 20px;
71
+ width: 100%;
72
+ padding: 0 40px;
73
+ box-sizing: border-box;
74
+ display: flex;
75
+ justify-content: space-between;
76
+ font-size: 1.5rem;
77
+ text-shadow: 0 0 5px #fff;
78
+ pointer-events: none;
79
+ }
80
+ #player-hud {
81
+ color: #0ff;
82
+ }
83
+ #ai-hud {
84
+ color: #f0f;
85
+ }
86
+ #timer-hud {
87
+ color: #fff;
88
+ }
89
+ </style>
90
+ </head>
91
+ <body>
92
+ <div id="hud" class="hidden">
93
+ <div id="player-hud">Player: 0 | Lives: 3</div>
94
+ <div id="timer-hud">3:00</div>
95
+ <div id="ai-hud">Rival: 0 | Lives: 3</div>
96
+ </div>
97
+
98
+ <div id="menu" class="ui-overlay">
99
+ <h1 class="text-6xl font-bold">PROJECT SUPERNOVA</h1>
100
+ <p class="text-xl">An AI has challenged you to a duel amidst the asteroids.</p>
101
+ <div>
102
+ <button id="easy-btn" class="ui-button">Easy</button>
103
+ <button id="medium-btn" class="ui-button">Medium</button>
104
+ <button id="hard-btn" class="ui-button">Hard</button>
105
+ </div>
106
+ </div>
107
+
108
+ <div id="game-over" class="ui-overlay hidden">
109
+ <h1 id="winner-text" class="text-6xl font-bold"></h1>
110
+ <p id="final-scores" class="text-xl"></p>
111
+ <button id="play-again-btn" class="ui-button">Play Again</button>
112
+ </div>
113
+
114
+ <canvas id="gameCanvas"></canvas>
115
+
116
+ <script type="module">
117
+ // --- Game Setup ---
118
+ const canvas = document.getElementById('gameCanvas');
119
+ const ctx = canvas.getContext('2d');
120
+ const menu = document.getElementById('menu');
121
+ const gameOverScreen = document.getElementById('game-over');
122
+ const hud = document.getElementById('hud');
123
+
124
+ let game;
125
+
126
+ function resizeCanvas() {
127
+ canvas.width = window.innerWidth;
128
+ canvas.height = window.innerHeight;
129
+ }
130
+ window.addEventListener('resize', resizeCanvas);
131
+ resizeCanvas();
132
+
133
+ // --- Utility Classes & Functions ---
134
+ class Vector {
135
+ constructor(x = 0, y = 0) {
136
+ this.x = x;
137
+ this.y = y;
138
+ }
139
+ add(v) { return new Vector(this.x + v.x, this.y + v.y); }
140
+ subtract(v) { return new Vector(this.x - v.x, this.y - v.y); }
141
+ multiply(s) { return new Vector(this.x * s, this.y * s); }
142
+ get magnitude() { return Math.sqrt(this.x ** 2 + this.y ** 2); }
143
+ get angle() { return Math.atan2(this.y, this.x); }
144
+ normalize() {
145
+ const mag = this.magnitude;
146
+ return mag === 0 ? new Vector() : new Vector(this.x / mag, this.y / mag);
147
+ }
148
+ static fromAngle(angle, magnitude = 1) {
149
+ return new Vector(magnitude * Math.cos(angle), magnitude * Math.sin(angle));
150
+ }
151
+ }
152
+
153
+ function isColliding(obj1, obj2) {
154
+ const dist = obj1.pos.subtract(obj2.pos).magnitude;
155
+ return dist < obj1.radius + obj2.radius;
156
+ }
157
+
158
+ function wrapEdges(obj) {
159
+ if (obj.pos.x < -obj.radius) obj.pos.x = canvas.width + obj.radius;
160
+ if (obj.pos.x > canvas.width + obj.radius) obj.pos.x = -obj.radius;
161
+ if (obj.pos.y < -obj.radius) obj.pos.y = canvas.height + obj.radius;
162
+ if (obj.pos.y > canvas.height + obj.radius) obj.pos.y = -obj.radius;
163
+ }
164
+
165
+ // --- Game Object Classes ---
166
+
167
+ // Base class for Player and AI
168
+ class Ship {
169
+ constructor(x, y, color, name) {
170
+ this.name = name;
171
+ this.pos = new Vector(x, y);
172
+ this.vel = new Vector();
173
+ this.angle = -Math.PI / 2; // Point up
174
+ this.radius = 15;
175
+ this.color = color;
176
+ this.isThrusting = false;
177
+ this.rotation = 0;
178
+ this.lives = 3;
179
+ this.score = 0;
180
+ this.isInvincible = false;
181
+ this.invincibilityTimer = 0;
182
+ this.shootCooldown = 0;
183
+ }
184
+
185
+ draw() {
186
+ ctx.save();
187
+ ctx.translate(this.pos.x, this.pos.y);
188
+ ctx.rotate(this.angle);
189
+
190
+ if (this.isInvincible && Math.floor(Date.now() / 100) % 2) {
191
+ ctx.strokeStyle = '#fff';
192
+ ctx.fillStyle = 'rgba(255, 255, 255, 0.5)';
193
+ } else {
194
+ ctx.strokeStyle = this.color;
195
+ ctx.fillStyle = 'rgba(10, 10, 10, 0.5)';
196
+ }
197
+
198
+ // Ship body
199
+ ctx.shadowColor = this.color;
200
+ ctx.shadowBlur = 15;
201
+ ctx.lineWidth = 2;
202
+ ctx.beginPath();
203
+ ctx.moveTo(15, 0);
204
+ ctx.lineTo(-10, -10);
205
+ ctx.lineTo(-10, 10);
206
+ ctx.closePath();
207
+ ctx.stroke();
208
+ ctx.fill();
209
+
210
+ // Thrust flame
211
+ if (this.isThrusting && Math.random() > 0.2) {
212
+ ctx.fillStyle = '#fff';
213
+ ctx.beginPath();
214
+ ctx.moveTo(-10, 0);
215
+ ctx.lineTo(-18 - Math.random() * 5, 5);
216
+ ctx.lineTo(-18 - Math.random() * 5, -5);
217
+ ctx.closePath();
218
+ ctx.fill();
219
+ }
220
+ ctx.restore();
221
+ }
222
+
223
+ update() {
224
+ this.angle += this.rotation;
225
+ if (this.isThrusting) {
226
+ const force = Vector.fromAngle(this.angle, 0.1);
227
+ this.vel = this.vel.add(force);
228
+ }
229
+ // Apply friction
230
+ this.vel = this.vel.multiply(0.99);
231
+ if (this.vel.magnitude > 5) {
232
+ this.vel = this.vel.normalize().multiply(5);
233
+ }
234
+ this.pos = this.pos.add(this.vel);
235
+ wrapEdges(this);
236
+
237
+ if (this.shootCooldown > 0) this.shootCooldown--;
238
+ if (this.isInvincible) {
239
+ this.invincibilityTimer--;
240
+ if (this.invincibilityTimer <= 0) {
241
+ this.isInvincible = false;
242
+ }
243
+ }
244
+ }
245
+
246
+ shoot() {
247
+ if (this.shootCooldown === 0) {
248
+ const bulletVel = Vector.fromAngle(this.angle, 7).add(this.vel);
249
+ const bulletPos = this.pos.add(Vector.fromAngle(this.angle, 15));
250
+ game.bullets.push(new Bullet(bulletPos.x, bulletPos.y, bulletVel, this.color, this.name));
251
+ this.shootCooldown = 15; // Cooldown frames
252
+ }
253
+ }
254
+
255
+ hit() {
256
+ if (this.isInvincible) return;
257
+ this.lives--;
258
+ game.createExplosion(this.pos, this.color, 40);
259
+ if (this.lives > 0) {
260
+ this.resetPosition();
261
+ } else {
262
+ game.handlePlayerDeath(this);
263
+ }
264
+ }
265
+
266
+ resetPosition() {
267
+ this.pos = new Vector(Math.random() * canvas.width, Math.random() * canvas.height);
268
+ this.vel = new Vector();
269
+ this.isInvincible = true;
270
+ this.invincibilityTimer = 180; // 3 seconds
271
+ }
272
+ }
273
+
274
+ class Player extends Ship {
275
+ constructor(x, y) {
276
+ super(x, y, '#0ff', 'Player');
277
+ this.keys = {};
278
+ this.setupControls();
279
+ }
280
+
281
+ setupControls() {
282
+ window.addEventListener('keydown', (e) => this.keys[e.code] = true);
283
+ window.addEventListener('keyup', (e) => this.keys[e.code] = false);
284
+ }
285
+
286
+ update() {
287
+ this.rotation = 0;
288
+ if (this.keys['KeyA'] || this.keys['ArrowLeft']) this.rotation = -0.05;
289
+ if (this.keys['KeyD'] || this.keys['ArrowRight']) this.rotation = 0.05;
290
+
291
+ this.isThrusting = this.keys['KeyW'] || this.keys['ArrowUp'];
292
+
293
+ if (this.keys['Space']) {
294
+ this.shoot();
295
+ }
296
+ super.update();
297
+ }
298
+ }
299
+
300
+ class AI extends Ship {
301
+ constructor(x, y, difficulty) {
302
+ super(x, y, '#f0f', 'Rival');
303
+ this.difficulty = difficulty;
304
+ this.state = 'HUNTING_ASTEROID';
305
+ this.target = null;
306
+ this.stateTimer = 0;
307
+ this.setupDifficulty();
308
+ }
309
+
310
+ setupDifficulty() {
311
+ switch(this.difficulty) {
312
+ case 'easy':
313
+ this.accuracy = 0.3; this.aggression = 0.1; this.reactionTime = 30;
314
+ break;
315
+ case 'hard':
316
+ this.accuracy = 0.9; this.aggression = 0.6; this.reactionTime = 5;
317
+ break;
318
+ case 'medium':
319
+ default:
320
+ this.accuracy = 0.6; this.aggression = 0.3; this.reactionTime = 15;
321
+ break;
322
+ }
323
+ }
324
+
325
+ update() {
326
+ this.stateTimer--;
327
+ if (this.stateTimer <= 0) {
328
+ this.chooseNewState();
329
+ }
330
+
331
+ this.executeState();
332
+
333
+ super.update();
334
+ }
335
+
336
+ chooseNewState() {
337
+ this.stateTimer = Math.random() * 120 + 60; // 1-3 seconds
338
+ const dangerousObject = this.findClosestThreat();
339
+ if (dangerousObject) {
340
+ this.state = 'AVOIDING_DANGER';
341
+ this.target = dangerousObject;
342
+ return;
343
+ }
344
+
345
+ if (Math.random() < this.aggression || (game.player && game.player.score > this.score + 1000)) {
346
+ this.state = 'ATTACKING_PLAYER';
347
+ this.target = game.player;
348
+ } else {
349
+ this.state = 'HUNTING_ASTEROID';
350
+ this.target = this.findClosestAsteroid();
351
+ }
352
+ }
353
+
354
+ executeState() {
355
+ if (!this.target) {
356
+ this.isThrusting = false;
357
+ this.rotation = 0;
358
+ return;
359
+ }
360
+
361
+ let targetPos = this.target.pos;
362
+
363
+ // Lead the target
364
+ if (this.target.vel) {
365
+ const timeToIntercept = this.pos.subtract(this.target.pos).magnitude / 7; // 7 is bullet speed
366
+ targetPos = this.target.pos.add(this.target.vel.multiply(timeToIntercept * (this.accuracy * 20)));
367
+ }
368
+
369
+ const vectorToTarget = targetPos.subtract(this.pos);
370
+ const desiredAngle = vectorToTarget.angle;
371
+ let angleDiff = desiredAngle - this.angle;
372
+
373
+ // Normalize angle difference
374
+ while (angleDiff < -Math.PI) angleDiff += 2 * Math.PI;
375
+ while (angleDiff > Math.PI) angleDiff -= 2 * Math.PI;
376
+
377
+ if (this.state === 'AVOIDING_DANGER') {
378
+ // Turn away from the threat
379
+ angleDiff += Math.PI;
380
+ }
381
+
382
+ // Rotation
383
+ this.rotation = Math.sign(angleDiff) * 0.05;
384
+ if(Math.abs(angleDiff) < 0.1) this.rotation = 0;
385
+
386
+ // Thrusting
387
+ const shouldThrust = this.state === 'AVOIDING_DANGER' || vectorToTarget.magnitude > 100;
388
+ this.isThrusting = (Math.abs(angleDiff) < Math.PI / 2) && shouldThrust;
389
+
390
+ // Shooting
391
+ if (this.state !== 'AVOIDING_DANGER' && Math.abs(angleDiff) < 0.2 && Math.random() < this.accuracy) {
392
+ this.shoot();
393
+ }
394
+ }
395
+
396
+ findClosestThreat() {
397
+ let closestDist = Infinity;
398
+ let closestThreat = null;
399
+ const checkRadius = 150;
400
+
401
+ for (const asteroid of game.asteroids) {
402
+ const dist = this.pos.subtract(asteroid.pos).magnitude;
403
+ if (dist < checkRadius && dist < closestDist) {
404
+ closestDist = dist;
405
+ closestThreat = asteroid;
406
+ }
407
+ }
408
+ return closestThreat;
409
+ }
410
+
411
+ findClosestAsteroid() {
412
+ let closestDist = Infinity;
413
+ let closestAsteroid = null;
414
+ for (const asteroid of game.asteroids) {
415
+ const dist = this.pos.subtract(asteroid.pos).magnitude;
416
+ if (dist < closestDist) {
417
+ closestDist = dist;
418
+ closestAsteroid = asteroid;
419
+ }
420
+ }
421
+ return closestAsteroid;
422
+ }
423
+ }
424
+
425
+ class Asteroid {
426
+ constructor(x, y, radius) {
427
+ this.pos = new Vector(x, y);
428
+ this.radius = radius;
429
+ this.vel = Vector.fromAngle(Math.random() * Math.PI * 2, Math.random() * 2 + 0.5);
430
+ this.angle = 0;
431
+ this.rotationSpeed = (Math.random() - 0.5) * 0.02;
432
+ this.shape = [];
433
+ const numVertices = Math.floor(Math.random() * 5) + 7;
434
+ for (let i = 0; i < numVertices; i++) {
435
+ const angle = (i / numVertices) * Math.PI * 2;
436
+ const r = this.radius * (Math.random() * 0.4 + 0.8);
437
+ this.shape.push(new Vector(Math.cos(angle) * r, Math.sin(angle) * r));
438
+ }
439
+ }
440
+
441
+ draw() {
442
+ ctx.save();
443
+ ctx.translate(this.pos.x, this.pos.y);
444
+ ctx.rotate(this.angle);
445
+ ctx.strokeStyle = '#fff';
446
+ ctx.shadowColor = '#fff';
447
+ ctx.shadowBlur = 5;
448
+ ctx.lineWidth = 1.5;
449
+ ctx.beginPath();
450
+ ctx.moveTo(this.shape[0].x, this.shape[0].y);
451
+ for (let i = 1; i < this.shape.length; i++) {
452
+ ctx.lineTo(this.shape[i].x, this.shape[i].y);
453
+ }
454
+ ctx.closePath();
455
+ ctx.stroke();
456
+ ctx.restore();
457
+ }
458
+
459
+ update() {
460
+ this.pos = this.pos.add(this.vel);
461
+ this.angle += this.rotationSpeed;
462
+ wrapEdges(this);
463
+ }
464
+
465
+ breakApart() {
466
+ game.createExplosion(this.pos, '#fff', Math.floor(this.radius / 2));
467
+ if (this.radius > 30) {
468
+ for (let i = 0; i < 2; i++) {
469
+ game.asteroids.push(new Asteroid(this.pos.x, this.pos.y, this.radius / 2));
470
+ }
471
+ } else if (this.radius > 15) {
472
+ for (let i = 0; i < 2; i++) {
473
+ game.asteroids.push(new Asteroid(this.pos.x, this.pos.y, this.radius / 2));
474
+ }
475
+ }
476
+ }
477
+ }
478
+
479
+ class Bullet {
480
+ constructor(x, y, vel, color, owner) {
481
+ this.pos = new Vector(x, y);
482
+ this.vel = vel;
483
+ this.radius = 3;
484
+ this.color = color;
485
+ this.lifespan = 80; // frames
486
+ this.owner = owner;
487
+ }
488
+ draw() {
489
+ ctx.fillStyle = this.color;
490
+ ctx.shadowColor = this.color;
491
+ ctx.shadowBlur = 10;
492
+ ctx.beginPath();
493
+ ctx.arc(this.pos.x, this.pos.y, this.radius, 0, Math.PI * 2);
494
+ ctx.fill();
495
+ }
496
+ update() {
497
+ this.pos = this.pos.add(this.vel);
498
+ this.lifespan--;
499
+ }
500
+ }
501
+
502
+ class Particle {
503
+ constructor(x, y, color) {
504
+ this.pos = new Vector(x, y);
505
+ this.vel = Vector.fromAngle(Math.random() * Math.PI * 2, Math.random() * 3);
506
+ this.radius = Math.random() * 2 + 1;
507
+ this.color = color;
508
+ this.lifespan = 50;
509
+ this.alpha = 1;
510
+ }
511
+ draw() {
512
+ ctx.save();
513
+ ctx.globalAlpha = this.alpha;
514
+ ctx.fillStyle = this.color;
515
+ ctx.beginPath();
516
+ ctx.arc(this.pos.x, this.pos.y, this.radius, 0, Math.PI * 2);
517
+ ctx.fill();
518
+ ctx.restore();
519
+ }
520
+ update() {
521
+ this.pos = this.pos.add(this.vel);
522
+ this.vel = this.vel.multiply(0.98);
523
+ this.lifespan--;
524
+ this.alpha = this.lifespan / 50;
525
+ }
526
+ }
527
+
528
+
529
+ // --- Main Game Class ---
530
+ class Game {
531
+ constructor(difficulty) {
532
+ this.difficulty = difficulty;
533
+ this.player = new Player(canvas.width / 4, canvas.height / 2);
534
+ this.ai = new AI(canvas.width * 3 / 4, canvas.height / 2, this.difficulty);
535
+ this.asteroids = [];
536
+ this.bullets = [];
537
+ this.particles = [];
538
+ this.isGameOver = false;
539
+ this.roundTime = 180 * 1000; // 3 minutes in ms
540
+ this.startTime = Date.now();
541
+ this.winner = null;
542
+ this.initAsteroids(8);
543
+ }
544
+
545
+ initAsteroids(count) {
546
+ for (let i = 0; i < count; i++) {
547
+ const x = Math.random() * canvas.width;
548
+ const y = Math.random() * canvas.height;
549
+ // Ensure asteroids don't spawn on top of ships
550
+ const playerDist = new Vector(x, y).subtract(this.player.pos).magnitude;
551
+ const aiDist = new Vector(x, y).subtract(this.ai.pos).magnitude;
552
+ if(playerDist < 100 || aiDist < 100) {
553
+ i--;
554
+ continue;
555
+ }
556
+ this.asteroids.push(new Asteroid(x, y, 40));
557
+ }
558
+ }
559
+
560
+ update() {
561
+ if (this.isGameOver) return;
562
+
563
+ // Update all objects
564
+ this.player.update();
565
+ this.ai.update();
566
+ this.asteroids.forEach(a => a.update());
567
+ this.bullets.forEach(b => b.update());
568
+ this.particles.forEach(p => p.update());
569
+
570
+ // Filter out dead objects
571
+ this.bullets = this.bullets.filter(b => b.lifespan > 0);
572
+ this.particles = this.particles.filter(p => p.lifespan > 0);
573
+
574
+ // Check collisions
575
+ this.handleCollisions();
576
+
577
+ // Check for more asteroids
578
+ if (this.asteroids.length < 5) {
579
+ this.initAsteroids(3);
580
+ }
581
+
582
+ // Check game end conditions
583
+ const elapsedTime = Date.now() - this.startTime;
584
+ if (elapsedTime >= this.roundTime) {
585
+ this.endGameByTime();
586
+ }
587
+ }
588
+
589
+ draw() {
590
+ ctx.clearRect(0, 0, canvas.width, canvas.height);
591
+ ctx.shadowBlur = 0; // Reset shadow for background
592
+ ctx.fillStyle = 'rgba(0,0,0,0.9)'; // Slight fade effect
593
+ ctx.fillRect(0, 0, canvas.width, canvas.height);
594
+
595
+ this.player.draw();
596
+ this.ai.draw();
597
+ this.asteroids.forEach(a => a.draw());
598
+ this.bullets.forEach(b => b.draw());
599
+ this.particles.forEach(p => p.draw());
600
+
601
+ this.updateHUD();
602
+ }
603
+
604
+ updateHUD() {
605
+ document.getElementById('player-hud').innerText = `Player: ${this.player.score} | Lives: ${this.player.lives}`;
606
+ document.getElementById('ai-hud').innerText = `Rival: ${this.ai.score} | Lives: ${this.ai.lives}`;
607
+
608
+ const remainingTime = Math.max(0, this.roundTime - (Date.now() - this.startTime));
609
+ const minutes = Math.floor(remainingTime / 60000);
610
+ const seconds = Math.floor((remainingTime % 60000) / 1000).toString().padStart(2, '0');
611
+ document.getElementById('timer-hud').innerText = `${minutes}:${seconds}`;
612
+ }
613
+
614
+ handleCollisions() {
615
+ // Bullets vs Asteroids
616
+ for (let i = this.bullets.length - 1; i >= 0; i--) {
617
+ const bullet = this.bullets[i];
618
+ for (let j = this.asteroids.length - 1; j >= 0; j--) {
619
+ const asteroid = this.asteroids[j];
620
+ if (isColliding(bullet, asteroid)) {
621
+ this.bullets.splice(i, 1);
622
+
623
+ let points = 0;
624
+ if (asteroid.radius > 30) points = 20;
625
+ else if (asteroid.radius > 15) points = 50;
626
+ else points = 100;
627
+
628
+ if (bullet.owner === 'Player') this.player.score += points;
629
+ else if (bullet.owner === 'Rival') this.ai.score += points;
630
+
631
+ asteroid.breakApart();
632
+ this.asteroids.splice(j, 1);
633
+ break;
634
+ }
635
+ }
636
+ }
637
+
638
+ // Ships vs Asteroids
639
+ [this.player, this.ai].forEach(ship => {
640
+ for (const asteroid of this.asteroids) {
641
+ if (isColliding(ship, asteroid)) {
642
+ ship.hit();
643
+ }
644
+ }
645
+ });
646
+
647
+ // Bullets vs Ships
648
+ for (let i = this.bullets.length - 1; i >= 0; i--) {
649
+ const bullet = this.bullets[i];
650
+
651
+ if (bullet.owner !== 'Player' && isColliding(bullet, this.player)) {
652
+ this.player.hit();
653
+ this.ai.score += 500;
654
+ this.bullets.splice(i, 1);
655
+ continue;
656
+ }
657
+
658
+ if (bullet.owner !== 'Rival' && isColliding(bullet, this.ai)) {
659
+ this.ai.hit();
660
+ this.player.score += 500;
661
+ this.bullets.splice(i, 1);
662
+ continue;
663
+ }
664
+ }
665
+ }
666
+
667
+ createExplosion(pos, color, count) {
668
+ for(let i=0; i < count; i++) {
669
+ this.particles.push(new Particle(pos.x, pos.y, color));
670
+ }
671
+ }
672
+
673
+ handlePlayerDeath(ship) {
674
+ if (ship === this.player && this.ai.lives > 0) {
675
+ this.endGame(this.ai);
676
+ } else if (ship === this.ai && this.player.lives > 0) {
677
+ this.endGame(this.player);
678
+ } else {
679
+ // This handles simultaneous death
680
+ this.endGameByTime();
681
+ }
682
+ }
683
+
684
+ endGameByTime() {
685
+ if (this.player.score > this.ai.score) {
686
+ this.endGame(this.player);
687
+ } else if (this.ai.score > this.player.score) {
688
+ this.endGame(this.ai);
689
+ } else {
690
+ this.endGame(null); // Draw
691
+ }
692
+ }
693
+
694
+ endGame(winner) {
695
+ if (this.isGameOver) return;
696
+ this.isGameOver = true;
697
+ this.winner = winner;
698
+
699
+ const winnerText = document.getElementById('winner-text');
700
+ if (winner) {
701
+ winnerText.innerText = `${winner.name} Wins!`;
702
+ winnerText.style.color = winner.color;
703
+ } else {
704
+ winnerText.innerText = 'Draw!';
705
+ winnerText.style.color = '#fff';
706
+ }
707
+
708
+ document.getElementById('final-scores').innerText = `Final Score: ${this.player.score} (Player) vs ${this.ai.score} (Rival)`;
709
+ gameOverScreen.classList.remove('hidden');
710
+ hud.classList.add('hidden');
711
+ }
712
+ }
713
+
714
+ // --- Main Loop ---
715
+ let animationFrameId;
716
+ function gameLoop() {
717
+ game.update();
718
+ game.draw();
719
+ animationFrameId = requestAnimationFrame(gameLoop);
720
+ }
721
+
722
+ function startGame(difficulty) {
723
+ menu.classList.add('hidden');
724
+ gameOverScreen.classList.add('hidden');
725
+ hud.classList.remove('hidden');
726
+ game = new Game(difficulty);
727
+ if (animationFrameId) cancelAnimationFrame(animationFrameId);
728
+ gameLoop();
729
+ }
730
+
731
+ // --- Event Listeners ---
732
+ document.getElementById('easy-btn').onclick = () => startGame('easy');
733
+ document.getElementById('medium-btn').onclick = () => startGame('medium');
734
+ document.getElementById('hard-btn').onclick = () => startGame('hard');
735
+ document.getElementById('play-again-btn').onclick = () => {
736
+ gameOverScreen.classList.add('hidden');
737
+ menu.classList.remove('hidden');
738
+ };
739
+
740
+ </script>
741
+ </body>
742
+ </html>