bbc123321 commited on
Commit
7185738
·
verified ·
1 Parent(s): 6a80455

Manual changes saved

Browse files
Files changed (1) hide show
  1. index.html +457 -347
index.html CHANGED
@@ -3,32 +3,28 @@
3
  <head>
4
  <meta charset="utf-8" />
5
  <meta name="viewport" content="width=device-width, initial-scale=1" />
6
- <title>BattleZone Royale - Updated</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
  #stormWarning { z-index:10; }
16
  #deathScreen { z-index:20; }
17
 
18
- /* HUD positions - small */
19
  #hudHealth { position:absolute; left:12px; bottom:12px; background:rgba(0,0,0,0.55); padding:6px 8px; border-radius:8px; font-weight:700; font-size:13px; display:flex; align-items:center; gap:8px; z-index:30; }
20
- #hudGear { position:absolute; right:12px; bottom:12px; background:rgba(0,0,0,0.0); padding:6px; border-radius:8px; z-index:30; display:flex; gap:6px; }
21
- .gear-slot { width:44px; height:36px; background: rgba(255,255,255,0.03); border-radius:6px; display:flex; align-items:center; justify-content:center; font-size:11px; color:#ddd; }
22
- /* Chest glow pulse */
23
- @keyframes chestGlow {
24
- 0% { box-shadow: 0 0 0px rgba(255,215,0,0.0); }
25
- 50% { box-shadow: 0 0 18px rgba(255,215,0,0.35); }
26
- 100% { box-shadow: 0 0 0px rgba(255,215,0,0.0); }
27
- }
28
  </style>
29
  </head>
30
  <body>
31
- <!-- Landing / Instructions screen -->
32
  <div id="landingScreen" class="flex flex-col items-center justify-center h-screen p-6">
33
  <h1 class="text-5xl font-bold text-yellow-400 mb-6 flex items-center">
34
  <i data-feather="target" class="mr-2"></i> BattleZone Royale
@@ -58,20 +54,22 @@
58
  </div>
59
  </div>
60
 
61
- <!-- Instructions -->
62
  <div class="bg-gray-800 p-4 rounded-lg max-w-xl w-full text-sm">
63
  <h3 class="font-bold text-yellow-400 mb-2">Controls</h3>
64
  <ul class="list-disc pl-5 space-y-1">
65
  <li><strong>WASD</strong> — Move</li>
66
- <li><strong>Mouse</strong> — Aim & Shoot</li>
67
- <li><strong>E</strong> — Interact / Loot / Harvest</li>
 
68
  <li><strong>Q</strong> — Build (costs 10 materials)</li>
69
- <li><strong>1-5</strong> — Switch weapons</li>
 
 
70
  </ul>
71
  </div>
72
  </div>
73
 
74
- <!-- Game screen -->
75
  <div id="gameScreen" class="hidden h-screen flex flex-col">
76
  <header class="flex justify-between items-center border-b border-yellow-500 px-6 py-4" style="flex-shrink:0;">
77
  <h1 class="text-3xl font-bold text-yellow-400 flex items-center"><i data-feather="target" class="mr-2"></i>BattleZone Royale</h1>
@@ -100,74 +98,74 @@
100
  </button>
101
  </div>
102
 
103
- <!-- Small health at bottom-left -->
104
  <div id="hudHealth" class="hidden">
105
  <svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="#ffd86b" stroke-width="1.6"><path d="M20.8 8.6a5.5 5.5 0 0 0-7.8 0L12 10.6l-1-1a5.5 5.5 0 0 0-7.8 7.8l1 1L12 22l7.8-3.6 1-1a5.5 5.5 0 0 0 0-7.8z"></path></svg>
106
  <span id="hudHealthText">100%</span>
107
  </div>
108
 
109
- <!-- Gear / small inventory at bottom-right -->
110
- <div id="hudGear" class="hidden" aria-label="gear"></div>
 
 
111
  </div>
112
-
113
- <!-- NOTE: Sidebar removed per request -->
114
  </div>
115
  </div>
116
 
117
  <script>
118
- // --- Globals & DOM ---
119
- let gameActive = false;
120
- let selectedBiome = 'forest';
121
- const landingScreenEl = document.getElementById('landingScreen');
122
- const gameScreenEl = document.getElementById('gameScreen');
123
  const canvas = document.getElementById('gameCanvas');
124
  const ctx = canvas.getContext('2d');
125
- const hudHealthEl = document.getElementById('hudHealth');
126
  const hudHealthText = document.getElementById('hudHealthText');
127
- const hudGearEl = document.getElementById('hudGear');
128
- const stormWarningEl = document.getElementById('stormWarning');
129
- const deathScreenEl = document.getElementById('deathScreen');
130
-
131
- // --- Make world bigger per request ---
132
- const WORLD = { width: 6000, height: 4000 }; // enlarged world
 
 
133
  let camera = { x:0, y:0 };
134
 
135
  function resizeCanvas(){
136
- const container = document.getElementById('canvasContainer');
137
- canvas.width = Math.max(600, Math.floor(container.clientWidth));
138
- canvas.height = Math.max(400, Math.floor(container.clientHeight));
139
  cameraUpdate();
140
  }
141
  window.addEventListener('resize', resizeCanvas);
142
 
143
- // --- Player ---
144
  const player = {
145
- id: 'player', x: WORLD.width/2, y: WORLD.height/2, radius: 16, angle:0, speed:220,
146
- health:100, armor:0, kills:0, materials:0, weapons:[], currentWeaponIndex:0, lastShot:0
 
 
 
 
147
  };
148
 
149
- // --- Input ---
150
- const keys = { w:false,a:false,s:false,d:false,e:false,q:false };
151
  const mouse = { canvasX:0, canvasY:0, worldX:0, worldY:0, down:false };
152
 
153
- window.addEventListener('keydown', (e)=>{
154
  const k = e.key.toLowerCase();
155
- if (!gameActive) return;
156
  if (k in keys) keys[k] = true;
157
- if (['1','2','3','4','5'].includes(k)){
158
- const idx = parseInt(k)-1;
159
- if (player.weapons[idx]) player.currentWeaponIndex = idx;
160
- }
161
  });
162
- window.addEventListener('keyup', (e)=>{
163
  const k = e.key.toLowerCase();
164
- if (!gameActive) return;
165
  if (k in keys) keys[k] = false;
166
- if (k === 'e') keys.e = true;
167
  if (k === 'q') keys.q = true;
168
  });
169
 
170
- canvas.addEventListener('mousemove', (e)=>{
171
  const rect = canvas.getBoundingClientRect();
172
  mouse.canvasX = e.clientX - rect.left;
173
  mouse.canvasY = e.clientY - rect.top;
@@ -175,20 +173,21 @@
175
  mouse.worldY = mouse.canvasY + camera.y;
176
  player.angle = Math.atan2(mouse.worldY - player.y, mouse.worldX - player.x);
177
  });
178
- canvas.addEventListener('mousedown', ()=> { if (gameActive) mouse.down = true; });
179
  window.addEventListener('mouseup', ()=> mouse.down = false);
180
 
181
- // --- Entities ---
182
  const bullets = [];
183
  const chests = [];
184
  const objects = []; // harvestables & walls
185
- const enemies = []; // 19 NPCs
 
186
 
187
  function rand(min,max){ return Math.random()*(max-min)+min; }
188
  function randInt(min,max){ return Math.floor(Math.random()*(max-min))+min; }
189
 
190
  function biomeAt(x,y){
191
- const bx = Math.floor(x / 400), by = Math.floor(y / 400);
192
  const seed = (bx*73856093) ^ (by*19349663);
193
  const r = Math.abs(Math.sin(seed)) % 1;
194
  if (r < 0.25) return 'desert';
@@ -197,33 +196,32 @@
197
  return 'ruins';
198
  }
199
 
 
 
 
 
 
200
  function generateLootForBiome(b){
201
  const roll = Math.random();
202
- if (roll < 0.4) return { type:'medkit', heal:25 };
203
- if (roll < 0.75) return { type:'materials', amount: randInt(5,20) };
204
  const weapons = [
205
- { name:'Pistol', dmg:12, rate:320, color:'#ffd86b' },
206
- { name:'SMG', dmg:6, rate:120, color:'#8ef0ff' },
207
- { name:'Shotgun', dmg:22, rate:800, color:'#ff9fb8' },
208
- { name:'Rifle', dmg:18, rate:400, color:'#c7ff9a' }
209
  ];
210
  return { type:'weapon', weapon: weapons[randInt(0, weapons.length)] };
211
  }
212
 
213
- // Populate world with more content (scaled to bigger map)
214
  function populateWorld(){
215
- chests.length = 0;
216
- objects.length = 0;
217
- enemies.length = 0;
218
-
219
- // chests
220
- for (let i=0;i<250;i++){
221
  const x = rand(150, WORLD.width-150);
222
  const y = rand(150, WORLD.height-150);
223
  chests.push({ x,y, opened:false, loot: generateLootForBiome(biomeAt(x,y)) });
224
  }
225
-
226
- // breakable objects and some walls
227
  for (let i=0;i<700;i++){
228
  const t = Math.random();
229
  let type='wood';
@@ -233,194 +231,276 @@
233
  const hp = type==='wood'?40 : (type==='stone'?80:160);
234
  objects.push({ x,y, type, hp, maxHp:hp, dead:false });
235
  }
236
-
237
- // enemies (19)
238
  for (let i=0;i<19;i++){
239
  const ex = rand(300, WORLD.width-300);
240
  const ey = rand(300, WORLD.height-300);
241
- const weaponPool = [
242
- { name:'Pistol', dmg:12, rate:320, color:'#ffd86b' },
243
- { name:'SMG', dmg:6, rate:140, color:'#8ef0ff' },
244
- { name:'Rifle', dmg:16, rate:380, color:'#c7ff9a' }
245
- ];
246
  enemies.push({
247
- id: 'e'+i, x:ex, y:ey, radius:14, angle:0, speed:120 + rand(-20,20),
248
- health: 80 + randInt(0,40), lastShot:0, weapon: weaponPool[randInt(0,weaponPool.length)],
249
- target: null, roamTimer: rand(0,3)
250
  });
251
  }
252
  updatePlayerCount();
253
  }
254
 
255
- // HUD: small health + gear slots bottom corners
256
  function initHUD(){
257
- hudHealthEl.classList.remove('hidden');
258
- hudGearEl.classList.remove('hidden');
259
- hudGearEl.innerHTML = '';
260
- for (let i=0;i<6;i++){
261
  const slot = document.createElement('div');
262
  slot.className = 'gear-slot';
263
  slot.dataset.index = i;
264
- slot.innerHTML = 'Empty';
265
- hudGearEl.appendChild(slot);
266
  }
 
 
267
  updateHUD();
268
  }
 
269
  function updateHUD(){
270
  hudHealthText.textContent = `${Math.max(0,Math.floor(player.health))}%`;
271
- const slots = hudGearEl.querySelectorAll('.gear-slot');
272
  slots.forEach(s => {
273
  const idx = parseInt(s.dataset.index);
274
- const w = player.weapons[idx];
275
- s.innerHTML = w ? `<div style="text-align:center;"><div style="font-size:11px">${w.name}</div><div style="color:${w.color}">●</div></div>` : 'Empty';
 
 
 
 
 
 
276
  });
 
 
 
 
277
  feather.replace();
278
  }
279
 
280
- // Camera centering
281
  function cameraUpdate(){
282
  if (!canvas.width || !canvas.height) return;
283
- if (WORLD.width <= canvas.width) camera.x = (WORLD.width - canvas.width)/2;
284
- else {
285
- camera.x = player.x - canvas.width/2;
286
- camera.x = Math.max(0, Math.min(camera.x, WORLD.width - canvas.width));
287
- }
288
- if (WORLD.height <= canvas.height) camera.y = (WORLD.height - canvas.height)/2;
289
- else {
290
- camera.y = player.y - canvas.height/2;
291
- camera.y = Math.max(0, Math.min(camera.y, WORLD.height - canvas.height));
292
- }
293
  }
294
  function worldToScreen(wx,wy){ return { x: Math.round(wx - camera.x), y: Math.round(wy - camera.y) }; }
295
 
296
- // Shooting bullets
297
- function shootBullet(originX, originY, targetX, targetY, weapon, shooterId){
 
298
  const speed = 1100;
299
  const angle = Math.atan2(targetY-originY, targetX-originX);
 
300
  bullets.push({
301
  x: originX, y: originY, vx: Math.cos(angle)*speed, vy: Math.sin(angle)*speed,
302
- dmg: weapon ? weapon.dmg : 8, color: weapon ? weapon.color : '#fff', life: 1.6, traveled:0,
303
  shooter: shooterId, born: performance.now(), tracer:false
304
  });
 
 
 
 
 
 
 
 
 
 
 
 
 
 
305
  }
306
 
307
- // Interaction: loot & harvest
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
308
  function interactNearby(){
 
 
 
 
 
 
 
 
 
 
 
309
  const range = 56;
 
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
- // glow pop tracers
324
- for (let i=0;i<6;i++) 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, traveled:0, shooter:null, tracer:true, born:performance.now() });
325
  updateHUD();
