bbc123321 commited on
Commit
7da48a4
·
verified ·
1 Parent(s): c2c84b6

Manual changes saved

Browse files
Files changed (1) hide show
  1. index.html +210 -80
index.html CHANGED
@@ -3,7 +3,7 @@
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>
@@ -169,37 +169,27 @@
169
 
170
  // Helper: equip slot index (or -1 for pickaxe)
171
  function equipSlot(index){
172
- // index: -1 for pickaxe, 0-4 for inventory
173
  player.equippedIndex = index;
174
- // if equipping a slot that is empty, still equip it (shows nothing but slot is 'equipped')
175
  updateHUD();
176
  }
177
 
178
  window.addEventListener('keydown',(e)=>{
179
  const k = e.key.toLowerCase();
180
-
181
- // Movement & action keys only processed when game active
182
  if (gameActive){
183
  if (k in keys) keys[k] = true;
184
-
185
  if (['1','2','3','4','5'].includes(k)) {
186
  const idx = parseInt(k)-1;
187
  player.selectedSlot = idx;
188
- // Equip the pressed slot (this ensures weapons swap immediately)
189
  equipSlot(idx);
190
  updateHUD();
191
  }
192
-
193
  if (k === 'f') {
194
- // toggle between pickaxe and selected slot
195
  if (player.equippedIndex === player.selectedSlot) equipSlot(-1);
196
  else equipSlot(player.selectedSlot);
197
  updateHUD();
198
  }
199
-
200
  if (k === 'r') keys.r = true;
201
  } else {
202
- // allow selection on landing screen
203
  if (['1','2','3','4','5'].includes(k)) {
204
  player.selectedSlot = parseInt(k)-1;
205
  updateHUD();
@@ -294,7 +284,14 @@
294
  enemies.push({
295
  id:'e'+i, x:ex, y:ey, radius:14, angle:0, speed:110+rand(-20,20),
296
  health: 80 + randInt(0,40), lastMelee:0, meleeRate:800 + randInt(-200,200),
297
- roamTimer: rand(0,3), weaponPickup:null, lastShot:0
 
 
 
 
 
 
 
298
  });
299
  }
300
  updatePlayerCount();
@@ -312,17 +309,14 @@
312
  slot.addEventListener('click', ()=> {
313
  const idx = parseInt(slot.dataset.index);
314
  player.selectedSlot = idx;
315
- // if clicking currently equipped slot -> toggle unequip to pickaxe
316
  if (player.equippedIndex === idx) equipSlot(-1);
317
  else equipSlot(idx);
318
  updateHUD();
319
  });
320
  hudGear.appendChild(slot);
321
  }
322
- // pickaxe click toggles equip (explicit: clicking pickaxe equips/unequips it)
323
  pickaxeSlot.onclick = () => {
324
  if (player.equippedIndex === -1) {
325
- // pickaxe already equipped -> equip selected slot instead
326
  equipSlot(player.selectedSlot);
327
  } else {
328
  equipSlot(-1);
@@ -346,7 +340,6 @@
346
  else if (it.type === 'materials') s.innerHTML = `<div style="font-size:11px">Mat</div><div class="medkit-count">x${it.amount}</div>`;
347
  else s.innerHTML = 'Item';
348
  });
349
- // pickaxe highlight when equipped
350
  pickaxeSlot.classList.toggle('selected', player.equippedIndex === -1);
351
  pickaxeSlot.title = (player.equippedIndex === -1) ? 'Pickaxe (equipped)' : 'Pickaxe (click or press F to equip)';
352
  feather.replace();
@@ -367,7 +360,6 @@
367
  if (!weaponObj || (typeof weaponObj.ammoInMag === 'number' && weaponObj.ammoInMag <= 0)) return false;
368
  const speed = 1100;
369
  const angle = Math.atan2(targetY-originY, targetX-originX);
370
- // decrement ammo if present
371
  if (typeof weaponObj.ammoInMag === 'number') weaponObj.ammoInMag -= 1;
372
  const dmg = weaponObj.weapon && weaponObj.weapon.dmg ? weaponObj.weapon.dmg : (weaponObj.dmg || 10);
373
  const color = (weaponObj.weapon && weaponObj.weapon.color) || weaponObj.color || '#fff';
@@ -379,16 +371,20 @@
379
  return true;
380
  }
381
 
382
- function reloadEquipped(){
383
- let item = null;
384
- if (player.equippedIndex >= 0) item = player.inventory[player.equippedIndex];
385
- else item = player.inventory[player.selectedSlot];
386
  if (!item || item.type !== 'weapon') return;
387
  const need = item.weapon.magSize - item.ammoInMag;
388
  if (need <= 0 || item.ammoReserve <= 0) return;
389
  const take = Math.min(need, item.ammoReserve);
390
  item.ammoInMag += take;
391
  item.ammoReserve -= take;
 
 
 
 
 
 
 
392
  updateHUD();
393
  }
394
 
@@ -396,7 +392,6 @@
396
  const now = performance.now();
397
  if (now - player.lastMelee < 350) return;
398
  player.lastMelee = now;
399
- // damage enemies and objects (pickaxe)
400
  for (const e of enemies){
401
  if (e.health <= 0) continue;
402
  const d = Math.hypot(e.x - player.x, e.y - player.y);
@@ -413,9 +408,7 @@
413
  }
414
 
415
  // Interact: use medkit in selected slot OR loot chests / pickup items.
416
- // Builds (objects) are NOT harvested via E anymore; only via pickaxe melee or shooting.
417
  function interactNearby(){
418
- // use medkit in selected slot
419
  const sel = player.selectedSlot;
420
  const selItem = player.inventory[sel];
421
  if (selItem && selItem.type === 'medkit'){
@@ -427,13 +420,11 @@
427
  }
428
 
429
  const range = 56;
430
- // chests
431
  for (const chest of chests){
432
  if (chest.opened) continue;
433
  const d = Math.hypot(chest.x - player.x, chest.y - player.y);
434
  if (d < range){
435
  chest.opened = true;
436
- // spawn pickup on ground (weapon/medkit/materials/ammo)
437
  const loot = chest.loot;
438
  const px = chest.x + rand(-20,20), py = chest.y + rand(-20,20);
439
  if (loot.type === 'weapon'){
@@ -451,7 +442,6 @@
451
  }
452
  }
453
 
454
- // pick up ground pickups if near
455
  for (let i=pickups.length-1;i>=0;i--){
456
  const p = pickups[i];
457
  const d = Math.hypot(p.x - player.x, p.y - player.y);
@@ -466,7 +456,6 @@
466
 
467
  function pickupCollect(p){
468
  if (p.type === 'weapon'){
469
- // place in first empty or merge ammo
470
  let merged=false;
471
  for (let s=0;s<5;s++){
472
  const it = player.inventory[s];
@@ -488,13 +477,77 @@
488
  } else if (p.type === 'materials'){
489
  player.materials += p.amount;
490
  } else if (p.type === 'ammo'){
491
- // add ammo to any weapon or to materials as fallback
492
  let added=false;
493
  for (let s=0;s<5;s++){ const it=player.inventory[s]; if (it && it.type==='weapon'){ it.ammoReserve += p.amount; added=true; break; } }
494
  if (!added) player.materials += p.amount;
495
  }
496
  }
497
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
498
  // Build Q
499
  function tryBuild(){
500
  if (player.materials < 10) return;
@@ -504,8 +557,17 @@
504
  objects.push({ x:bx, y:by, type:'wall', hp:160, maxHp:160, dead:false });
505
  updateHUD();
506
  }
 
 
 
 
 
 
 
 
 
507
 
508
- // LOS helper
509
  function hasLineOfSight(x1,y1,x2,y2){
510
  const vx = x2 - x1, vy = y2 - y1;
511
  const vlen2 = vx*vx + vy*vy;
@@ -523,8 +585,26 @@
523
  }
524
  return true;
525
  }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
526
 
527
- // Bullets update: bullets do NOT break wood/stone (only walls)
528
  function bulletsUpdate(dt){
529
  for (let i=bullets.length-1;i>=0;i--){
530
  const b = bullets[i];
@@ -566,7 +646,6 @@
566
  for (const chest of chests){
567
  if (!chest.opened && Math.hypot(chest.x - b.x, chest.y - b.y) < 18){
568
  chest.opened = true;
569
- // spawn pickup from chest
570
  const loot = chest.loot;
571
  const px = chest.x + rand(-20,20), py = chest.y + rand(-20,20);
572
  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) });
@@ -581,12 +660,13 @@
581
  }
582
  }
583
 
584
- // Enemies: can loot chests; shooting now can happen from any distance (LOS required).
585
  function updateEnemies(dt, now){
586
  for (const e of enemies){
587
  if (e.health <= 0) continue;
588
  e.roamTimer -= dt;
589
- // choose target (player or nearby enemy)
 
590
  let target = player;
591
  let bestDist = Math.hypot(player.x - e.x, player.y - e.y);
592
  for (const other of enemies){
@@ -594,7 +674,8 @@
594
  const d = Math.hypot(other.x - e.x, other.y - e.y);
595
  if (d < bestDist && Math.random() < 0.6){ bestDist = d; target = other; }
596
  }
597
- // move
 
598
  if (bestDist < 900 || e.roamTimer <= 0){
599
  e.angle = Math.atan2(target.y - e.y, target.x - e.x);
600
  const avoid = e.health < 20 && Math.random() < 0.6;
@@ -623,22 +704,81 @@
623
  }
624
  }
625
 
626
- // enemies pick up nearby pickups automatically
627
  for (let i=pickups.length-1;i>=0;i--){
628
  const p = pickups[i];
629
  if (Math.hypot(p.x - e.x, p.y - e.y) < 18){
630
- if (p.type === 'weapon' && !e.weaponPickup){
631
- e.weaponPickup = { weapon:p.weapon, ammoInMag:p.ammoInMag, ammoReserve:p.ammoReserve };
632
- pickups.splice(i,1);
633
- } else if (p.type === 'medkit'){
634
- if (e.health < 60) e.health = Math.min(120, e.health + 50);
635
- pickups.splice(i,1);
636
- } else pickups.splice(i,1);
637
  }
638
  }
639
 
640
- // attack: melee if close; ranged if they have weapon and LOS (no distance cap)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
641
  const distToTarget = Math.hypot(target.x - e.x, target.y - e.y);
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
642
  if (distToTarget < 34 && now - e.lastMelee > e.meleeRate){
643
  e.lastMelee = now;
644
  const dmg = 10 + randInt(0,8);
@@ -649,13 +789,16 @@
649
  target.health -= dmg;
650
  if (target.health <= 0) target.health = 0;
651
  }
652
- } else if (e.weaponPickup && now - (e.lastShot||0) > (e.weaponPickup.weapon.rate || 300)){
653
- // can shoot from any distance as long as LOS and ammo
654
- if (hasLineOfSight(e.x, e.y, target.x, target.y) && e.weaponPickup.ammoInMag > 0){
655
- e.lastShot = now;
656
- e.weaponPickup.ammoInMag -= 1;
657
- const angle = Math.atan2(target.y - e.y, target.x - e.x);
658
- shootBullet(e.x + Math.cos(angle)*12, e.y + Math.sin(angle)*12, target.x + (Math.random()-0.5)*6, target.y + (Math.random()-0.5)*6, e.weaponPickup, e.id);
 
 
 
659
  }
660
  }
661
  }
@@ -669,7 +812,6 @@
669
  if (!storm.active) return;
670
  storm.radius -= storm.closingSpeed * dt * 60;
671
  if (storm.radius < 120) storm.radius = 120;
672
- // damage player
673
  if (playerInStorm()){
674
  const prog = 1 - storm.radius / storm.maxRadius;
675
  const rate = storm.damagePerSecond * (1 + prog*4);
@@ -680,7 +822,6 @@
680
  if (player.health <= 0){ player.health = 0; playerDeath(); }
681
  }
682
  } else stormDamageAccumulator = 0;
683
- // damage enemies
684
  for (const e of enemies){
685
  if (e.health <= 0) continue;
686
  const d = Math.hypot(e.x - storm.centerX, e.y - storm.centerY);
@@ -695,17 +836,14 @@
695
  function updatePlayerCount(){
696
  const aliveEnemies = enemies.filter(e => e.health > 0).length;
697
  document.getElementById('playerCount').textContent = `${1 + aliveEnemies}/20`;
698
-
699
- // Victory check: if all enemies dead and player alive & game active -> show victory
700
  if (gameActive && aliveEnemies === 0 && player.health > 0){
701
- // stop game loop and show victory UI
702
  gameActive = false;
703
  victoryScreen.classList.remove('hidden');
704
  clearInterval(timerInterval);
705
  }
706
  }
707
 
708
- // Drawing (chests no text; glow; pickups visible)
709
  function drawWorld(){
710
  const TILE = 600;
711
  const cols = Math.ceil(WORLD.width / TILE);
@@ -743,6 +881,12 @@
743
  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();
744
  ctx.fillStyle = obj.type==='wood'? '#6b3b1a' : (obj.type==='stone'? '#6b6b6b' : '#8b5a32');
745
  ctx.fillRect(s.x-12, s.y-h, 24, h);
 
 
 
 
 
 
746
  ctx.restore();
747
  }
748
  }
@@ -796,9 +940,12 @@
796
  ctx.fillStyle='rgba(0,0,0,0.18)'; ctx.beginPath(); ctx.ellipse(0,12,14,6,0,0,Math.PI*2); ctx.fill();
797
  ctx.fillStyle='#ff6b6b'; ctx.beginPath(); ctx.moveTo(12,0); ctx.lineTo(-10,-8); ctx.lineTo(-10,8); ctx.closePath(); ctx.fill();
798
  ctx.fillStyle='rgba(0,0,0,0.6)'; ctx.fillRect(-18,-22,36,6);
799
- const hpPct = Math.max(0, e.health/120);
800
  ctx.fillStyle='#ff6b6b'; ctx.fillRect(-18,-22,36*hpPct,6);
801
- if (e.weaponPickup) { ctx.fillStyle = e.weaponPickup.weapon.color || '#fff'; ctx.fillRect(-10,12,8,6); }
 
 
 
802
  ctx.restore();
803
  }
