bbc123321 commited on
Commit
2f162ab
·
verified ·
1 Parent(s): 492f5c0

Manual changes saved

Browse files
Files changed (1) hide show
  1. index.html +70 -200
index.html CHANGED
@@ -332,8 +332,18 @@
332
  }
333
 
334
  // Populate world
 
335
  function populateWorld(){
336
  chests.length = 0; objects.length = 0; enemies.length = 0; pickups.length = 0;
 
 
 
 
 
 
 
 
 
337
  for (let i=0;i<260;i++){
338
  const x = rand(150, WORLD.width-150);
339
  const y = rand(150, WORLD.height-150);
@@ -354,7 +364,9 @@
354
  for (let i=0;i<49;i++){
355
  const ex = rand(300, WORLD.width-300);
356
  const ey = rand(300, WORLD.height-300);
357
- enemies.push({
 
 
358
  id:'e'+i, x:ex, y:ey, radius:14, angle:rand(0,Math.PI*2), speed:110+rand(-20,20),
359
  health: 80 + randInt(0,40), lastMelee:0, meleeRate:800 + randInt(-200,200),
360
  roamTimer: rand(0,3),
@@ -374,7 +386,24 @@
374
  tempTarget: null,
375
  tempTargetExpiry: 0,
376
  prioritizeChestsUntil: 0
377
- });
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
378
  }
379
  updatePlayerCount();
380
  }
@@ -559,67 +588,48 @@
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];
@@ -628,14 +638,14 @@
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++){
@@ -714,21 +724,17 @@
714
  else if (blocker.type === 'stone') br = 22;
715
  else if (blocker.type === 'wood') br = 16;
716
  else br = 18;
717
- // vector from blocker to from
718
  const vx = fromX - blocker.x;
719
  const vy = fromY - blocker.y;
720
  const len = Math.hypot(vx, vy) || 0.0001;
721
- // perp directions
722
  const px = -vy / len;
723
  const py = vx / len;
724
  const radius = br + padding + 8;
725
  const cand1 = { x: blocker.x + px * radius, y: blocker.y + py * radius };
726
  const cand2 = { x: blocker.x - px * radius, y: blocker.y - py * radius };
727
- // choose candidate that's closer to goal and not inside other solids (approx)
728
  const d1 = Math.hypot(cand1.x - goalX, cand1.y - goalY);
729
  const d2 = Math.hypot(cand2.x - goalX, cand2.y - goalY);
730
  const chosen = d1 < d2 ? cand1 : cand2;
731
- // clamp to world
732
  chosen.x = Math.max(16, Math.min(WORLD.width-16, chosen.x));
733
  chosen.y = Math.max(16, Math.min(WORLD.height-16, chosen.y));
734
  return chosen;
@@ -805,42 +811,17 @@
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){
812
  if (e.health <= 0) continue;
813
  if (!e.spawnSafeUntil) e.spawnSafeUntil = performance.now() + SPAWN_PROTECT_MS;
814
 
815
- // clear expired tempTarget
816
- if (e.tempTarget && now > (e.tempTargetExpiry || 0)) { e.tempTarget = null; e.tempTargetExpiry = 0; }
817
-
818
- // If in storm move towards safe zone, using detours around blockers
819
  if (storm.active){
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){
826
- e.angle = Math.atan2(e.tempTarget.y - e.y, e.tempTarget.x - e.x);
827
- const dx = Math.cos(e.angle) * e.speed * dt * 0.9;
828
- const dy = Math.sin(e.angle) * e.speed * dt * 0.9;
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;
@@ -850,7 +831,9 @@
850
  }
851
  }
852
 
853
- // Healing logic
 
 
854
  if (e.health < 60 && now >= (e.nextHealTime || 0)){
855
  let medIdx = -1;
856
  for (let s=0;s<5;s++){
@@ -867,19 +850,13 @@
867
  }
868
  }
869
 
870
- // State transitions
871
  const hasUsefulWeapon = e.inventory.some(it => it && it.type === 'weapon' && (it.ammoInMag > 0 || it.ammoReserve > 0));
872
  if ((e.gatherTimeLeft <= 0 || e.materials >= 20 || hasUsefulWeapon) && e.state === 'gather') e.state = 'combat';
873
 