326
  return;
327
  }
328
  }
 
 
329
  for (const obj of objects){
330
  if (obj.dead) continue;
331
  const d = Math.hypot(obj.x - player.x, obj.y - player.y);
332
  if (d < range){
333
- const gain = obj.type === 'wood' ? 2 : 5;
334
- player.materials += gain;
335
- obj.dead = true;
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
336
  updateHUD();
337
  return;
338
  }
339
  }
340
  }
341
 
342
- // Build with Q: place wall in front if have >=10 materials
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
343
  function tryBuild(){
344
  if (player.materials < 10) return;
345
  player.materials -= 10;
346
- const bx = player.x + Math.cos(player.angle)*48;
347
- const by = player.y + Math.sin(player.angle)*48;
348
  objects.push({ x:bx, y:by, type:'wall', hp:160, maxHp:160, dead:false });
349
  updateHUD();
350
  }
351
 
352
- // Visibility / Line of Sight: returns true if unobstructed
353
  function hasLineOfSight(x1,y1,x2,y2){
354
- // For each blocking object, treat as circle/box and check distance to segment
355
  const vx = x2 - x1, vy = y2 - y1;
356
  const vlen2 = vx*vx + vy*vy;
357
  for (const obj of objects){
358
  if (obj.dead) continue;
359
- // consider blocking types: wall and stone primarily; wood also blocks a little
360
- let blockRadius = 0;
361
- if (obj.type === 'wall') blockRadius = 28;
362
- else if (obj.type === 'stone') blockRadius = 22;
363
- else if (obj.type === 'wood') blockRadius = 16;
364
- else continue;
365
  const wx = obj.x - x1, wy = obj.y - y1;
366
  const c1 = vx*wx + vy*wy;
367
- const t = vlen2 > 0 ? c1 / vlen2 : 0;
368
  if (t < 0 || t > 1) continue;
369
  const projx = x1 + vx * t, projy = y1 + vy * t;
370
  const dist = Math.hypot(projx - obj.x, projy - obj.y);
371
- if (dist < blockRadius) return false;
372
  }
373
- // world bounds not considered blocking
374
  return true;
375
  }
376
 
377
- // Bullets update & collisions
378
  function bulletsUpdate(dt){
379
  for (let i=bullets.length-1;i>=0;i--){
380
  const b = bullets[i];
381
  b.x += b.vx * dt; b.y += b.vy * dt;
382
  b.traveled += Math.hypot(b.vx*dt, b.vy*dt);
383
  b.life -= dt;
384
- const bornAgo = performance.now() - (b.born || performance.now());
385
-
386
- // Bullet hit player
387
  if (b.dmg > 0 && b.shooter !== 'player'){
388
- if (Math.hypot(player.x - b.x, player.y - b.y) < 16 && bornAgo > 30){
389
  player.health -= b.dmg;
390
  if (player.health <= 0){ player.health = 0; playerDeath(); }
391
  bullets.splice(i,1); continue;
392
  }
393
  }
394
-
395
- // Bullet hits enemies
396
  if (b.dmg > 0){
397
  for (const e of enemies){
398
  if (e.health <= 0) continue;
399
- if (b.shooter === e.id) continue; // don't hit shooter
400
- if (Math.hypot(e.x - b.x, e.y - b.y) < 14 && bornAgo > 30){
401
  e.health -= b.dmg;
402
- if (e.health <= 0){
403
- e.health = 0;
404
- if (b.shooter === 'player') player.kills++;
405
- // small material reward for kills if player shot
406
- if (b.shooter === 'player') player.materials += 2;
407
- }
408
  bullets.splice(i,1); break;
409
  }
410
  }
411
  if (!bullets[i]) continue;
412
  }
413
-
414
  // collision with objects & chests
415
  if (!b.tracer && b.dmg > 0){
416
  for (const obj of objects){
417
  if (obj.dead) continue;
418
- if (Math.hypot(obj.x - b.x, obj.y - b.y) < 18){
419
  obj.hp -= b.dmg;
420
- if (obj.hp <= 0 && !obj.dead){
421
- obj.dead = true;
422
- if (b.shooter === 'player') player.materials += (obj.type==='wood'?3:6);
423
- }
424
  bullets.splice(i,1); break;
425
  }
426
  }
@@ -428,34 +508,35 @@
428
  for (const chest of chests){
429
  if (!chest.opened && Math.hypot(chest.x - b.x, chest.y - b.y) < 18){
430
  chest.opened = true;
 
 
 
 
 
 
431
  bullets.splice(i,1); break;
432
  }
433
  }
434
  if (!bullets[i]) continue;
435
  }
436
-
437
- if (b.life <= 0 || b.traveled > 2400) bullets.splice(i,1);
438
  }
439
  }
440
 
441
- // Enemy AI: choose nearby visible targets; only shoot if target in visible range
442
  function updateEnemies(dt, now){
443
  for (const e of enemies){
444
  if (e.health <= 0) continue;
445
  e.roamTimer -= dt;
446
-
447
- // choose limited set of potential targets: player and a few nearby enemies
448
  let target = player;
449
  let bestDist = Math.hypot(player.x - e.x, player.y - e.y);
450
-
451
  for (const other of enemies){
452
  if (other === e || other.health <= 0) continue;
453
  const d = Math.hypot(other.x - e.x, other.y - e.y);
454
- // prefer closer opponents sometimes
455
- if (d < bestDist && Math.random() < 0.65) { bestDist = d; target = other; }
456
  }
457
-
458
- // movement: move toward target if reasonably close, else roam
459
  if (bestDist < 900 || e.roamTimer <= 0){
460
  e.angle = Math.atan2(target.y - e.y, target.x - e.x);
461
  const avoid = e.health < 20 && Math.random() < 0.6;
@@ -463,71 +544,124 @@
463
  e.x += Math.cos(e.angle) * e.speed * dt * moveDir;
464
  e.y += Math.sin(e.angle) * e.speed * dt * moveDir;
465
  } else {
466
- // slow roam
467
  e.x += Math.cos(e.angle) * e.speed * dt * 0.25;
468
  e.y += Math.sin(e.angle) * e.speed * dt * 0.25;
469
  if (Math.random() < 0.01) e.angle += (Math.random()-0.5)*2;
470
  }
471
-
472
- // clamp
473
  e.x = Math.max(12, Math.min(WORLD.width-12, e.x));
474
  e.y = Math.max(12, Math.min(WORLD.height-12, e.y));
475
 
476
- // Shooting logic: only if within distance threshold and hasLineOfSight to the chosen target
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
477
  const distToTarget = Math.hypot(target.x - e.x, target.y - e.y);
478
- const shootRange = 650; // visible range threshold
479
- if (distToTarget < shootRange && now - e.lastShot > e.weapon.rate){
480
- // check line of sight (blocks by objects)
481
- if (hasLineOfSight(e.x, e.y, target.x, target.y)){
 
 
 
 
 
 
 
 
482
  e.lastShot = now;
483
- // fire slightly lead/randomness
484
- const aimX = target.x + (Math.random()-0.5)*8;
485
- const aimY = target.y + (Math.random()-0.5)*8;
486
- shootBullet(e.x + Math.cos(e.angle)*12, e.y + Math.sin(e.angle)*12, aimX, aimY, e.weapon, e.id);
487
  }
488
  }
489
  }
490
  }
491
 
492
- // Utility: update player count
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
493
  function updatePlayerCount(){
494
- const aliveEnemies = enemies.filter(e => e.health > 0).length;
495
- document.getElementById('playerCount').textContent = `${1 + aliveEnemies}/20`;
496
  }
497
 
