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

Manual changes saved

Browse files
Files changed (1) hide show
  1. index.html +194 -123
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 - Enemies Improved</title>
7
  <script src="https://cdn.tailwindcss.com"></script>
8
  <script src="https://unpkg.com/feather-icons"></script>
9
  <style>
@@ -167,11 +167,7 @@
167
  const keys = { w:false,a:false,s:false,d:false,e:false,q:false,r:false,f:false };
168
  const mouse = { canvasX:0, canvasY:0, worldX:0, worldY:0, down:false };
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();
@@ -200,7 +196,7 @@
200
  window.addEventListener('keyup',(e)=>{
201
  const k = e.key.toLowerCase();
202
  if (k in keys) keys[k] = false;
203
- if (k === 'e') keys.e = true; // single-use flag
204
  if (k === 'q') keys.q = true;
205
  });
206
 
@@ -285,19 +281,23 @@
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();
298
  }
299
 
300
- // HUD init (pickaxe left of inventory)
301
  function initHUD(){
302
  hudHealth.classList.remove('hidden');
303
  hudGearWrap.classList.remove('hidden');
@@ -345,7 +345,7 @@
345
  feather.replace();
346
  }
347
 
348
- // Camera
349
  function cameraUpdate(){
350
  if (!canvas.width || !canvas.height) return;
351
  camera.x = player.x - canvas.width/2;
@@ -355,7 +355,7 @@
355
  }
356
  function worldToScreen(wx,wy){ return { x: Math.round(wx - camera.x), y: Math.round(wy - camera.y) }; }
357
 
358
- // Shooting, reload, melee
359
  function shootBullet(originX, originY, targetX, targetY, weaponObj, shooterId){
360
  if (!weaponObj || (typeof weaponObj.ammoInMag === 'number' && weaponObj.ammoInMag <= 0)) return false;
361
  const speed = 1100;
@@ -395,7 +395,7 @@
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);
398
- if (d < 36){ e.health -= 18; if (e.health <= 0) { e.health = 0; player.kills++; player.materials += 2; updatePlayerCount(); } }
399
  }
400
  for (const obj of objects){
401
  if (obj.dead) continue;
@@ -407,7 +407,7 @@
407
  }
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];
@@ -418,22 +418,18 @@
418
  updateHUD();
419
  return;
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'){
431
- 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) });
432
- } else if (loot.type === 'medkit') {
433
- pickups.push({ x:px, y:py, type:'medkit', amount: loot.amount || 1 });
434
- } else if (loot.type === 'materials') {
435
- pickups.push({ x:px, y:py, type:'materials', amount: loot.amount || 5 });
436
- }
437
  if (Math.random() < 0.25){
438
  pickups.push({ x:px+8, y:py+8, type:'ammo', forWeapon: null, amount: randInt(6,30) });
439
  }
@@ -441,7 +437,6 @@
441
  return;
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);
@@ -485,7 +480,6 @@
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++){
@@ -500,20 +494,18 @@
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++){
@@ -521,28 +513,24 @@
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
  }
@@ -559,7 +547,6 @@
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;
@@ -604,7 +591,7 @@
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];
@@ -626,6 +613,7 @@
626
  if (b.shooter === e.id) continue;
627
  if (Math.hypot(e.x - b.x, e.y - b.y) < 14){
628
  e.health -= b.dmg;
 
629
  if (e.health <= 0){ e.health = 0; if (b.shooter === 'player'){ player.kills++; player.materials += 2; updatePlayerCount(); } }
630
  bullets.splice(i,1); break;
631
  }