874
- if (e.state === 'gather') e.gatherTimeLeft -= dt;
875
-
876
- // Early-game chest priority: force chest-seeking for first few seconds to equip weapons quickly
877
- const prioritizeChests = (e.prioritizeChestsUntil && now < e.prioritizeChestsUntil);
878
  if (e.state === 'gather'){
879
  if (now >= (e.spawnSafeUntil || 0)){
880
- // pickups nearby priority (quick ammo/weapon)
881
  let p = findNearestPickup(e, 240);
882
- if (p && !prioritizeChests){
883
  const angle = Math.atan2(p.y - e.y, p.x - e.x);
884
  e.angle = angle;
885
  const dx = Math.cos(e.angle) * e.speed * dt * 0.9;
@@ -889,78 +866,40 @@
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
  }
897
  }
898
 
899
- // If we don't have a useful weapon yet, prioritize chests strongly (extended radius)
900
- let chestSearchRadius = prioritizeChests || !hasUsefulWeapon ? 1400 : 900;
901
- let chestTarget = findNearestChest(e, chestSearchRadius);
902
  if (chestTarget){
903
  const d = Math.hypot(chestTarget.x - e.x, chestTarget.y - e.y);
904
  if (d > 20){
905
  e.angle = Math.atan2(chestTarget.y - e.y, chestTarget.x - e.x);
906
- // navigate with detour logic if blocked
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;
913
- }
914
- if (e.tempTarget){
915
- const td = Math.hypot(e.tempTarget.x - e.x, e.tempTarget.y - e.y);
916
- if (td > 8){
917
- e.angle = Math.atan2(e.tempTarget.y - e.y, e.tempTarget.x - e.x);
918
- const dx = Math.cos(e.angle) * e.speed * dt * 0.9;
919
- const dy = Math.sin(e.angle) * e.speed * dt * 0.9;
920
- moveEntityWithCollision(e, dx, dy, e.radius);
921
- continue;
922
- } else {
923
- e.tempTarget = null;
924
- e.tempTargetExpiry = 0;
925
- }
926
- }
927
- }
928
- }
929
- // If unobstructed or detour not set/needed, move directly
930
- const dx = Math.cos(e.angle) * e.speed * dt * 0.95;
931
- const dy = Math.sin(e.angle) * e.speed * dt * 0.95;
932
  moveEntityWithCollision(e, dx, dy, e.radius);
933
  } else {
934
- // open chest and collect
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';
955
- e.lastAttackedTime = now;
956
- }
957
  }
958
  }
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);
@@ -990,7 +929,7 @@
990
  continue;
991
  }
992
 
993
- // Combat state
994
  if (e.equippedIndex === -1) enemyEquipBestWeapon(e);
