bbc123321 commited on
Commit
5f0ec06
·
verified ·
1 Parent(s): e19e778

Manual changes saved

Browse files
Files changed (1) hide show
  1. index.html +493 -459
index.html CHANGED
@@ -1,628 +1,662 @@
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" />
6
- <title>BattleZone Royale</title>
7
  <script src="https://cdn.tailwindcss.com"></script>
8
  <script src="https://unpkg.com/feather-icons"></script>
9
  <style>
10
- html,
11
- body,
12
- #root {
13
- height: 100%;
14
- margin: 0;
15
- background-color: #111827; /* Tailwind gray-900 */
16
- color: white;
17
- font-family: monospace;
18
- }
19
- #gameCanvas {
20
- background-image: url('http://static.photos/nature/1200x630/42');
21
- background-size: cover;
22
- background-position: center;
23
- display: block;
24
- box-shadow: 0 0 20px rgba(0, 0, 0, 0.3);
25
- user-select: none;
26
- cursor: crosshair;
27
- }
28
- .biome-selector {
29
- transition: all 0.3s ease;
30
- }
31
- .biome-selector:hover {
32
- transform: scale(1.05);
33
- box-shadow: 0 0 15px rgba(255, 215, 0, 0.5);
34
- }
35
- .storm-animation {
36
- animation: stormPulse 3s infinite;
37
- }
38
- @keyframes stormPulse {
39
- 0% {
40
- box-shadow: 0 0 10px rgba(100, 149, 237, 0.5);
41
- }
42
- 50% {
43
- box-shadow: 0 0 30px rgba(100, 149, 237, 0.8);
44
- }
45
- 100% {
46
- box-shadow: 0 0 10px rgba(100, 149, 237, 0.5);
47
- }
48
- }
49
- #stormWarning {
50
- z-index: 10;
51
- }
52
- #deathScreen {
53
- z-index: 20;
54
- }
55
- /* Fullscreen canvas container */
56
- #canvasContainer {
57
- position: relative;
58
- flex: 1;
59
- display: flex;
60
- justify-content: center;
61
- align-items: center;
62
- height: 100vh;
63
- overflow: hidden;
64
- }
65
  </style>
66
  </head>
67
  <body>
68
- <div id="landingScreen" class="flex flex-col items-center justify-center h-screen bg-gray-900 p-6">
69
- <h1 class="text-5xl font-bold text-yellow-400 mb-8 flex items-center">
 
70
  <i data-feather="target" class="mr-2"></i> BattleZone Royale
71
  </h1>
72
- <h2 class="text-3xl font-semibold text-yellow-400 mb-6 flex items-center">
73
- <i data-feather="map-pin" class="mr-2"></i> Choose Landing Zone
74
- </h2>
75
- <div class="grid grid-cols-2 gap-6 max-w-xl w-full">
76
- <div
77
- class="biome-selector bg-gray-700 p-6 rounded-lg cursor-pointer hover:bg-yellow-800 flex flex-col items-center"
78
- data-biome="desert"
79
- >
80
  <i data-feather="sun" class="text-yellow-300 mb-2"></i>
81
  <h3 class="font-bold text-lg">Golden Dunes</h3>
82
  <p class="text-xs">High loot, high risk</p>
83
  </div>
84
- <div
85
- class="biome-selector bg-gray-700 p-6 rounded-lg cursor-pointer hover:bg-green-800 flex flex-col items-center"
86
- data-biome="forest"
87
- >
88
  <i data-feather="tree" class="text-green-300 mb-2"></i>
89
  <h3 class="font-bold text-lg">Shadow Forest</h3>
90
  <p class="text-xs">Good cover, medium loot</p>
91
  </div>
92
- <div
93
- class="biome-selector bg-gray-700 p-6 rounded-lg cursor-pointer hover:bg-blue-800 flex flex-col items-center"
94
- data-biome="oasis"
95
- >
96
  <i data-feather="droplet" class="text-blue-300 mb-2"></i>
97
  <h3 class="font-bold text-lg">Crystal Oasis</h3>
98
  <p class="text-xs">Medium loot, low risk</p>
99
  </div>
100
- <div
101
- class="biome-selector bg-gray-700 p-6 rounded-lg cursor-pointer hover:bg-purple-800 flex flex-col items-center"
102
- data-biome="ruins"
103
- >
104
  <i data-feather="layers" class="text-purple-300 mb-2"></i>
105
  <h3 class="font-bold text-lg">Ancient Ruins</h3>
106
  <p class="text-xs">Legendary loot, dangerous</p>
107
  </div>
108
  </div>
 
 
 
 
 
 
 
 
 
 
 
109
  </div>
110
 
 
111
  <div id="gameScreen" class="hidden h-screen flex flex-col">
112
- <!-- Game Header -->
113
- <header
114
- class="flex justify-between items-center border-b border-yellow-500 px-6 py-4"
115
- style="flex-shrink: 0;"
116
- >
117
- <h1 class="text-4xl font-bold text-yellow-400 flex items-center">
118
- <i data-feather="target" class="mr-2"></i> BattleZone Royale
119
- </h1>
120
- <div class="flex space-x-4 text-white">
121
- <div class="bg-gray-800 px-4 py-2 rounded-lg flex items-center">
122
- <i data-feather="users" class="mr-2"></i>
123
- <span id="playerCount">1/100</span>
124
- </div>
125
- <div class="bg-gray-800 px-4 py-2 rounded-lg flex items-center">
126
- <i data-feather="clock" class="mr-2"></i>
127
- <span id="gameTimer">5:00</span>
128
- </div>
129
  </div>
130
  </header>
131
 
132
  <div class="flex flex-1 overflow-hidden">
133
- <!-- Main Game Canvas -->
134
- <div
135
- id="canvasContainer"
136
- class="relative bg-gray-900 border-4 border-gray-700 rounded-xl max-w-full max-h-full"
137
- >
138
  <canvas id="gameCanvas"></canvas>
139
 
140
- <!-- Storm Warning -->
141
- <div
142
- id="stormWarning"
143
- class="hidden absolute top-6 left-1/2 transform -translate-x-1/2 bg-red-900 bg-opacity-80 text-white px-6 py-3 rounded-lg flex items-center storm-animation"
144
- >
145
  <i data-feather="alert-circle" class="mr-2"></i>
146
  <span>STORM APPROACHING! MOVE TO SAFE ZONE!</span>
147
  </div>
148
 
149
- <!-- Death Screen -->
150
- <div
151
- id="deathScreen"
152
- class="hidden absolute inset-0 bg-black bg-opacity-80 flex flex-col items-center justify-center rounded-xl"
153
- >
154
  <i data-feather="skull" class="w-16 h-16 text-red-500 mb-4"></i>
155
  <h2 class="text-4xl font-bold text-red-500 mb-4">YOU DIED!</h2>
156
  <p class="text-xl mb-6">Better luck next time, soldier!</p>
157
- <button
158
- id="respawnBtn"
159
- class="bg-yellow-500 hover:bg-yellow-600 text-black font-bold py-3 px-6 rounded-lg flex items-center"
160
- >
161
  <i data-feather="refresh-cw" class="mr-2"></i> Respawn
162
  </button>
163
  </div>
164
  </div>
165
 