498
- // Drawing functions (chests glow gold)
499
  function drawWorld(){
500
- const TILE = 600; // slightly larger tiles to reduce draw calls in big world
501
  const cols = Math.ceil(WORLD.width / TILE);
502
  const rows = Math.ceil(WORLD.height / TILE);
503
- for (let by=0; by<rows; by++){
504
- for (let bx=0; bx<cols; bx++){
505
- const x = bx*TILE, y = by*TILE;
506
- const b = biomeAt(x+1,y+1);
507
- let color = '#203a2b';
508
- if (b === 'desert') color = '#cbb78b';
509
- else if (b === 'forest') color = '#16411f';
510
- else if (b === 'oasis') color = '#274b52';
511
- else if (b === 'ruins') color = '#4a3b3b';
512
  const s = worldToScreen(x,y);
513
- ctx.fillStyle = color; ctx.fillRect(s.x, s.y, TILE, TILE);
514
- ctx.strokeStyle = 'rgba(0,0,0,0.08)'; ctx.strokeRect(s.x, s.y, TILE, TILE);
515
  }
516
  }
517
- // storm overlay (big shrinking circle)
518
  if (storm.active){
519
- const screenCenter = worldToScreen(storm.centerX, storm.centerY);
520
  ctx.save();
521
- // outer shading
522
- const grad = ctx.createRadialGradient(screenCenter.x, screenCenter.y, storm.radius*0.15, screenCenter.x, screenCenter.y, storm.radius);
523
- grad.addColorStop(0, 'rgba(100,149,237,0.02)'); grad.addColorStop(1, 'rgba(100,149,237,0.45)');
524
- ctx.fillStyle = grad; ctx.beginPath(); ctx.arc(screenCenter.x, screenCenter.y, storm.radius, 0, Math.PI*2); ctx.fill();
525
- // visible circle outline
526
- ctx.strokeStyle = 'rgba(255,200,80,0.9)';
527
- ctx.lineWidth = 4;
528
- ctx.beginPath();
529
- ctx.arc(screenCenter.x, screenCenter.y, storm.radius, 0, Math.PI*2);
530
- ctx.stroke();
531
  ctx.restore();
532
  }
533
  }
@@ -537,12 +671,9 @@
537
  if (obj.dead) continue;
538
  const s = worldToScreen(obj.x, obj.y);
539
  ctx.save();
540
- const h = obj.type === 'wood' ? 18 : (obj.type==='stone'?12:28);
541
- // shadow
542
- ctx.fillStyle = 'rgba(0,0,0,0.18)';
543
- ctx.beginPath(); ctx.ellipse(s.x, s.y+8, 18, 8, 0,0,Math.PI*2); ctx.fill();
544
- // body
545
- ctx.fillStyle = obj.type === 'wood' ? '#6b3b1a' : (obj.type==='stone' ? '#6b6b6b' : '#8b5a32');
546
  ctx.fillRect(s.x-12, s.y-h, 24, h);
547
  ctx.restore();
548
  }
@@ -554,113 +685,100 @@
554
  if (chest.opened) continue;
555
  const s = worldToScreen(chest.x, chest.y);
556
  ctx.save();
557
- // glowing halo
558
- const glowRadius = 30 + Math.sin(now/300 + chest.x*0.001)*6;
559
  const g = ctx.createRadialGradient(s.x, s.y-6, 6, s.x, s.y-6, glowRadius);
560
- g.addColorStop(0, 'rgba(255,215,0,0.95)');
561
- g.addColorStop(0.6, 'rgba(255,215,0,0.25)');
562
- g.addColorStop(1, 'rgba(255,215,0,0.00)');
563
- ctx.fillStyle = g;
564
- ctx.beginPath(); ctx.arc(s.x, s.y-6, glowRadius, 0, Math.PI*2); ctx.fill();
565
-
566
- // chest base
567
- ctx.fillStyle = '#a56b2a'; ctx.fillRect(s.x-18, s.y-12, 36, 20);
568
- // lid
569
- ctx.fillStyle = '#caa15e';
 
570
  ctx.save();
571
- ctx.translate(s.x, s.y-12);
572
- ctx.fillRect(-18, -16, 36, 8);
 
 
 
 
 
 
 
 
 
 
 
 
 
 
573
  ctx.restore();
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
574
  ctx.restore();
575
  }
576
  }
577
 
578
  function drawBullets(){
579
  for (const b of bullets){
580
- const s = worldToScreen(b.x, b.y);
581
  ctx.save();
582
- if (b.tracer){
583
- ctx.fillStyle = b.color; ctx.globalAlpha = 0.9; ctx.beginPath(); ctx.arc(s.x, s.y, 3, 0, Math.PI*2); ctx.fill();
584
- } else {
585
  const backX = s.x - (b.vx * 0.02);
586
  const backY = s.y - (b.vy * 0.02);
587
- const grad = ctx.createLinearGradient(backX, backY, s.x, s.y);
588
- grad.addColorStop(0, 'rgba(255,255,255,0)'); grad.addColorStop(0.7, b.color); grad.addColorStop(1, '#fff');
589
- ctx.strokeStyle = grad; ctx.lineWidth = 3; ctx.beginPath(); ctx.moveTo(backX, backY); ctx.lineTo(s.x, s.y); ctx.stroke();
590
- ctx.fillStyle = '#fff'; ctx.beginPath(); ctx.arc(s.x, s.y, 2.5, 0, Math.PI*2); ctx.fill();
591
  }
592
  ctx.restore();
593
  }
594
  }
595
 
596
  function drawPlayer(){
597
- const s = worldToScreen(player.x, player.y);
598
  ctx.save();
599
- ctx.translate(s.x, s.y); ctx.rotate(player.angle);
600
- ctx.fillStyle = 'rgba(0,0,0,0.25)'; ctx.beginPath(); ctx.ellipse(0, 14, 18, 8, 0,0,Math.PI*2); ctx.fill();
601
- ctx.fillStyle = 'yellow'; ctx.beginPath(); ctx.moveTo(18,0); ctx.lineTo(-12,-10); ctx.lineTo(-12,10); ctx.closePath(); ctx.fill();
602
  ctx.restore();
603
  }
604
 
605
- function drawEnemies(){
606
- for (const e of enemies){
607
- if (e.health <= 0) continue;
608
- const s = worldToScreen(e.x, e.y);
609
- ctx.save();
610
- ctx.translate(s.x, s.y); ctx.rotate(e.angle);
611
- ctx.fillStyle = 'rgba(0,0,0,0.18)'; ctx.beginPath(); ctx.ellipse(0, 12, 14, 6, 0,0,Math.PI*2); ctx.fill();
612
- ctx.fillStyle = '#ff6b6b'; ctx.beginPath(); ctx.moveTo(12,0); ctx.lineTo(-10,-8); ctx.lineTo(-10,8); ctx.closePath(); ctx.fill();
613
- // small health bar above
614
- ctx.fillStyle = 'rgba(0,0,0,0.6)'; ctx.fillRect(-18, -22, 36, 6);
615
- const hpPct = Math.max(0, e.health / 120);
616
- ctx.fillStyle = '#ff6b6b'; ctx.fillRect(-18, -22, 36*hpPct, 6);
617
- ctx.restore();
618
- }
619
- }
620
-
621
  function drawCrosshair(){
622
  const screenX = mouse.canvasX, screenY = mouse.canvasY;
623
- ctx.save(); ctx.strokeStyle = 'yellow'; ctx.lineWidth = 2; ctx.beginPath();
624
- ctx.moveTo(screenX-8, screenY); ctx.lineTo(screenX+8, screenY);
625
- ctx.moveTo(screenX, screenY-8); ctx.lineTo(screenX, screenY+8); ctx.stroke(); ctx.restore();
626
  }
627
 
628
- // --- Storm mechanics: random center when activated ---
629
- const storm = { maxRadius: 2200, radius: 2200, centerX: WORLD.width/2, centerY: WORLD.height/2, damagePerSecond:1, closingSpeed: 6, active:false };
630
- let stormDamageAccumulator = 0;
631
- function playerInStorm(){
632
- return Math.hypot(player.x - storm.centerX, player.y - storm.centerY) > storm.radius;
633
- }
634
- function updateStorm(dt){
635
- if (!storm.active) return;
636
- storm.radius -= storm.closingSpeed * dt * 60; // shrink noticeably
637
- if (storm.radius < 120) storm.radius = 120;
638
- if (playerInStorm()){
639
- const progress = 1 - storm.radius / storm.maxRadius;
640
- const rate = storm.damagePerSecond * (1 + progress*4);
641
- stormDamageAccumulator += rate * dt;
642
- while (stormDamageAccumulator >= 1){
643
- stormDamageAccumulator -= 1;
644
- player.health -= 1;
645
- if (player.health <= 0){ player.health = 0; playerDeath(); }
646
- }
647
- } else stormDamageAccumulator = 0;
648
- }
649
-
650
- // --- Main loop & game logic ---
651
  let lastTime = 0;