804
  }
@@ -824,12 +971,9 @@
824
  const s = worldToScreen(player.x,player.y);
825
  ctx.save();
826
  ctx.translate(s.x,s.y); ctx.rotate(player.angle);
827
- // shadow
828
  ctx.fillStyle='rgba(0,0,0,0.25)'; ctx.beginPath(); ctx.ellipse(0,14,18,8,0,0,Math.PI*2); ctx.fill();
829
- // body
830
  ctx.fillStyle='yellow'; ctx.beginPath(); ctx.moveTo(18,0); ctx.lineTo(-12,-10); ctx.lineTo(-12,10); ctx.closePath(); ctx.fill();
831
 
832
- // draw currently held item: pickaxe or weapon
833
  let activeWeaponItem = null;
834
  if (player.equippedIndex >= 0) activeWeaponItem = player.inventory[player.equippedIndex];
835
  else {
@@ -838,27 +982,22 @@
838
  }
839
 
840
  if (player.equippedIndex === -1){
841
- // draw pickaxe in right hand
842
  ctx.save();
843
  ctx.translate(12, 6);
844
  ctx.rotate(-0.2);
845
- // handle
846
  ctx.fillStyle = '#8b6b4a';
847
  ctx.fillRect(-2,0,4,18);
848
- // head
849
  ctx.fillStyle = '#cfcfcf';
850
  ctx.fillRect(-8,-6,12,6);
851
  ctx.restore();
852
  } else if (activeWeaponItem && activeWeaponItem.type === 'weapon'){
853
- // draw weapon (simple shape) in right hand
854
  ctx.save();
855
  ctx.translate(12, 2);
856
  ctx.rotate(-0.05);
857
  ctx.fillStyle = activeWeaponItem.weapon.color || '#fff';
858
- ctx.fillRect(0,-4,26,8); // body
859
  ctx.fillStyle = '#222';
860
- ctx.fillRect(18,-2,8,4); // muzzle or detail
861
- // small magazine indicator with ammo fraction
862
  const magPct = Math.max(0, activeWeaponItem.ammoInMag / activeWeaponItem.weapon.magSize);
863
  ctx.fillStyle = 'rgba(0,0,0,0.35)';
864
  ctx.fillRect(4,6,18,4);
@@ -869,10 +1008,7 @@
869
  ctx.restore();
870
  }