166
- <!-- Game Controls and Info -->
167
- <aside
168
- class="w-80 bg-gray-800 rounded-xl p-6 overflow-y-auto flex-shrink-0 flex flex-col gap-8"
169
- style="max-height: 100vh;"
170
- >
171
- <!-- Player Stats -->
172
  <section>
173
- <h2
174
- class="text-2xl font-bold text-yellow-400 mb-4 flex items-center"
175
- >
176
- <i data-feather="activity" class="mr-2"></i> Player Stats
177
- </h2>
178
  <div class="space-y-3">
179
- <div class="flex justify-between">
180
- <span>Health:</span>
181
- <span id="playerHealth" class="font-bold">100%</span>
182
- </div>
183
- <div class="flex justify-between">
184
- <span>Armor:</span>
185
- <span id="playerArmor" class="font-bold">0%</span>
186
- </div>
187
- <div class="flex justify-between">
188
- <span>Kills:</span>
189
- <span id="playerKills" class="font-bold text-red-400">0</span>
190
- </div>
191
  </div>
192
  </section>
193
 
194
- <!-- Inventory -->
195
  <section>
196
- <h2
197
- class="text-2xl font-bold text-yellow-400 mb-4 flex items-center"
198
- >
199
- <i data-feather="briefcase" class="mr-2"></i> Inventory
200
- </h2>
201
- <div
202
- id="inventorySlots"
203
- class="grid grid-cols-3 gap-2"
204
- aria-label="Inventory slots"
205
- ></div>
206
- </section>
207
-
208
- <!-- Controls Info -->
209
- <section>
210
- <h2
211
- class="text-2xl font-bold text-yellow-400 mb-4 flex items-center"
212
- >
213
- <i data-feather="command" class="mr-2"></i> Controls
214
- </h2>
215
- <div class="space-y-2 text-sm">
216
- <div class="flex items-center">
217
- <span
218
- class="bg-gray-700 px-2 py-1 rounded mr-2"
219
- >WASD</span
220
- >
221
- <span>Move character</span>
222
- </div>
223
- <div class="flex items-center">
224
- <span
225
- class="bg-gray-700 px-2 py-1 rounded mr-2"
226
- >Mouse</span
227
- >
228
- <span>Aim and shoot</span>
229
- </div>
230
- <div class="flex items-center">
231
- <span
232
- class="bg-gray-700 px-2 py-1 rounded mr-2"
233
- >E</span
234
- >
235
- <span>Loot chests/open doors</span>
236
- </div>
237
- <div class="flex items-center">
238
- <span
239
- class="bg-gray-700 px-2 py-1 rounded mr-2"
240
- >1-5</span
241
- >
242
- <span>Switch weapons</span>
243
- </div>
244
- </div>
245
  </section>
246
  </aside>
247
  </div>
248
  </div>
249
 
250
  <script>
251
- // Game variables
252
  let gameActive = false;
253
- let playerHealth = 100;
254
- let playerArmor = 0;
255
- let playerKills = 0;
256
- let gameTime = 300; // 5 minutes in seconds
257
- let stormActive = false;
258
- let selectedBiome = null;
259
-
260
- // Canvas and context
261
- const canvas = document.getElementById('gameCanvas');
262
- const ctx = canvas.getContext('2d');
263
- const stormWarningEl = document.getElementById('stormWarning');
264
- const deathScreenEl = document.getElementById('deathScreen');
265
  const landingScreenEl = document.getElementById('landingScreen');
266
  const gameScreenEl = document.getElementById('gameScreen');
 
 
267
 
268
- // Player and storm state
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
269
  const player = {
270
- x: 0,
271
- y: 0,
272
- radius: 15,
273
- angle: 0,
274
- speed: 3,
275
- health: 100,
276
- armor: 0,
277
- kills: 0,
278
  };
279
 
280
- const storm = {
281
- maxRadius: 600,
282
- radius: 600,
283
- centerX: 0,
284
- centerY: 0,
285
- damagePerSecond: 1,
286
- closingSpeed: 0.5, // radius shrink per second
287
- };
288
 
289
- // Movement input
290
- const keys = {
291
- w: false,
292
- a: false,
293
- s: false,
294
- d: false,
295
- };
 
 
 
 
 
 
 
 
296
 
297
- // Mouse
298
- const mouse = {
299
- x: 0,
300
- y: 0,
301
- canvasX: 0,
302
- canvasY: 0,
303
- };
 
 
 
304
 
305
- // Resize canvas to fill container
306
- function resizeCanvas() {
307
- const container = document.getElementById('canvasContainer');
308
- canvas.width = container.clientWidth;
309
- canvas.height = container.clientHeight;
310
- // Reset storm center to canvas center on resize
311
- storm.centerX = canvas.width / 2;
312
- storm.centerY = canvas.height / 2;
 
 
 
 
 
 
 
 
 
 
 
 
313
  }
314
- window.addEventListener('resize', resizeCanvas);
315
 
316
- // Initialize player position center of canvas on start
317
- function initPlayerPosition() {
318
- player.x = canvas.width / 2;
319
- player.y = canvas.height / 2;
 
 
 
 
 
 
 
320
  }
321
 
322
- // Update player stats UI
323
- function updatePlayerStats() {
324
- document.getElementById('playerHealth').textContent = `${playerHealth}%`;
325
- document.getElementById('playerArmor').textContent = `${playerArmor}%`;
326
- document.getElementById('playerKills').textContent = playerKills;
 
 
 
 
 
 
 
 
 
 
 
 
 
327
  }
328
 
329
- // Initialize inventory slots
330
  function initInventory() {
331
  const inventorySlots = document.getElementById('inventorySlots');
332
  inventorySlots.innerHTML = '';
333
-
334
- for (let i = 0; i < 6; i++) {
335
  const slot = document.createElement('div');
336
- slot.className =
337
- 'bg-gray-700 h-16 rounded-lg flex flex-col items-center justify-center text-xs';
338
- slot.innerHTML = '<i data-feather="box" class="text-gray-400"></i><span>Empty</span>';
339
  inventorySlots.appendChild(slot);
340
  }
 
 
 
 
 
 
 
 
341
  }
342
 
343
- // Handle keyboard input for movement
344
- window.addEventListener('keydown', (e) => {
345
- if (!gameActive) return;
346
- const key = e.key.toLowerCase();
347
- if (keys.hasOwnProperty(key)) {
348
- keys[key] = true;
 
 
 
 
 
 
 
 
 
 
 
 
 
349
  }
350
- });
351
- window.addEventListener('keyup', (e) => {
352
- if (!gameActive) return;
353
- const key = e.key.toLowerCase();
354
- if (keys.hasOwnProperty(key)) {
355
- keys[key] = false;
356
  }
357
- });
358
 
359
- // Handle mouse movement for aiming
360
- canvas.addEventListener('mousemove', (e) => {
361
- const rect = canvas.getBoundingClientRect();
362
- mouse.canvasX = e.clientX - rect.left;
363
- mouse.canvasY = e.clientY - rect.top;
 
 
 
 
 
 
 
 
 
364
 
365
- // Calculate angle from player to mouse position
366
- const dx = mouse.canvasX - player.x;
367
- const dy = mouse.canvasY - player.y;
368
- player.angle = Math.atan2(dy, dx);
369
- });
 
 
 
370
 
