Spaces:
Running
Running
Manual changes saved
Browse files- 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
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 568 |
if (score > bestScore){ bestScore = score; bestIdx = i; }
|
| 569 |
}
|
| 570 |
}
|
| 571 |
-
if (bestIdx !== -1)
|
| 572 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 573 |
}
|
| 574 |
|
| 575 |
function enemyPickupCollect(e, p){
|
| 576 |
-
|
| 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 ===
|
| 582 |
-
it.ammoReserve
|
| 583 |
-
it.ammoInMag = Math.min(it.weapon.magSize, it.ammoInMag +
|
| 584 |
enemyEquipBestWeapon(e);
|
| 585 |
-
|
|
|
|
|
|
|
| 586 |
e.lastAttackedTime = performance.now();
|
| 587 |
return;
|
| 588 |
}
|
| 589 |
}
|
|
|
|
| 590 |
for (let s=0;s<5;s++){
|
| 591 |
-
if (!e.inventory[s])
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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:
|
| 604 |
enemyEquipBestWeapon(e);
|
| 605 |
e.state = 'combat';
|
| 606 |
e.lastAttackedTime = performance.now();
|
| 607 |
} else {
|
| 608 |
-
e.materials += Math.floor((
|
| 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')
|
| 927 |
-
|
|
|
|
|
|
|
| 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
|
| 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 |
-
//
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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 |
-
|
|
|
|
| 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 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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);
|