871
 
872
- // Do not draw the yellow crosshair - rely on native cursor (one crosshair only)
873
- function drawCrosshair(){
874
- // no-op: native cursor is used (black/system crosshair)
875
- }
876
 
877
  // Main loop
878
  let lastTime = 0;
@@ -897,10 +1033,9 @@
897
  mouse.worldX = mouse.canvasX + camera.x;
898
  mouse.worldY = mouse.canvasY + camera.y;
899
 
900
- // Ensure the player angle always faces the current mouse world position (covers camera/player movement)
901
  player.angle = Math.atan2(mouse.worldY - player.y, mouse.worldX - player.x);
902
 
903
- // determine active weapon: prefer equipped slot if >=0; else if selectedSlot has weapon allow firing from selected slot; if equippedIndex === -1 it's pickaxe
904
  let activeWeaponItem = null;
905
  if (player.equippedIndex >= 0) activeWeaponItem = player.inventory[player.equippedIndex];
906
  else {
@@ -911,7 +1046,6 @@
911
  // shooting or melee on mouse down
912
  if (mouse.down){
913
  if (player.equippedIndex === -1){
914
- // pickaxe melee
915
  playerMeleeHit();
916
  } else if (activeWeaponItem && activeWeaponItem.type === 'weapon'){
917
  const now = performance.now();
@@ -920,8 +1054,6 @@
920
  player.lastShot = now;
921
  shootBullet(player.x + Math.cos(player.angle)*18, player.y + Math.sin(player.angle)*18, mouse.worldX, mouse.worldY, activeWeaponItem, 'player');
922
  updateHUD();
923
- } else {
924
- // out of mag - do nothing until reload with R
925
  }
926
  }
927
  }