652
  function gameLoop(ts){
653
  if (!gameActive) return;
654
  if (!lastTime) lastTime = ts;
655
- const dt = Math.min(0.05, (ts - lastTime) / 1000);
656
  lastTime = ts;
657
 
658
- // Player movement
659
  let dx=0, dy=0;
660
- if (keys.w) dy -= 1;
661
- if (keys.s) dy += 1;
662
- if (keys.a) dx -= 1;
663
- if (keys.d) dx += 1;
664
  if (dx !== 0 || dy !== 0){
665
  const len = Math.hypot(dx,dy) || 1;
666
  player.x += (dx/len) * player.speed * dt;
@@ -669,81 +787,96 @@
669
  player.x = Math.max(16, Math.min(WORLD.width-16, player.x));
670
  player.y = Math.max(16, Math.min(WORLD.height-16, player.y));
671
 
672
- // camera & mouse world mapping
673
  cameraUpdate();
674
  mouse.worldX = mouse.canvasX + camera.x;
675
  mouse.worldY = mouse.canvasY + camera.y;
676
 
677
- // player shooting
 
 
 
 
 
 
 
 
678
  if (mouse.down){
679
- const weapon = player.weapons[player.currentWeaponIndex] || { dmg:10, rate:300, color:'#fff' };
680
- if (performance.now() - player.lastShot > (weapon.rate || 300)){
681
- player.lastShot = performance.now();
682
- shootBullet(player.x + Math.cos(player.angle)*18, player.y + Math.sin(player.angle)*18, mouse.worldX, mouse.worldY, weapon, 'player');
 
 
 
 
 
 
 
 
 
 
683
  }
684
  }
685
 
686
- // interactions
 
 
 
687
  if (keys.e){ interactNearby(); keys.e = false; }
 
 
688
  if (keys.q){ tryBuild(); keys.q = false; }
689
 
690
- // enemy AI
691
  updateEnemies(dt, performance.now());
692
-
693
- // bullets & collisions
694
  bulletsUpdate(dt);
695
 
696
- // cleanup dead objects
 
 
 
 
 
 
697
  for (let i=objects.length-1;i>=0;i--) if (objects[i].dead) objects.splice(i,1);
698
 
699
  updatePlayerCount();
700
-
701
- // storm
702
  updateStorm(dt);
703
 
704
  // render
705
  ctx.clearRect(0,0,canvas.width,canvas.height);
706
- drawWorld();
707
- drawObjects();
708
- drawChests();
709
- drawEnemies();
710
- drawBullets();
711
- drawPlayer();
712
- drawCrosshair();
713
-
714
- // HUD
715
  updateHUD();
716
 
717
  // storm warning
718
- if (storm.active && playerInStorm()) stormWarningEl.classList.remove('hidden'); else stormWarningEl.classList.add('hidden');
719
 
720
  requestAnimationFrame(gameLoop);
721
  }
722
 
723
- // --- Game start / end ---
724
  let timerInterval = null;
725
  let gameTime = 300;
 
 
726
  function startGame(){
727
  gameActive = true;
728
- landingScreenEl.classList.add('hidden');
729
- gameScreenEl.classList.remove('hidden');
730
  resizeCanvas();
731
 
732
- // reset player & world
733
  player.x = WORLD.width/2 + (Math.random()-0.5)*400;
734
  player.y = WORLD.height/2 + (Math.random()-0.5)*400;
735
  player.health = 100; player.armor = 0; player.kills = 0; player.materials = 0;
736
- player.weapons = []; player.weapons[0] = { name:'Pistol', dmg:12, rate:320, color:'#ffd86b' };
737
- player.currentWeaponIndex = 0;
738
 
739
  populateWorld();
740
  initHUD();
741
  cameraUpdate();
742
 
743
- // timer + storm schedule: when storm activates, pick random center on map
744
- gameTime = 300;
745
- storm.active = false;
746
- storm.radius = storm.maxRadius;
747
  document.getElementById('gameTimer').textContent = '5:00';
748
  timerInterval && clearInterval(timerInterval);
749
  timerInterval = setInterval(()=>{
@@ -751,14 +884,13 @@
751
  gameTime--;
752
  const m = Math.floor(gameTime/60), s = gameTime%60;
753
  document.getElementById('gameTimer').textContent = `${m}:${s<10?'0'+s:s}`;
754
- // start storm at 4 minutes left: pick a random center inside world margin
755
  if (gameTime === 240 && !storm.active){
756
  storm.active = true;
757
  storm.centerX = rand(400, WORLD.width-400);
758
  storm.centerY = rand(400, WORLD.height-400);
759
  storm.radius = storm.maxRadius;
760
- stormWarningEl.classList.remove('hidden');
761
- setTimeout(()=>stormWarningEl.classList.add('hidden'),4000);
762
  }
763
  if (gameTime <= 0){ clearInterval(timerInterval); endGame(); }
764
  }, 1000);
@@ -767,37 +899,15 @@
767
  requestAnimationFrame(gameLoop);
768
  }
769
 
770
- function endGame(){
771
- gameActive = false;
772
- alert('Match over!');
773
- }
774
 
775
- function playerDeath(){
776
- gameActive = false;
777
- deathScreenEl.classList.remove('hidden');
778
- }
779
-
780
- document.getElementById('respawnBtn').addEventListener('click', ()=>{
781
- deathScreenEl.classList.add('hidden');
782
- landingScreenEl.classList.remove('hidden');
783
- });
784
-
785
- // Biome selectors start the game
786
- document.querySelectorAll('.biome-selector').forEach(el=>{
787
- el.addEventListener('click', ()=>{
788
- selectedBiome = el.getAttribute('data-biome');
789
- startGame();
790
- });
791
- });
792
 
793
- // allow E/Q from anywhere
794
- window.addEventListener('keydown', (e)=>{
795
- const k = e.key.toLowerCase();
796
- if (k === 'e' && gameActive) keys.e = true;
797
- if (k === 'q' && gameActive) keys.q = true;
798
- });
799
 
800
- // Initialize
801
  resizeCanvas();
802
  populateWorld();
803
  feather.replace();
 
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
  #stormWarning { z-index:10; }
14
  #deathScreen { z-index:20; }
15
 
16
+ /* HUD */
17
  #hudHealth { position:absolute; left:12px; bottom:12px; background:rgba(0,0,0,0.55); padding:6px 8px; border-radius:8px; font-weight:700; font-size:13px; display:flex; align-items:center; gap:8px; z-index:30; }
18
+ #hudGearWrap { position:absolute; right:12px; bottom:12px; display:flex; gap:8px; align-items:center; z-index:30; }
19
+ .pickaxe-slot { width:46px; height:46px; background: rgba(255,255,255,0.03); border-radius:6px; display:flex; align-items:center; justify-content:center; cursor:pointer; }
20
+ .gear-slot { min-width:46px; height:36px; background: rgba(255,255,255,0.03); border-radius:6px; display:flex; align-items:center; justify-content:center; font-size:11px; color:#ddd; padding:4px; position:relative; cursor:pointer; }
21
+ .selected { outline: 2px solid rgba(255,215,0,0.9); box-shadow: 0 0 6px rgba(255,215,0,0.12); }
22
+ .equipped { box-shadow: inset 0 -6px 14px rgba(255,255,255,0.03); border:1px solid rgba(255,255,255,0.04); }
23
+ .medkit-count { position:absolute; right:4px; bottom:2px; font-size:10px; color:#ffd; }
 
 
24
  </style>
25
  </head>
26
  <body>
27
+ <!-- Landing -->
28
  <div id="landingScreen" class="flex flex-col items-center justify-center h-screen p-6">
29
  <h1 class="text-5xl font-bold text-yellow-400 mb-6 flex items-center">
30
  <i data-feather="target" class="mr-2"></i> BattleZone Royale
 
54
  </div>
55
  </div>
56
 
 
57
  <div class="bg-gray-800 p-4 rounded-lg max-w-xl w-full text-sm">
58
  <h3 class="font-bold text-yellow-400 mb-2">Controls</h3>
59
  <ul class="list-disc pl-5 space-y-1">
60
  <li><strong>WASD</strong> — Move</li>
61
+ <li><strong>Mouse</strong> — Aim</li>
62
+ <li><strong>Left Click</strong> — Shoot (if weapon equipped or in selected slot) or pickaxe melee</li>
63
+ <li><strong>E</strong> — Use medkit in selected slot OR Interact / Loot / Harvest</li>
64
  <li><strong>Q</strong> — Build (costs 10 materials)</li>
65
+ <li><strong>R</strong> — Reload selected weapon</li>
66
+ <li><strong>1-5</strong> — Select inventory slot</li>
67
+ <li><strong>F</strong> — Equip / Unequip selected slot (pickaxe is the default)</li>
68
  </ul>
69
  </div>
70
  </div>
71
 
72
+ <!-- Game -->
73
  <div id="gameScreen" class="hidden h-screen flex flex-col">
74
  <header class="flex justify-between items-center border-b border-yellow-500 px-6 py-4" style="flex-shrink:0;">
75
  <h1 class="text-3xl font-bold text-yellow-400 flex items-center"><i data-feather="target" class="mr-2"></i>BattleZone Royale</h1>
 
98
  </button>
99
  </div>
100
 
 
101
  <div id="hudHealth" class="hidden">
102
  <svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="#ffd86b" stroke-width="1.6"><path d="M20.8 8.6a5.5 5.5 0 0 0-7.8 0L12 10.6l-1-1a5.5 5.5 0 0 0-7.8 7.8l1 1L12 22l7.8-3.6 1-1a5.5 5.5 0 0 0 0-7.8z"></path></svg>
103
  <span id="hudHealthText">100%</span>
104
  </div>
105
 
106
+ <div id="hudGearWrap" class="hidden">
107
+ <div id="pickaxeSlot" class="pickaxe-slot" title="Pickaxe (press F to equip)">⛏️</div>
108
+ <div id="hudGear" aria-label="gear"></div>
109
+ </div>
110
  </div>
 
 
111
  </div>
112
  </div>
113
 
114
  <script>
115
+ // DOM
116
+ const landingScreen = document.getElementById('landingScreen');
117
+ const gameScreen = document.getElementById('gameScreen');
 
 
118
  const canvas = document.getElementById('gameCanvas');
119
  const ctx = canvas.getContext('2d');
120
+ const hudHealth = document.getElementById('hudHealth');
121
  const hudHealthText = document.getElementById('hudHealthText');
122
+ const hudGearWrap = document.getElementById('hudGearWrap');
123
+ const hudGear = document.getElementById('hudGear');
124
+ const pickaxeSlot = document.getElementById('pickaxeSlot');
125
+ const stormWarning = document.getElementById('stormWarning');
126
+ const deathScreen = document.getElementById('deathScreen');
127
+
128
+ // World (bigger)
129
+ const WORLD = { width: 6000, height: 4000 };
130
  let camera = { x:0, y:0 };
131
 
132
  function resizeCanvas(){
133
+ const ctn = document.getElementById('canvasContainer');
134
+ canvas.width = Math.max(900, Math.floor(ctn.clientWidth));
135
+ canvas.height = Math.max(560, Math.floor(ctn.clientHeight));
136
  cameraUpdate();
137
  }
138
  window.addEventListener('resize', resizeCanvas);
139
 
140
+ // Player (pickaxe separate from 5 inventory slots)
141
  const player = {
142
+ id:'player', x: WORLD.width/2, y: WORLD.height/2, radius:16, angle:0, speed:220,
143
+ health:100, armor:0, kills:0, materials:0,
144
+ inventory: [null,null,null,null,null], // 5 slots
145
+ selectedSlot:0,
146
+ equippedIndex: -1, // -1 = pickaxe equipped, >=0 = inventory slot equipped
147
+ lastShot:0, lastMelee:0
148
  };
149
 
150
+ // Input
151
+ const keys = { w:false,a:false,s:false,d:false,e:false,q:false,r:false,f:false };
152
  const mouse = { canvasX:0, canvasY:0, worldX:0, worldY:0, down:false };
153
 
154
+ window.addEventListener('keydown',(e)=>{
155
  const k = e.key.toLowerCase();
 
156
  if (k in keys) keys[k] = true;
157
+ if (['1','2','3','4','5'].includes(k)) { player.selectedSlot = parseInt(k)-1; updateHUD(); }
158
+ if (k === 'f') { player.equippedIndex = (player.equippedIndex === player.selectedSlot) ? -1 : player.selectedSlot; updateHUD(); }
159
+ if (k === 'r') keys.r = true;
 
160
  });
161
+ window.addEventListener('keyup',(e)=>{
162
  const k = e.key.toLowerCase();
 
163
  if (k in keys) keys[k] = false;
164
+ if (k === 'e') keys.e = true; // single-use flag
165
  if (k === 'q') keys.q = true;
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;
 
173
  mouse.worldY = mouse.canvasY + camera.y;
174
  player.angle = Math.atan2(mouse.worldY - player.y, mouse.worldX - player.x);
175
  });
176
+ canvas.addEventListener('mousedown', ()=> mouse.down = true);
177
  window.addEventListener('mouseup', ()=> mouse.down = false);
178
 
179
+ // Entities
180
  const bullets = [];
181
  const chests = [];
182
  const objects = []; // harvestables & walls
183
+ const enemies = [];
184
+ const pickups = []; // visible ground pickups
185
 
186
  function rand(min,max){ return Math.random()*(max-min)+min; }
187
  function randInt(min,max){ return Math.floor(Math.random()*(max-min))+min; }
188
 
189
  function biomeAt(x,y){
190
+ const bx = Math.floor(x / 500), by = Math.floor(y / 500);
191
  const seed = (bx*73856093) ^ (by*19349663);
192
  const r = Math.abs(Math.sin(seed)) % 1;
193
  if (r < 0.25) return 'desert';
 
196
  return 'ruins';
197
  }
198
 
199
+ // Weapon factory
200
+ function makeWeaponProto(w){
201
+ return { name:w.name, dmg:w.dmg, rate:w.rate, color:w.color, magSize:w.magSize || 12, startReserve:w.startReserve || (w.magSize*2 || 24) };
202
+ }
203
+
204
  function generateLootForBiome(b){
205
  const roll = Math.random();
206
+ if (roll < 0.35) return { type:'medkit', amount:1 };
207
+ if (roll < 0.7) return { type:'materials', amount: randInt(5,20) };
208
  const weapons = [
209
+ { name:'Pistol', dmg:12, rate:320, color:'#ffd86b', magSize:12, startReserve:24 },
210
+ { name:'SMG', dmg:6, rate:120, color:'#8ef0ff', magSize:30, startReserve:90 },
211
+ { name:'Shotgun', dmg:22, rate:800, color:'#ff9fb8', magSize:6, startReserve:18 },
212
+ { name:'Rifle', dmg:18, rate:400, color:'#c7ff9a', magSize:20, startReserve:60 }
213
  ];
214
  return { type:'weapon', weapon: weapons[randInt(0, weapons.length)] };
215
  }
216
 
217
+ // Populate world
218
  function populateWorld(){
219
+ chests.length = 0; objects.length = 0; enemies.length = 0; pickups.length = 0;
220
+ for (let i=0;i<260;i++){
 
 
 
 
221
  const x = rand(150, WORLD.width-150);
222
  const y = rand(150, WORLD.height-150);
223
  chests.push({ x,y, opened:false, loot: generateLootForBiome(biomeAt(x,y)) });
224
  }
 
 
225
  for (let i=0;i<700;i++){
226
  const t = Math.random();
227
  let type='wood';
 
231
  const hp = type==='wood'?40 : (type==='stone'?80:160);
232
  objects.push({ x,y, type, hp, maxHp:hp, dead:false });
233
  }
 
 
234
  for (let i=0;i<19;i++){
235
  const ex = rand(300, WORLD.width-300);
236
  const ey = rand(300, WORLD.height-300);
 
 
 
 
 
237
  enemies.push({
238
+ id:'e'+i, x:ex, y:ey, radius:14, angle:0, speed:110+rand(-20,20),
239
+ health: 80 + randInt(0,40), lastMelee:0, meleeRate:800 + randInt(-200,200),
240
+ roamTimer: rand(0,3), weaponPickup:null
241
  });
242
  }
243
  updatePlayerCount();
244
  }
245
 
246
+ // HUD init (pickaxe left of inventory)
247
  function initHUD(){
248
+ hudHealth.classList.remove('hidden');
249
+ hudGearWrap.classList.remove('hidden');
250
+ hudGear.innerHTML = '';
251
+ for (let i=0;i<5;i++){
252
  const slot = document.createElement('div');
253
  slot.className = 'gear-slot';
254
  slot.dataset.index = i;
255
+ slot.addEventListener('click', ()=> { player.selectedSlot = i; updateHUD(); });
256
+ hudGear.appendChild(slot);
257
  }
258
+ // pickaxe click toggles equip
259
+ pickaxeSlot.onclick = () => { player.equippedIndex = (player.equippedIndex === -1) ? player.selectedSlot : -1; updateHUD(); };
260
  updateHUD();
261
  }
262
+
263
  function updateHUD(){
264
  hudHealthText.textContent = `${Math.max(0,Math.floor(player.health))}%`;
265
+ const slots = hudGear.querySelectorAll('.gear-slot');
266
  slots.forEach(s => {
267
  const idx = parseInt(s.dataset.index);
268
+ const it = player.inventory[idx];
269
+ s.classList.toggle('selected', idx === player.selectedSlot);
270
+ s.classList.toggle('equipped', player.equippedIndex === idx);
271
+ if (!it) s.innerHTML = 'Empty';
272
+ else if (it.type === 'weapon') s.innerHTML = `<div style="text-align:center;"><div style="font-size:11px">${it.weapon.name}</div><div style="color:${it.weapon.color}">${it.ammoInMag}/${it.ammoReserve}</div></div>`;
273
+ else if (it.type === 'medkit') s.innerHTML = `<div style="font-size:12px">Med</div><div class="medkit-count">x${it.amount}</div>`;
274
+ else if (it.type === 'materials') s.innerHTML = `<div style="font-size:11px">Mat</div><div class="medkit-count">x${it.amount}</div>`;
275
+ else s.innerHTML = 'Item';
276
  });
277
+ // pickaxe highlight when equipped
278
+ pickaxeSlot.classList.toggle('selected', player.equippedIndex === -1);
279
+ // also indicate pickaxe equipped visually
280
+ pickaxeSlot.title = (player.equippedIndex === -1) ? 'Pickaxe (equipped)' : 'Pickaxe (click or press F to equip)';
281
  feather.replace();
282
  }
283
 
284
+ // Camera
285
  function cameraUpdate(){
286
  if (!canvas.width || !canvas.height) return;
287
+ camera.x = player.x - canvas.width/2;
288
+ camera.y = player.y - canvas.height/2;
289
+ camera.x = Math.max(0, Math.min(camera.x, WORLD.width - canvas.width));
290
+ camera.y = Math.max(0, Math.min(camera.y, WORLD.height - canvas.height));
 
 
 
 
 
 
291
  }
292
  function worldToScreen(wx,wy){ return { x: Math.round(wx - camera.x), y: Math.round(wy - camera.y) }; }
293
 
294
+ // Shooting, reload, melee
295
+ function shootBullet(originX, originY, targetX, targetY, weaponObj, shooterId){
296
+ if (!weaponObj || weaponObj.ammoInMag <= 0) return false;
297
  const speed = 1100;
298
  const angle = Math.atan2(targetY-originY, targetX-originX);
299
+ weaponObj.ammoInMag -= 1;
300
  bullets.push({
301
  x: originX, y: originY, vx: Math.cos(angle)*speed, vy: Math.sin(angle)*speed,
302
+ dmg: weaponObj.weapon.dmg, color: weaponObj.weapon.color, life:1.6, traveled:0,
303
  shooter: shooterId, born: performance.now(), tracer:false
304
  });
305
+ return true;
306
+ }
307
+
308
+ function reloadEquipped(){
309
+ let item = null;
310
+ if (player.equippedIndex >= 0) item = player.inventory[player.equippedIndex];
311
+ else item = player.inventory[player.selectedSlot];
312
+ if (!item || item.type !== 'weapon') return;
313
+ const need = item.weapon.magSize - item.ammoInMag;
314
+ if (need <= 0 || item.ammoReserve <= 0) return;
315
+ const take = Math.min(need, item.ammoReserve);
316
+ item.ammoInMag += take;
317
+ item.ammoReserve -= take;
318
+ updateHUD();
319
  }
320
 
321
+ function playerMeleeHit(){
322
+ const now = performance.now();
323
+ if (now - player.lastMelee < 350) return;
324
+ player.lastMelee = now;
325
+ // damage enemies and objects (pickaxe)
326
+ for (const e of enemies){
327
+ if (e.health <= 0) continue;
328
+ const d = Math.hypot(e.x - player.x, e.y - player.y);
329
+ if (d < 36){ e.health -= 18; if (e.health <= 0) { e.health = 0; player.kills++; player.materials += 2; updatePlayerCount(); } }
330
+ }
331
+ for (const obj of objects){
332
+ if (obj.dead) continue;
333
+ const d = Math.hypot(obj.x - player.x, obj.y - player.y);
334
+ if (d < 36){
335
+ obj.hp -= 20;
336
+ if (obj.hp <= 0){ obj.dead = true; player.materials += (obj.type === 'wood' ? 3 : 6); }
337
+ }
338
+ }
339
+ }
340
+
341
+ // Interact: use medkit in selected slot OR loot chests / harvest (pickups)
342
  function interactNearby(){
343
+ // use medkit in selected slot
344
+ const sel = player.selectedSlot;
345
+ const selItem = player.inventory[sel];
346
+ if (selItem && selItem.type === 'medkit'){
347
+ selItem.amount -= 1;
348
+ player.health = Math.min(100, player.health + 50);
349
+ if (selItem.amount <= 0) player.inventory[sel] = null;
350
+ updateHUD();
351
+ return;
352
+ }
353
+
354
  const range = 56;
355
+ // chests
356
  for (const chest of chests){
357
  if (chest.opened) continue;
358
  const d = Math.hypot(chest.x - player.x, chest.y - player.y);
359
  if (d < range){
360
  chest.opened = true;
361
+ // spawn pickup on ground (weapon/medkit/materials/ammo)
362
  const loot = chest.loot;
363
+ const px = chest.x + rand(-20,20), py = chest.y + rand(-20,20);
364
+ if (loot.type === 'weapon'){
365
+ pickups.push({ x:px, y:py, type:'weapon', weapon: makeWeaponProto(loot.weapon), ammoInMag: loot.weapon.magSize || 12, ammoReserve: loot.weapon.startReserve || (loot.weapon.magSize*2 || 24) });
366
+ } else if (loot.type === 'medkit') {
367
+ pickups.push({ x:px, y:py, type:'medkit', amount: loot.amount || 1 });
368
+ } else if (loot.type === 'materials'){
369
+ pickups.push({ x:px, y:py, type:'materials', amount: loot.amount || 5 });
370
+ }
371
+ // some chests drop ammo too sometimes
372
+ if (Math.random() < 0.25){
373
+ pickups.push({ x:px+8, y:py+8, type:'ammo', forWeapon: null, amount: randInt(6,30) });
374
  }
 
 
375
  updateHUD();
376
  return;
377
  }
378
  }
379
+
380
+ // harvest objects (only via pickaxe)
381
  for (const obj of objects){
382
  if (obj.dead) continue;
383
  const d = Math.hypot(obj.x - player.x, obj.y - player.y);
384
  if (d < range){
385
+ // only harvest (break) if using pickaxe (equippedIndex === -1) AND clicking E triggers same as swing
386
+ if (player.equippedIndex === -1){
387
+ const gain = obj.type === 'wood' ? 2 : 5;
388
+ player.materials += gain;
389
+ obj.dead = true;
390
+ updateHUD();
391
+ return;
392
+ }
393
+ }
394
+ }
395
+
396
+ // pick up ground pickups if near
397
+ for (let i=pickups.length-1;i>=0;i--){
398
+ const p = pickups[i];
399
+ const d = Math.hypot(p.x - player.x, p.y - player.y);
400
+ if (d < range){
401
+ pickupCollect(p);
402
+ pickups.splice(i,1);
403
  updateHUD();
404
  return;
405
  }
406
  }
407
  }
408
 
409
+ function pickupCollect(p){
410
+ if (p.type === 'weapon'){
411
+ // place in first empty or merge ammo
412
+ let merged=false;
413
+ for (let s=0;s<5;s++){
414
+ const it = player.inventory[s];
415
+ if (it && it.type==='weapon' && it.weapon.name === p.weapon.name){
416
+ it.ammoReserve += p.ammoReserve;
417
+ it.ammoInMag = Math.min(it.weapon.magSize, it.ammoInMag + p.ammoInMag);
418
+ merged=true; break;
419
+ }
420
+ }
421
+ if (!merged){
422
+ let placed=false;
423
+ for (let s=0;s<5;s++){ if (!player.inventory[s]) { player.inventory[s] = { type:'weapon', weapon:p.weapon, ammoInMag:p.ammoInMag, ammoReserve:p.ammoReserve }; placed=true; break; } }
424
+ if (!placed) player.inventory[player.selectedSlot] = { type:'weapon', weapon:p.weapon, ammoInMag:p.ammoInMag, ammoReserve:p.ammoReserve };
425
+ }
426
+ } else if (p.type === 'medkit'){
427
+ let stacked=false;
428
+ for (let s=0;s<5;s++){ const it=player.inventory[s]; if (it && it.type==='medkit'){ it.amount += p.amount; stacked=true; break; } }
429
+ if (!stacked){ let placed=false; for (let s=0;s<5;s++){ if (!player.inventory[s]) { player.inventory[s] = { type:'medkit', amount:p.amount }; placed=true; break; } } if (!placed) player.materials += 5; }
430
+ } else if (p.type === 'materials'){
431
+ player.materials += p.amount;
432
+ } else if (p.type === 'ammo'){
433
+ // add ammo to any weapon or to materials as fallback
434
+ let added=false;
435
+ for (let s=0;s<5;s++){ const it=player.inventory[s]; if (it && it.type==='weapon'){ it.ammoReserve += p.amount; added=true; break; } }
436
+ if (!added) player.materials += p.amount;
437
+ }
438
+ }
439
+
440
+ // Build Q
441
  function tryBuild(){
442
  if (player.materials < 10) return;
443
  player.materials -= 10;
444
+ const bx = player.x + Math.cos(player.angle) * 48;
445
+ const by = player.y + Math.sin(player.angle) * 48;
446
  objects.push({ x:bx, y:by, type:'wall', hp:160, maxHp:160, dead:false });
447
  updateHUD();
448
  }
449
 
450
+ // LOS helper
451
  function hasLineOfSight(x1,y1,x2,y2){
 
452
  const vx = x2 - x1, vy = y2 - y1;
453
  const vlen2 = vx*vx + vy*vy;
454
  for (const obj of objects){
455
  if (obj.dead) continue;
456
+ let br=0;
457
+ if (obj.type==='wall') br=28; else if (obj.type==='stone') br=22; else if (obj.type==='wood') br=16; else continue;
 
 
 
 
458
  const wx = obj.x - x1, wy = obj.y - y1;
459
  const c1 = vx*wx + vy*wy;
460
+ const t = vlen2>0 ? c1/vlen2 : 0;
461
  if (t < 0 || t > 1) continue;
462
  const projx = x1 + vx * t, projy = y1 + vy * t;
463
  const dist = Math.hypot(projx - obj.x, projy - obj.y);
464
+ if (dist < br) return false;
465
  }
 
466
  return true;
467
  }
468
 
469
+ // Bullets update: bullets do NOT break wood/stone (only walls)
470
  function bulletsUpdate(dt){
471
  for (let i=bullets.length-1;i>=0;i--){
472
  const b = bullets[i];
473
  b.x += b.vx * dt; b.y += b.vy * dt;
474
  b.traveled += Math.hypot(b.vx*dt, b.vy*dt);
475
  b.life -= dt;
476
+ // hit player
 
 
477
  if (b.dmg > 0 && b.shooter !== 'player'){
478
+ if (Math.hypot(player.x - b.x, player.y - b.y) < 16){
479
  player.health -= b.dmg;
480
  if (player.health <= 0){ player.health = 0; playerDeath(); }
481
  bullets.splice(i,1); continue;
482
  }
483
  }
484
+ // hit enemies
 
485
  if (b.dmg > 0){
486
  for (const e of enemies){
487
  if (e.health <= 0) continue;
488
+ if (b.shooter === e.id) continue;
489
+ if (Math.hypot(e.x - b.x, e.y - b.y) < 14){
490
  e.health -= b.dmg;
491
+ if (e.health <= 0){ e.health = 0; if (b.shooter === 'player'){ player.kills++; player.materials += 2; updatePlayerCount(); } }
 
 
 
 
 
492
  bullets.splice(i,1); break;
493
  }
494
  }
495
  if (!bullets[i]) continue;
496
  }
 
497
  // collision with objects & chests
498
  if (!b.tracer && b.dmg > 0){
499
  for (const obj of objects){
500
  if (obj.dead) continue;
501
+ if (obj.type === 'wall' && Math.hypot(obj.x - b.x, obj.y - b.y) < 18){
502
  obj.hp -= b.dmg;
503
+ if (obj.hp <= 0 && !obj.dead){ obj.dead = true; if (b.shooter === 'player') player.materials += 6; }
 
 
 
504
  bullets.splice(i,1); break;
505
  }
506
  }
 
508
  for (const chest of chests){
509
  if (!chest.opened && Math.hypot(chest.x - b.x, chest.y - b.y) < 18){
510
  chest.opened = true;
511
+ // spawn pickup from chest
512
+ const loot = chest.loot;
513
+ const px = chest.x + rand(-20,20), py = chest.y + rand(-20,20);
514
+ if (loot.type === 'weapon') pickups.push({ x:px, y:py, type:'weapon', weapon: makeWeaponProto(loot.weapon), ammoInMag: loot.weapon.magSize || 12, ammoReserve: loot.weapon.startReserve || (loot.weapon.magSize*2 || 24) });
515
+ else if (loot.type === 'medkit') pickups.push({ x:px, y:py, type:'medkit', amount: loot.amount || 1 });
516
+ else if (loot.type === 'materials') pickups.push({ x:px, y:py, type:'materials', amount: loot.amount || 5 });
517
  bullets.splice(i,1); break;
518
  }
519
  }
520
  if (!bullets[i]) continue;
521
  }
522
+ if (b.life <= 0 || b.traveled > 2600) bullets.splice(i,1);
 
523
  }
524
  }
525
 
526
+ // Enemies: can loot chests; shooting only when within 30px and LOS; they can pick up weapons and use them.
527
  function updateEnemies(dt, now){
528
  for (const e of enemies){
529
  if (e.health <= 0) continue;
530
  e.roamTimer -= dt;
531
+ // choose target (player or nearby enemy)
 
532
  let target = player;
533
  let bestDist = Math.hypot(player.x - e.x, player.y - e.y);
 
534
  for (const other of enemies){
535
  if (other === e || other.health <= 0) continue;
536
  const d = Math.hypot(other.x - e.x, other.y - e.y);
537
+ if (d < bestDist && Math.random() < 0.6){ bestDist = d; target = other; }
 
538
  }
539
+ // move
 
540
  if (bestDist < 900 || e.roamTimer <= 0){
541
  e.angle = Math.atan2(target.y - e.y, target.x - e.x);
542
  const avoid = e.health < 20 && Math.random() < 0.6;
 
544
  e.x += Math.cos(e.angle) * e.speed * dt * moveDir;
545
  e.y += Math.sin(e.angle) * e.speed * dt * moveDir;
546
  } else {
 
547
  e.x += Math.cos(e.angle) * e.speed * dt * 0.25;
548
  e.y += Math.sin(e.angle) * e.speed * dt * 0.25;
549
  if (Math.random() < 0.01) e.angle += (Math.random()-0.5)*2;
550
  }
 
 
551
  e.x = Math.max(12, Math.min(WORLD.width-12, e.x));
552
  e.y = Math.max(12, Math.min(WORLD.height-12, e.y));
553
 
554
+ // enemy loot chests if close
555
+ for (const chest of chests){
556
+ if (chest.opened) continue;
557
+ const d = Math.hypot(chest.x - e.x, chest.y - e.y);
558
+ if (d < 24){
559
+ chest.opened = true;
560
+ const loot = chest.loot;
561
+ const px = chest.x + rand(-12,12), py = chest.y + rand(-12,12);
562
+ if (loot.type === 'weapon') pickups.push({ x:px, y:py, type:'weapon', weapon: makeWeaponProto(loot.weapon), ammoInMag: loot.weapon.magSize || 12, ammoReserve: loot.weapon.startReserve || (loot.weapon.magSize*2 || 24) });
563
+ else if (loot.type === 'medkit') pickups.push({ x:px, y:py, type:'medkit', amount: loot.amount || 1 });
564
+ else if (loot.type === 'materials') pickups.push({ x:px, y:py, type:'materials', amount: loot.amount || 5 });
565
+ }
566
+ }
567
+
568
+ // enemies pick up nearby pickups automatically
569
+ for (let i=pickups.length-1;i>=0;i--){
570
+ const p = pickups[i];
571
+ if (Math.hypot(p.x - e.x, p.y - e.y) < 18){
572
+ if (p.type === 'weapon' && !e.weaponPickup){
573
+ e.weaponPickup = { weapon:p.weapon, ammoInMag:p.ammoInMag, ammoReserve:p.ammoReserve };
574
+ pickups.splice(i,1);
575
+ } else if (p.type === 'medkit'){
576
+ if (e.health < 60) e.health = Math.min(120, e.health + 50);
577
+ pickups.splice(i,1);
578
+ } else pickups.splice(i,1);
579
+ }
580
+ }
581
+
582
+ // attack: melee if close; ranged only if they have weapon and within 30px AND LOS
583
  const distToTarget = Math.hypot(target.x - e.x, target.y - e.y);
584
+ if (distToTarget < 34 && now - e.lastMelee > e.meleeRate){
585
+ e.lastMelee = now;
586
+ const dmg = 10 + randInt(0,8);
587
+ if (target === player){
588
+ player.health -= dmg;
589
+ if (player.health <= 0) { player.health = 0; playerDeath(); }
590
+ } else {
591
+ target.health -= dmg;
592
+ if (target.health <= 0) target.health = 0;
593
+ }
594
+ } else if (e.weaponPickup && distToTarget <= 30 && now - (e.lastShot||0) > (e.weaponPickup.weapon.rate || 300)){
595
+ if (hasLineOfSight(e.x, e.y, target.x, target.y) && e.weaponPickup.ammoInMag > 0){
596
  e.lastShot = now;
597
+ e.weaponPickup.ammoInMag -= 1;
598
+ shootBullet(e.x + Math.cos(e.angle)*12, e.y + Math.sin(e.angle)*12, target.x + (Math.random()-0.5)*6, target.y + (Math.random()-0.5)*6, e.weaponPickup, e.id);
 
 
599
  }
600
  }
601
  }
602
  }
603
 
604
+ // Storm: slower, damages enemies too
605
+ const storm = { maxRadius: 2400, radius:2400, centerX: WORLD.width/2, centerY: WORLD.height/2, damagePerSecond:1, closingSpeed: 0.6, active:false };
606
+ let stormDamageAccumulator = 0;
607
+ function playerInStorm(){ return Math.hypot(player.x - storm.centerX, player.y - storm.centerY) > storm.radius; }
608
+ function updateStorm(dt){
609
+ if (!storm.active) return;
610
+ storm.radius -= storm.closingSpeed * dt * 60;
611
+ if (storm.radius < 120) storm.radius = 120;
612
+ // damage player
613
+ if (playerInStorm()){
614
+ const prog = 1 - storm.radius / storm.maxRadius;
615
+ const rate = storm.damagePerSecond * (1 + prog*4);
616
+ stormDamageAccumulator += rate * dt;
617
+ while (stormDamageAccumulator >= 1){
618
+ stormDamageAccumulator -=1;
619
+ player.health -= 1;
620
+ if (player.health <= 0){ player.health = 0; playerDeath(); }
621
+ }
622
+ } else stormDamageAccumulator = 0;
623
+ // damage enemies
624
+ for (const e of enemies){
625
+ if (e.health <= 0) continue;
626
+ const d = Math.hypot(e.x - storm.centerX, e.y - storm.centerY);
627
+ if (d > storm.radius){
628
+ e.health -= 8 * dt;
629
+ if (e.health <= 0){ e.health = 0; updatePlayerCount(); }
630
+ }
631
+ }
632
+ }
633
+
634
+ // Utility
635
  function updatePlayerCount(){
636
+ const alive = enemies.filter(e => e.health > 0).length;
637
+ document.getElementById('playerCount').textContent = `${1 + alive}/20`;
638
  }
639
 
640
+ // Drawing (chests no text; glow; pickups visible)
641
  function drawWorld(){
642
+ const TILE = 600;
643
  const cols = Math.ceil(WORLD.width / TILE);
644
  const rows = Math.ceil(WORLD.height / TILE);
645
+ for (let by=0;by<rows;by++){
646
+ for (let bx=0;bx<cols;bx++){
647
+ const x=bx*TILE,y=by*TILE; const b=biomeAt(x+1,y+1);
648
+ let color='#203a2b';
649
+ if (b==='desert') color='#cbb78b';
650
+ else if (b==='forest') color='#16411f';
651
+ else if (b==='oasis') color='#274b52';
652
+ else if (b==='ruins') color='#4a3b3b';
 
653
  const s = worldToScreen(x,y);
654
+ ctx.fillStyle = color; ctx.fillRect(s.x,s.y,TILE,TILE);
655
+ ctx.strokeStyle = 'rgba(0,0,0,0.08)'; ctx.strokeRect(s.x,s.y,TILE,TILE);
656
  }
657
  }
 
658
  if (storm.active){
659
+ const sc = worldToScreen(storm.centerX, storm.centerY);
660
  ctx.save();
661
+ const grad = ctx.createRadialGradient(sc.x, sc.y, storm.radius*0.15, sc.x, sc.y, storm.radius);
662
+ grad.addColorStop(0,'rgba(100,149,237,0.02)'); grad.addColorStop(1,'rgba(100,149,237,0.45)');
663
+ ctx.fillStyle = grad; ctx.beginPath(); ctx.arc(sc.x, sc.y, storm.radius, 0, Math.PI*2); ctx.fill();
664
+ ctx.strokeStyle = 'rgba(255,200,80,0.9)'; ctx.lineWidth=4; ctx.beginPath(); ctx.arc(sc.x, sc.y, storm.radius, 0, Math.PI*2); ctx.stroke();
 
 
 
 
 
 
665
  ctx.restore();
666
  }
667
  }
 
671
  if (obj.dead) continue;
672
  const s = worldToScreen(obj.x, obj.y);
673
  ctx.save();
674
+ const h = obj.type==='wood'?18:(obj.type==='stone'?12:28);
675
+ ctx.fillStyle='rgba(0,0,0,0.18)'; ctx.beginPath(); ctx.ellipse(s.x,s.y+8,18,8,0,0,Math.PI*2); ctx.fill();
676
+ ctx.fillStyle = obj.type==='wood'? '#6b3b1a' : (obj.type==='stone'? '#6b6b6b' : '#8b5a32');
 
 
 
677
  ctx.fillRect(s.x-12, s.y-h, 24, h);
678
  ctx.restore();
679
  }
 
685
  if (chest.opened) continue;
686
  const s = worldToScreen(chest.x, chest.y);
687
  ctx.save();
688
+ const glowRadius = 28 + Math.sin(now/300 + chest.x*0.001)*6;
 
689
  const g = ctx.createRadialGradient(s.x, s.y-6, 6, s.x, s.y-6, glowRadius);
690
+ g.addColorStop(0,'rgba(255,215,0,0.95)'); g.addColorStop(0.6,'rgba(255,215,0,0.25)'); g.addColorStop(1,'rgba(255,215,0,0.00)');
691
+ ctx.fillStyle = g; ctx.beginPath(); ctx.arc(s.x, s.y-6, glowRadius, 0, Math.PI*2); ctx.fill();
692
+ ctx.fillStyle='#a56b2a'; ctx.fillRect(s.x-18,s.y-12,36,20);
693
+ ctx.fillStyle='#caa15e'; ctx.fillRect(s.x-18,s.y-20,36,8);
694
+ ctx.restore();
695
+ }
696
+ }
697
+
698
+ function drawPickups(){
699
+ for (const p of pickups){
700
+ const s = worldToScreen(p.x,p.y);
701
  ctx.save();
702
+ const glowRadius = 14 + Math.sin(performance.now()/250 + p.x*0.001)*4;
703
+ if (p.type === 'weapon'){
704
+ const g = ctx.createRadialGradient(s.x,s.y,2,s.x,s.y,glowRadius);
705
+ g.addColorStop(0,'rgba(255,255,255,0.95)'); g.addColorStop(0.6, `${p.weapon.color}33`); g.addColorStop(1,'rgba(255,255,255,0)');
706
+ ctx.fillStyle = g; ctx.beginPath(); ctx.arc(s.x,s.y,glowRadius,0,Math.PI*2); ctx.fill();
707
+ ctx.fillStyle = p.weapon.color || '#ffd86b'; ctx.fillRect(s.x-10,s.y-6,20,12);
708
+ } else if (p.type === 'medkit'){
709
+ ctx.fillStyle = '#ff6b6b'; ctx.beginPath(); ctx.arc(s.x,s.y,10,0,Math.PI*2); ctx.fill();
710
+ ctx.fillStyle = '#fff'; ctx.font='10px monospace'; ctx.textAlign='center'; ctx.fillText('MED', s.x, s.y+2);
711
+ } else if (p.type === 'materials'){
712
+ ctx.fillStyle = '#cfe0a6'; ctx.fillRect(s.x-8,s.y-8,16,16);
713
+ ctx.fillStyle = '#000'; ctx.font='10px monospace'; ctx.textAlign='center'; ctx.fillText('MAT', s.x, s.y+2);
714
+ } else if (p.type === 'ammo'){
715
+ ctx.fillStyle = '#e6e6e6'; ctx.fillRect(s.x-6,s.y-6,12,12);
716
+ ctx.fillStyle = '#000'; ctx.font='9px monospace'; ctx.textAlign='center'; ctx.fillText('AM', s.x, s.y+2);
717
+ }
718
  ctx.restore();
719
+ }
720
+ }
721
+
722
+ function drawEnemies(){
723
+ for (const e of enemies){
724
+ if (e.health <= 0) continue;
725
+ const s = worldToScreen(e.x,e.y);
726
+ ctx.save();
727
+ ctx.translate(s.x,s.y); ctx.rotate(e.angle);
728
+ ctx.fillStyle='rgba(0,0,0,0.18)'; ctx.beginPath(); ctx.ellipse(0,12,14,6,0,0,Math.PI*2); ctx.fill();
729
+ ctx.fillStyle='#ff6b6b'; ctx.beginPath(); ctx.moveTo(12,0); ctx.lineTo(-10,-8); ctx.lineTo(-10,8); ctx.closePath(); ctx.fill();
730
+ ctx.fillStyle='rgba(0,0,0,0.6)'; ctx.fillRect(-18,-22,36,6);
731
+ const hpPct = Math.max(0, e.health/120);
732
+ ctx.fillStyle='#ff6b6b'; ctx.fillRect(-18,-22,36*hpPct,6);
733
+ if (e.weaponPickup) { ctx.fillStyle = e.weaponPickup.weapon.color || '#fff'; ctx.fillRect(-10,12,8,6); }
734
  ctx.restore();
735
  }
736
  }