371
- // Move player based on keys pressed, constrained within canvas
372
- function movePlayer() {
373
- if (!gameActive) return;
374
- let dx = 0,
375
- dy = 0;
376
- if (keys.w) dy -= player.speed;
377
- if (keys.s) dy += player.speed;
378
- if (keys.a) dx -= player.speed;
379
- if (keys.d) dx += player.speed;
380
-
381
- // Normalize diagonal movement
382
- if (dx !== 0 && dy !== 0) {
383
- dx *= Math.SQRT1_2;
384
- dy *= Math.SQRT1_2;
 
 
 
 
 
 
 
 
 
385
  }
 
 
 
 
 
 
 
 
 
 
 
 
 
386
 
387
- player.x += dx;
388
- player.y += dy;
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
389
 
390
- // Confine player inside canvas
391
- player.x = Math.min(Math.max(player.radius, player.x), canvas.width - player.radius);
392
- player.y = Math.min(Math.max(player.radius, player.y), canvas.height - player.radius);
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
393
  }
394
 
395
- // Draw player as a triangle pointing to aim direction
396
  function drawPlayer() {
 
397
  ctx.save();
398
- ctx.translate(player.x, player.y);
399
  ctx.rotate(player.angle);
400
-
401
- // Player body (triangle)
 
 
402
  ctx.fillStyle = 'yellow';
403
- ctx.beginPath();
404
- ctx.moveTo(20, 0);
405
- ctx.lineTo(-15, -12);
406
- ctx.lineTo(-15, 12);
407
- ctx.closePath();
408
- ctx.fill();
409
-
410
  ctx.restore();
411
  }
412
 
413
- // Draw the shrinking storm circle with increasing damage indication
414
- function drawStorm() {
415
  ctx.save();
416
-
417
- // Storm fill with transparent blue
418
- const gradient = ctx.createRadialGradient(
419
- storm.centerX,
420
- storm.centerY,
421
- storm.radius * 0.6,
422
- storm.centerX,
423
- storm.centerY,
424
- storm.radius
425
- );
426
- gradient.addColorStop(0, 'rgba(100, 149, 237, 0.1)');
427
- gradient.addColorStop(1, 'rgba(100, 149, 237, 0.6)');
428
- ctx.fillStyle = gradient;
429
- ctx.beginPath();
430
- ctx.arc(storm.centerX, storm.centerY, storm.radius, 0, Math.PI * 2);
431
- ctx.fill();
432
-
433
- // Storm outline thickening and color shifting as radius shrinks
434
- const stormProgress = 1 - storm.radius / storm.maxRadius; // 0 to 1
435
- const outlineWidth = 10 + stormProgress * 20;
436
- const redIntensity = Math.min(255, Math.floor(100 + stormProgress * 155));
437
- ctx.strokeStyle = `rgba(${redIntensity}, 50, 150, 0.8)`;
438
- ctx.lineWidth = outlineWidth;
439
  ctx.beginPath();
440
- ctx.arc(storm.centerX, storm.centerY, storm.radius, 0, Math.PI * 2);
 
441
  ctx.stroke();
442
-
443
  ctx.restore();
444
  }
445
 
446
- // Check if player is outside storm radius
 
447
  function playerInStorm() {
448
- const dx = player.x - storm.centerX;
449
- const dy = player.y - storm.centerY;
450
- const dist = Math.sqrt(dx * dx + dy * dy);
451
- return dist > storm.radius;
452
  }
453
-
454
- // Storm damage application
455
  let stormDamageAccumulator = 0;
456
-
457
- function applyStormDamage(deltaTime) {
458
- if (!stormActive) return;
459
-
460
  if (playerInStorm()) {
461
- // Damage increases as storm closes
462
- const stormProgress = 1 - storm.radius / storm.maxRadius; // 0 to 1
463
- const damageRate = storm.damagePerSecond * (1 + stormProgress * 4); // up to 5x damage
464
- stormDamageAccumulator += damageRate * deltaTime;
465
  while (stormDamageAccumulator >= 1) {
466
  stormDamageAccumulator -= 1;
467
- applyDamage(1);
 
468
  }
469
  } else {
470
  stormDamageAccumulator = 0;
471
  }
472
  }
473
 
474
- // Apply damage to player health considering armor
475
- function applyDamage(amount) {
476
- if (playerArmor > 0) {
477
- const armorAbsorb = Math.min(playerArmor, amount * 0.7); // Armor absorbs 70% of damage
478
- playerArmor -= armorAbsorb;
479
- amount -= armorAbsorb;
 
 
 
 
 
 
 
 
 
 
 
 
480
  }
481
- playerHealth -= amount;
482
- if (playerHealth <= 0) {
483
- playerHealth = 0;
484
- playerDeath();
 
 
 
 
 
 
 
 
 
 
 
 
485
  }
486
- updatePlayerStats();
487
- }
488
 
489
- // Storm radius shrinking over time
490
- function updateStorm(deltaTime) {
491
- if (!stormActive) return;
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
492
 
493
- storm.radius -= storm.closingSpeed * deltaTime;
494
- if (storm.radius < 50) storm.radius = 50;
495
- }
496
 
497
- // Draw aim crosshair at mouse position
498
- function drawCrosshair() {
499
- ctx.save();
500
- ctx.strokeStyle = 'yellow';
501
- ctx.lineWidth = 2;
502
- ctx.beginPath();
503
- const size = 10;
504
- ctx.moveTo(mouse.canvasX - size, mouse.canvasY);
505
- ctx.lineTo(mouse.canvasX + size, mouse.canvasY);
506
- ctx.moveTo(mouse.canvasX, mouse.canvasY - size);
507
- ctx.lineTo(mouse.canvasX, mouse.canvasY + size);
508
- ctx.stroke();
509
- ctx.restore();
 
 
 
 
 
 
 
 
510
  }
511
 
512
- // Game timer and storm activation
513
- let timerInterval;
 
 
514
  function startGame() {
515
  gameActive = true;
516
- selectedBiome && console.log('Selected biome:', selectedBiome);
517
  landingScreenEl.classList.add('hidden');
518
  gameScreenEl.classList.remove('hidden');
519
- stormWarningEl.classList.add('hidden');
520
 
521
- // Reset stats
522
- playerHealth = 100;
523
- playerArmor = 0;
524
- playerKills = 0;
525
- updatePlayerStats();
 
 
 
 
 
 
 
526
  initInventory();
527
- initPlayerPosition();
528
- storm.radius = storm.maxRadius;
529
- stormActive = false;
530
- stormWarningEl.classList.add('hidden');
531
 
532
- gameTime = 300; // 5 minutes
 
 
 
533
  document.getElementById('gameTimer').textContent = '5:00';
534
-
535
  timerInterval && clearInterval(timerInterval);
536
  timerInterval = setInterval(() => {
537
- if (!gameActive) {
538
- clearInterval(timerInterval);
539
- return;
540
- }
541
  gameTime--;
542
- const minutes = Math.floor(gameTime / 60);
543
- const seconds = gameTime % 60;
544
- document.getElementById('gameTimer').textContent = `${minutes}:${seconds < 10 ? '0' + seconds : seconds}`;
545
-
546
- // Start storm after 60 seconds (4 minutes left)
547
- if (gameTime === 240 && !stormActive) {
548
- stormActive = true;
549
- stormWarningEl.classList.remove('hidden');
550
- setTimeout(() => stormWarningEl.classList.add('hidden'), 5000);
551
- }
552
-
553
- // End game when timer hits 0
554
- if (gameTime <= 0) {
555
- clearInterval(timerInterval);
556
- endGame();
557
- }
558
  }, 1000);
559
 
560
- lastFrameTime = performance.now();
561
  requestAnimationFrame(gameLoop);
562
  }