@@ -957,7 +1089,6 @@
957
  drawWorld(); drawObjects(); drawChests(); drawPickups(); drawEnemies(); drawBullets(); drawPlayer(); drawCrosshair();
958
  updateHUD();
959
 
960
- // storm warning
961
  if (storm.active && playerInStorm()) stormWarning.classList.remove('hidden'); else stormWarning.classList.add('hidden');
962
 
963
  requestAnimationFrame(gameLoop);
@@ -987,7 +1118,6 @@
987
  initHUD();
988
  cameraUpdate();
989
 
990
- // timer & storm: pick random center at activation
991
  gameTime = 300; storm.active = false; storm.radius = storm.maxRadius;
992
  document.getElementById('gameTimer').textContent = '5:00';
993
  timerInterval && clearInterval(timerInterval);
 
3
  <head>
4
  <meta charset="utf-8" />
5
  <meta name="viewport" content="width=device-width, initial-scale=1" />
6
+ <title>BattleZone Royale - Enemies Improved</title>
7
  <script src="https://cdn.tailwindcss.com"></script>
8
  <script src="https://unpkg.com/feather-icons"></script>
9
  <style>
 
169
 
170
  // Helper: equip slot index (or -1 for pickaxe)
171
  function equipSlot(index){
 
172
  player.equippedIndex = index;
 
173
  updateHUD();
174
  }
175
 