@@ -660,83 +648,142 @@
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){
673
- if (other === e || other.health <= 0) continue;
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;
682
- const moveDir = avoid ? -1 : 1;
683
- e.x += Math.cos(e.angle) * e.speed * dt * moveDir;
684
- e.y += Math.sin(e.angle) * e.speed * dt * moveDir;
685
- } else {
686
- e.x += Math.cos(e.angle) * e.speed * dt * 0.25;
687
- e.y += Math.sin(e.angle) * e.speed * dt * 0.25;
688
- if (Math.random() < 0.01) e.angle += (Math.random()-0.5)*2;
689
- }
690
- e.x = Math.max(12, Math.min(WORLD.width-12, e.x));
691
- e.y = Math.max(12, Math.min(WORLD.height-12, e.y));
692
-
693
- // enemy loot chests if close
694
- for (const chest of chests){
695
- if (chest.opened) continue;
696
- const d = Math.hypot(chest.x - e.x, chest.y - e.y);
697
- if (d < 24){
698
- chest.opened = true;
699
- const loot = chest.loot;
700
- const px = chest.x + rand(-12,12), py = chest.y + rand(-12,12);
701
- 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) });
702
- else if (loot.type === 'medkit') pickups.push({ x:px, y:py, type:'medkit', amount: loot.amount || 1 });
703
- else if (loot.type === 'materials') pickups.push({ x:px, y:py, type:'materials', amount: loot.amount || 5 });
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){
@@ -744,21 +791,32 @@
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)){
@@ -768,43 +826,58 @@
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);
785
- if (target === player){
786
- player.health -= dmg;
787
- if (player.health <= 0) { player.health = 0; playerDeath(); }
788
- } else {
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
  }
805
  }
806
 
807
- // Storm: slower, damages enemies too
808
  const storm = { maxRadius: 2400, radius:2400, centerX: WORLD.width/2, centerY: WORLD.height/2, damagePerSecond:1, closingSpeed: 0.6, active:false };
809
  let stormDamageAccumulator = 0;
810
  function playerInStorm(){ return Math.hypot(player.x - storm.centerX, player.y - storm.centerY) > storm.radius; }
@@ -881,7 +954,6 @@
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);
@@ -946,6 +1018,9 @@
946
  ctx.fillStyle = e.inventory[e.equippedIndex].weapon.color || '#fff';
947
  ctx.fillRect(-10,12,8,6);
948
  }
 
 
 
949
  ctx.restore();
950
  }
951
  }
@@ -1018,7 +1093,7 @@
1018
  const dt = Math.min(0.05, (ts - lastTime)/1000);
1019
  lastTime = ts;
1020
 
1021
- // movement
1022
  let dx=0, dy=0;
1023
  if (keys.w) dy -= 1; if (keys.s) dy += 1; if (keys.a) dx -= 1; if (keys.d) dx += 1;
1024
  if (dx !== 0 || dy !== 0){
@@ -1043,7 +1118,7 @@
1043
  if (selected && selected.type === 'weapon') activeWeaponItem = selected;
1044
  }
1045
 
1046
- // shooting or melee on mouse down
1047
  if (mouse.down){
1048
  if (player.equippedIndex === -1){
1049
  playerMeleeHit();
@@ -1059,20 +1134,16 @@
1059
  }
1060
  }
1061
 
1062
- // reload
1063
  if (keys.r) { reloadEquipped(); keys.r = false; }
1064
-
1065
- // interact/use
1066
  if (keys.e){ interactNearby(); keys.e = false; }
1067
-
1068
- // build
1069
  if (keys.q){ tryBuild(); keys.q = false; }
1070
 
1071
- // enemy AI and bullets
1072
  updateEnemies(dt, performance.now());
1073
  bulletsUpdate(dt);
1074
 
1075
- // pickup auto-collect when standing on them
1076
  for (let i=pickups.length-1;i>=0;i--){
1077
  const p = pickups[i];
1078
  if (Math.hypot(p.x - player.x, p.y - player.y) < 18){ pickupCollect(p); pickups.splice(i,1); updateHUD(); }
 
3
  <head>
4
  <meta charset="utf-8" />
5
  <meta name="viewport" content="width=device-width, initial-scale=1" />
6
+ <title>BattleZone Royale - Enemies Gather First</title>
7
  <script src="https://cdn.tailwindcss.com"></script>
8
  <script src="https://unpkg.com/feather-icons"></script>
9
  <style>
 
167
  const keys = { w:false,a:false,s:false,d:false,e:false,q:false,r:false,f:false };
168
  const mouse = { canvasX:0, canvasY:0, worldX:0, worldY:0, down:false };
169
 
170
+ function equipSlot(index){ player.equippedIndex = index; updateHUD(); }
 
 
 
 
171
 
172
  window.addEventListener('keydown',(e)=>{
173
  const k = e.key.toLowerCase();
 
196
  window.addEventListener('keyup',(e)=>{
197
  const k = e.key.toLowerCase();
198
  if (k in keys) keys[k] = false;
199
+ if (k === 'e') keys.e = true;
200
  if (k === 'q') keys.q = true;
201
  });
202
 
 
281
  id:'e'+i, x:ex, y:ey, radius:14, angle:0, speed:110+rand(-20,20),
282
  health: 80 + randInt(0,40), lastMelee:0, meleeRate:800 + randInt(-200,200),
283
  roamTimer: rand(0,3),
284
+ // enemy state machine and inventory
285
  inventory: [null,null,null,null,null],
286
  selectedSlot: 0,
287
+ equippedIndex: -1,
288
  materials: 0,
289
+ lastShot: 0,
290
+ reloadingUntil: 0,
291
+ lastAttackedTime: 0,
292
+ state: 'gather', // 'gather' -> 'combat' -> 'defend' ...
293
+ gatherTimeLeft: rand(8,16), // seconds to try gather before switching
294
+ target: null // {type:'chest'|'object'|'pickup'|'player', ref:...}
295
  });
296
  }
