bbc123321 commited on
Commit
e3a82b5
·
verified ·
1 Parent(s): 6b0ea5f

Manual changes saved

Browse files
Files changed (1) hide show
  1. index.html +107 -63
index.html CHANGED
@@ -559,39 +559,68 @@
559
 
560
  // Enemy helpers
561
  function enemyEquipBestWeapon(e){
 
562
  let bestIdx = -1;
563
  let bestScore = -Infinity;
564
  for (let i=0;i<5;i++){
565
  const it = e.inventory[i];
566
  if (it && it.type === 'weapon'){
567
- const score = (it.weapon.dmg || 1) / (it.weapon.rate || 300);
 
 
 
 
 
568
  if (score > bestScore){ bestScore = score; bestIdx = i; }
569
  }
570
  }
571
- if (bestIdx !== -1) e.equippedIndex = bestIdx;
572
- else e.equippedIndex = -1;
 
 
 
 
 
 
 
 
 
 
 
573
  }
574
 
575
  function enemyPickupCollect(e, p){
576
- // p is a pickup-like object
577
- if (!e) return;
578
  if (p.type === 'weapon'){
 
 
579
  for (let s=0;s<5;s++){
580
  const it = e.inventory[s];
581
- if (it && it.type==='weapon' && it.weapon.name === p.weapon.name){
582
- it.ammoReserve += p.ammoReserve;
583
- it.ammoInMag = Math.min(it.weapon.magSize, it.ammoInMag + p.ammoInMag);
584
  enemyEquipBestWeapon(e);
585
- e.state = 'combat'; // become aggressive when stocked
 
 
586
  e.lastAttackedTime = performance.now();
587
  return;
588
  }
589
  }
 
590
  for (let s=0;s<5;s++){
591
- if (!e.inventory[s]) { e.inventory[s] = { type:'weapon', weapon:p.weapon, ammoInMag:p.ammoInMag, ammoReserve:p.ammoReserve }; enemyEquipBestWeapon(e); e.state = 'combat'; e.lastAttackedTime = performance.now(); return; }
 
 
 
 
 
 
 
 
592
  }
 
593
  let worstIdx = -1, worstScore = Infinity;
594
- const pickupScore = (p.weapon.dmg || 1) / (p.weapon.rate || 300);
595
  for (let s=0;s<5;s++){
596
  const it = e.inventory[s];
597
  if (it && it.type==='weapon'){
@@ -599,13 +628,14 @@
599
  if (score < worstScore){ worstScore = score; worstIdx = s; }
600
  }
601
  }
 
602
  if (pickupScore > worstScore && worstIdx !== -1){
603
- e.inventory[worstIdx] = { type:'weapon', weapon:p.weapon, ammoInMag:p.ammoInMag, ammoReserve:p.ammoReserve };
604
  enemyEquipBestWeapon(e);
605
  e.state = 'combat';
606
  e.lastAttackedTime = performance.now();
607
  } else {
608
- e.materials += Math.floor((p.ammoReserve || 0) / 2);
609
  }
610
  } else if (p.type === 'medkit'){
611
  for (let s=0;s<5;s++){
@@ -775,7 +805,7 @@
775
  return best;
776
  }
777
 
778
- // Enemy AI (improved: detour around obstacles and prioritize chest looting early)
779
  function updateEnemies(dt, now){
780
  const minSeparation = 20;
781
  for (const e of enemies){
@@ -790,7 +820,6 @@
790
  const distToSafeCenter = Math.hypot(e.x - storm.centerX, e.y - storm.centerY);
791
  if (distToSafeCenter > storm.radius){
792
  e.state = 'toSafe';
793
- // If we have a tempTarget navigate to it until it's reached or LOS to center is available
794
  if (e.tempTarget){
795
  const td = Math.hypot(e.tempTarget.x - e.x, e.tempTarget.y - e.y);
796
  if (td > 8){
@@ -800,36 +829,18 @@
800
  moveEntityWithCollision(e, dx, dy, e.radius);
801
  continue;
802
  } else {
803
- // reached temp target - clear it if LOS to center exists
804
  e.tempTarget = null;
805
  e.tempTargetExpiry = 0;
806
- if (hasLineOfSight(e.x, e.y, storm.centerX, storm.centerY) === false){
807
- // set another detour if still blocked
808
- const blocker = findBlockingObject(e.x, e.y, storm.centerX, storm.centerY);
809
- if (blocker){
810
- e.tempTarget = computeDetourWaypoint(e.x, e.y, blocker, storm.centerX, storm.centerY);
811
- e.tempTargetExpiry = now + 2500;
812
- if (e.tempTarget) { e.angle = Math.atan2(e.tempTarget.y - e.y, e.tempTarget.x - e.x); const dx = Math.cos(e.angle) * e.speed * dt * 0.9; const dy = Math.sin(e.angle) * e.speed * dt * 0.9; moveEntityWithCollision(e, dx, dy, e.radius); continue; }
813
- }
814
- }
815
  }
816
  }
817
- // No temp target or cleared: try to go directly to center, if blocked compute detour
818
  if (!hasLineOfSight(e.x, e.y, storm.centerX, storm.centerY)){
819
  const blocker = findBlockingObject(e.x, e.y, storm.centerX, storm.centerY);
820
  if (blocker){
821
  e.tempTarget = computeDetourWaypoint(e.x, e.y, blocker, storm.centerX, storm.centerY);
822
  e.tempTargetExpiry = now + 2500;
823
- if (e.tempTarget){
824
- e.angle = Math.atan2(e.tempTarget.y - e.y, e.tempTarget.x - e.x);
825
- const dx = Math.cos(e.angle) * e.speed * dt * 0.95;
826
- const dy = Math.sin(e.angle) * e.speed * dt * 0.95;
827
- moveEntityWithCollision(e, dx, dy, e.radius);
828
- continue;
829
- }
830
  }
831
  }
832
- // If unobstructed or unable to create temp target, head straight to safe center
833
  e.angle = Math.atan2(storm.centerY - e.y, storm.centerX - e.x);
834
  const dx = Math.cos(e.angle) * e.speed * dt * 0.95;
835
  const dy = Math.sin(e.angle) * e.speed * dt * 0.95;
@@ -878,6 +889,8 @@
878
  enemyPickupCollect(e, p);
879
  const idx = pickups.indexOf(p);
880
  if (idx >= 0) pickups.splice(idx,1);
 
 
881
  }
882
  continue;
883
  }
@@ -894,7 +907,6 @@
894
  if (!hasLineOfSight(e.x, e.y, chestTarget.x, chestTarget.y)){
895
  const blocker = findBlockingObject(e.x, e.y, chestTarget.x, chestTarget.y);
896
  if (blocker){
897
- // set a tempTarget detour
898
  if (!e.tempTarget){
899
  e.tempTarget = computeDetourWaypoint(e.x, e.y, blocker, chestTarget.x, chestTarget.y);
900
  e.tempTargetExpiry = now + 2500;
@@ -923,9 +935,20 @@
923
  if (!chestTarget.opened){
924
  chestTarget.opened = true;
925
  const loot = chestTarget.loot;
926
- if (loot.type === 'weapon') enemyPickupCollect(e, { type:'weapon', weapon: makeWeaponProto(loot.weapon), ammoInMag:loot.weapon.magSize||12, ammoReserve:loot.weapon.startReserve||24 });
927
- else if (loot.type === 'medkit') enemyPickupCollect(e, { type:'medkit', amount: loot.amount || 1 });
 
 
928
  else if (loot.type === 'materials') enemyPickupCollect(e, { type:'materials', amount: loot.amount || 10 });
 
 
 
 
 
 
 
 
 
929
  // after opening chest, prioritize combat if weapon obtained
930
  if (e.inventory.some(it => it && it.type==='weapon')) {
931
  e.state = 'combat';
@@ -936,9 +959,8 @@
936
  continue;
937
  }
938
 
939
- // Harvest nodes (lower priority than chests/pickups)
940
  let objTarget = findNearestHarvestable(e, 700);
941
- if (objTarget && !prioritizeChests && hasLineOfSight(e.x, e.y, objTarget.x, objTarget.y)){
942
  const d = Math.hypot(objTarget.x - e.x, objTarget.y - e.y);
943
  if (d > 26){
944
  e.angle = Math.atan2(objTarget.y - e.y, objTarget.x - e.x);
@@ -961,18 +983,6 @@
961
  continue;
962
  }
963
 
964
- // Roam
965
- if (e.tempTarget){
966
- const td = Math.hypot(e.tempTarget.x - e.x, e.tempTarget.y - e.y);
967
- if (td > 8){
968
- e.angle = Math.atan2(e.tempTarget.y - e.y, e.tempTarget.x - e.x);
969
- const dx = Math.cos(e.angle) * e.speed * dt * 0.5;
970
- const dy = Math.sin(e.angle) * e.speed * dt * 0.5;
971
- moveEntityWithCollision(e, dx, dy, e.radius);
972
- continue;
973
- } else { e.tempTarget = null; e.tempTargetExpiry = 0; }
974
- }
975
-
976
  const dx = Math.cos(e.angle) * e.speed * dt * 0.25;
977
  const dy = Math.sin(e.angle) * e.speed * dt * 0.25;
978
  moveEntityWithCollision(e, dx, dy, e.radius);
@@ -998,7 +1008,34 @@
998
  }
999
  }
1000
 
1001
- // Target selection: prefer player if in range; otherwise prefer nearest visible enemy or pickup/chest when not armed
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1002
  let target = player;
1003
  let bestDist = Math.hypot(player.x - e.x, player.y - e.y);
1004
  if (bestDist > VIEW_RANGE){
@@ -1047,24 +1084,19 @@
1047
  const blocker = findBlockingObject(e.x, e.y, target.x, target.y);
1048
  if (blocker){
1049
  const db = Math.hypot(blocker.x - e.x, blocker.y - e.y);
1050
- // if close to blocker, try to break it quickly
1051
  if (db < 36){
1052
  blocker.hp -= 18 * dt * 2;
1053
  if (blocker.hp <= 0){ blocker.dead = true; e.materials += (blocker.type === 'wood' ? 3 : 6); }
1054
- continue;
1055
  } else {
1056
- // if ranged weapon and can shoot through, do so; otherwise compute detour
1057
  if (e.equippedIndex >= 0){
1058
  const eq = e.inventory[e.equippedIndex];
1059
  if (eq && eq.type === 'weapon' && eq.ammoInMag > 0 && !e.reloadPending && now - e.lastShot > (eq.weapon.rate || 300)){
1060
- // attempt to shoot at blocker to create opening, but also compute detour as fallback
1061
  e.lastShot = now;
1062
  eq.ammoInMag -= 1;
1063
  const angle = Math.atan2(blocker.y - e.y, blocker.x - e.x);
1064
  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);
1065
  }
1066
  }
1067
- // Compute or reuse a detour waypoint
1068
  if (!e.tempTarget){
1069
  const waypoint = computeDetourWaypoint(e.x, e.y, blocker, target.x, target.y);
1070
  if (waypoint){
@@ -1082,16 +1114,15 @@
1082
  e.tempTargetExpiry = 0;
1083
  }
1084
  } else {
1085
- // fallback: nudge towards blocker to try to clear
1086
  e.angle = Math.atan2(blocker.y - e.y, blocker.x - e.x);
1087
- moveEntityWithCollision(e, Math.cos(e.angle) * e.speed * dt * 0.6, Math.sin(e.angle) * e.speed * dt * 0.6, e.radius);
 
1088
  }
1089
  }
1090
  continue;
1091
  }
1092
  }
1093
 
1094
- // If target is player, handle combat vs player
1095
  if (target === player){
1096
  if (distToPlayer < 36 && now - e.lastMelee > e.meleeRate){
1097
  e.lastMelee = now;
@@ -1103,6 +1134,14 @@
1103
  const eq = e.inventory[e.equippedIndex];
1104
  if (eq && eq.type === 'weapon' && !e.reloadPending && now - (e.lastShot||0) > (eq.weapon.rate || 300)){
1105
  if (eq.ammoInMag <= 0){
 
 
 
 
 
 
 
 
1106
  } else {
1107
  if (hasLineOfSight(e.x, e.y, target.x, target.y)){
1108
  e.lastShot = now;
@@ -1124,7 +1163,6 @@
1124
  moveEntityWithCollision(e, Math.cos(e.angle) * e.speed * dt * 0.9, Math.sin(e.angle) * e.speed * dt * 0.9, e.radius);
1125
  }
1126
  } else {
1127
- // Target is an item, chest, harvestable, or another enemy
1128
  const td = Math.hypot(target.x - e.x, target.y - e.y);
1129
  if (target.type === 'weapon' || target.type === 'medkit' || target.type === 'materials' || target.type === 'ammo'){
1130
  if (td > 20){
@@ -1135,6 +1173,7 @@
1135
  const idx = pickups.indexOf(target);
1136
  if (idx >= 0) pickups.splice(idx,1);
1137
  e.state = 'gather';
 
1138
  }
1139
  } else if (target.hasOwnProperty('loot')){
1140
  if (td > 20){
@@ -1148,7 +1187,13 @@
1148
  else if (loot.type === 'medkit') enemyPickupCollect(e, { type:'medkit', amount: loot.amount || 1 });
1149
  else if (loot.type === 'materials') enemyPickupCollect(e, { type:'materials', amount: loot.amount || 10 });
1150
  e.state = 'gather';
1151
- // immediately go to combat if they got a weapon
 
 
 
 
 
 
1152
  if (e.inventory.some(it => it && it.type === 'weapon')){ e.state = 'combat'; e.lastAttackedTime = now; }
1153
  }
1154
  }
@@ -1165,7 +1210,6 @@
1165
  e.state = 'gather';
1166
  }
1167
  } else {
1168
- // target is another enemy (melee)
1169
  if (td > 40){
1170
  e.angle = Math.atan2(target.y - e.y, target.x - e.x);
1171
  moveEntityWithCollision(e, Math.cos(e.angle) * e.speed * dt * 0.7, Math.sin(e.angle) * e.speed * dt * 0.7, e.radius);
 
559
 
560
  // Enemy helpers
561
  function enemyEquipBestWeapon(e){
562
+ // prefer weapons that have ammo loaded or reserve; compute score and prefer loaded ones
563
  let bestIdx = -1;
564
  let bestScore = -Infinity;
565
  for (let i=0;i<5;i++){
566
  const it = e.inventory[i];
567
  if (it && it.type === 'weapon'){
568
+ const dmg = (it.weapon && it.weapon.dmg) ? it.weapon.dmg : 1;
569
+ const rate = (it.weapon && it.weapon.rate) ? it.weapon.rate : 300;
570
+ let score = dmg / rate;
571
+ // bonus if currently has ammo in mag
572
+ if (typeof it.ammoInMag === 'number' && it.ammoInMag > 0) score += 0.5;
573
+ if (typeof it.ammoReserve === 'number' && it.ammoReserve > 0) score += 0.2;
574
  if (score > bestScore){ bestScore = score; bestIdx = i; }
575
  }
576
  }
577
+ if (bestIdx !== -1) {
578
+ e.equippedIndex = bestIdx;
579
+ } else {
580
+ e.equippedIndex = -1;
581
+ }
582
+ }
583
+
584
+ function normalizeWeaponPickup(p){
585
+ // ensure fields exist and are numbers
586
+ const w = p.weapon ? p.weapon : makeWeaponProto(p);
587
+ const ammoInMag = (typeof p.ammoInMag === 'number') ? p.ammoInMag : (w.magSize || w.magSize || 12);
588
+ const ammoReserve = (typeof p.ammoReserve === 'number') ? p.ammoReserve : (w.startReserve || (w.magSize*2) || 24);
589
+ return { type:'weapon', weapon: w, ammoInMag: Math.max(0, Math.floor(ammoInMag)), ammoReserve: Math.max(0, Math.floor(ammoReserve)) };
590
  }
591
 
592
  function enemyPickupCollect(e, p){
593
+ if (!e || !p) return;
 
594
  if (p.type === 'weapon'){
595
+ const normalized = normalizeWeaponPickup(p);
596
+ // merge into existing same weapon
597
  for (let s=0;s<5;s++){
598
  const it = e.inventory[s];
599
+ if (it && it.type==='weapon' && it.weapon.name === normalized.weapon.name){
600
+ it.ammoReserve = (it.ammoReserve || 0) + (normalized.ammoReserve || 0);
601
+ it.ammoInMag = Math.min(it.weapon.magSize, (it.ammoInMag || 0) + (normalized.ammoInMag || 0));
602
  enemyEquipBestWeapon(e);
603
+ // ensure they equip a loaded weapon if present
604
+ if (it.ammoInMag > 0) e.equippedIndex = s;
605
+ e.state = 'combat';
606
  e.lastAttackedTime = performance.now();
607
  return;
608
  }
609
  }
610
+ // place into empty slot if available
611
  for (let s=0;s<5;s++){
612
+ if (!e.inventory[s]){
613
+ e.inventory[s] = { type:'weapon', weapon: normalized.weapon, ammoInMag: normalized.ammoInMag, ammoReserve: normalized.ammoReserve };
614
+ enemyEquipBestWeapon(e);
615
+ // equip this weapon if it has ammo or if no other better weapon
616
+ if (e.inventory[s].ammoInMag > 0) e.equippedIndex = s;
617
+ e.state = 'combat';
618
+ e.lastAttackedTime = performance.now();
619
+ return;
620
+ }
621
  }
622
+ // replace worst weapon if pickup is better
623
  let worstIdx = -1, worstScore = Infinity;
 
624
  for (let s=0;s<5;s++){
625
  const it = e.inventory[s];
626
  if (it && it.type==='weapon'){
 
628
  if (score < worstScore){ worstScore = score; worstIdx = s; }
629
  }
630
  }
631
+ const pickupScore = (normalized.weapon.dmg || 1) / (normalized.weapon.rate || 300);
632
  if (pickupScore > worstScore && worstIdx !== -1){
633
+ e.inventory[worstIdx] = { type:'weapon', weapon: normalized.weapon, ammoInMag: normalized.ammoInMag, ammoReserve: normalized.ammoReserve };
634
  enemyEquipBestWeapon(e);
635
  e.state = 'combat';
636
  e.lastAttackedTime = performance.now();
637
  } else {
638
+ e.materials += Math.floor((normalized.ammoReserve || 0) / 2);
639
  }
640
  } else if (p.type === 'medkit'){
641
  for (let s=0;s<5;s++){
 
805
  return best;
806
  }
807
 
808
+ // Enemy AI (improved: detour around obstacles and prioritize chest looting early, ensure equipping)
809
  function updateEnemies(dt, now){
810
  const minSeparation = 20;
811
  for (const e of enemies){
 
820
  const distToSafeCenter = Math.hypot(e.x - storm.centerX, e.y - storm.centerY);
821
  if (distToSafeCenter > storm.radius){
822
  e.state = 'toSafe';
 
823
  if (e.tempTarget){
824
  const td = Math.hypot(e.tempTarget.x - e.x, e.tempTarget.y - e.y);
825
  if (td > 8){
 
829
  moveEntityWithCollision(e, dx, dy, e.radius);
830
  continue;
831
  } else {
 
832
  e.tempTarget = null;
833
  e.tempTargetExpiry = 0;
 
 
 
 
 
 
 
 
 
834
  }
835
  }
 
836
  if (!hasLineOfSight(e.x, e.y, storm.centerX, storm.centerY)){
837
  const blocker = findBlockingObject(e.x, e.y, storm.centerX, storm.centerY);
838
  if (blocker){
839
  e.tempTarget = computeDetourWaypoint(e.x, e.y, blocker, storm.centerX, storm.centerY);
840
  e.tempTargetExpiry = now + 2500;
841
+ if (e.tempTarget) { e.angle = Math.atan2(e.tempTarget.y - e.y, e.tempTarget.x - e.x); const dx = Math.cos(e.angle) * e.speed * dt * 0.95; const dy = Math.sin(e.angle) * e.speed * dt * 0.95; moveEntityWithCollision(e, dx, dy, e.radius); continue; }
 
 
 
 
 
 
842
  }
843
  }
 
844
  e.angle = Math.atan2(storm.centerY - e.y, storm.centerX - e.x);
845
  const dx = Math.cos(e.angle) * e.speed * dt * 0.95;
846
  const dy = Math.sin(e.angle) * e.speed * dt * 0.95;
 
889
  enemyPickupCollect(e, p);
890
  const idx = pickups.indexOf(p);
891
  if (idx >= 0) pickups.splice(idx,1);
892
+ // equip best weapon immediately if any
893
+ enemyEquipBestWeapon(e);
894
  }
895
  continue;
896
  }
 
907
  if (!hasLineOfSight(e.x, e.y, chestTarget.x, chestTarget.y)){
908
  const blocker = findBlockingObject(e.x, e.y, chestTarget.x, chestTarget.y);
909
  if (blocker){
 
910
  if (!e.tempTarget){
911
  e.tempTarget = computeDetourWaypoint(e.x, e.y, blocker, chestTarget.x, chestTarget.y);
912
  e.tempTargetExpiry = now + 2500;
 
935
  if (!chestTarget.opened){
936
  chestTarget.opened = true;
937
  const loot = chestTarget.loot;
938
+ if (loot.type === 'weapon') {
939
+ const wp = { type:'weapon', weapon: makeWeaponProto(loot.weapon), ammoInMag:loot.weapon.magSize||12, ammoReserve:loot.weapon.startReserve||24 };
940
+ enemyPickupCollect(e, wp);
941
+ } else if (loot.type === 'medkit') enemyPickupCollect(e, { type:'medkit', amount: loot.amount || 1 });
942
  else if (loot.type === 'materials') enemyPickupCollect(e, { type:'materials', amount: loot.amount || 10 });
943
+ // ensure equip
944
+ enemyEquipBestWeapon(e);
945
+ if (e.equippedIndex !== -1){
946
+ const eq = e.inventory[e.equippedIndex];
947
+ if (eq && eq.type==='weapon' && eq.ammoInMag <= 0 && eq.ammoReserve > 0){
948
+ // immediate reload
949
+ reloadItem(eq);
950
+ }
951
+ }
952
  // after opening chest, prioritize combat if weapon obtained
953
  if (e.inventory.some(it => it && it.type==='weapon')) {
954
  e.state = 'combat';
 
959
  continue;
960
  }
961
 
 
962
  let objTarget = findNearestHarvestable(e, 700);
963
+ if (objTarget && !prioritizeChests){
964
  const d = Math.hypot(objTarget.x - e.x, objTarget.y - e.y);
965
  if (d > 26){
966
  e.angle = Math.atan2(objTarget.y - e.y, objTarget.x - e.x);
 
983
  continue;
984
  }
985
 
 
 
 
 
 
 
 
 
 
 
 
 
986
  const dx = Math.cos(e.angle) * e.speed * dt * 0.25;
987
  const dy = Math.sin(e.angle) * e.speed * dt * 0.25;
988
  moveEntityWithCollision(e, dx, dy, e.radius);
 
1008
  }
1009
  }
1010
 
1011
+ // If equipped weapon is empty and no reserve, try to swap to another loaded weapon
1012
+ if (e.equippedIndex >= 0){
1013
+ const eq = e.inventory[e.equippedIndex];
1014
+ if (eq && eq.type === 'weapon' && eq.ammoInMag <= 0 && eq.ammoReserve <= 0){
1015
+ // try to find another weapon with ammo
1016
+ for (let i=0;i<5;i++){
1017
+ const it = e.inventory[i];
1018
+ if (it && it.type === 'weapon' && it.ammoInMag > 0){
1019
+ e.equippedIndex = i;
1020
+ break;
1021
+ }
1022
+ }
1023
+ // if still no mag ammo, pick weapon with reserve
1024
+ if (e.equippedIndex === -1 || (e.inventory[e.equippedIndex] && e.inventory[e.equippedIndex].ammoInMag <= 0)){
1025
+ for (let i=0;i<5;i++){
1026
+ const it = e.inventory[i];
1027
+ if (it && it.type === 'weapon' && it.ammoReserve > 0){
1028
+ e.equippedIndex = i;
1029
+ // schedule reload
1030
+ e.reloadPending = true;
1031
+ e.reloadingUntil = now + 500 + rand(-80,80);
1032
+ break;
1033
+ }
1034
+ }
1035
+ }
1036
+ }
1037
+ }
1038
+
1039
  let target = player;
1040
  let bestDist = Math.hypot(player.x - e.x, player.y - e.y);
1041
  if (bestDist > VIEW_RANGE){
 
1084
  const blocker = findBlockingObject(e.x, e.y, target.x, target.y);
1085
  if (blocker){
1086
  const db = Math.hypot(blocker.x - e.x, blocker.y - e.y);
 
1087
  if (db < 36){
1088
  blocker.hp -= 18 * dt * 2;
1089
  if (blocker.hp <= 0){ blocker.dead = true; e.materials += (blocker.type === 'wood' ? 3 : 6); }
 
1090
  } else {
 
1091
  if (e.equippedIndex >= 0){
1092
  const eq = e.inventory[e.equippedIndex];
1093
  if (eq && eq.type === 'weapon' && eq.ammoInMag > 0 && !e.reloadPending && now - e.lastShot > (eq.weapon.rate || 300)){
 
1094
  e.lastShot = now;
1095
  eq.ammoInMag -= 1;
1096
  const angle = Math.atan2(blocker.y - e.y, blocker.x - e.x);
1097
  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);
1098
  }
1099
  }
 
1100
  if (!e.tempTarget){
1101
  const waypoint = computeDetourWaypoint(e.x, e.y, blocker, target.x, target.y);
1102
  if (waypoint){
 
1114
  e.tempTargetExpiry = 0;
1115
  }
1116
  } else {
 
1117
  e.angle = Math.atan2(blocker.y - e.y, blocker.x - e.x);
1118
+ e.x += Math.cos(e.angle) * e.speed * dt * 0.6;
1119
+ e.y += Math.sin(e.angle) * e.speed * dt * 0.6;
1120
  }
1121
  }
1122
  continue;
1123
  }
1124
  }
1125
 
 
1126
  if (target === player){
1127
  if (distToPlayer < 36 && now - e.lastMelee > e.meleeRate){
1128
  e.lastMelee = now;
 
1134
  const eq = e.inventory[e.equippedIndex];
1135
  if (eq && eq.type === 'weapon' && !e.reloadPending && now - (e.lastShot||0) > (eq.weapon.rate || 300)){
1136
  if (eq.ammoInMag <= 0){
1137
+ // try reload or swap
1138
+ if (eq.ammoReserve > 0 && !e.reloadPending){
1139
+ e.reloadPending = true;
1140
+ e.reloadingUntil = now + 600 + rand(-100,100);
1141
+ } else {
1142
+ // swap to other weapon
1143
+ enemyEquipBestWeapon(e);
1144
+ }
1145
  } else {
1146
  if (hasLineOfSight(e.x, e.y, target.x, target.y)){
1147
  e.lastShot = now;
 
1163
  moveEntityWithCollision(e, Math.cos(e.angle) * e.speed * dt * 0.9, Math.sin(e.angle) * e.speed * dt * 0.9, e.radius);
1164
  }
1165
  } else {
 
1166
  const td = Math.hypot(target.x - e.x, target.y - e.y);
1167
  if (target.type === 'weapon' || target.type === 'medkit' || target.type === 'materials' || target.type === 'ammo'){
1168
  if (td > 20){
 
1173
  const idx = pickups.indexOf(target);
1174
  if (idx >= 0) pickups.splice(idx,1);
1175
  e.state = 'gather';
1176
+ enemyEquipBestWeapon(e);
1177
  }
1178
  } else if (target.hasOwnProperty('loot')){
1179
  if (td > 20){
 
1187
  else if (loot.type === 'medkit') enemyPickupCollect(e, { type:'medkit', amount: loot.amount || 1 });
1188
  else if (loot.type === 'materials') enemyPickupCollect(e, { type:'materials', amount: loot.amount || 10 });
1189
  e.state = 'gather';
1190
+ enemyEquipBestWeapon(e);
1191
+ if (e.equippedIndex !== -1){
1192
+ const eq = e.inventory[e.equippedIndex];
1193
+ if (eq && eq.type==='weapon' && eq.ammoInMag <= 0 && eq.ammoReserve > 0){
1194
+ reloadItem(eq);
1195
+ }
1196
+ }
1197
  if (e.inventory.some(it => it && it.type === 'weapon')){ e.state = 'combat'; e.lastAttackedTime = now; }
1198
  }
1199
  }
 
1210
  e.state = 'gather';
1211
  }
1212
  } else {
 
1213
  if (td > 40){
1214
  e.angle = Math.atan2(target.y - e.y, target.x - e.x);
1215
  moveEntityWithCollision(e, Math.cos(e.angle) * e.speed * dt * 0.7, Math.sin(e.angle) * e.speed * dt * 0.7, e.radius);