563
 
564
- // End game
565
  function endGame() {
566
  gameActive = false;
567
- alert('Congratulations! You survived the BattleZone!');
568
  }
569
 
570
- // Player death
571
  function playerDeath() {
572
  gameActive = false;
573
  deathScreenEl.classList.remove('hidden');
574
  }
575
 
576
- // Respawn button handler
577
  document.getElementById('respawnBtn').addEventListener('click', () => {
578
  deathScreenEl.classList.add('hidden');
579
  landingScreenEl.classList.remove('hidden');
580
  });
581
 
582
- // Biome selection handler
583
- document.querySelectorAll('.biome-selector').forEach((selector) => {
584
- selector.addEventListener('click', () => {
585
- selectedBiome = selector.getAttribute('data-biome');
586
  startGame();
587
  });
588
  });
589
 
590
- // Game loop with delta time for smooth movement and storm update
591
- let lastFrameTime = 0;
592
- function gameLoop(timestamp) {
593
- if (!gameActive) return;
594
- if (!lastFrameTime) lastFrameTime = timestamp;
595
- const deltaTime = (timestamp - lastFrameTime) / 1000; // seconds
596
- lastFrameTime = timestamp;
597
-
598
- // Update
599
- movePlayer();
600
- updateStorm(deltaTime);
601
- applyStormDamage(deltaTime);
602
-
603
- // Clear
604
- ctx.clearRect(0, 0, canvas.width, canvas.height);
605
-
606
- // Draw
607
- drawStorm();
608
- drawPlayer();
609
- drawCrosshair();
610
-
611
- // Show storm warning if player is outside storm radius while storm active
612
- if (stormActive && playerInStorm()) {
613
- stormWarningEl.classList.remove('hidden');
614
- } else {
615
- stormWarningEl.classList.add('hidden');
616
- }
617
-
618
- requestAnimationFrame(gameLoop);
619
- }
620
 
621
- // Initialize feather icons and setup
622
- feather.replace();
623
  resizeCanvas();
 
624
  initInventory();
625
  updatePlayerStats();
 
626
  </script>
627
  </body>
628
  </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" />
6
+ <title>BattleZone Royale - Fixed</title>
7
  <script src="https://cdn.tailwindcss.com"></script>
8
  <script src="https://unpkg.com/feather-icons"></script>
9
  <style>