297
  updatePlayerCount();
298
  }
299
 
300
+ // HUD init
301
  function initHUD(){
302
  hudHealth.classList.remove('hidden');
303
  hudGearWrap.classList.remove('hidden');
 
345
  feather.replace();
346
  }
347
 
348
+ // Camera helpers
349
  function cameraUpdate(){
350
  if (!canvas.width || !canvas.height) return;
351
  camera.x = player.x - canvas.width/2;
 
355
  }
356
  function worldToScreen(wx,wy){ return { x: Math.round(wx - camera.x), y: Math.round(wy - camera.y) }; }
357
 
358
+ // Shooting, reload, melee (player)
359
  function shootBullet(originX, originY, targetX, targetY, weaponObj, shooterId){
360
  if (!weaponObj || (typeof weaponObj.ammoInMag === 'number' && weaponObj.ammoInMag <= 0)) return false;
361
  const speed = 1100;
 
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);
398
+ if (d < 36){ e.health -= 18; if (e.health <= 0) { e.health = 0; player.kills++; player.materials += 2; updatePlayerCount(); e.lastAttackedTime = performance.now(); } }
399
  }
400
  for (const obj of objects){
401
  if (obj.dead) continue;
 
407
  }
408
  }
409
 
410
+ // Interact (player)
411
  function interactNearby(){
412
  const sel = player.selectedSlot;
413
  const selItem = player.inventory[sel];
 
418
  updateHUD();
419
  return;
420
  }
 
421
  const range = 56;
422
  for (const chest of chests){
423
  if (chest.opened) continue;
424
  const d = Math.hypot(chest.x - player.x, chest.y - player.y);
425
  if (d < range){
426
+ // open and spawn pickups (player)
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') 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) });
431
+ else if (loot.type === 'medkit') pickups.push({ x:px, y:py, type:'medkit', amount: loot.amount || 1 });
432
+ else if (loot.type === 'materials') pickups.push({ x:px, y:py, type:'materials', amount: loot.amount || 5 });
 
 
 
 
433
  if (Math.random() < 0.25){
434
  pickups.push({ x:px+8, y:py+8, type:'ammo', forWeapon: null, amount: randInt(6,30) });
435
  }
 
437
  return;
438
  }
439
  }
 