176
  window.addEventListener('keydown',(e)=>{
177
  const k = e.key.toLowerCase();
 
 
178
  if (gameActive){
179
  if (k in keys) keys[k] = true;
 
180
  if (['1','2','3','4','5'].includes(k)) {
181
  const idx = parseInt(k)-1;
182
  player.selectedSlot = idx;
 
183
  equipSlot(idx);
184
  updateHUD();
185
  }
 
186
  if (k === 'f') {
 
187
  if (player.equippedIndex === player.selectedSlot) equipSlot(-1);
188
  else equipSlot(player.selectedSlot);
189
  updateHUD();
190
  }
 
191
  if (k === 'r') keys.r = true;
192
  } else {
 
193
  if (['1','2','3','4','5'].includes(k)) {
194
  player.selectedSlot = parseInt(k)-1;
195
  updateHUD();
 
284
  enemies.push({
285
  id:'e'+i, x:ex, y:ey, radius:14, angle:0, speed:110+rand(-20,20),
286
  health: 80 + randInt(0,40), lastMelee:0, meleeRate:800 + randInt(-200,200),
287
+ roamTimer: rand(0,3),
288
+ // new enemy-specific fields:
289
+ inventory: [null,null,null,null,null],
290
+ selectedSlot: 0,
291
+ equippedIndex: -1, // -1 = pickaxe
292
+ materials: 0,
293
+ lastShot:0,
294
+ reloadingUntil: 0
295
  });
296
  }
297
  updatePlayerCount();
 
309
  slot.addEventListener('click', ()=> {
310
  const idx = parseInt(slot.dataset.index);
311
  player.selectedSlot = idx;
 
312
  if (player.equippedIndex === idx) equipSlot(-1);
313
  else equipSlot(idx);
314
  updateHUD();
315
  });
316
  hudGear.appendChild(slot);
317
  }
 
318
  pickaxeSlot.onclick = () => {
319
  if (player.equippedIndex === -1) {
 
320
  equipSlot(player.selectedSlot);
321
  } else {
322
  equipSlot(-1);
 
340
  else if (it.type === 'materials') s.innerHTML = `<div style="font-size:11px">Mat</div><div class="medkit-count">x${it.amount}</div>`;
341
  else s.innerHTML = 'Item';
342
  });
 
343
  pickaxeSlot.classList.toggle('selected', player.equippedIndex === -1);
344
  pickaxeSlot.title = (player.equippedIndex === -1) ? 'Pickaxe (equipped)' : 'Pickaxe (click or press F to equip)';
345
  feather.replace();
 
360
  if (!weaponObj || (typeof weaponObj.ammoInMag === 'number' && weaponObj.ammoInMag <= 0)) return false;
361
  const speed = 1100;
362
  const angle = Math.atan2(targetY-originY, targetX-originX);
 
363
  if (typeof weaponObj.ammoInMag === 'number') weaponObj.ammoInMag -= 1;
364
  const dmg = weaponObj.weapon && weaponObj.weapon.dmg ? weaponObj.weapon.dmg : (weaponObj.dmg || 10);
365
  const color = (weaponObj.weapon && weaponObj.weapon.color) || weaponObj.color || '#fff';
 
371
  return true;
372
  }
373
 
374
+ function reloadItem(item){
 
 
 
375
  if (!item || item.type !== 'weapon') return;
376
  const need = item.weapon.magSize - item.ammoInMag;
377
  if (need <= 0 || item.ammoReserve <= 0) return;
378
  const take = Math.min(need, item.ammoReserve);
379
  item.ammoInMag += take;
380
  item.ammoReserve -= take;
381
+ }
382
+
383
+ function reloadEquipped(){
384
+ let item = null;
385
+ if (player.equippedIndex >= 0) item = player.inventory[player.equippedIndex];
386
+ else item = player.inventory[player.selectedSlot];
387
+ reloadItem(item);
388
  updateHUD();
389
  }
390
 
 
392
  const now = performance.now();
393
  if (now - player.lastMelee < 350) return;
394
  player.lastMelee = now;
 
395
  for (const e of enemies){
396
  if (e.health <= 0) continue;
397
  const d = Math.hypot(e.x - player.x, e.y - player.y);
 
408
  }
409
 
410
  // Interact: use medkit in selected slot OR loot chests / pickup items.
 
411
  function interactNearby(){
 
412
  const sel = player.selectedSlot;
413
  const selItem = player.inventory[sel];
414
  if (selItem && selItem.type === 'medkit'){
 
420
  }
421
 
422
  const range = 56;
 
423
  for (const chest of chests){
424
  if (chest.opened) continue;
425
  const d = Math.hypot(chest.x - player.x, chest.y - player.y);
426
  if (d < range){
427
  chest.opened = true;
 
428
  const loot = chest.loot;
429
  const px = chest.x + rand(-20,20), py = chest.y + rand(-20,20);
430
  if (loot.type === 'weapon'){
 
442
  }
443
  }
444
 
 
445
  for (let i=pickups.length-1;i>=0;i--){
446
  const p = pickups[i];
447
  const d = Math.hypot(p.x - player.x, p.y - player.y);
 
456
 
457
  function pickupCollect(p){
458
  if (p.type === 'weapon'){
 
459
  let merged=false;
460
  for (let s=0;s<5;s++){
461
  const it = player.inventory[s];
 
477
  } else if (p.type === 'materials'){
478
  player.materials += p.amount;
479
  } else if (p.type === 'ammo'){
 
480
  let added=false;
481
  for (let s=0;s<5;s++){ const it=player.inventory[s]; if (it && it.type==='weapon'){ it.ammoReserve += p.amount; added=true; break; } }
482
  if (!added) player.materials += p.amount;
483
  }
484
  }
485
 
486
+ // Enemy helpers: equip best weapon, reload, collect pickups
487
+ function enemyEquipBestWeapon(e){
488
+ // choose weapon with highest effective DPS (approx = dmg / rate)
489
+ let bestIdx = -1;
490
+ let bestScore = -Infinity;
491
+ for (let i=0;i<5;i++){
492
+ const it = e.inventory[i];
493
+ if (it && it.type === 'weapon'){
494
+ const score = (it.weapon.dmg || 1) / (it.weapon.rate || 300);
495
+ if (score > bestScore){ bestScore = score; bestIdx = i; }
496
+ }
497
+ }
498
+ if (bestIdx !== -1) e.equippedIndex = bestIdx;
499
+ }
500
+
501
+ function enemyPickupCollect(e, p){
502
+ if (p.type === 'weapon'){
503
+ // try to merge into existing same weapon
504
+ for (let s=0;s<5;s++){
505
+ const it = e.inventory[s];
506
+ if (it && it.type==='weapon' && it.weapon.name === p.weapon.name){
507
+ it.ammoReserve += p.ammoReserve;
508
+ it.ammoInMag = Math.min(it.weapon.magSize, it.ammoInMag + p.ammoInMag);
509
+ return;
510
+ }
511
+ }
512
+ // place in empty slot
513
+ for (let s=0;s<5;s++){
514
+ if (!e.inventory[s]) { e.inventory[s] = { type:'weapon', weapon:p.weapon, ammoInMag:p.ammoInMag, ammoReserve:p.ammoReserve }; enemyEquipBestWeapon(e); return; }
515
+ }
516
+ // replace worst weapon if pickup is better
517
+ let worstIdx = -1, worstScore = Infinity;
518
+ const pickupScore = (p.weapon.dmg || 1) / (p.weapon.rate || 300);
519
+ for (let s=0;s<5;s++){
520
+ const it = e.inventory[s];
521
+ if (it && it.type==='weapon'){
522
+ const score = (it.weapon.dmg || 1) / (it.weapon.rate || 300);
523
+ if (score < worstScore){ worstScore = score; worstIdx = s; }
524
+ } else { worstIdx = s; worstScore = -Infinity; }
525
+ }
526
+ if (pickupScore > worstScore && worstIdx !== -1){
527
+ e.inventory[worstIdx] = { type:'weapon', weapon:p.weapon, ammoInMag:p.ammoInMag, ammoReserve:p.ammoReserve };
528
+ enemyEquipBestWeapon(e);
529
+ } else {
530
+ // otherwise stash as materials/ammo
531
+ e.materials += Math.floor((p.ammoReserve || 0) / 2);
532
+ }
533
+ } else if (p.type === 'medkit'){
534
+ // stack or place
535
+ for (let s=0;s<5;s++){
536
+ const it = e.inventory[s];
537
+ if (it && it.type==='medkit'){ it.amount += p.amount; return; }
538
+ }
539
+ for (let s=0;s<5;s++){ if (!e.inventory[s]) { e.inventory[s] = { type:'medkit', amount:p.amount }; return; } }
540
+ // stash as materials fallback
541
+ e.materials += 3;
542
+ } else if (p.type === 'materials'){
543
+ e.materials += p.amount;
544
+ } else if (p.type === 'ammo'){
545
+ // add to any weapon
546
+ for (let s=0;s<5;s++){ const it=e.inventory[s]; if (it && it.type==='weapon'){ it.ammoReserve += p.amount; return; } }
547
+ e.materials += p.amount;
548
+ }
549
+ }
550
+
551
  // Build Q
552
  function tryBuild(){
553
  if (player.materials < 10) return;
 
557
  objects.push({ x:bx, y:by, type:'wall', hp:160, maxHp:160, dead:false });
558
  updateHUD();
559
  }
560
+ function enemyTryBuild(e){
561
+ if (e.materials < 10) return false;
562
+ // build with small chance or when under pressure
563
+ e.materials -= 10;
564
+ const bx = e.x + Math.cos(e.angle) * 48;
565
+ const by = e.y + Math.sin(e.angle) * 48;
566
+ objects.push({ x:bx, y:by, type:'wall', hp:160, maxHp:160, dead:false });
567
+ return true;
568
+ }
569
 
570
+ // LOS helper + blocker finder
571
  function hasLineOfSight(x1,y1,x2,y2){
572
  const vx = x2 - x1, vy = y2 - y1;
573
  const vlen2 = vx*vx + vy*vy;
 
585
  }
586
  return true;
587
  }
588
+ function findBlockingObject(x1,y1,x2,y2){
589
+ const vx = x2 - x1, vy = y2 - y1;
590
+ const vlen2 = vx*vx + vy*vy;
591
+ let closest = null; let closestT = Infinity;
592
+ for (const obj of objects){
593
+ if (obj.dead) continue;
594
+ let br=0;
595
+ if (obj.type==='wall') br=28; else if (obj.type==='stone') br=22; else if (obj.type==='wood') br=16; else continue;
596
+ const wx = obj.x - x1, wy = obj.y - y1;
597
+ const c1 = vx*wx + vy*wy;
598
+ const t = vlen2>0 ? c1/vlen2 : 0;
599
+ if (t < 0 || t > 1) continue;
600
+ const projx = x1 + vx * t, projy = y1 + vy * t;
601
+ const dist = Math.hypot(projx - obj.x, projy - obj.y);
602
+ if (dist < br && t < closestT){ closestT = t; closest = obj; }
603
+ }
604
+ return closest;
605
+ }
606
 
607
+ // Bullets update
608
  function bulletsUpdate(dt){
609
  for (let i=bullets.length-1;i>=0;i--){
610
  const b = bullets[i];
 
646
  for (const chest of chests){
647
  if (!chest.opened && Math.hypot(chest.x - b.x, chest.y - b.y) < 18){
648
  chest.opened = true;
 
649
  const loot = chest.loot;
650
  const px = chest.x + rand(-20,20), py = chest.y + rand(-20,20);
651
  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) });
 
660
  }
661
  }
662
 
663
+ // Enemies: can loot chests; improved behaviors added
664
  function updateEnemies(dt, now){
665
  for (const e of enemies){
666
  if (e.health <= 0) continue;
667
  e.roamTimer -= dt;
668
+
669
+ // pick target (player or nearby enemy) like before
670
  let target = player;
671
  let bestDist = Math.hypot(player.x - e.x, player.y - e.y);
672
  for (const other of enemies){
 
674
  const d = Math.hypot(other.x - e.x, other.y - e.y);
675
  if (d < bestDist && Math.random() < 0.6){ bestDist = d; target = other; }
676
  }
677
+
678
+ // movement: approach target but avoid if low HP
679
  if (bestDist < 900 || e.roamTimer <= 0){
680
  e.angle = Math.atan2(target.y - e.y, target.x - e.x);
681
  const avoid = e.health < 20 && Math.random() < 0.6;
 
704
  }
705
  }
706
 
707
+ // enemies pick up nearby pickups automatically using enemyPickupCollect
708
  for (let i=pickups.length-1;i>=0;i--){
709
  const p = pickups[i];
710
  if (Math.hypot(p.x - e.x, p.y - e.y) < 18){
711
+ enemyPickupCollect(e, p);
712
+ pickups.splice(i,1);
 
 
 
 
 
713
  }
714
  }
715
 
716
+ // Auto-heal: if medkit in inventory and health low -> use it
717
+ if (e.health < 60){
718
+ for (let s=0;s<5;s++){
719
+ const it = e.inventory[s];
720
+ if (it && it.type === 'medkit'){
721
+ it.amount -= 1;
722
+ e.health = Math.min(120, e.health + 50);
723
+ if (it.amount <= 0) e.inventory[s] = null;
724
+ break;
725
+ }
726
+ }
727
+ }
728
+
729
+ // Decide to build: if player nearby and enemy has materials, occasional build to block / protect
730
+ if (e.materials >= 10 && Math.random() < 0.006 && Math.hypot(player.x - e.x, player.y - e.y) < 400){
731
+ enemyTryBuild(e);
732
+ }
733
+
734
+ // If the enemy has weapons, ensure it's equipped
735
+ if (e.equippedIndex === -1){ // no weapon equipped
736
+ enemyEquipBestWeapon(e);
737
+ }
738
+
739
+ // Attempt reload if equipped weapon empty and ammoReserve > 0 and not currently reloading
740
+ if (e.equippedIndex >= 0){
741
+ const eq = e.inventory[e.equippedIndex];
742
+ if (eq && eq.type === 'weapon' && eq.ammoInMag <= 0 && eq.ammoReserve > 0){
743
+ reloadItem(eq);
744
+ }
745
+ }
746
+
747
+ // Determine target for attack
748
  const distToTarget = Math.hypot(target.x - e.x, target.y - e.y);
749
+
750
+ // If there's a blocking object between enemy and target, consider breaking it
751
+ const blocked = !hasLineOfSight(e.x, e.y, target.x, target.y);
752
+ if (blocked){
753
+ const blocker = findBlockingObject(e.x, e.y, target.x, target.y);
754
+ if (blocker){
755
+ const db = Math.hypot(blocker.x - e.x, blocker.y - e.y);
756
+ // if close, melee to break
757
+ if (db < 36){
758
+ blocker.hp -= 18 * dt * 2; // faster breaking via melee while close
759
+ if (blocker.hp <= 0){ blocker.dead = true; if (e.materials !== undefined) e.materials += (blocker.type === 'wood' ? 3 : 6); }
760
+ } else {
761
+ // shoot at blocker if has gun
762
+ if (e.equippedIndex >= 0){
763
+ const eq = e.inventory[e.equippedIndex];
764
+ if (eq && eq.type === 'weapon' && eq.ammoInMag > 0 && now - e.lastShot > (eq.weapon.rate || 300)){
765
+ e.lastShot = now;
766
+ eq.ammoInMag -= 1;
767
+ const angle = Math.atan2(blocker.y - e.y, blocker.x - e.x);
768
+ shootBullet(e.x + Math.cos(angle)*12, e.y + Math.sin(angle)*12, blocker.x + (Math.random()-0.5)*6, blocker.y + (Math.random()-0.5)*6, eq, e.id);
769
+ }
770
+ } else {
771
+ // move towards blocker to melee
772
+ e.angle = Math.atan2(blocker.y - e.y, blocker.x - e.x);
773
+ e.x += Math.cos(e.angle) * e.speed * dt * 0.8;
774
+ e.y += Math.sin(e.angle) * e.speed * dt * 0.8;
775
+ }
776
+ }
777
+ continue; // handle blocking before attempting to hit the target
778
+ }
779
+ }
780
+
781
+ // Attack: melee if close; ranged if they have equipped weapon & LOS
782
  if (distToTarget < 34 && now - e.lastMelee > e.meleeRate){
783
  e.lastMelee = now;
784
  const dmg = 10 + randInt(0,8);
 
789
  target.health -= dmg;
790
  if (target.health <= 0) target.health = 0;
791
  }
792
+ } else if (e.equippedIndex >= 0){
793
+ const eq = e.inventory[e.equippedIndex];
794
+ if (eq && eq.type === 'weapon' && now - (e.lastShot||0) > (eq.weapon.rate || 300)){
795
+ // check LOS; can shoot from any distance if line-of-sight
796
+ if (hasLineOfSight(e.x, e.y, target.x, target.y) && eq.ammoInMag > 0){
797
+ e.lastShot = now;
798
+ eq.ammoInMag -= 1;
799
+ const angle = Math.atan2(target.y - e.y, target.x - e.x);
800
+ shootBullet(e.x + Math.cos(angle)*12, e.y + Math.sin(angle)*12, target.x + (Math.random()-0.5)*6, target.y + (Math.random()-0.5)*6, eq, e.id);
801
+ }
802
  }
803
  }
804
  }
 
812
  if (!storm.active) return;
813
  storm.radius -= storm.closingSpeed * dt * 60;
814
  if (storm.radius < 120) storm.radius = 120;
 
815
  if (playerInStorm()){
816
  const prog = 1 - storm.radius / storm.maxRadius;
817
  const rate = storm.damagePerSecond * (1 + prog*4);
 
822
  if (player.health <= 0){ player.health = 0; playerDeath(); }
823
  }
824
  } else stormDamageAccumulator = 0;
 
825
  for (const e of enemies){
826
  if (e.health <= 0) continue;
827
  const d = Math.hypot(e.x - storm.centerX, e.y - storm.centerY);
 
836
  function updatePlayerCount(){
837
  const aliveEnemies = enemies.filter(e => e.health > 0).length;
838
  document.getElementById('playerCount').textContent = `${1 + aliveEnemies}/20`;
 
 
839
  if (gameActive && aliveEnemies === 0 && player.health > 0){
 
840
  gameActive = false;
841
  victoryScreen.classList.remove('hidden');
842
  clearInterval(timerInterval);
843
  }
844
  }
845
 
846
+ // Drawing (unchanged)
847
  function drawWorld(){
848
  const TILE = 600;
849
  const cols = Math.ceil(WORLD.width / TILE);
 
881
  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();
882
  ctx.fillStyle = obj.type==='wood'? '#6b3b1a' : (obj.type==='stone'? '#6b6b6b' : '#8b5a32');
883
  ctx.fillRect(s.x-12, s.y-h, 24, h);
884
+ // HP bar for walls
885
+ if (obj.type==='wall'){
886
+ ctx.fillStyle='rgba(0,0,0,0.6)'; ctx.fillRect(s.x-18, s.y-h-10, 36, 6);
887
+ const hpPct = Math.max(0, obj.hp / obj.maxHp);
888
+ ctx.fillStyle = '#ffc966'; ctx.fillRect(s.x-18, s.y-h-10, 36*hpPct, 6);
889
+ }
890
  ctx.restore();
891
  }
892
  }
 
940
  ctx.fillStyle='rgba(0,0,0,0.18)'; ctx.beginPath(); ctx.ellipse(0,12,14,6,0,0,Math.PI*2); ctx.fill();
941
  ctx.fillStyle='#ff6b6b'; ctx.beginPath(); ctx.moveTo(12,0); ctx.lineTo(-10,-8); ctx.lineTo(-10,8); ctx.closePath(); ctx.fill();
942
  ctx.fillStyle='rgba(0,0,0,0.6)'; ctx.fillRect(-18,-22,36,6);
943
+ const hpPct = Math.max(0, Math.min(1, e.health/120));
944
  ctx.fillStyle='#ff6b6b'; ctx.fillRect(-18,-22,36*hpPct,6);
945
+ if (e.equippedIndex >= 0 && e.inventory[e.equippedIndex] && e.inventory[e.equippedIndex].type === 'weapon') {
946
+ ctx.fillStyle = e.inventory[e.equippedIndex].weapon.color || '#fff';
947
+ ctx.fillRect(-10,12,8,6);
948
+ }
949
  ctx.restore();
950
  }