737
 
738
  function drawBullets(){
739
  for (const b of bullets){
740
+ const s = worldToScreen(b.x,b.y);
741
  ctx.save();
742
+ if (b.tracer){ ctx.fillStyle = b.color; ctx.globalAlpha=0.9; ctx.beginPath(); ctx.arc(s.x,s.y,3,0,Math.PI*2); ctx.fill(); }
743
+ else {
 
744
  const backX = s.x - (b.vx * 0.02);
745
  const backY = s.y - (b.vy * 0.02);
746
+ const grad = ctx.createLinearGradient(backX,backY,s.x,s.y);
747
+ grad.addColorStop(0,'rgba(255,255,255,0)'); grad.addColorStop(0.7,b.color); grad.addColorStop(1,'#fff');
748
+ ctx.strokeStyle = grad; ctx.lineWidth = 3; ctx.beginPath(); ctx.moveTo(backX,backY); ctx.lineTo(s.x,s.y); ctx.stroke();
749
+ ctx.fillStyle = '#fff'; ctx.beginPath(); ctx.arc(s.x,s.y,2.5,0,Math.PI*2); ctx.fill();
750
  }
751
  ctx.restore();
752
  }
753
  }
754
 
755
  function drawPlayer(){
756
+ const s = worldToScreen(player.x,player.y);
757
  ctx.save();
758
+ ctx.translate(s.x,s.y); ctx.rotate(player.angle);
759
+ ctx.fillStyle='rgba(0,0,0,0.25)'; ctx.beginPath(); ctx.ellipse(0,14,18,8,0,0,Math.PI*2); ctx.fill();
760
+ ctx.fillStyle='yellow'; ctx.beginPath(); ctx.moveTo(18,0); ctx.lineTo(-12,-10); ctx.lineTo(-12,10); ctx.closePath(); ctx.fill();
761
  ctx.restore();
762
  }