10
+ html, body { height:100%; margin:0; background:#0b1220; color:#fff; font-family:monospace; }
11
+ #canvasContainer { position:relative; flex:1; display:flex; justify-content:center; align-items:center; height:100vh; overflow:hidden; }
12
+ #gameCanvas { display:block; user-select:none; cursor:crosshair; box-shadow:0 0 20px rgba(0,0,0,.5); width:100%; height:100%; }
13
+ .biome-selector { transition:all .25s ease; }
14
+ .biome-selector:hover { transform:scale(1.03); box-shadow:0 0 10px rgba(255,215,0,.15); }
15
+ .hud-pill { background:#111827; padding:.5rem .75rem; border-radius:.5rem; }
16
+ #stormWarning { z-index:10; }
17
+ #deathScreen { z-index:20; }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
18
  </style>
19
  </head>
20
  <body>
21
+ <!-- Landing / Instructions screen -->
22
+ <div id="landingScreen" class="flex flex-col items-center justify-center h-screen p-6">
23
+ <h1 class="text-5xl font-bold text-yellow-400 mb-6 flex items-center">
24
  <i data-feather="target" class="mr-2"></i> BattleZone Royale
25
  </h1>
26
+
27
+ <h2 class="text-2xl text-yellow-300 mb-4">Choose Landing Zone</h2>
28
+ <div class="grid grid-cols-2 gap-6 max-w-xl w-full mb-6">
29
+ <div class="biome-selector bg-gray-700 p-6 rounded-lg cursor-pointer hover:bg-yellow-800 flex flex-col items-center" data-biome="desert">
 
 
 
 
30
  <i data-feather="sun" class="text-yellow-300 mb-2"></i>
31
  <h3 class="font-bold text-lg">Golden Dunes</h3>
32
  <p class="text-xs">High loot, high risk</p>
33
  </div>
34
+ <div class="biome-selector bg-gray-700 p-6 rounded-lg cursor-pointer hover:bg-green-800 flex flex-col items-center" data-biome="forest">
 
 
 
35
  <i data-feather="tree" class="text-green-300 mb-2"></i>
36
  <h3 class="font-bold text-lg">Shadow Forest</h3>
37
  <p class="text-xs">Good cover, medium loot</p>
38
  </div>
39
+ <div class="biome-selector bg-gray-700 p-6 rounded-lg cursor-pointer hover:bg-blue-800 flex flex-col items-center" data-biome="oasis">
 
 
 
40
  <i data-feather="droplet" class="text-blue-300 mb-2"></i>
41
  <h3 class="font-bold text-lg">Crystal Oasis</h3>
42
  <p class="text-xs">Medium loot, low risk</p>
43
  </div>
44
+ <div class="biome-selector bg-gray-700 p-6 rounded-lg cursor-pointer hover:bg-purple-800 flex flex-col items-center" data-biome="ruins">
 
 
 
45
  <i data-feather="layers" class="text-purple-300 mb-2"></i>
46
  <h3 class="font-bold text-lg">Ancient Ruins</h3>
47
  <p class="text-xs">Legendary loot, dangerous</p>
48
  </div>
49
  </div>
50
+
51
+ <!-- Instructions moved here -->
52
+ <div class="bg-gray-800 p-4 rounded-lg max-w-xl w-full text-sm">
53
+ <h3 class="font-bold text-yellow-400 mb-2">Controls</h3>
54
+ <ul class="list-disc pl-5 space-y-1">
55
+ <li><strong>WASD</strong> — Move</li>
56
+ <li><strong>Mouse</strong> — Aim & Shoot</li>
57
+ <li><strong>E</strong> — Interact / Loot / Harvest</li>
58
+ <li><strong>1-5</strong> — Switch weapons</li>
59
+ </ul>
60
+ </div>
61
  </div>
62
 
63
+ <!-- Game screen -->
64
  <div id="gameScreen" class="hidden h-screen flex flex-col">
65
+ <header class="flex justify-between items-center border-b border-yellow-500 px-6 py-4" style="flex-shrink:0;">
66
+ <h1 class="text-3xl font-bold text-yellow-400 flex items-center"><i data-feather="target" class="mr-2"></i>BattleZone Royale</h1>
67
+ <div class="flex space-x-4 items-center">
68
+ <div class="hud-pill flex items-center space-x-2"><i data-feather="map-pin"></i><span id="currentBiome">-</span></div>
69
+ <div class="hud-pill flex items-center space-x-2"><i data-feather="clock"></i><span id="gameTimer">5:00</span></div>
70
+ <div class="hud-pill flex items-center space-x-2"><i data-feather="users"></i><span id="playerCount">1/100</span></div>
 
 
 
 
 
 
 
 
 
 
 
71
  </div>
72
  </header>
73
 
74
  <div class="flex flex-1 overflow-hidden">
75
+ <div id="canvasContainer" class="relative bg-gray-900 border-4 border-gray-700 rounded-xl max-w-full max-h-full">
 
 
 
 
76
  <canvas id="gameCanvas"></canvas>
77
 
78
+ <div id="stormWarning" class="hidden absolute top-6 left-1/2 transform -translate-x-1/2 bg-red-900 bg-opacity-80 text-white px-6 py-3 rounded-lg flex items-center">
 
 
 
 
79
  <i data-feather="alert-circle" class="mr-2"></i>
80
  <span>STORM APPROACHING! MOVE TO SAFE ZONE!</span>
81
  </div>
82
 
83
+ <div id="deathScreen" class="hidden absolute inset-0 bg-black bg-opacity-80 flex flex-col items-center justify-center rounded-xl">
 
 
 
 
84
  <i data-feather="skull" class="w-16 h-16 text-red-500 mb-4"></i>
85
  <h2 class="text-4xl font-bold text-red-500 mb-4">YOU DIED!</h2>
86
  <p class="text-xl mb-6">Better luck next time, soldier!</p>
87
+ <button id="respawnBtn" class="bg-yellow-500 hover:bg-yellow-600 text-black font-bold py-3 px-6 rounded-lg flex items-center">
 
 
 
88
  <i data-feather="refresh-cw" class="mr-2"></i> Respawn
89
  </button>
90
  </div>
91
  </div>
92
 
93
+ <!-- Sidebar: stats & inventory only (no instructions) -->
94
+ <aside class="w-80 bg-gray-800 rounded-xl p-6 overflow-y-auto flex-shrink-0 flex flex-col gap-6" style="max-height:100vh;">
 
 
 
 
95
  <section>
96
+ <h2 class="text-2xl font-bold text-yellow-400 mb-4 flex items-center"><i data-feather="activity" class="mr-2"></i>Player Stats</h2>
 
 
 
 
97
  <div class="space-y-3">
98
+ <div class="flex justify-between"><span>Health:</span><span id="playerHealth" class="font-bold">100%</span></div>
99
+ <div class="flex justify-between"><span>Armor:</span><span id="playerArmor" class="font-bold">0%</span></div>
100
+ <div class="flex justify-between"><span>Materials:</span><span id="playerMaterials" class="font-bold">0</span></div>
101
+ <div class="flex justify-between"><span>Kills:</span><span id="playerKills" class="font-bold text-red-400">0</span></div>
 
 
 
 
 
 
 
 
102
  </div>
103
  </section>
104
 
 
105
  <section>
106
+ <h2 class="text-2xl font-bold text-yellow-400 mb-4 flex items-center"><i data-feather="briefcase" class="mr-2"></i>Inventory</h2>
107
+ <div id="inventorySlots" class="grid grid-cols-3 gap-2" aria-label="Inventory slots"></div>
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
108
  </section>
109
  </aside>
110
  </div>
111
  </div>
112
 
113
  <script>
114
+ // --- Basic globals ---
115
  let gameActive = false;
116
+ let selectedBiome = 'forest';
117
+
118
+ // DOM elements
 
 
 
 
 
 
 
 
 
119
  const landingScreenEl = document.getElementById('landingScreen');
120
  const gameScreenEl = document.getElementById('gameScreen');
121
+ const stormWarningEl = document.getElementById('stormWarning');
122
+ const deathScreenEl = document.getElementById('deathScreen');
123
 
124
+ const canvas = document.getElementById('gameCanvas');
125
+ const ctx = canvas.getContext('2d');
126
+
127
+ // --- World & camera ---
128
+ const WORLD = { width: 3000, height: 2000 };
129
+ let camera = { x: 0, y: 0 };
130
+
131
+ function resizeCanvas() {
132
+ const container = document.getElementById('canvasContainer');
133
+ // set logical canvas size to container size to keep consistent drawing coordinates
134
+ canvas.width = Math.max(300, Math.floor(container.clientWidth));
135
+ canvas.height = Math.max(200, Math.floor(container.clientHeight));
136
+ // adjust camera after resize
137
+ cameraUpdate();
138
+ }
139
+ window.addEventListener('resize', resizeCanvas);
140
+
141
+ // --- Player ---
142
  const player = {
143
+ x: WORLD.width/2, y: WORLD.height/2, radius: 16, angle: 0,
144
+ speed: 220, health: 100, armor: 0, kills: 0, materials: 0,
145
+ weapons: [], currentWeaponIndex:0, lastShot:0
 
 
 
 
 
146
  };
147
 
148
+ // --- Input ---
149
+ const keys = { w:false, a:false, s:false, d:false, e:false };
150
+ const mouse = { canvasX:0, canvasY:0, worldX:0, worldY:0, down:false };
 
 
 
 
 
151
 
152
+ window.addEventListener('keydown', (e) => {
153
+ if (!gameActive) return;
154
+ const k = e.key.toLowerCase();
155
+ if (k in keys) keys[k] = true;
156
+ if (['1','2','3','4','5'].includes(k)) {
157
+ const idx = parseInt(k)-1;
158
+ if (player.weapons[idx]) player.currentWeaponIndex = idx;
159
+ }
160
+ });
161
+ window.addEventListener('keyup', (e) => {
162
+ if (!gameActive) return;
163
+ const k = e.key.toLowerCase();
164
+ if (k in keys) keys[k] = false;
165
+ if (k === 'e') { keys.e = true; } // mark for interact, will be cleared after handling
166
+ });
167
 
168
+ canvas.addEventListener('mousemove', (e) => {
169
+ const rect = canvas.getBoundingClientRect();
170
+ mouse.canvasX = e.clientX - rect.left;
171
+ mouse.canvasY = e.clientY - rect.top;
172
+ mouse.worldX = mouse.canvasX + camera.x;
173
+ mouse.worldY = mouse.canvasY + camera.y;
174
+ const dx = mouse.worldX - player.x;
175
+ const dy = mouse.worldY - player.y;
176
+ player.angle = Math.atan2(dy, dx);
177
+ });
178
 
179
+ canvas.addEventListener('mousedown', () => { if (gameActive) mouse.down = true; });
180
+ window.addEventListener('mouseup', () => { mouse.down = false; });
181
+
182
+ // --- World entities ---
183
+ const bullets = [];
184
+ const chests = [];
185
+ const objects = []; // breakables
186
+
187
+ function randInt(min,max){ return Math.floor(Math.random()*(max-min))+min; }
188
+ function rand(min,max){ return Math.random()*(max-min)+min; }
189
+
190
+ function biomeAt(x,y) {
191
+ const bx = Math.floor(x / 400);
192
+ const by = Math.floor(y / 400);
193
+ const seed = (bx*73856093) ^ (by*19349663);
194
+ const r = Math.abs(Math.sin(seed)) % 1;
195
+ if (r < 0.25) return 'desert';
196
+ if (r < 0.55) return 'forest';
197
+ if (r < 0.75) return 'oasis';
198
+ return 'ruins';
199
  }
 
200
 
201
+ function generateLootForBiome(b) {
202
+ const roll = Math.random();
203
+ if (roll < 0.4) return { type:'medkit', heal:25 };
204
+ if (roll < 0.75) return { type:'materials', amount: randInt(5,20) };
205
+ const weapons = [
206
+ { name:'Pistol', dmg:12, rate:320, color:'#ffd86b' },
207
+ { name:'SMG', dmg:6, rate:120, color:'#8ef0ff' },
208
+ { name:'Shotgun', dmg:22, rate:800, color:'#ff9fb8' },
209
+ { name:'Rifle', dmg:18, rate:400, color:'#c7ff9a' }
210
+ ];
211
+ return { type:'weapon', weapon: weapons[randInt(0,weapons.length)] };
212
  }
213
 
214
+ function populateWorld() {
215
+ chests.length = 0;
216
+ objects.length = 0;
217
+ for (let i=0;i<150;i++){
218
+ const x = rand(100, WORLD.width-100);
219
+ const y = rand(100, WORLD.height-100);
220
+ chests.push({ x,y, opened:false, openProgress:0, loot: generateLootForBiome(biomeAt(x,y)) });
221
+ }
222
+ for (let i=0;i<400;i++){
223
+ const typeRand = Math.random();
224
+ let type='wood';
225
+ if (typeRand>0.75) type='stone';
226
+ if (typeRand>0.95) type='wall';
227
+ const x = rand(50, WORLD.width-50);
228
+ const y = rand(50, WORLD.height-50);
229
+ const hp = type==='wood'? 40 : (type==='stone'? 80:120);
230
+ objects.push({ x,y, type, hp, maxHp:hp, dead:false });
231
+ }
232
  }
233
 
234
+ // --- Inventory UI ---
235
  function initInventory() {
236
  const inventorySlots = document.getElementById('inventorySlots');
237
  inventorySlots.innerHTML = '';
238
+ for (let i=0;i<6;i++){
 
239
  const slot = document.createElement('div');
240
+ slot.className = 'bg-gray-700 h-16 rounded-lg flex flex-col items-center justify-center text-xs';
241
+ slot.dataset.index = i;
242
+ updateSlot(slot, player.weapons[i] || null);
243
  inventorySlots.appendChild(slot);
244
  }
245
+ updatePlayerStats();
246
+ }
247
+ function updateSlot(elem, weapon) {
248
+ if (weapon) {
249
+ elem.innerHTML = `<div style="font-size:11px">${weapon.name}</div><div style="color:${weapon.color}">●</div>`;
250
+ } else {
251
+ elem.innerHTML = '<i data-feather="box" class="text-gray-400"></i><span>Empty</span>';
252
+ }
253
  }
254
 
255
+ function updatePlayerStats() {
256
+ document.getElementById('playerHealth').textContent = `${Math.max(0,Math.floor(player.health))}%`;
257
+ document.getElementById('playerArmor').textContent = `${Math.floor(player.armor)}%`;
258
+ document.getElementById('playerKills').textContent = player.kills;
259
+ document.getElementById('playerMaterials').textContent = player.materials;
260
+ document.getElementById('currentBiome').textContent = biomeAt(player.x, player.y);
261
+ const slots = document.querySelectorAll('#inventorySlots > div');
262
+ slots.forEach(s => updateSlot(s, player.weapons[parseInt(s.dataset.index)] || null));
263
+ feather.replace();
264
+ }
265
+
266
+ // --- Camera logic (centers on player; handles world smaller than canvas) ---
267
+ function cameraUpdate() {
268
+ if (!canvas.width || !canvas.height) return;
269
+ if (WORLD.width <= canvas.width) {
270
+ camera.x = (WORLD.width - canvas.width) / 2;
271
+ } else {
272
+ camera.x = player.x - canvas.width / 2;
273
+ camera.x = Math.max(0, Math.min(camera.x, WORLD.width - canvas.width));
274
  }
275
+ if (WORLD.height <= canvas.height) {
276
+ camera.y = (WORLD.height - canvas.height) / 2;
277
+ } else {
278
+ camera.y = player.y - canvas.height / 2;
279
+ camera.y = Math.max(0, Math.min(camera.y, WORLD.height - canvas.height));
 
280
  }
281
+ }
282
 
283
+ function worldToScreen(wx, wy) { return { x: Math.round(wx - camera.x), y: Math.round(wy - camera.y) }; }
284
+
285
+ // --- Interaction / combat ---
286
+ function shootBullet(originX, originY, targetX, targetY, weapon) {
287
+ const speed = 1000;
288
+ const angle = Math.atan2(targetY-originY, targetX-originX);
289
+ bullets.push({
290
+ x: originX, y: originY,
291
+ vx: Math.cos(angle)*speed, vy: Math.sin(angle)*speed,
292
+ dmg: weapon ? weapon.dmg : 8,
293
+ color: weapon ? weapon.color : '#ffffff',
294
+ life: 1.5, traveled:0, tracer:false
295
+ });
296
+ }
297
 
298
+ function applyDamageToObject(obj, dmg) {
299
+ obj.hp -= dmg;
300
+ if (obj.hp <= 0 && !obj.dead) {
301
+ obj.dead = true;
302
+ const gained = obj.type==='wood'? randInt(3,10) : randInt(8,20);
303
+ player.materials += gained;
304
+ }
305
+ }
306
 
307
+ function interactNearby() {
308
+ const range = 48;
309
+ // chests
310
+ for (const chest of chests) {
311
+ if (chest.opened) continue;
312
+ const d = Math.hypot(chest.x - player.x, chest.y - player.y);
313
+ if (d < range) {
314
+ chest.opened = true;
315
+ const loot = chest.loot;
316
+ if (loot.type === 'medkit') player.health = Math.min(100, player.health + loot.heal);
317
+ else if (loot.type === 'materials') player.materials += loot.amount;
318
+ else if (loot.type === 'weapon') {
319
+ let placed=false;
320
+ for (let i=0;i<6;i++){ if (!player.weapons[i]) { player.weapons[i] = loot.weapon; player.currentWeaponIndex = i; placed=true; break; } }
321
+ if (!placed) player.weapons[player.currentWeaponIndex] = loot.weapon;
322
+ }
323
+ // small pop effect
324
+ for (let i=0;i<6;i++) {
325
+ bullets.push({ x:chest.x, y:chest.y, vx:(Math.random()-0.5)*200, vy:(Math.random()-0.5)*200, dmg:0, color:'#ffd86b', life:0.6, tracer:true });
326
+ }
327
+ updatePlayerStats();
328
+ return;
329
+ }
330
  }
331
+ // breakable objects (harvest)
332
+ for (const obj of objects) {
333
+ if (obj.dead) continue;
334
+ const d = Math.hypot(obj.x - player.x, obj.y - player.y);
335
+ if (d < range) {
336
+ const gain = obj.type==='wood'? 2 : 5;
337
+ player.materials += gain;
338
+ obj.dead = true;
339
+ updatePlayerStats();
340
+ return;
341
+ }
342
+ }
343
+ }
344
 
345
+ // --- Drawing ---
346
+ function drawWorld() {
347
+ // tiles
348
+ const TILE = 400;
349
+ const cols = Math.ceil(WORLD.width / TILE);
350
+ const rows = Math.ceil(WORLD.height / TILE);
351
+ for (let by=0; by<rows; by++) {
352
+ for (let bx=0; bx<cols; bx++) {
353
+ const x = bx*TILE, y = by*TILE;
354
+ const b = biomeAt(x+1, y+1);
355
+ let color = '#203a2b';
356
+ if (b === 'desert') color = '#cbb78b';
357
+ else if (b === 'forest') color = '#1f4d2c';
358
+ else if (b === 'oasis') color = '#2a4f5a';
359
+ else if (b === 'ruins') color = '#4a3b3b';
360
+ const s = worldToScreen(x,y);
361
+ ctx.fillStyle = color;
362
+ ctx.fillRect(s.x, s.y, TILE, TILE);
363
+ ctx.strokeStyle = 'rgba(0,0,0,0.12)';
364
+ ctx.strokeRect(s.x, s.y, TILE, TILE);
365
+ }
366
+ }
367
 
368
+ // storm overlay
369
+ if (storm.active) {
370
+ const screenCenter = worldToScreen(storm.centerX, storm.centerY);
371
+ ctx.save();
372
+ const grad = ctx.createRadialGradient(screenCenter.x, screenCenter.y, storm.radius*0.15, screenCenter.x, screenCenter.y, storm.radius);
373
+ grad.addColorStop(0, 'rgba(100,149,237,0.02)');
374
+ grad.addColorStop(1, 'rgba(100,149,237,0.35)');
375
+ ctx.fillStyle = grad;
376
+ ctx.beginPath();
377
+ ctx.arc(screenCenter.x, screenCenter.y, storm.radius, 0, Math.PI*2);
378
+ ctx.fill();
379
+ ctx.restore();
380
+ }
381
+ }
382
+
383
+ function drawObjects() {
384
+ for (const obj of objects) {
385
+ if (obj.dead) continue;
386
+ const s = worldToScreen(obj.x, obj.y);
387
+ ctx.save();
388
+ const h = obj.type === 'wood' ? 18 : (obj.type==='stone'? 12:24);
389
+ // shadow
390
+ ctx.fillStyle = 'rgba(0,0,0,0.18)';
391
+ ctx.beginPath();
392
+ ctx.ellipse(s.x, s.y + 8, 18, 8, 0, 0, Math.PI*2);
393
+ ctx.fill();
394
+ // body
395
+ ctx.fillStyle = obj.type === 'wood' ? '#6b3b1a' : (obj.type==='stone' ? '#6b6b6b' : '#8b5a32');
396
+ ctx.fillRect(s.x-12, s.y-h, 24, h);
397
+ // health bar
398
+ ctx.fillStyle = 'black';
399
+ ctx.fillRect(s.x-14, s.y-h-8, 28, 5);
400
+ ctx.fillStyle = 'lime';
401
+ const hpPct = Math.max(0, obj.hp / obj.maxHp);
402
+ ctx.fillRect(s.x-14, s.y-h-8, 28*hpPct, 5);
403
+ ctx.restore();
404
+ }
405
+ }
406
+
407
+ function drawChests() {
408
+ for (const chest of chests) {
409
+ const s = worldToScreen(chest.x, chest.y);
410
+ ctx.save();
411
+ ctx.fillStyle = 'rgba(0,0,0,0.25)';
412
+ ctx.beginPath();
413
+ ctx.ellipse(s.x, s.y+10, 22, 10, 0,0,Math.PI*2); ctx.fill();
414
+ ctx.fillStyle = chest.opened ? '#7b3e00' : '#a56b2a';
415
+ ctx.fillRect(s.x-18, s.y-12, 36, 20);
416
+ ctx.translate(s.x, s.y-12);
417
+ const openAngle = chest.opened ? Math.PI*0.45 : 0;
418
+ ctx.save();
419
+ ctx.rotate(-openAngle);
420
+ ctx.fillStyle = chest.opened ? '#ffd86b' : '#caa15e';
421
+ ctx.fillRect(-18, -12-4, 36, 8);
422
+ ctx.restore();
423
+ ctx.restore();
424
+ }
425
+ }
426
+
427
+ function drawBullets() {
428
+ for (const b of bullets) {
429
+ const s = worldToScreen(b.x, b.y);
430
+ ctx.save();
431
+ if (b.tracer) {
432
+ ctx.fillStyle = b.color;
433
+ ctx.globalAlpha = 0.9;
434
+ ctx.beginPath(); ctx.arc(s.x, s.y, 3, 0, Math.PI*2); ctx.fill();
435
+ } else {
436
+ const backX = s.x - (b.vx * 0.02);
437
+ const backY = s.y - (b.vy * 0.02);
438
+ const grad = ctx.createLinearGradient(backX, backY, s.x, s.y);
439
+ grad.addColorStop(0, 'rgba(255,255,255,0)');
440
+ grad.addColorStop(0.7, b.color);
441
+ grad.addColorStop(1, '#fff');
442
+ ctx.strokeStyle = grad;
443
+ ctx.lineWidth = 3;
444
+ ctx.beginPath(); ctx.moveTo(backX, backY); ctx.lineTo(s.x, s.y); ctx.stroke();
445
+ ctx.fillStyle = '#fff'; ctx.beginPath(); ctx.arc(s.x, s.y, 2.5, 0, Math.PI*2); ctx.fill();
446
+ }
447
+ ctx.restore();
448
+ }
449
  }
450
 
 
451
  function drawPlayer() {
452
+ const s = worldToScreen(player.x, player.y);
453
  ctx.save();
454
+ ctx.translate(s.x, s.y);
455
  ctx.rotate(player.angle);
456
+ // shadow
457
+ ctx.fillStyle = 'rgba(0,0,0,0.25)';
458
+ ctx.beginPath(); ctx.ellipse(0, 14, 18, 8, 0,0,Math.PI*2); ctx.fill();
459
+ // body (triangle)
460
  ctx.fillStyle = 'yellow';
461
+ ctx.beginPath(); ctx.moveTo(16,0); ctx.lineTo(-12,-10); ctx.lineTo(-12,10); ctx.closePath(); ctx.fill();
 
 
 
 
 
 
462
  ctx.restore();
463
  }
464
 
465
+ function drawCrosshair() {
466
+ const screenX = mouse.canvasX, screenY = mouse.canvasY;
467
  ctx.save();
468
+ ctx.strokeStyle = 'yellow';
469
+ ctx.lineWidth = 2;
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
470
  ctx.beginPath();
471
+ ctx.moveTo(screenX-8, screenY); ctx.lineTo(screenX+8, screenY);
472
+ ctx.moveTo(screenX, screenY-8); ctx.lineTo(screenX, screenY+8);
473
  ctx.stroke();
 
474
  ctx.restore();
475
  }
476
 
477
+ // --- Storm mechanics ---
478
+ const storm = { maxRadius: 900, radius: 900, centerX: WORLD.width/2, centerY: WORLD.height/2, damagePerSecond:1, closingSpeed: 8, active:false };
479
  function playerInStorm() {
480
+ return Math.hypot(player.x - storm.centerX, player.y - storm.centerY) > storm.radius;
 
 
 
481
  }
 
 
482
  let stormDamageAccumulator = 0;
483
+ function updateStorm(dt) {
484
+ if (!storm.active) return;
485
+ storm.radius -= storm.closingSpeed * dt * 50;
486
+ if (storm.radius < 80) storm.radius = 80;
487
  if (playerInStorm()) {
488
+ const progress = 1 - storm.radius / storm.maxRadius;
489
+ const rate = storm.damagePerSecond * (1 + progress*4);
490
+ stormDamageAccumulator += rate * dt;
 
491
  while (stormDamageAccumulator >= 1) {
492
  stormDamageAccumulator -= 1;
493
+ player.health -= 1;
494
+ if (player.health <= 0) { player.health = 0; playerDeath(); }
495
  }
496
  } else {
497
  stormDamageAccumulator = 0;
498
  }
499
  }
500
 
501
+ // --- Game loop & updates ---
502
+ let lastTime = 0;
503
+ function gameLoop(ts) {
504
+ if (!gameActive) return;
505
+ if (!lastTime) lastTime = ts;
506
+ const dt = Math.min(0.05, (ts - lastTime) / 1000);
507
+ lastTime = ts;
508
+
509
+ // movement
510
+ let dx=0, dy=0;
511
+ if (keys.w) dy -= 1;
512
+ if (keys.s) dy += 1;
513
+ if (keys.a) dx -= 1;
514
+ if (keys.d) dx += 1;
515
+ if (dx !== 0 || dy !== 0) {
516
+ const len = Math.hypot(dx,dy) || 1;
517
+ player.x += (dx/len) * player.speed * dt;
518
+ player.y += (dy/len) * player.speed * dt;
519
  }
520
+ // clamp
521
+ player.x = Math.max(16, Math.min(WORLD.width-16, player.x));
522
+ player.y = Math.max(16, Math.min(WORLD.height-16, player.y));
523
+
524
+ // camera & mouse world mapping
525
+ cameraUpdate();
526
+ mouse.worldX = mouse.canvasX + camera.x;
527
+ mouse.worldY = mouse.canvasY + camera.y;
528
+
529
+ // shooting
530
+ if (mouse.down) {
531
+ const weapon = player.weapons[player.currentWeaponIndex] || { dmg:10, rate:300, color:'#fff' };
532
+ if (ts - player.lastShot > (weapon.rate || 300)) {
533
+ player.lastShot = ts;
534
+ shootBullet(player.x + Math.cos(player.angle)*20, player.y + Math.sin(player.angle)*20, mouse.worldX, mouse.worldY, weapon);
535
+ }
536
  }
 
 
537
 
538
+ // bullets update & collisions
539
+ for (let i=bullets.length-1;i>=0;i--) {
540
+ const b = bullets[i];
541
+ b.x += b.vx * dt;
542
+ b.y += b.vy * dt;
543
+ b.traveled += Math.hypot(b.vx*dt, b.vy*dt);
544
+ b.life -= dt;
545
+ if (!b.tracer && b.dmg > 0) {
546
+ for (const obj of objects) {
547
+ if (obj.dead) continue;
548
+ if (Math.hypot(obj.x - b.x, obj.y - b.y) < 20) { applyDamageToObject(obj, b.dmg); b.life = -1; break; }
549
+ }
550
+ for (const chest of chests) {
551
+ if (!chest.opened && Math.hypot(chest.x - b.x, chest.y - b.y) < 20) { chest.opened = true; chest.openProgress = 1; b.life = -1; }
552
+ }
553
+ }
554
+ if (b.life <= 0 || b.traveled > 2000) bullets.splice(i,1);
555
+ }
556
 
557
+ // cleanup dead objects
558
+ for (let i=objects.length-1;i>=0;i--) if (objects[i].dead) objects.splice(i,1);
 
559
 
560
+ // interact (E)
561
+ if (keys.e) { interactNearby(); keys.e = false; }
562
+
563
+ // update storm
564
+ updateStorm(dt);
565
+
566
+ // draw
567
+ ctx.clearRect(0,0,canvas.width,canvas.height);
568
+ drawWorld();
569
+ drawObjects();
570
+ drawChests();
571
+ drawBullets();
572
+ drawPlayer();
573
+ drawCrosshair();
574
+
575
+ // storm warning UI
576
+ if (storm.active && playerInStorm()) stormWarningEl.classList.remove('hidden'); else stormWarningEl.classList.add('hidden');
577
+
578
+ updatePlayerStats();
579
+
580
+ requestAnimationFrame(gameLoop);
581
  }
582
 
583
+ // --- Game start / end handlers ---
584
+ let timerInterval = null;
585
+ let gameTime = 300;
586
+
587
  function startGame() {
588
  gameActive = true;
 
589
  landingScreenEl.classList.add('hidden');
590
  gameScreenEl.classList.remove('hidden');
 
591
 
592
+ // ensure canvas size and camera correct (fix for "not showing")
593
+ resizeCanvas();
594
+
595
+ // reset player/world
596
+ player.x = WORLD.width/2 + (Math.random()-0.5)*200;
597
+ player.y = WORLD.height/2 + (Math.random()-0.5)*200;
598
+ player.health = 100; player.armor = 0; player.kills = 0; player.materials = 0;
599
+ player.weapons = [];
600
+ player.weapons[0] = { name:'Pistol', dmg:12, rate:320, color:'#ffd86b' };
601
+ player.currentWeaponIndex = 0;
602
+
603
+ populateWorld();
604
  initInventory();
605
+ cameraUpdate();
 
 
 
606
 
607
+ // timer & storm
608
+ gameTime = 300;
609
+ storm.active = false;
610
+ storm.radius = storm.maxRadius;
611
  document.getElementById('gameTimer').textContent = '5:00';
 
612
  timerInterval && clearInterval(timerInterval);
613
  timerInterval = setInterval(() => {
614
+ if (!gameActive) { clearInterval(timerInterval); return; }
 
 
 
615
  gameTime--;
616
+ const m = Math.floor(gameTime/60), s = gameTime%60;
617
+ document.getElementById('gameTimer').textContent = `${m}:${s<10?'0'+s:s}`;
618
+ if (gameTime === 240) { storm.active = true; stormWarningEl.classList.remove('hidden'); setTimeout(()=>stormWarningEl.classList.add('hidden'),4000); }
619
+ if (gameTime <= 0) { clearInterval(timerInterval); endGame(); }
 
 
 
 
 
 
 
 
 
 
 
 
620
  }, 1000);
621
 
622
+ lastTime = performance.now();
623
  requestAnimationFrame(gameLoop);
624
  }
625
 
 
626
  function endGame() {
627
  gameActive = false;
628
+ alert('Match over!');
629
  }
630
 
 
631
  function playerDeath() {
632
  gameActive = false;
633
  deathScreenEl.classList.remove('hidden');
634
  }
635
 
 
636
  document.getElementById('respawnBtn').addEventListener('click', () => {
637
  deathScreenEl.classList.add('hidden');
638
  landingScreenEl.classList.remove('hidden');
639
  });
640
 
641
+ // Biome selectors start the game
642
+ document.querySelectorAll('.biome-selector').forEach(el => {
643
+ el.addEventListener('click', () => {
644
+ selectedBiome = el.getAttribute('data-biome');
645
  startGame();
646
  });
647
  });
648
 
649
+ // Ensure E key can be used from anywhere
650
+ window.addEventListener('keydown', (e) => {
651
+ if (e.key.toLowerCase() === 'e' && gameActive) keys.e = true;
652
+ });
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
653
 
654
+ // Initialize on load
 
655
  resizeCanvas();
656
+ populateWorld();
657
  initInventory();
658
  updatePlayerStats();
659
+ feather.replace();
660
  </script>
661
  </body>
662
  </html>