951
  }
 
971
  const s = worldToScreen(player.x,player.y);
972
  ctx.save();
973
  ctx.translate(s.x,s.y); ctx.rotate(player.angle);
 
974
  ctx.fillStyle='rgba(0,0,0,0.25)'; ctx.beginPath(); ctx.ellipse(0,14,18,8,0,0,Math.PI*2); ctx.fill();
 
975
  ctx.fillStyle='yellow'; ctx.beginPath(); ctx.moveTo(18,0); ctx.lineTo(-12,-10); ctx.lineTo(-12,10); ctx.closePath(); ctx.fill();
976
 
 
977
  let activeWeaponItem = null;
978
  if (player.equippedIndex >= 0) activeWeaponItem = player.inventory[player.equippedIndex];
979
  else {
 
982
  }
983
 
984
  if (player.equippedIndex === -1){
 
985
  ctx.save();
986
  ctx.translate(12, 6);
987
  ctx.rotate(-0.2);
 
988
  ctx.fillStyle = '#8b6b4a';
989
  ctx.fillRect(-2,0,4,18);
 
990
  ctx.fillStyle = '#cfcfcf';
991
  ctx.fillRect(-8,-6,12,6);
992
  ctx.restore();
993
  } else if (activeWeaponItem && activeWeaponItem.type === 'weapon'){
 
994
  ctx.save();
995
  ctx.translate(12, 2);
996
  ctx.rotate(-0.05);
997
  ctx.fillStyle = activeWeaponItem.weapon.color || '#fff';
998
+ ctx.fillRect(0,-4,26,8);
999
  ctx.fillStyle = '#222';
1000
+ ctx.fillRect(18,-2,8,4);
 
1001
  const magPct = Math.max(0, activeWeaponItem.ammoInMag / activeWeaponItem.weapon.magSize);
1002
  ctx.fillStyle = 'rgba(0,0,0,0.35)';
1003
  ctx.fillRect(4,6,18,4);
 
1008
  ctx.restore();
1009
  }