763
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
764
  function drawCrosshair(){
765
  const screenX = mouse.canvasX, screenY = mouse.canvasY;
766
+ ctx.save(); ctx.strokeStyle='yellow'; ctx.lineWidth=2; ctx.beginPath();
767
+ ctx.moveTo(screenX-8,screenY); ctx.lineTo(screenX+8,screenY);
768
+ ctx.moveTo(screenX,screenY-8); ctx.lineTo(screenX,screenY+8); ctx.stroke(); ctx.restore();
769
  }
770
 
771
+ // Main loop
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
772
  let lastTime = 0;
773
  function gameLoop(ts){
774
  if (!gameActive) return;
775
  if (!lastTime) lastTime = ts;
776
+ const dt = Math.min(0.05, (ts - lastTime)/1000);
777
  lastTime = ts;
778
 
779
+ // movement
780
  let dx=0, dy=0;
781
+ if (keys.w) dy -= 1; if (keys.s) dy += 1; if (keys.a) dx -= 1; if (keys.d) dx += 1;
 
 
 
782
  if (dx !== 0 || dy !== 0){
783
  const len = Math.hypot(dx,dy) || 1;
784
  player.x += (dx/len) * player.speed * dt;
 
787
  player.x = Math.max(16, Math.min(WORLD.width-16, player.x));
788
  player.y = Math.max(16, Math.min(WORLD.height-16, player.y));
789
 
 
790
  cameraUpdate();
791
  mouse.worldX = mouse.canvasX + camera.x;
792
  mouse.worldY = mouse.canvasY + camera.y;
793
 
794
+ // determine active weapon: prefer equipped slot if >=0; else if selectedSlot has weapon allow firing from selected slot; if equippedIndex === -1 it's pickaxe
795
+ let activeWeaponItem = null;
796
+ if (player.equippedIndex >= 0) activeWeaponItem = player.inventory[player.equippedIndex];
797
+ else {
798
+ const selected = player.inventory[player.selectedSlot];
799
+ if (selected && selected.type === 'weapon') activeWeaponItem = selected;
800
+ }
801
+
802
+ // shooting or melee on mouse down
803
  if (mouse.down){
804
+ if (player.equippedIndex === -1){
805
+ // pickaxe melee
806
+ playerMeleeHit();
807
+ } else if (activeWeaponItem && activeWeaponItem.type === 'weapon'){
808
+ const now = performance.now();
809
+ if (now - player.lastShot > (activeWeaponItem.weapon.rate || 300)){
810
+ if (activeWeaponItem.ammoInMag > 0){
811
+ player.lastShot = now;
812
+ shootBullet(player.x + Math.cos(player.angle)*18, player.y + Math.sin(player.angle)*18, mouse.worldX, mouse.worldY, activeWeaponItem, 'player');
813
+ updateHUD();
814
+ } else {
815
+ // out of mag - do nothing until reload with R
816
+ }
817
+ }
818
  }
819
  }
820
 
821
+ // reload
822
+ if (keys.r) { reloadEquipped(); keys.r = false; }
823
+
824
+ // interact/use
825
  if (keys.e){ interactNearby(); keys.e = false; }
826
+
827
+ // build
828
  if (keys.q){ tryBuild(); keys.q = false; }
829
 
830
+ // enemy AI and bullets
831
  updateEnemies(dt, performance.now());
 
 
832
  bulletsUpdate(dt);
833
 
834
+ // pickup auto-collect when standing on them
835
+ for (let i=pickups.length-1;i>=0;i--){
836
+ const p = pickups[i];
837
+ if (Math.hypot(p.x - player.x, p.y - player.y) < 18){ pickupCollect(p); pickups.splice(i,1); updateHUD(); }
838
+ }
839
+
840
+ // clean dead objects
841
  for (let i=objects.length-1;i>=0;i--) if (objects[i].dead) objects.splice(i,1);
842
 
843
  updatePlayerCount();
 
 
844
  updateStorm(dt);
845
 
846
  // render
847
  ctx.clearRect(0,0,canvas.width,canvas.height);
848
+ drawWorld(); drawObjects(); drawChests(); drawPickups(); drawEnemies(); drawBullets(); drawPlayer(); drawCrosshair();
 
 
 
 
 
 
 
 
849
  updateHUD();
850
 
851
  // storm warning
852
+ if (storm.active && playerInStorm()) stormWarning.classList.remove('hidden'); else stormWarning.classList.add('hidden');
853
 
854
  requestAnimationFrame(gameLoop);
855
  }
856
 
857
+ // Start/end
858
  let timerInterval = null;
859
  let gameTime = 300;
860
+ let gameActive = false;
861
+
862
  function startGame(){
863
  gameActive = true;
864
+ landingScreen.classList.add('hidden');
865
+ gameScreen.classList.remove('hidden');
866
  resizeCanvas();
867
 
 
868
  player.x = WORLD.width/2 + (Math.random()-0.5)*400;
869
  player.y = WORLD.height/2 + (Math.random()-0.5)*400;
870
  player.health = 100; player.armor = 0; player.kills = 0; player.materials = 0;
871
+ player.inventory = [null,null,null,null,null];
872
+ player.selectedSlot = 0; player.equippedIndex = -1; player.lastShot = 0; player.lastMelee = 0;
873
 
874
  populateWorld();
875
  initHUD();
876
  cameraUpdate();
877
 
878
+ // timer & storm: pick random center at activation
879
+ gameTime = 300; storm.active = false; storm.radius = storm.maxRadius;
 
 
880
  document.getElementById('gameTimer').textContent = '5:00';
881
  timerInterval && clearInterval(timerInterval);
882
  timerInterval = setInterval(()=>{
 
884
  gameTime--;
885
  const m = Math.floor(gameTime/60), s = gameTime%60;
886
  document.getElementById('gameTimer').textContent = `${m}:${s<10?'0'+s:s}`;
 
887
  if (gameTime === 240 && !storm.active){
888
  storm.active = true;
889
  storm.centerX = rand(400, WORLD.width-400);
890
  storm.centerY = rand(400, WORLD.height-400);
891
  storm.radius = storm.maxRadius;
892
+ stormWarning.classList.remove('hidden');
893
+ setTimeout(()=>stormWarning.classList.add('hidden'),4000);
894
  }
895
  if (gameTime <= 0){ clearInterval(timerInterval); endGame(); }
896
  }, 1000);
 
899
  requestAnimationFrame(gameLoop);
900
  }
901
 
902
+ function endGame(){ gameActive = false; alert('Match over!'); }
903
+ function playerDeath(){ gameActive = false; deathScreen.classList.remove('hidden'); }
 
 
904
 
905
+ document.getElementById('respawnBtn').addEventListener('click', ()=>{ deathScreen.classList.add('hidden'); landingScreen.classList.remove('hidden'); });
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
906
 
907
+ document.querySelectorAll('.biome-selector').forEach(el => el.addEventListener('click', ()=> startGame()));
908
+ window.addEventListener('keydown', (e)=>{ const k = e.key.toLowerCase(); if (!gameActive) return; if (k==='e') keys.e = true; if (k==='q') keys.q = true; if (k==='f') { player.equippedIndex = (player.equippedIndex === player.selectedSlot) ? -1 : player.selectedSlot; updateHUD(); } if (k==='r') keys.r = true; });
 
 
 
 
909
 
910
+ // init
911
  resizeCanvas();
912
  populateWorld();
913
  feather.replace();