995
  if (e.reloadPending){
996
  if (now >= e.reloadingUntil){
@@ -1008,34 +947,6 @@
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){
@@ -1078,7 +989,6 @@
1078
  continue;
1079
  }
1080
 
1081
- // If a target exists but blocked, try detour waypoint instead of always breaking/shooting blocker
1082
  const blocked = !hasLineOfSight(e.x, e.y, target.x, target.y);
1083
  if (blocked){
1084
  const blocker = findBlockingObject(e.x, e.y, target.x, target.y);
@@ -1096,27 +1006,10 @@
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){
1103
- e.tempTarget = waypoint;
1104
- e.tempTargetExpiry = now + 3000;
1105
- }
1106
- }
1107
- if (e.tempTarget){
1108
- const td = Math.hypot(e.tempTarget.x - e.x, e.tempTarget.y - e.y);
1109
- if (td > 8){
1110
- e.angle = Math.atan2(e.tempTarget.y - e.y, e.tempTarget.x - e.x);
1111
- moveEntityWithCollision(e, Math.cos(e.angle) * e.speed * dt * 0.8, Math.sin(e.angle) * e.speed * dt * 0.8, e.radius);
1112
- } else {
1113
- e.tempTarget = null;
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;
@@ -1134,14 +1027,6 @@
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;
@@ -1173,29 +1058,18 @@
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){
1180
  e.angle = Math.atan2(target.y - e.y, target.x - e.x);
1181
  moveEntityWithCollision(e, Math.cos(e.angle) * e.speed * dt * 0.9, Math.sin(e.angle) * e.speed * dt * 0.9, e.radius);
1182
  } else {
1183
- if (!target.opened){
1184
- target.opened = true;
1185
- const loot = target.loot;
1186
- if (loot.type === 'weapon') enemyPickupCollect(e, { type:'weapon', weapon: makeWeaponProto(loot.weapon), ammoInMag:loot.weapon.magSize||12, ammoReserve:loot.weapon.startReserve||24 });
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
  }
1200
  } else if (target.hasOwnProperty('type') && (target.type === 'wood' || target.type === 'stone')){
1201
  if (td > 26){
@@ -1300,7 +1174,7 @@
1300
  }
1301
  }
1302
 
1303
- // Drawing world & entities
1304
  function drawWorld(){
1305
  const TILE = 600;
1306
  const cols = Math.ceil(WORLD.width / TILE);
@@ -1721,23 +1595,19 @@
1721
  initHUD();
1722
  cameraUpdate();
1723
 
1724
- // Set each enemy to prioritize chests for a short time so they grab weapons early, and reposition away from player spawn
1725
  for (const e of enemies){
1726
  if (Math.hypot(e.x - player.x, e.y - player.y) < 180){
1727
  e.x += (Math.random()<0.5? -1:1) * rand(160,260);
1728
  e.y += (Math.random()<0.5? -1:1) * rand(160,260);
1729
  }
1730
- e.inventory = [null,null,null,null,null];
1731
- e.equippedIndex = -1;
1732
  e.spawnSafeUntil = performance.now() + SPAWN_PROTECT_MS;
1733
  e.state = 'gather';
1734
  e.nextHealTime = 0;
1735
  e.tempTarget = null;
1736
  e.tempTargetExpiry = 0;
1737
- // this flag makes them aggressively seek chests in the first few seconds
1738
- e.prioritizeChestsUntil = performance.now() + 6000 + rand(0,3000);
1739
- // also slightly reduce gatherTimeLeft so they don't linger on harvesting
1740
- e.gatherTimeLeft = Math.min(e.gatherTimeLeft, 6);
1741
  }
1742
 
1743
  gameTime = 300; storm.active = false; storm.radius = storm.maxRadius;
 
332
  }
333
 
334
  // Populate world
335
+ // Updated: enemies spawn already equipped with a weapon in inventory slot 0 (equipped)
336
  function populateWorld(){
337
  chests.length = 0; objects.length = 0; enemies.length = 0; pickups.length = 0;
338
+
339
+ // helper weapon pool for enemy spawns
340
+ const enemyWeapons = [
341
+ { name:'Pistol', dmg:12, rate:320, color:'#ffd86b', magSize:12, startReserve:24 },
342
+ { name:'SMG', dmg:6, rate:120, color:'#8ef0ff', magSize:30, startReserve:90 },
343
+ { name:'Shotgun', dmg:22, rate:800, color:'#ff9fb8', magSize:6, startReserve:18 },
344
+ { name:'Rifle', dmg:18, rate:400, color:'#c7ff9a', magSize:20, startReserve:60 }
345
+ ];
346
+
347
  for (let i=0;i<260;i++){
348
  const x = rand(150, WORLD.width-150);
349
  const y = rand(150, WORLD.height-150);
 
364
  for (let i=0;i<49;i++){
365
  const ex = rand(300, WORLD.width-300);
366
  const ey = rand(300, WORLD.height-300);
367
+
368
+ // create enemy base
369
+ const enemy = {
370
  id:'e'+i, x:ex, y:ey, radius:14, angle:rand(0,Math.PI*2), speed:110+rand(-20,20),
371
  health: 80 + randInt(0,40), lastMelee:0, meleeRate:800 + randInt(-200,200),
372
  roamTimer: rand(0,3),
 
386
  tempTarget: null,
387
  tempTargetExpiry: 0,
388
  prioritizeChestsUntil: 0
389
+ };
390
+
391
+ // Give the enemy a weapon on spawn (random pick)
392
+ const w = enemyWeapons[randInt(0, enemyWeapons.length)];
393
+ const proto = makeWeaponProto(w);
394
+ enemy.inventory[0] = {
395
+ type: 'weapon',
396
+ weapon: proto,
397
+ ammoInMag: proto.magSize,
398
+ ammoReserve: proto.startReserve
399
+ };
400
+ enemy.equippedIndex = 0; // equip the weapon immediately
401
+ // Slightly bias them to seek chests early so they can refill/reload if needed
402
+ enemy.prioritizeChestsUntil = now + 4000 + rand(0,2000);
403
+ // Reduce gather time so they don't idle unnecessarily
404
+ enemy.gatherTimeLeft = Math.min(enemy.gatherTimeLeft, 6);
405
+
406
+ enemies.push(enemy);
407
  }
408
  updatePlayerCount();
409
  }
 
588
 
589
  // Enemy helpers
590
  function enemyEquipBestWeapon(e){
 
591
  let bestIdx = -1;
592
  let bestScore = -Infinity;
593
  for (let i=0;i<5;i++){
594
  const it = e.inventory[i];
595
  if (it && it.type === 'weapon'){
596
+ const score = (it.weapon.dmg || 1) / (it.weapon.rate || 300) + (it.ammoInMag > 0 ? 0.5 : 0);
 
 
 
 
 
597
  if (score > bestScore){ bestScore = score; bestIdx = i; }
598
  }
599
  }
600
+ if (bestIdx !== -1) e.equippedIndex = bestIdx;
601
+ else e.equippedIndex = -1;
 
 
 
 
 
 
 
 
 
 
 
602
  }
603
 
604
  function enemyPickupCollect(e, p){
605
  if (!e || !p) return;
606
  if (p.type === 'weapon'){
607
+ const w = p.weapon ? p.weapon : makeWeaponProto(p);
608
+ const ammoInMag = (typeof p.ammoInMag === 'number') ? p.ammoInMag : (w.magSize || 12);
609
+ const ammoReserve = (typeof p.ammoReserve === 'number') ? p.ammoReserve : (w.startReserve || 24);
610
  for (let s=0;s<5;s++){
611
  const it = e.inventory[s];
612
+ if (it && it.type==='weapon' && it.weapon.name === w.name){
613
+ it.ammoReserve += ammoReserve;
614
+ it.ammoInMag = Math.min(it.weapon.magSize, (it.ammoInMag || 0) + ammoInMag);
615
  enemyEquipBestWeapon(e);
 
616
  if (it.ammoInMag > 0) e.equippedIndex = s;
617
  e.state = 'combat';
618
  e.lastAttackedTime = performance.now();
619
  return;
620
  }
621
  }
 
622
  for (let s=0;s<5;s++){
623
+ if (!e.inventory[s]) {
624
+ e.inventory[s] = { type:'weapon', weapon: w, ammoInMag: ammoInMag, ammoReserve: ammoReserve };
625
  enemyEquipBestWeapon(e);
 
626
  if (e.inventory[s].ammoInMag > 0) e.equippedIndex = s;
627
  e.state = 'combat';
628
  e.lastAttackedTime = performance.now();
629
  return;
630
  }
631
  }
632
+ // replace worst if pickup is better
633
  let worstIdx = -1, worstScore = Infinity;
634
  for (let s=0;s<5;s++){
635
  const it = e.inventory[s];
 
638
  if (score < worstScore){ worstScore = score; worstIdx = s; }
639
  }
640
  }
641
+ const pickupScore = (w.dmg || 1) / (w.rate || 300);
642
  if (pickupScore > worstScore && worstIdx !== -1){
643
+ e.inventory[worstIdx] = { type:'weapon', weapon: w, ammoInMag: ammoInMag, ammoReserve: ammoReserve };
644
  enemyEquipBestWeapon(e);
645
  e.state = 'combat';
646
  e.lastAttackedTime = performance.now();
647
  } else {
648
+ e.materials += Math.floor((ammoReserve || 0) / 2);
649
  }
650
  } else if (p.type === 'medkit'){
651
  for (let s=0;s<5;s++){
 
724
  else if (blocker.type === 'stone') br = 22;
725
  else if (blocker.type === 'wood') br = 16;
726
  else br = 18;
 
727
  const vx = fromX - blocker.x;
728
  const vy = fromY - blocker.y;
729
  const len = Math.hypot(vx, vy) || 0.0001;
 
730
  const px = -vy / len;
731
  const py = vx / len;
732
  const radius = br + padding + 8;
733
  const cand1 = { x: blocker.x + px * radius, y: blocker.y + py * radius };
734
  const cand2 = { x: blocker.x - px * radius, y: blocker.y - py * radius };
 
735
  const d1 = Math.hypot(cand1.x - goalX, cand1.y - goalY);
736
  const d2 = Math.hypot(cand2.x - goalX, cand2.y - goalY);
737
  const chosen = d1 < d2 ? cand1 : cand2;
 
738
  chosen.x = Math.max(16, Math.min(WORLD.width-16, chosen.x));
739
  chosen.y = Math.max(16, Math.min(WORLD.height-16, chosen.y));
740
  return chosen;
 
811
  return best;
812
  }
813
 
814
+ // Enemy AI (kept mostly same; enemies spawn equipped now)
815
  function updateEnemies(dt, now){
816
  const minSeparation = 20;
817
  for (const e of enemies){
818
  if (e.health <= 0) continue;
819
  if (!e.spawnSafeUntil) e.spawnSafeUntil = performance.now() + SPAWN_PROTECT_MS;
820
 
 
 
 
 
821
  if (storm.active){
822
  const distToSafeCenter = Math.hypot(e.x - storm.centerX, e.y - storm.centerY);
823
  if (distToSafeCenter > storm.radius){
824
  e.state = 'toSafe';
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
825
  e.angle = Math.atan2(storm.centerY - e.y, storm.centerX - e.x);
826
  const dx = Math.cos(e.angle) * e.speed * dt * 0.95;
827
  const dy = Math.sin(e.angle) * e.speed * dt * 0.95;
 
831
  }
832
  }
833
 
834
+ if (now - e.lastAttackedTime < 4000) e.state = 'combat';
835
+ if (e.state === 'gather') e.gatherTimeLeft -= dt;
836
+
837
  if (e.health < 60 && now >= (e.nextHealTime || 0)){
838
  let medIdx = -1;
839
  for (let s=0;s<5;s++){
 
850
  }
851
  }
852
 
 
853
  const hasUsefulWeapon = e.inventory.some(it => it && it.type === 'weapon' && (it.ammoInMag > 0 || it.ammoReserve > 0));
854
  if ((e.gatherTimeLeft <= 0 || e.materials >= 20 || hasUsefulWeapon) && e.state === 'gather') e.state = 'combat';
855
 
 
 
 
 
856
  if (e.state === 'gather'){
857
  if (now >= (e.spawnSafeUntil || 0)){
 
858
  let p = findNearestPickup(e, 240);
859
+ if (p){
860
  const angle = Math.atan2(p.y - e.y, p.x - e.x);
861
  e.angle = angle;
862
  const dx = Math.cos(e.angle) * e.speed * dt * 0.9;
 
866
  enemyPickupCollect(e, p);
867
  const idx = pickups.indexOf(p);
868
  if (idx >= 0) pickups.splice(idx,1);
 
 
869
  }
870
  continue;
871
  }
872
  }
873
 
874
+ let chestTarget = findNearestChest(e, 900);
 
 
875
  if (chestTarget){
876
  const d = Math.hypot(chestTarget.x - e.x, chestTarget.y - e.y);
877
  if (d > 20){
878
  e.angle = Math.atan2(chestTarget.y - e.y, chestTarget.x - e.x);
879
+ const dx = Math.cos(e.angle) * e.speed * dt * 0.9;
880
+ const dy = Math.sin(e.angle) * e.speed * dt * 0.9;
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
881
  moveEntityWithCollision(e, dx, dy, e.radius);
882
  } else {
 
883
  if (!chestTarget.opened){
884
  chestTarget.opened = true;
885
  const loot = chestTarget.loot;
886
+ if (loot.type === 'weapon') enemyPickupCollect(e, { type:'weapon', weapon: makeWeaponProto(loot.weapon), ammoInMag:loot.weapon.magSize||12, ammoReserve:loot.weapon.startReserve||24 });
887
+ else if (loot.type === 'medkit') enemyPickupCollect(e, { type:'medkit', amount: loot.amount || 1 });
 
 
888
  else if (loot.type === 'materials') enemyPickupCollect(e, { type:'materials', amount: loot.amount || 10 });
 
889
  enemyEquipBestWeapon(e);
890
  if (e.equippedIndex !== -1){
891
  const eq = e.inventory[e.equippedIndex];
892
  if (eq && eq.type==='weapon' && eq.ammoInMag <= 0 && eq.ammoReserve > 0){
 
893
  reloadItem(eq);
894
  }
895
  }
 
 
 
 
 
896
  }
897
  }
898
  continue;
899
  }
900
 
901
  let objTarget = findNearestHarvestable(e, 700);
902
+ if (objTarget){
903
  const d = Math.hypot(objTarget.x - e.x, objTarget.y - e.y);
904
  if (d > 26){
905
  e.angle = Math.atan2(objTarget.y - e.y, objTarget.x - e.x);
 
929
  continue;
930
  }
931
 
932
+ // Combat
933
  if (e.equippedIndex === -1) enemyEquipBestWeapon(e);
934
  if (e.reloadPending){
935
  if (now >= e.reloadingUntil){
 
947
  }
948
  }
949
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
950
  let target = player;
951
  let bestDist = Math.hypot(player.x - e.x, player.y - e.y);
952
  if (bestDist > VIEW_RANGE){
 
989
  continue;
990
  }
991
 
 
992
  const blocked = !hasLineOfSight(e.x, e.y, target.x, target.y);
993
  if (blocked){
994
  const blocker = findBlockingObject(e.x, e.y, target.x, target.y);
 
1006
  const angle = Math.atan2(blocker.y - e.y, blocker.x - e.x);
1007
  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);
1008
  }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1009
  } else {
1010
  e.angle = Math.atan2(blocker.y - e.y, blocker.x - e.x);
1011
+ e.x += Math.cos(e.angle) * e.speed * dt * 0.8;
1012
+ e.y += Math.sin(e.angle) * e.speed * dt * 0.8;
1013
  }
1014
  }
1015
  continue;
 
1027
  const eq = e.inventory[e.equippedIndex];
1028
  if (eq && eq.type === 'weapon' && !e.reloadPending && now - (e.lastShot||0) > (eq.weapon.rate || 300)){
1029
  if (eq.ammoInMag <= 0){
 
 
 
 
 
 
 
 
1030
  } else {
1031
  if (hasLineOfSight(e.x, e.y, target.x, target.y)){
1032
  e.lastShot = now;
 
1058
  const idx = pickups.indexOf(target);
1059
  if (idx >= 0) pickups.splice(idx,1);
1060
  e.state = 'gather';
 
1061
  }
1062
  } else if (target.hasOwnProperty('loot')){
1063
  if (td > 20){
1064
  e.angle = Math.atan2(target.y - e.y, target.x - e.x);
1065
  moveEntityWithCollision(e, Math.cos(e.angle) * e.speed * dt * 0.9, Math.sin(e.angle) * e.speed * dt * 0.9, e.radius);
1066
  } else {
1067
+ target.opened = true;
1068
+ const loot = target.loot;
1069
+ if (loot.type === 'weapon') enemyPickupCollect(e, { type:'weapon', weapon: makeWeaponProto(loot.weapon), ammoInMag:loot.weapon.magSize||12, ammoReserve:loot.weapon.startReserve||24 });
1070
+ else if (loot.type === 'medkit') enemyPickupCollect(e, { type:'medkit', amount: loot.amount || 1 });
1071
+ else if (loot.type === 'materials') enemyPickupCollect(e, { type:'materials', amount: loot.amount || 10 });
1072
+ e.state = 'gather';
 
 
 
 
 
 
 
 
 
 
1073
  }
1074
  } else if (target.hasOwnProperty('type') && (target.type === 'wood' || target.type === 'stone')){
1075
  if (td > 26){
 
1174
  }
1175
  }
1176
 
1177
+ // Drawing world & entities (unchanged)
1178
  function drawWorld(){
1179
  const TILE = 600;
1180
  const cols = Math.ceil(WORLD.width / TILE);
 
1595
  initHUD();
1596
  cameraUpdate();
1597
 
 
1598
  for (const e of enemies){
1599
  if (Math.hypot(e.x - player.x, e.y - player.y) < 180){
1600
  e.x += (Math.random()<0.5? -1:1) * rand(160,260);
1601
  e.y += (Math.random()<0.5? -1:1) * rand(160,260);
1602
  }
1603
+ e.inventory = e.inventory || [null,null,null,null,null];
1604
+ e.equippedIndex = (e.inventory && e.inventory.findIndex(it => it && it.type === 'weapon') !== -1) ? e.inventory.findIndex(it => it && it.type === 'weapon') : -1;
1605
  e.spawnSafeUntil = performance.now() + SPAWN_PROTECT_MS;
1606
  e.state = 'gather';
1607
  e.nextHealTime = 0;
1608
  e.tempTarget = null;
1609
  e.tempTargetExpiry = 0;
1610
+ e.prioritizeChestsUntil = e.prioritizeChestsUntil || (performance.now() + 6000 + rand(0,3000));
 
 
 
1611
  }
1612
 
1613
  gameTime = 300; storm.active = false; storm.radius = storm.maxRadius;