1010
 
1011
+ function drawCrosshair(){ }
 
 
 
1012
 
1013
  // Main loop
1014
  let lastTime = 0;
 
1033
  mouse.worldX = mouse.canvasX + camera.x;
1034
  mouse.worldY = mouse.canvasY + camera.y;
1035
 
 
1036
  player.angle = Math.atan2(mouse.worldY - player.y, mouse.worldX - player.x);
1037
 
1038
+ // determine active weapon for player
1039
  let activeWeaponItem = null;
1040
  if (player.equippedIndex >= 0) activeWeaponItem = player.inventory[player.equippedIndex];
1041
  else {
 
1046
  // shooting or melee on mouse down
1047
  if (mouse.down){
1048
  if (player.equippedIndex === -1){
 
1049
  playerMeleeHit();
1050
  } else if (activeWeaponItem && activeWeaponItem.type === 'weapon'){
1051
  const now = performance.now();
 
1054
  player.lastShot = now;
1055
  shootBullet(player.x + Math.cos(player.angle)*18, player.y + Math.sin(player.angle)*18, mouse.worldX, mouse.worldY, activeWeaponItem, 'player');
1056
  updateHUD();
 
 
1057
  }
1058
  }
1059
  }
 
1089
  drawWorld(); drawObjects(); drawChests(); drawPickups(); drawEnemies(); drawBullets(); drawPlayer(); drawCrosshair();
1090
  updateHUD();
1091
 
 
1092
  if (storm.active && playerInStorm()) stormWarning.classList.remove('hidden'); else stormWarning.classList.add('hidden');
1093
 
1094
  requestAnimationFrame(gameLoop);
 
1118
  initHUD();
1119
  cameraUpdate();
1120
 
 
1121
  gameTime = 300; storm.active = false; storm.radius = storm.maxRadius;
1122
  document.getElementById('gameTimer').textContent = '5:00';
1123
  timerInterval && clearInterval(timerInterval);