440
  for (let i=pickups.length-1;i>=0;i--){
441
  const p = pickups[i];
442
  const d = Math.hypot(p.x - player.x, p.y - player.y);
 
480
 
481
  // Enemy helpers: equip best weapon, reload, collect pickups
482
  function enemyEquipBestWeapon(e){
 
483
  let bestIdx = -1;
484
  let bestScore = -Infinity;
485
  for (let i=0;i<5;i++){
 
494
 
495
  function enemyPickupCollect(e, p){
496
  if (p.type === 'weapon'){
 
497
  for (let s=0;s<5;s++){
498
  const it = e.inventory[s];
499
  if (it && it.type==='weapon' && it.weapon.name === p.weapon.name){
500
  it.ammoReserve += p.ammoReserve;
501
  it.ammoInMag = Math.min(it.weapon.magSize, it.ammoInMag + p.ammoInMag);
502
+ enemyEquipBestWeapon(e);
503
  return;
504
  }
505
  }
 
506
  for (let s=0;s<5;s++){
507
  if (!e.inventory[s]) { e.inventory[s] = { type:'weapon', weapon:p.weapon, ammoInMag:p.ammoInMag, ammoReserve:p.ammoReserve }; enemyEquipBestWeapon(e); return; }
508
  }
 
509
  let worstIdx = -1, worstScore = Infinity;
510
  const pickupScore = (p.weapon.dmg || 1) / (p.weapon.rate || 300);
511
  for (let s=0;s<5;s++){
 
513
  if (it && it.type==='weapon'){
514
  const score = (it.weapon.dmg || 1) / (it.weapon.rate || 300);
515
  if (score < worstScore){ worstScore = score; worstIdx = s; }
516
+ }
517
  }
518
  if (pickupScore > worstScore && worstIdx !== -1){
519
  e.inventory[worstIdx] = { type:'weapon', weapon:p.weapon, ammoInMag:p.ammoInMag, ammoReserve:p.ammoReserve };
520
  enemyEquipBestWeapon(e);
521
  } else {
 
522
  e.materials += Math.floor((p.ammoReserve || 0) / 2);
523
  }
524
  } else if (p.type === 'medkit'){
 
525
  for (let s=0;s<5;s++){
526
  const it = e.inventory[s];
527
  if (it && it.type==='medkit'){ it.amount += p.amount; return; }
528
  }
529
  for (let s=0;s<5;s++){ if (!e.inventory[s]) { e.inventory[s] = { type:'medkit', amount:p.amount }; return; } }
 
530
  e.materials += 3;
531
  } else if (p.type === 'materials'){
532
  e.materials += p.amount;
533
  } else if (p.type === 'ammo'){
 
534
  for (let s=0;s<5;s++){ const it=e.inventory[s]; if (it && it.type==='weapon'){ it.ammoReserve += p.amount; return; } }
535
  e.materials += p.amount;
536
  }
 
547
  }
548
  function enemyTryBuild(e){
549
  if (e.materials < 10) return false;
 
550
  e.materials -= 10;
551
  const bx = e.x + Math.cos(e.angle) * 48;
552
  const by = e.y + Math.sin(e.angle) * 48;
 
591
  return closest;
592
  }
593
 
594
+ // Bullets update: mark enemies as attacked when hit
595
  function bulletsUpdate(dt){
596
  for (let i=bullets.length-1;i>=0;i--){
597
  const b = bullets[i];
 
613
  if (b.shooter === e.id) continue;
614
  if (Math.hypot(e.x - b.x, e.y - b.y) < 14){
615
  e.health -= b.dmg;
616
+ e.lastAttackedTime = performance.now();
617
  if (e.health <= 0){ e.health = 0; if (b.shooter === 'player'){ player.kills++; player.materials += 2; updatePlayerCount(); } }
618
  bullets.splice(i,1); break;
619
  }
 
648
  }
649
  }
650
 
651
+ // Find nearest unopened chest or nearest harvestable object
652
+ function findNearestChest(e, maxDist = 1200){
653
+ let best = null; let bd = Infinity;
654
+ for (const c of chests){ if (c.opened) continue; const d = Math.hypot(c.x - e.x, c.y - e.y); if (d < bd && d <= maxDist){ bd = d; best = c; } }
655
+ return best;
656
+ }
657
+ function findNearestHarvestable(e, maxDist = 1000){
658
+ let best = null; let bd = Infinity;
659
+ for (const o of objects){ if (o.dead || o.type === 'wall') continue; const d = Math.hypot(o.x - e.x, o.y - e.y); if (d < bd && d <= maxDist){ bd = d; best = o; } }
660
+ return best;
661
+ }
662
+ function findNearestPickup(e, maxDist = 500){
663
+ let best = null; let bd = Infinity;
664
+ for (const p of pickups){ const d = Math.hypot(p.x - e.x, p.y - e.y); if (d < bd && d <= maxDist){ bd = d; best = p; } }
665
+ return best;
666
+ }
667
+
668
+ // Enemies: improved state machine - gather then combat
669
  function updateEnemies(dt, now){
670
  for (const e of enemies){
671
  if (e.health <= 0) continue;
 
672
 
673
+ // If recently attacked by player, switch to combat immediately
674
+ if (now - e.lastAttackedTime < 4000){
675
+ e.state = 'combat';
 
 
 
 
676
  }
677
 
678
+ // Decrease gather timer if still gathering
679
+ if (e.state === 'gather'){
680
+ e.gatherTimeLeft -= dt;
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
681
  }
682
 
683
+ // If underheal and has medkit, heal immediately (even while gathering)
684
  if (e.health < 60){
685
+ let used = false;
686
  for (let s=0;s<5;s++){
687
  const it = e.inventory[s];
688
  if (it && it.type === 'medkit'){
689
  it.amount -= 1;
690
  e.health = Math.min(120, e.health + 50);
691
  if (it.amount <= 0) e.inventory[s] = null;
692
+ used = true;
693
  break;
694
  }
695
  }
696
+ if (used) continue; // use time to heal this tick
697
  }
698
 
699
+ // Determine transition from 'gather' to 'combat'
700
+ const hasUsefulWeapon = e.inventory.some(it => it && it.type === 'weapon' && (it.ammoInMag > 0 || it.ammoReserve > 0));
701
+ if ((e.gatherTimeLeft <= 0 || e.materials >= 20 || hasUsefulWeapon) && e.state === 'gather'){
702
+ e.state = 'combat';
703
  }
704
 
705
+ // State behavior:
706
+ if (e.state === 'gather'){
707
+ // prefer pickups, then chests, then harvest objects
708
+ let targetPickup = findNearestPickup(e, 240);
709
+ if (targetPickup){
710
+ // move to pickup
711
+ const angle = Math.atan2(targetPickup.y - e.y, targetPickup.x - e.x);
712
+ e.angle = angle;
713
+ e.x += Math.cos(e.angle) * e.speed * dt * 0.9;
714
+ e.y += Math.sin(e.angle) * e.speed * dt * 0.9;
715
+ if (Math.hypot(targetPickup.x - e.x, targetPickup.y - e.y) < 18){
716
+ enemyPickupCollect(e, targetPickup);
717
+ // remove pickup from global array
718
+ const idx = pickups.indexOf(targetPickup);
719
+ if (idx >= 0) pickups.splice(idx,1);
720
+ }
721
+ continue;
722
+ }
723
+
724
+ let chestTarget = findNearestChest(e, 900);
725
+ if (chestTarget){
726
+ const d = Math.hypot(chestTarget.x - e.x, chestTarget.y - e.y);
727
+ if (d > 20){
728
+ e.angle = Math.atan2(chestTarget.y - e.y, chestTarget.x - e.x);
729
+ e.x += Math.cos(e.angle) * e.speed * dt * 0.9;
730
+ e.y += Math.sin(e.angle) * e.speed * dt * 0.9;
731
+ } else {
732
+ // open chest and directly give loot to enemy (avoid leaving pickups, faster)
733
+ chestTarget.opened = true;
734
+ const loot = chestTarget.loot;
735
+ if (loot.type === 'weapon'){
736
+ enemyPickupCollect(e, { type:'weapon', weapon: makeWeaponProto(loot.weapon), ammoInMag:loot.weapon.magSize||12, ammoReserve:loot.weapon.startReserve||24 });
737
+ } else if (loot.type === 'medkit'){
738
+ enemyPickupCollect(e, { type:'medkit', amount: loot.amount || 1 });
739
+ } else if (loot.type === 'materials'){
740
+ enemyPickupCollect(e, { type:'materials', amount: loot.amount || 5 });
741
+ }
742
+ }
743
+ continue;
744
+ }
745
+
746
+ // no chest nearby -> try harvest nearest wood/stone
747
+ let objTarget = findNearestHarvestable(e, 700);
748
+ if (objTarget){
749
+ const d = Math.hypot(objTarget.x - e.x, objTarget.y - e.y);
750
+ if (d > 26){
751
+ e.angle = Math.atan2(objTarget.y - e.y, objTarget.x - e.x);
752
+ e.x += Math.cos(e.angle) * e.speed * dt * 0.8;
753
+ e.y += Math.sin(e.angle) * e.speed * dt * 0.8;
754
+ } else {
755
+ // melee hit slice: reduce object hp over time
756
+ objTarget.hp -= 40 * dt; // faster than player to let enemies gather
757
+ if (objTarget.hp <= 0 && !objTarget.dead){
758
+ objTarget.dead = true;
759
+ const matGain = objTarget.type === 'wood' ? 3 : 6;
760
+ e.materials += matGain;
761
+ // small chance to spawn a weapon or medkit when breaking
762
+ if (Math.random() < 0.08){
763
+ const lootRoll = Math.random();
764
+ if (lootRoll < 0.4) enemyPickupCollect(e, { type:'medkit', amount:1 });
765
+ else if (lootRoll < 0.9) enemyPickupCollect(e, { type:'materials', amount: randInt(3,8) });
766
+ else enemyPickupCollect(e, { type:'weapon', weapon: makeWeaponProto({ name:'Pistol', dmg:12, rate:320, color:'#ffd86b', magSize:12, startReserve:24 }), ammoInMag:12, ammoReserve:24 });
767
+ }
768
+ }
769
+ }
770
+ continue;
771
+ }
772
+
773
+ // nothing to gather -> roam slowly
774
+ e.x += Math.cos(e.angle) * e.speed * dt * 0.25;
775
+ e.y += Math.sin(e.angle) * e.speed * dt * 0.25;
776
+ if (Math.random() < 0.01) e.angle += (Math.random()-0.5)*2;
777
+ continue;
778
+ } // end gather
779
+
780
+ // Combat or defend state
781
+ // ensure equipped weapon selected if present
782
+ if (e.equippedIndex === -1){
783
  enemyEquipBestWeapon(e);
784
  }
785
 
786
+ // auto-reload if needed
787
  if (e.equippedIndex >= 0){
788
  const eq = e.inventory[e.equippedIndex];
789
  if (eq && eq.type === 'weapon' && eq.ammoInMag <= 0 && eq.ammoReserve > 0){
 
791
  }
792
  }
793
 
794
+ // pick nearest target (player preferred)
795
+ let target = player;
796
+ let bestDist = Math.hypot(player.x - e.x, player.y - e.y);
797
+ for (const other of enemies){
798
+ if (other === e || other.health <= 0) continue;
799
+ const d = Math.hypot(other.x - e.x, other.y - e.y);
800
+ if (d < bestDist && Math.random() < 0.6){ bestDist = d; target = other; }
801
+ }
802
+
803
+ // If target is player & player is very close while enemy has low HP: try to heal/build/retreat rather than attack
804
+ const distToPlayer = Math.hypot(player.x - e.x, player.y - e.y);
805
+ if (distToPlayer < 160 && e.health < 35 && e.materials >= 10 && Math.random() < 0.5){
806
+ enemyTryBuild(e);
807
+ }
808
 
809
+ // If target blocked by structure, try to break it first
810
  const blocked = !hasLineOfSight(e.x, e.y, target.x, target.y);
811
  if (blocked){
812
  const blocker = findBlockingObject(e.x, e.y, target.x, target.y);
813
  if (blocker){
814
  const db = Math.hypot(blocker.x - e.x, blocker.y - e.y);
 
815
  if (db < 36){
816
+ blocker.hp -= 18 * dt * 2;
817
+ if (blocker.hp <= 0){ blocker.dead = true; e.materials += (blocker.type === 'wood' ? 3 : 6); }
818
  } else {
819
+ // shoot at blocker if possible
820
  if (e.equippedIndex >= 0){
821
  const eq = e.inventory[e.equippedIndex];
822
  if (eq && eq.type === 'weapon' && eq.ammoInMag > 0 && now - e.lastShot > (eq.weapon.rate || 300)){
 
826
  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);
827
  }
828
  } else {
 
829
  e.angle = Math.atan2(blocker.y - e.y, blocker.x - e.x);
830
  e.x += Math.cos(e.angle) * e.speed * dt * 0.8;
831
  e.y += Math.sin(e.angle) * e.speed * dt * 0.8;
832
  }
833
  }
834
+ continue;
835
  }
836
  }
837
 
838
+ // Attack logic: melee if close, otherwise ranged if has weapon and LOS
839
+ if (distToPlayer < 36 && now - e.lastMelee > e.meleeRate){
840
  e.lastMelee = now;
841
  const dmg = 10 + randInt(0,8);
842
+ player.health -= dmg;
843
+ e.lastAttackedTime = now;
844
+ if (player.health <= 0) { player.health = 0; playerDeath(); }
 
 
 
 
845
  } else if (e.equippedIndex >= 0){
846
  const eq = e.inventory[e.equippedIndex];
847
  if (eq && eq.type === 'weapon' && now - (e.lastShot||0) > (eq.weapon.rate || 300)){
848
+ if (eq.ammoInMag <= 0){
849
+ // reload if ammoReserve available
850
+ if (eq.ammoReserve > 0) reloadItem(eq);
851
+ } else {
852
+ if (hasLineOfSight(e.x, e.y, target.x, target.y)){
853
+ e.lastShot = now;
854
+ eq.ammoInMag -= 1;
855
+ const angle = Math.atan2(target.y - e.y, target.x - e.x);
856
+ 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);
857
+ e.lastAttackedTime = now;
858
+ } else {
859
+ // move to get LOS
860
+ e.angle = Math.atan2(target.y - e.y, target.x - e.x);
861
+ e.x += Math.cos(e.angle) * e.speed * dt * 0.6;
862
+ e.y += Math.sin(e.angle) * e.speed * dt * 0.6;
863
+ }
864
  }
865
+ } else {
866
+ // no weapon -> move toward target to melee
867
+ e.angle = Math.atan2(target.y - e.y, target.x - e.x);
868
+ e.x += Math.cos(e.angle) * e.speed * dt * 0.9;
869
+ e.y += Math.sin(e.angle) * e.speed * dt * 0.9;
870
  }
871
+ } else {
872
+ // unarmed: move in and melee
873
+ e.angle = Math.atan2(target.y - e.y, target.x - e.x);
874
+ e.x += Math.cos(e.angle) * e.speed * dt * 0.9;
875
+ e.y += Math.sin(e.angle) * e.speed * dt * 0.9;
876
  }
877
  }
878
  }
879
 
880
+ // Storm (unchanged)
881
  const storm = { maxRadius: 2400, radius:2400, centerX: WORLD.width/2, centerY: WORLD.height/2, damagePerSecond:1, closingSpeed: 0.6, active:false };
882
  let stormDamageAccumulator = 0;
883
  function playerInStorm(){ return Math.hypot(player.x - storm.centerX, player.y - storm.centerY) > storm.radius; }
 
954
  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();
955
  ctx.fillStyle = obj.type==='wood'? '#6b3b1a' : (obj.type==='stone'? '#6b6b6b' : '#8b5a32');
956
  ctx.fillRect(s.x-12, s.y-h, 24, h);
 
957
  if (obj.type==='wall'){
958
  ctx.fillStyle='rgba(0,0,0,0.6)'; ctx.fillRect(s.x-18, s.y-h-10, 36, 6);
959
  const hpPct = Math.max(0, obj.hp / obj.maxHp);
 
1018
  ctx.fillStyle = e.inventory[e.equippedIndex].weapon.color || '#fff';
1019
  ctx.fillRect(-10,12,8,6);
1020
  }
1021
+ // draw state marker small
1022
+ ctx.fillStyle = e.state === 'gather' ? 'rgba(0,200,200,0.9)' : (e.state === 'combat' ? 'rgba(255,80,80,0.95)' : 'rgba(255,200,80,0.9)');
1023
+ ctx.beginPath(); ctx.arc(18, -18, 5, 0, Math.PI*2); ctx.fill();
1024
  ctx.restore();
1025
  }
1026
  }
 
1093
  const dt = Math.min(0.05, (ts - lastTime)/1000);
1094
  lastTime = ts;
1095
 
1096
+ // player movement
1097
  let dx=0, dy=0;
1098
  if (keys.w) dy -= 1; if (keys.s) dy += 1; if (keys.a) dx -= 1; if (keys.d) dx += 1;
1099
  if (dx !== 0 || dy !== 0){
 
1118
  if (selected && selected.type === 'weapon') activeWeaponItem = selected;
1119
  }
1120
 
1121
+ // player attack
1122
  if (mouse.down){
1123
  if (player.equippedIndex === -1){
1124
  playerMeleeHit();
 
1134
  }
1135
  }
1136
 
1137
+ // reload/interact/build
1138
  if (keys.r) { reloadEquipped(); keys.r = false; }
 
 
1139
  if (keys.e){ interactNearby(); keys.e = false; }
 
 
1140
  if (keys.q){ tryBuild(); keys.q = false; }
1141
 
1142
+ // update
1143
  updateEnemies(dt, performance.now());
1144
  bulletsUpdate(dt);
1145
 
1146
+ // pickups auto-collect for player
1147
  for (let i=pickups.length-1;i>=0;i--){
1148
  const p = pickups[i];
1149
  if (Math.hypot(p.x - player.x, p.y - player.y) < 18){ pickupCollect(p); pickups.splice(i,1); updateHUD(); }