Spaces:
Running
Running
Manual changes saved
Browse files- index.html +210 -139
index.html
CHANGED
|
@@ -110,9 +110,8 @@
|
|
| 110 |
.progress-wrap { width:100%; background: rgba(255,255,255,0.04); height:14px; border-radius:8px; overflow:hidden; }
|
| 111 |
.progress-bar { height:100%; width:0%; background: linear-gradient(90deg,#ffd86b,#8ef0ff); transition: width 0.12s linear; }
|
| 112 |
|
| 113 |
-
.loading-count { font-size:12px; color:#dfe9f9;
|
| 114 |
|
| 115 |
-
/* disable pointer events visually */
|
| 116 |
.disabled-pane { pointer-events: none; opacity: 0.6; filter: grayscale(12%); }
|
| 117 |
|
| 118 |
@media (max-width: 820px){
|
|
@@ -315,7 +314,9 @@
|
|
| 315 |
inventory: [null,null,null,null,null],
|
| 316 |
selectedSlot:0,
|
| 317 |
equippedIndex: -1,
|
| 318 |
-
lastShot:0, lastMelee:0
|
|
|
|
|
|
|
| 319 |
};
|
| 320 |
|
| 321 |
// Input
|
|
@@ -460,18 +461,9 @@
|
|
| 460 |
}
|
| 461 |
|
| 462 |
// Populate world
|
| 463 |
-
// Updated: enemies spawn already equipped with a weapon in inventory slot 0 (equipped)
|
| 464 |
function populateWorld(){
|
| 465 |
chests.length = 0; objects.length = 0; enemies.length = 0; pickups.length = 0;
|
| 466 |
|
| 467 |
-
// helper weapon pool for enemy spawns
|
| 468 |
-
const enemyWeapons = [
|
| 469 |
-
{ name:'Pistol', dmg:12, rate:320, color:'#ffd86b', magSize:12, startReserve:24 },
|
| 470 |
-
{ name:'SMG', dmg:6, rate:120, color:'#8ef0ff', magSize:30, startReserve:90 },
|
| 471 |
-
{ name:'Shotgun', dmg:22, rate:800, color:'#ff9fb8', magSize:6, startReserve:18 },
|
| 472 |
-
{ name:'Rifle', dmg:18, rate:400, color:'#c7ff9a', magSize:20, startReserve:60 }
|
| 473 |
-
];
|
| 474 |
-
|
| 475 |
for (let i=0;i<260;i++){
|
| 476 |
const x = rand(150, WORLD.width-150);
|
| 477 |
const y = rand(150, WORLD.height-150);
|
|
@@ -493,7 +485,6 @@
|
|
| 493 |
const ex = rand(300, WORLD.width-300);
|
| 494 |
const ey = rand(300, WORLD.height-300);
|
| 495 |
|
| 496 |
-
// create enemy base
|
| 497 |
const enemy = {
|
| 498 |
id:'e'+i, x:ex, y:ey, radius:14, angle:rand(0,Math.PI*2), speed:110+rand(-20,20),
|
| 499 |
health: 80 + randInt(0,40), lastMelee:0, meleeRate:800 + randInt(-200,200),
|
|
@@ -513,13 +504,12 @@
|
|
| 513 |
spawnSafeUntil: now + SPAWN_PROTECT_MS,
|
| 514 |
tempTarget: null,
|
| 515 |
tempTargetExpiry: 0,
|
| 516 |
-
prioritizeChestsUntil: 0
|
|
|
|
|
|
|
| 517 |
};
|
| 518 |
|
| 519 |
-
enemy.equippedIndex = -1;
|
| 520 |
-
// Slightly bias them to seek chests early so they can refill/reload if needed
|
| 521 |
enemy.prioritizeChestsUntil = now + 10000 + rand(0,2000);
|
| 522 |
-
// Reduce gather time so they don't idle unnecessarily
|
| 523 |
enemy.gatherTimeLeft = Math.min(enemy.gatherTimeLeft, 10);
|
| 524 |
|
| 525 |
enemies.push(enemy);
|
|
@@ -557,7 +547,13 @@
|
|
| 557 |
}
|
| 558 |
|
| 559 |
function updateHUD(){
|
| 560 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 561 |
const slots = hudGear.querySelectorAll('.gear-slot');
|
| 562 |
slots.forEach(s => {
|
| 563 |
const idx = parseInt(s.dataset.index);
|
|
@@ -585,7 +581,7 @@
|
|
| 585 |
}
|
| 586 |
function worldToScreen(wx,wy){ return { x: Math.round(wx - camera.x), y: Math.round(wy - camera.y) }; }
|
| 587 |
|
| 588 |
-
// Combat utilities
|
| 589 |
function shootBullet(originX, originY, targetX, targetY, weaponObj, shooterId){
|
| 590 |
if (!weaponObj || (typeof weaponObj.ammoInMag === 'number' && weaponObj.ammoInMag <= 0)) return false;
|
| 591 |
const speed = 1100;
|
|
@@ -637,17 +633,63 @@
|
|
| 637 |
}
|
| 638 |
}
|
| 639 |
|
| 640 |
-
// Player interact
|
| 641 |
-
function
|
| 642 |
const sel = player.selectedSlot;
|
| 643 |
const selItem = player.inventory[sel];
|
| 644 |
if (selItem && selItem.type === 'medkit'){
|
| 645 |
-
|
| 646 |
-
|
| 647 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 648 |
updateHUD();
|
| 649 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 650 |
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 651 |
const range = 56;
|
| 652 |
for (const chest of chests){
|
| 653 |
if (chest.opened) continue;
|
|
@@ -705,7 +747,7 @@
|
|
| 705 |
}
|
| 706 |
}
|
| 707 |
|
| 708 |
-
// Enemy helpers
|
| 709 |
function enemyEquipBestWeapon(e){
|
| 710 |
let bestIdx = -1;
|
| 711 |
let bestScore = -Infinity;
|
|
@@ -720,22 +762,37 @@
|
|
| 720 |
else e.equippedIndex = -1;
|
| 721 |
}
|
| 722 |
|
| 723 |
-
function
|
|
|
|
| 724 |
for (let s=0;s<5;s++){
|
| 725 |
const it = e.inventory[s];
|
| 726 |
if (it && it.type === 'medkit' && it.amount > 0){
|
| 727 |
-
|
| 728 |
-
e.
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 729 |
it.amount -= 1;
|
| 730 |
e.health = Math.min(120, e.health + 50);
|
| 731 |
if (it.amount <= 0) e.inventory[s] = null;
|
| 732 |
-
e.nextHealTime = now + 6000;
|
| 733 |
-
// after healing, prefer to have weapon equipped again
|
| 734 |
-
enemyEquipBestWeapon(e);
|
| 735 |
-
return true;
|
| 736 |
}
|
| 737 |
}
|
| 738 |
-
|
|
|
|
|
|
|
|
|
|
| 739 |
}
|
| 740 |
|
| 741 |
function enemyPickupCollect(e, p){
|
|
@@ -766,7 +823,6 @@
|
|
| 766 |
return;
|
| 767 |
}
|
| 768 |
}
|
| 769 |
-
// replace worst if pickup is better
|
| 770 |
let worstIdx = -1, worstScore = Infinity;
|
| 771 |
for (let s=0;s<5;s++){
|
| 772 |
const it = e.inventory[s];
|
|
@@ -788,13 +844,15 @@
|
|
| 788 |
for (let s=0;s<5;s++){
|
| 789 |
const it = e.inventory[s];
|
| 790 |
if (it && it.type==='medkit'){ it.amount += p.amount;
|
| 791 |
-
|
| 792 |
-
|
|
|
|
|
|
|
| 793 |
return;
|
| 794 |
}
|
| 795 |
}
|
| 796 |
for (let s=0;s<5;s++){ if (!e.inventory[s]) { e.inventory[s] = { type:'medkit', amount:p.amount };
|
| 797 |
-
if (e.health < 60)
|
| 798 |
return; } }
|
| 799 |
e.materials += 3;
|
| 800 |
} else if (p.type === 'materials'){
|
|
@@ -867,76 +925,113 @@
|
|
| 867 |
else if (blocker.type === 'stone') br = 22;
|
| 868 |
else if (blocker.type === 'wood') br = 16;
|
| 869 |
else br = 18;
|
| 870 |
-
// vector from blocker to from
|
| 871 |
const vx = fromX - blocker.x;
|
| 872 |
const vy = fromY - blocker.y;
|
| 873 |
const len = Math.hypot(vx, vy) || 0.0001;
|
| 874 |
-
// perp directions
|
| 875 |
const px = -vy / len;
|
| 876 |
const py = vx / len;
|
| 877 |
const radius = br + padding + 8;
|
| 878 |
const cand1 = { x: blocker.x + px * radius, y: blocker.y + py * radius };
|
| 879 |
const cand2 = { x: blocker.x - px * radius, y: blocker.y - py * radius };
|
| 880 |
-
// choose candidate that's closer to goal and not inside other solids (approx)
|
| 881 |
const d1 = Math.hypot(cand1.x - goalX, cand1.y - goalY);
|
| 882 |
const d2 = Math.hypot(cand2.x - goalX, cand2.y - goalY);
|
| 883 |
const chosen = d1 < d2 ? cand1 : cand2;
|
| 884 |
-
// clamp to world
|
| 885 |
chosen.x = Math.max(16, Math.min(WORLD.width-16, chosen.x));
|
| 886 |
chosen.y = Math.max(16, Math.min(WORLD.height-16, chosen.y));
|
| 887 |
return chosen;
|
| 888 |
}
|
| 889 |
|
| 890 |
-
// Bullets update
|
| 891 |
function bulletsUpdate(dt){
|
| 892 |
for (let i=bullets.length-1;i>=0;i--){
|
| 893 |
const b = bullets[i];
|
| 894 |
b.x += b.vx * dt; b.y += b.vy * dt;
|
| 895 |
b.traveled += Math.hypot(b.vx*dt, b.vy*dt);
|
| 896 |
b.life -= dt;
|
| 897 |
-
|
| 898 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 899 |
player.health -= b.dmg;
|
|
|
|
|
|
|
| 900 |
if (player.health <= 0){ player.health = 0; playerDeath(); }
|
| 901 |
-
bullets.splice(i,1);
|
|
|
|
| 902 |
}
|
| 903 |
}
|
| 904 |
-
|
| 905 |
-
|
| 906 |
-
|
| 907 |
-
|
| 908 |
-
|
| 909 |
-
|
| 910 |
-
|
| 911 |
-
|
| 912 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 913 |
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 914 |
}
|
| 915 |
-
if (!bullets[i]) continue;
|
| 916 |
}
|
| 917 |
-
if (
|
| 918 |
-
|
| 919 |
-
|
| 920 |
-
|
| 921 |
-
|
| 922 |
-
|
| 923 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 924 |
}
|
|
|
|
|
|
|
| 925 |
}
|
| 926 |
-
|
| 927 |
-
|
| 928 |
-
|
| 929 |
-
|
| 930 |
-
|
| 931 |
-
|
| 932 |
-
|
| 933 |
-
|
| 934 |
-
|
| 935 |
-
|
| 936 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 937 |
}
|
| 938 |
-
if (!bullets[i]) continue;
|
| 939 |
}
|
|
|
|
|
|
|
|
|
|
| 940 |
if (b.life <= 0 || b.traveled > 2600) bullets.splice(i,1);
|
| 941 |
}
|
| 942 |
}
|
|
@@ -958,19 +1053,17 @@
|
|
| 958 |
return best;
|
| 959 |
}
|
| 960 |
|
| 961 |
-
// Enemy AI (
|
| 962 |
function updateEnemies(dt, now){
|
| 963 |
const minSeparation = 20;
|
| 964 |
for (const e of enemies){
|
| 965 |
if (e.health <= 0) continue;
|
| 966 |
if (!e.spawnSafeUntil) e.spawnSafeUntil = performance.now() + SPAWN_PROTECT_MS;
|
| 967 |
|
| 968 |
-
//
|
| 969 |
-
// If an unopened chest is very close, open it immediately and collect its loot.
|
| 970 |
for (const chest of chests){
|
| 971 |
if (chest.opened) continue;
|
| 972 |
const distChest = Math.hypot(chest.x - e.x, chest.y - e.y);
|
| 973 |
-
// Use a slightly larger threshold so enemies who are 'touching' the chest will open it.
|
| 974 |
if (distChest <= (e.radius + chestRadius() + 6)){
|
| 975 |
chest.opened = true;
|
| 976 |
const loot = chest.loot;
|
|
@@ -981,46 +1074,46 @@
|
|
| 981 |
} else if (loot.type === 'materials'){
|
| 982 |
enemyPickupCollect(e, { type:'materials', amount: loot.amount || 10 });
|
| 983 |
}
|
| 984 |
-
// If enemy has acquired a weapon, ensure they equip the best available now.
|
| 985 |
enemyEquipBestWeapon(e);
|
| 986 |
break;
|
| 987 |
}
|
| 988 |
}
|
| 989 |
|
| 990 |
-
//
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 991 |
if (storm.active){
|
| 992 |
const distToSafeCenter = Math.hypot(e.x - storm.centerX, e.y - storm.centerY);
|
| 993 |
if (distToSafeCenter > storm.radius){
|
| 994 |
e.state = 'toSafe';
|
| 995 |
-
|
| 996 |
-
// If the enemy already has a temporary waypoint, move toward it until expiry
|
| 997 |
if (e.tempTarget && now < (e.tempTargetExpiry || 0)){
|
| 998 |
const td = Math.hypot(e.tempTarget.x - e.x, e.tempTarget.y - e.y);
|
| 999 |
if (td > 8){
|
| 1000 |
e.angle = Math.atan2(e.tempTarget.y - e.y, e.tempTarget.x - e.x);
|
| 1001 |
moveEntityWithCollision(e, Math.cos(e.angle) * e.speed * dt * 0.95, Math.sin(e.angle) * e.speed * dt * 0.95, e.radius);
|
| 1002 |
continue;
|
| 1003 |
-
} else {
|
| 1004 |
-
e.tempTarget = null;
|
| 1005 |
-
e.tempTargetExpiry = 0;
|
| 1006 |
-
}
|
| 1007 |
}
|
| 1008 |
-
|
| 1009 |
-
// Direct sight to safe center -> run straight
|
| 1010 |
if (hasLineOfSight(e.x, e.y, storm.centerX, storm.centerY)){
|
| 1011 |
e.angle = Math.atan2(storm.centerY - e.y, storm.centerX - e.x);
|
| 1012 |
moveEntityWithCollision(e, Math.cos(e.angle) * e.speed * dt * 1.0, Math.sin(e.angle) * e.speed * dt * 1.0, e.radius);
|
| 1013 |
continue;
|
| 1014 |
}
|
| 1015 |
-
|
| 1016 |
-
// If no direct sight, find the blocking object
|
| 1017 |
const blocker = findBlockingObject(e.x, e.y, storm.centerX, storm.centerY);
|
| 1018 |
if (blocker){
|
| 1019 |
const db = Math.hypot(blocker.x - e.x, blocker.y - e.y);
|
| 1020 |
-
|
| 1021 |
-
// If very close to blocker -> attempt to break it physically (melee) or shoot it if has ammo
|
| 1022 |
if (db < 48){
|
| 1023 |
-
// If has a weapon with ammo, shoot at blocker
|
| 1024 |
if (e.equippedIndex >= 0){
|
| 1025 |
const eq = e.inventory[e.equippedIndex];
|
| 1026 |
if (eq && eq.type === 'weapon' && eq.ammoInMag > 0 && now - (e.lastShot||0) > (eq.weapon.rate || 300)){
|
|
@@ -1028,26 +1121,21 @@
|
|
| 1028 |
eq.ammoInMag -= 1;
|
| 1029 |
const angle = Math.atan2(blocker.y - e.y, blocker.x - e.x);
|
| 1030 |
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);
|
| 1031 |
-
// small nudge away/around while shooting
|
| 1032 |
e.angle = angle + (Math.random()-0.10)*0.10;
|
| 1033 |
moveEntityWithCollision(e, Math.cos(e.angle) * e.speed * dt * 0.50, Math.sin(e.angle) * e.speed * dt * 0.50, e.radius);
|
| 1034 |
continue;
|
| 1035 |
}
|
| 1036 |
}
|
| 1037 |
-
// No ammo or no weapon -> use melee to whack the blocker
|
| 1038 |
blocker.hp -= 28 * dt;
|
| 1039 |
if (blocker.hp <= 0){
|
| 1040 |
blocker.dead = true;
|
| 1041 |
e.materials += (blocker.type === 'wood' ? 3 : 6);
|
| 1042 |
} else {
|
| 1043 |
-
// move closer and keep hitting
|
| 1044 |
e.angle = Math.atan2(blocker.y - e.y, blocker.x - e.x);
|
| 1045 |
moveEntityWithCollision(e, Math.cos(e.angle) * e.speed * dt * 0.4, Math.sin(e.angle) * e.speed * dt * 0.4, e.radius);
|
| 1046 |
}
|
| 1047 |
continue;
|
| 1048 |
}
|
| 1049 |
-
|
| 1050 |
-
// If current blocker is not adjacent, compute a detour waypoint around it
|
| 1051 |
const waypoint = computeDetourWaypoint(e.x, e.y, blocker, storm.centerX, storm.centerY, 12);
|
| 1052 |
if (waypoint){
|
| 1053 |
e.tempTarget = waypoint;
|
|
@@ -1057,33 +1145,25 @@
|
|
| 1057 |
e.angle = Math.atan2(e.tempTarget.y - e.y, e.tempTarget.x - e.x);
|
| 1058 |
moveEntityWithCollision(e, Math.cos(e.angle) * e.speed * dt * 0.95, Math.sin(e.angle) * e.speed * dt * 0.95, e.radius);
|
| 1059 |
continue;
|
| 1060 |
-
} else {
|
| 1061 |
-
e.tempTarget = null;
|
| 1062 |
-
e.tempTargetExpiry = 0;
|
| 1063 |
-
}
|
| 1064 |
}
|
| 1065 |
-
|
| 1066 |
-
// Fallback: try to path straight toward blocker to push through or destroy it
|
| 1067 |
e.angle = Math.atan2(blocker.y - e.y, blocker.x - e.x);
|
| 1068 |
moveEntityWithCollision(e, Math.cos(e.angle) * e.speed * dt * 0.6, Math.sin(e.angle) * e.speed * dt * 0.6, e.radius);
|
| 1069 |
continue;
|
| 1070 |
}
|
| 1071 |
-
|
| 1072 |
-
// Default: move toward safe center (should not reach here often)
|
| 1073 |
e.angle = Math.atan2(storm.centerY - e.y, storm.centerX - e.x);
|
| 1074 |
moveEntityWithCollision(e, Math.cos(e.angle) * e.speed * dt * 0.95, Math.sin(e.angle) * e.speed * dt * 0.95, e.radius);
|
| 1075 |
continue;
|
| 1076 |
}
|
| 1077 |
}
|
| 1078 |
|
| 1079 |
-
//
|
| 1080 |
if (now - e.lastAttackedTime < 4000) e.state = 'combat';
|
| 1081 |
if (e.state === 'gather') e.gatherTimeLeft -= dt;
|
| 1082 |
|
| 1083 |
-
|
| 1084 |
-
|
| 1085 |
-
|
| 1086 |
-
if (enemyUseMedkit(e, now)){
|
| 1087 |
continue;
|
| 1088 |
}
|
| 1089 |
}
|
|
@@ -1167,7 +1247,7 @@
|
|
| 1167 |
continue;
|
| 1168 |
}
|
| 1169 |
|
| 1170 |
-
// Combat
|
| 1171 |
if (e.equippedIndex === -1) enemyEquipBestWeapon(e);
|
| 1172 |
if (e.reloadPending){
|
| 1173 |
if (now >= e.reloadingUntil){
|
|
@@ -1388,6 +1468,7 @@
|
|
| 1388 |
while (stormDamageAccumulator >= 1){
|
| 1389 |
stormDamageAccumulator -=1;
|
| 1390 |
player.health -= 1;
|
|
|
|
| 1391 |
if (player.health <= 0){ player.health = 0; playerDeath(); }
|
| 1392 |
}
|
| 1393 |
} else stormDamageAccumulator = 0;
|
|
@@ -1412,7 +1493,8 @@
|
|
| 1412 |
}
|
| 1413 |
}
|
| 1414 |
|
| 1415 |
-
// Drawing
|
|
|
|
| 1416 |
function drawWorld(){
|
| 1417 |
const TILE = 600;
|
| 1418 |
const cols = Math.ceil(WORLD.width / TILE);
|
|
@@ -1538,7 +1620,6 @@
|
|
| 1538 |
ctx.fillRect(-6,-4,10,4);
|
| 1539 |
ctx.restore();
|
| 1540 |
} else {
|
| 1541 |
-
// If equippedIndex refers to a medkit/other, show a small indicator
|
| 1542 |
const it = e.inventory[e.equippedIndex];
|
| 1543 |
if (it && it.type === 'medkit'){
|
| 1544 |
ctx.save();
|
|
@@ -1621,7 +1702,7 @@
|
|
| 1621 |
|
| 1622 |
function drawCrosshair(){}
|
| 1623 |
|
| 1624 |
-
// Minimap
|
| 1625 |
function buildMiniTerrainCache(){
|
| 1626 |
const mw = minimapCanvas.width;
|
| 1627 |
const mh = minimapCanvas.height;
|
|
@@ -1652,10 +1733,8 @@
|
|
| 1652 |
const mw = minimapCanvas.width;
|
| 1653 |
const mh = minimapCanvas.height;
|
| 1654 |
if (!miniTerrainCache) buildMiniTerrainCache();
|
| 1655 |
-
// draw base terrain
|
| 1656 |
miniCtx.putImageData(miniTerrainCache, 0, 0);
|
| 1657 |
|
| 1658 |
-
// draw world objects (trees/stones/walls) as tiny marks - chests excluded per request
|
| 1659 |
miniCtx.save();
|
| 1660 |
const scaleX = mw / WORLD.width;
|
| 1661 |
const scaleY = mh / WORLD.height;
|
|
@@ -1674,7 +1753,6 @@
|
|
| 1674 |
miniCtx.fillRect(px-2, py-2, 4, 4);
|
| 1675 |
}
|
| 1676 |
}
|
| 1677 |
-
// optionally show pickups (not enemies/chests) - small blue/green dots
|
| 1678 |
for (const p of pickups){
|
| 1679 |
const px = Math.round(p.x * scaleX);
|
| 1680 |
const py = Math.round(p.y * scaleY);
|
|
@@ -1689,22 +1767,17 @@
|
|
| 1689 |
}
|
| 1690 |
}
|
| 1691 |
|
| 1692 |
-
// Storm safe zone: draw overlay darkening outside safe zone and stroke the safe circle
|
| 1693 |
if (storm.active){
|
| 1694 |
-
// darken everything
|
| 1695 |
miniCtx.fillStyle = 'rgba(0,0,0,0.35)';
|
| 1696 |
miniCtx.fillRect(0,0,mw,mh);
|
| 1697 |
-
// carve out safe zone
|
| 1698 |
miniCtx.globalCompositeOperation = 'destination-out';
|
| 1699 |
const cx = storm.centerX * scaleX;
|
| 1700 |
const cy = storm.centerY * scaleY;
|
| 1701 |
-
// radius scaled by average axis to keep circle shape close
|
| 1702 |
const r = storm.radius * ((scaleX + scaleY) / 2);
|
| 1703 |
miniCtx.beginPath();
|
| 1704 |
miniCtx.arc(cx, cy, r, 0, Math.PI*2);
|
| 1705 |
miniCtx.fill();
|
| 1706 |
miniCtx.globalCompositeOperation = 'source-over';
|
| 1707 |
-
// border
|
| 1708 |
miniCtx.strokeStyle = 'rgba(255,200,80,0.95)';
|
| 1709 |
miniCtx.lineWidth = 2;
|
| 1710 |
miniCtx.beginPath();
|
|
@@ -1712,7 +1785,6 @@
|
|
| 1712 |
miniCtx.stroke();
|
| 1713 |
}
|
| 1714 |
|
| 1715 |
-
// draw player dot (always visible)
|
| 1716 |
const ppx = Math.round(player.x * (mw / WORLD.width));
|
| 1717 |
const ppy = Math.round(player.y * (mh / WORLD.height));
|
| 1718 |
miniCtx.fillStyle = '#ffff66';
|
|
@@ -1750,6 +1822,8 @@
|
|
| 1750 |
if (isCollidingSolid(player.x, player.y, player.radius)){
|
| 1751 |
player.y = oldY;
|
| 1752 |
}
|
|
|
|
|
|
|
| 1753 |
}
|
| 1754 |
player.x = Math.max(16, Math.min(WORLD.width-16, player.x));
|
| 1755 |
player.y = Math.max(16, Math.min(WORLD.height-16, player.y));
|
|
@@ -1781,7 +1855,7 @@
|
|
| 1781 |
}
|
| 1782 |
|
| 1783 |
if (keys.r) { reloadEquipped(); keys.r = false; }
|
| 1784 |
-
if (keys.e){
|
| 1785 |
if (keys.q){ tryBuild(); keys.q = false; }
|
| 1786 |
|
| 1787 |
updateEnemies(dt, performance.now());
|
|
@@ -1801,7 +1875,6 @@
|
|
| 1801 |
drawWorld(); drawObjects(); drawChests(); drawPickups(); drawEnemies(); drawBullets(); drawPlayer(); drawCrosshair();
|
| 1802 |
updateHUD();
|
| 1803 |
|
| 1804 |
-
// Draw the minimap last so it reflects the latest world state
|
| 1805 |
drawMinimap();
|
| 1806 |
|
| 1807 |
if (storm.active && playerInStorm()) stormWarning.classList.remove('hidden'); else stormWarning.classList.add('hidden');
|
|
@@ -1843,6 +1916,7 @@
|
|
| 1843 |
player.health = 100; player.armor = 0; player.kills = 0; player.materials = 0;
|
| 1844 |
player.inventory = [null,null,null,null,null];
|
| 1845 |
player.selectedSlot = 0; player.equippedIndex = -1; player.lastShot = 0; player.lastMelee = 0;
|
|
|
|
| 1846 |
|
| 1847 |
populateWorld();
|
| 1848 |
initHUD();
|
|
@@ -1861,6 +1935,7 @@
|
|
| 1861 |
e.tempTarget = null;
|
| 1862 |
e.tempTargetExpiry = 0;
|
| 1863 |
e.prioritizeChestsUntil = e.prioritizeChestsUntil || (performance.now() + 6000 + rand(0,3000));
|
|
|
|
| 1864 |
}
|
| 1865 |
|
| 1866 |
gameTime = 300; storm.active = false; storm.radius = storm.maxRadius;
|
|
@@ -1887,13 +1962,18 @@
|
|
| 1887 |
}
|
| 1888 |
|
| 1889 |
function endGame(){ gameActive = false; alert('Match over!'); }
|
| 1890 |
-
function playerDeath(){
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1891 |
|
| 1892 |
document.getElementById('respawnBtn').addEventListener('click', ()=>{ deathScreen.classList.add('hidden'); landingScreen.classList.remove('hidden'); });
|
| 1893 |
goHomeBtn.addEventListener('click', ()=>{ victoryScreen.classList.add('hidden'); gameScreen.classList.add('hidden'); landingScreen.classList.remove('hidden'); });
|
| 1894 |
continueBtn.addEventListener('click', ()=>{ victoryScreen.classList.add('hidden'); startGame(selectedBiome); });
|
| 1895 |
|
| 1896 |
-
//
|
| 1897 |
const loadingTips = [
|
| 1898 |
"Stick to cover when approaching buildings — open areas get you killed.",
|
| 1899 |
"Loot chests quickly and move — enemies can hear opening animations.",
|
|
@@ -1910,10 +1990,8 @@
|
|
| 1910 |
const LOADING_DURATION = 20000; // 20 seconds
|
| 1911 |
|
| 1912 |
function showLoadingForBiome(biome){
|
| 1913 |
-
// disable selectors visually & functionally
|
| 1914 |
document.querySelectorAll('.biome-selector').forEach(x => { x.classList.add('disabled-pane'); x.style.pointerEvents = 'none'; });
|
| 1915 |
selectedBiome = biome;
|
| 1916 |
-
// pick initial tip
|
| 1917 |
loadingTipEl.textContent = loadingTips[Math.floor(Math.random()*loadingTips.length)];
|
| 1918 |
loadingProgressEl.style.width = '0%';
|
| 1919 |
loadingTimerText.textContent = `Landing in ${Math.ceil(LOADING_DURATION/1000)}s`;
|
|
@@ -1921,7 +1999,6 @@
|
|
| 1921 |
loadingScreen.setAttribute('aria-hidden', 'false');
|
| 1922 |
loadingStart = performance.now();
|
| 1923 |
|
| 1924 |
-
// rotate tips every 4 seconds
|
| 1925 |
let tipIndex = Math.floor(Math.random()*loadingTips.length);
|
| 1926 |
tipRotateId = setInterval(()=>{
|
| 1927 |
tipIndex = (tipIndex + 1) % loadingTips.length;
|
|
@@ -1932,26 +2009,21 @@
|
|
| 1932 |
}, 180);
|
| 1933 |
}, 4000);
|
| 1934 |
|
| 1935 |
-
// progress updater
|
| 1936 |
loadingTimerId = setInterval(() => {
|
| 1937 |
const elapsed = performance.now() - loadingStart;
|
| 1938 |
const pct = Math.min(1, elapsed / LOADING_DURATION);
|
| 1939 |
loadingProgressEl.style.width = `${Math.floor(pct*100)}%`;
|
| 1940 |
const remaining = Math.max(0, Math.ceil((LOADING_DURATION - elapsed)/1000));
|
| 1941 |
loadingTimerText.textContent = `Landing in ${remaining}s`;
|
| 1942 |
-
// subtle pulse to spinner ring speed based on pct (cosmetic)
|
| 1943 |
if (pct >= 1){
|
| 1944 |
clearInterval(loadingTimerId);
|
| 1945 |
clearInterval(tipRotateId);
|
| 1946 |
-
// hide overlay and begin game
|
| 1947 |
setTimeout(()=> {
|
| 1948 |
loadingScreen.style.display = 'none';
|
| 1949 |
loadingScreen.setAttribute('aria-hidden', 'true');
|
| 1950 |
-
// re-enable selectors
|
| 1951 |
document.querySelectorAll('.biome-selector').forEach(x => { x.classList.remove('disabled-pane'); x.style.pointerEvents = ''; });
|
| 1952 |
-
// actually start the game
|
| 1953 |
startGame(biome);
|
| 1954 |
-
}, 220);
|
| 1955 |
}
|
| 1956 |
}, 100);
|
| 1957 |
}
|
|
@@ -1963,7 +2035,6 @@
|
|
| 1963 |
el.classList.add('biome-selected');
|
| 1964 |
const biome = el.dataset.biome;
|
| 1965 |
selectedBiome = biome;
|
| 1966 |
-
// Show the loading screen for 20s then start
|
| 1967 |
showLoadingForBiome(biome);
|
| 1968 |
});
|
| 1969 |
});
|
|
|
|
| 110 |
.progress-wrap { width:100%; background: rgba(255,255,255,0.04); height:14px; border-radius:8px; overflow:hidden; }
|
| 111 |
.progress-bar { height:100%; width:0%; background: linear-gradient(90deg,#ffd86b,#8ef0ff); transition: width 0.12s linear; }
|
| 112 |
|
| 113 |
+
.loading-count { font-size:12px; color:#dfe9f9; }
|
| 114 |
|
|
|
|
| 115 |
.disabled-pane { pointer-events: none; opacity: 0.6; filter: grayscale(12%); }
|
| 116 |
|
| 117 |
@media (max-width: 820px){
|
|
|
|
| 314 |
inventory: [null,null,null,null,null],
|
| 315 |
selectedSlot:0,
|
| 316 |
equippedIndex: -1,
|
| 317 |
+
lastShot:0, lastMelee:0,
|
| 318 |
+
// medkit usage state
|
| 319 |
+
usingMedkit: false, usingMedkitUntil: 0, usingMedkitStart: 0, usingMedkitTimeout: null
|
| 320 |
};
|
| 321 |
|
| 322 |
// Input
|
|
|
|
| 461 |
}
|
| 462 |
|
| 463 |
// Populate world
|
|
|
|
| 464 |
function populateWorld(){
|
| 465 |
chests.length = 0; objects.length = 0; enemies.length = 0; pickups.length = 0;
|
| 466 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 467 |
for (let i=0;i<260;i++){
|
| 468 |
const x = rand(150, WORLD.width-150);
|
| 469 |
const y = rand(150, WORLD.height-150);
|
|
|
|
| 485 |
const ex = rand(300, WORLD.width-300);
|
| 486 |
const ey = rand(300, WORLD.height-300);
|
| 487 |
|
|
|
|
| 488 |
const enemy = {
|
| 489 |
id:'e'+i, x:ex, y:ey, radius:14, angle:rand(0,Math.PI*2), speed:110+rand(-20,20),
|
| 490 |
health: 80 + randInt(0,40), lastMelee:0, meleeRate:800 + randInt(-200,200),
|
|
|
|
| 504 |
spawnSafeUntil: now + SPAWN_PROTECT_MS,
|
| 505 |
tempTarget: null,
|
| 506 |
tempTargetExpiry: 0,
|
| 507 |
+
prioritizeChestsUntil: 0,
|
| 508 |
+
// medkit state for enemies
|
| 509 |
+
usingMedkit: false, usingMedkitStart: 0, usingMedkitUntil: 0, usingMedkitSlot: -1
|
| 510 |
};
|
| 511 |
|
|
|
|
|
|
|
| 512 |
enemy.prioritizeChestsUntil = now + 10000 + rand(0,2000);
|
|
|
|
| 513 |
enemy.gatherTimeLeft = Math.min(enemy.gatherTimeLeft, 10);
|
| 514 |
|
| 515 |
enemies.push(enemy);
|
|
|
|
| 547 |
}
|
| 548 |
|
| 549 |
function updateHUD(){
|
| 550 |
+
const now = performance.now();
|
| 551 |
+
let healthText = `${Math.max(0,Math.floor(player.health))}%`;
|
| 552 |
+
if (player.usingMedkit){
|
| 553 |
+
const remaining = Math.max(0, Math.ceil((player.usingMedkitUntil - now)/1000));
|
| 554 |
+
healthText += ` (Healing ${remaining}s)`;
|
| 555 |
+
}
|
| 556 |
+
hudHealthText.textContent = healthText;
|
| 557 |
const slots = hudGear.querySelectorAll('.gear-slot');
|
| 558 |
slots.forEach(s => {
|
| 559 |
const idx = parseInt(s.dataset.index);
|
|
|
|
| 581 |
}
|
| 582 |
function worldToScreen(wx,wy){ return { x: Math.round(wx - camera.x), y: Math.round(wy - camera.y) }; }
|
| 583 |
|
| 584 |
+
// Combat utilities
|
| 585 |
function shootBullet(originX, originY, targetX, targetY, weaponObj, shooterId){
|
| 586 |
if (!weaponObj || (typeof weaponObj.ammoInMag === 'number' && weaponObj.ammoInMag <= 0)) return false;
|
| 587 |
const speed = 1100;
|
|
|
|
| 633 |
}
|
| 634 |
}
|
| 635 |
|
| 636 |
+
// Player interact & medkit usage
|
| 637 |
+
function attemptUseOrInteract(){
|
| 638 |
const sel = player.selectedSlot;
|
| 639 |
const selItem = player.inventory[sel];
|
| 640 |
if (selItem && selItem.type === 'medkit'){
|
| 641 |
+
// toggle or start medkit usage
|
| 642 |
+
if (player.usingMedkit){
|
| 643 |
+
cancelPlayerMedkitUse();
|
| 644 |
+
} else {
|
| 645 |
+
startPlayerMedkitUse(sel);
|
| 646 |
+
}
|
| 647 |
+
} else {
|
| 648 |
+
interactNearby();
|
| 649 |
+
}
|
| 650 |
+
}
|
| 651 |
+
|
| 652 |
+
function startPlayerMedkitUse(slot){
|
| 653 |
+
const it = player.inventory[slot];
|
| 654 |
+
if (!it || it.type !== 'medkit' || it.amount <= 0) return;
|
| 655 |
+
if (player.usingMedkit) return;
|
| 656 |
+
const now = performance.now();
|
| 657 |
+
player.usingMedkit = true;
|
| 658 |
+
player.usingMedkitStart = now;
|
| 659 |
+
player.usingMedkitUntil = now + 3000; // 3 seconds
|
| 660 |
+
// set a timeout to apply heal
|
| 661 |
+
player.usingMedkitTimeout = setTimeout(()=> {
|
| 662 |
+
// ensure still using and item present
|
| 663 |
+
if (!player.usingMedkit) return;
|
| 664 |
+
const cur = player.inventory[slot];
|
| 665 |
+
if (cur && cur.type === 'medkit'){
|
| 666 |
+
cur.amount -= 1;
|
| 667 |
+
player.health = Math.min(100, player.health + 50);
|
| 668 |
+
if (cur.amount <= 0) player.inventory[slot] = null;
|
| 669 |
+
}
|
| 670 |
+
player.usingMedkit = false;
|
| 671 |
+
player.usingMedkitTimeout = null;
|
| 672 |
updateHUD();
|
| 673 |
+
}, 3000);
|
| 674 |
+
updateHUD();
|
| 675 |
+
}
|
| 676 |
+
|
| 677 |
+
function cancelPlayerMedkitUse(){
|
| 678 |
+
if (!player.usingMedkit) return;
|
| 679 |
+
player.usingMedkit = false;
|
| 680 |
+
player.usingMedkitStart = 0;
|
| 681 |
+
player.usingMedkitUntil = 0;
|
| 682 |
+
if (player.usingMedkitTimeout){
|
| 683 |
+
clearTimeout(player.usingMedkitTimeout);
|
| 684 |
+
player.usingMedkitTimeout = null;
|
| 685 |
}
|
| 686 |
+
updateHUD();
|
| 687 |
+
}
|
| 688 |
+
|
| 689 |
+
function interactNearby(){
|
| 690 |
+
const sel = player.selectedSlot;
|
| 691 |
+
const selItem = player.inventory[sel];
|
| 692 |
+
// medkit immediate use removed — now 3s usage handled above
|
| 693 |
const range = 56;
|
| 694 |
for (const chest of chests){
|
| 695 |
if (chest.opened) continue;
|
|
|
|
| 747 |
}
|
| 748 |
}
|
| 749 |
|
| 750 |
+
// Enemy helpers (medkit usage now takes 3s)
|
| 751 |
function enemyEquipBestWeapon(e){
|
| 752 |
let bestIdx = -1;
|
| 753 |
let bestScore = -Infinity;
|
|
|
|
| 762 |
else e.equippedIndex = -1;
|
| 763 |
}
|
| 764 |
|
| 765 |
+
function enemyStartMedkitUse(e, now){
|
| 766 |
+
if (e.usingMedkit) return false;
|
| 767 |
for (let s=0;s<5;s++){
|
| 768 |
const it = e.inventory[s];
|
| 769 |
if (it && it.type === 'medkit' && it.amount > 0){
|
| 770 |
+
e.usingMedkit = true;
|
| 771 |
+
e.usingMedkitStart = now;
|
| 772 |
+
e.usingMedkitUntil = now + 3000;
|
| 773 |
+
e.usingMedkitSlot = s;
|
| 774 |
+
// set nextHealTime to prevent immediate re-trigger
|
| 775 |
+
e.nextHealTime = now + 5000;
|
| 776 |
+
return true;
|
| 777 |
+
}
|
| 778 |
+
}
|
| 779 |
+
return false;
|
| 780 |
+
}
|
| 781 |
+
|
| 782 |
+
function applyEnemyMedkitUseNow(e){
|
| 783 |
+
const s = e.usingMedkitSlot;
|
| 784 |
+
if (s >= 0){
|
| 785 |
+
const it = e.inventory[s];
|
| 786 |
+
if (it && it.type === 'medkit'){
|
| 787 |
it.amount -= 1;
|
| 788 |
e.health = Math.min(120, e.health + 50);
|
| 789 |
if (it.amount <= 0) e.inventory[s] = null;
|
|
|
|
|
|
|
|
|
|
|
|
|
| 790 |
}
|
| 791 |
}
|
| 792 |
+
e.usingMedkit = false;
|
| 793 |
+
e.usingMedkitSlot = -1;
|
| 794 |
+
e.usingMedkitStart = 0;
|
| 795 |
+
e.usingMedkitUntil = 0;
|
| 796 |
}
|
| 797 |
|
| 798 |
function enemyPickupCollect(e, p){
|
|
|
|
| 823 |
return;
|
| 824 |
}
|
| 825 |
}
|
|
|
|
| 826 |
let worstIdx = -1, worstScore = Infinity;
|
| 827 |
for (let s=0;s<5;s++){
|
| 828 |
const it = e.inventory[s];
|
|
|
|
| 844 |
for (let s=0;s<5;s++){
|
| 845 |
const it = e.inventory[s];
|
| 846 |
if (it && it.type==='medkit'){ it.amount += p.amount;
|
| 847 |
+
if (e.health < 60) {
|
| 848 |
+
// start medkit use if not already using
|
| 849 |
+
if (!e.usingMedkit) enemyStartMedkitUse(e, performance.now());
|
| 850 |
+
}
|
| 851 |
return;
|
| 852 |
}
|
| 853 |
}
|
| 854 |
for (let s=0;s<5;s++){ if (!e.inventory[s]) { e.inventory[s] = { type:'medkit', amount:p.amount };
|
| 855 |
+
if (e.health < 60) enemyStartMedkitUse(e, performance.now());
|
| 856 |
return; } }
|
| 857 |
e.materials += 3;
|
| 858 |
} else if (p.type === 'materials'){
|
|
|
|
| 925 |
else if (blocker.type === 'stone') br = 22;
|
| 926 |
else if (blocker.type === 'wood') br = 16;
|
| 927 |
else br = 18;
|
|
|
|
| 928 |
const vx = fromX - blocker.x;
|
| 929 |
const vy = fromY - blocker.y;
|
| 930 |
const len = Math.hypot(vx, vy) || 0.0001;
|
|
|
|
| 931 |
const px = -vy / len;
|
| 932 |
const py = vx / len;
|
| 933 |
const radius = br + padding + 8;
|
| 934 |
const cand1 = { x: blocker.x + px * radius, y: blocker.y + py * radius };
|
| 935 |
const cand2 = { x: blocker.x - px * radius, y: blocker.y - py * radius };
|
|
|
|
| 936 |
const d1 = Math.hypot(cand1.x - goalX, cand1.y - goalY);
|
| 937 |
const d2 = Math.hypot(cand2.x - goalX, cand2.y - goalY);
|
| 938 |
const chosen = d1 < d2 ? cand1 : cand2;
|
|
|
|
| 939 |
chosen.x = Math.max(16, Math.min(WORLD.width-16, chosen.x));
|
| 940 |
chosen.y = Math.max(16, Math.min(WORLD.height-16, chosen.y));
|
| 941 |
return chosen;
|
| 942 |
}
|
| 943 |
|
| 944 |
+
// Bullets update (rewritten for consistent collision handling)
|
| 945 |
function bulletsUpdate(dt){
|
| 946 |
for (let i=bullets.length-1;i>=0;i--){
|
| 947 |
const b = bullets[i];
|
| 948 |
b.x += b.vx * dt; b.y += b.vy * dt;
|
| 949 |
b.traveled += Math.hypot(b.vx*dt, b.vy*dt);
|
| 950 |
b.life -= dt;
|
| 951 |
+
|
| 952 |
+
// 1) Hit player (if bullet not from player) — damage then despawn
|
| 953 |
+
const hitRadiusPlayer = 16;
|
| 954 |
+
if (b.shooter !== 'player'){
|
| 955 |
+
const dPlayer = Math.hypot(player.x - b.x, player.y - b.y);
|
| 956 |
+
if (dPlayer < hitRadiusPlayer){
|
| 957 |
+
// apply damage
|
| 958 |
player.health -= b.dmg;
|
| 959 |
+
// cancel medkit usage if player is healing
|
| 960 |
+
if (player.usingMedkit) cancelPlayerMedkitUse();
|
| 961 |
if (player.health <= 0){ player.health = 0; playerDeath(); }
|
| 962 |
+
bullets.splice(i,1);
|
| 963 |
+
continue;
|
| 964 |
}
|
| 965 |
}
|
| 966 |
+
|
| 967 |
+
// 2) Hit any enemy (if not the shooter) — damage then despawn
|
| 968 |
+
let hitEnemy = null;
|
| 969 |
+
for (const e of enemies){
|
| 970 |
+
if (e.health <= 0) continue;
|
| 971 |
+
const d = Math.hypot(e.x - b.x, e.y - b.y);
|
| 972 |
+
if (d < 14 && b.shooter !== e.id){
|
| 973 |
+
// apply damage
|
| 974 |
+
e.health -= b.dmg;
|
| 975 |
+
e.lastAttackedTime = performance.now();
|
| 976 |
+
// cancel enemy medkit if they are healing
|
| 977 |
+
if (e.usingMedkit){
|
| 978 |
+
// if they were using medkit, cancel it when hit
|
| 979 |
+
e.usingMedkit = false; e.usingMedkitSlot = -1; e.usingMedkitStart = 0; e.usingMedkitUntil = 0;
|
| 980 |
}
|
| 981 |
+
if (e.health <= 0){
|
| 982 |
+
e.health = 0;
|
| 983 |
+
if (b.shooter === 'player'){ player.kills++; player.materials += 2; updatePlayerCount(); }
|
| 984 |
+
}
|
| 985 |
+
hitEnemy = e;
|
| 986 |
+
break;
|
| 987 |
}
|
|
|
|
| 988 |
}
|
| 989 |
+
if (hitEnemy){
|
| 990 |
+
bullets.splice(i,1);
|
| 991 |
+
continue;
|
| 992 |
+
}
|
| 993 |
+
|
| 994 |
+
// 3) Hit harvestable terrain or wall (wood/stone/wall) — reduce hp and despawn
|
| 995 |
+
let hitObj = null;
|
| 996 |
+
for (const obj of objects){
|
| 997 |
+
if (obj.dead) continue;
|
| 998 |
+
const rr = getObjectRadius(obj);
|
| 999 |
+
const d = Math.hypot(obj.x - b.x, obj.y - b.y);
|
| 1000 |
+
// use a small tolerance so bullets clearly collide
|
| 1001 |
+
if (d < rr + 2){
|
| 1002 |
+
obj.hp -= b.dmg;
|
| 1003 |
+
if (obj.hp <= 0){
|
| 1004 |
+
obj.dead = true;
|
| 1005 |
+
// reward materials if player shot the object
|
| 1006 |
+
if (b.shooter === 'player') player.materials += (obj.type === 'wood' ? 3 : 6);
|
| 1007 |
}
|
| 1008 |
+
hitObj = obj;
|
| 1009 |
+
break;
|
| 1010 |
}
|
| 1011 |
+
}
|
| 1012 |
+
if (hitObj){
|
| 1013 |
+
bullets.splice(i,1);
|
| 1014 |
+
continue;
|
| 1015 |
+
}
|
| 1016 |
+
|
| 1017 |
+
// 4) Hit chests (if wanted bullets can open chests) — keep old behaviour
|
| 1018 |
+
for (const chest of chests){
|
| 1019 |
+
if (chest.opened) continue;
|
| 1020 |
+
const d = Math.hypot(chest.x - b.x, chest.y - b.y);
|
| 1021 |
+
if (d < 18){
|
| 1022 |
+
chest.opened = true;
|
| 1023 |
+
const loot = chest.loot;
|
| 1024 |
+
const px = chest.x + rand(-20,20), py = chest.y + rand(-20,20);
|
| 1025 |
+
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) });
|
| 1026 |
+
else if (loot.type === 'medkit') pickups.push({ x:px, y:py, type:'medkit', amount: loot.amount || 1 });
|
| 1027 |
+
else if (loot.type === 'materials') pickups.push({ x:px, y:py, type:'materials', amount: loot.amount || 10 });
|
| 1028 |
+
bullets.splice(i,1);
|
| 1029 |
+
break;
|
| 1030 |
}
|
|
|
|
| 1031 |
}
|
| 1032 |
+
if (!bullets[i]) continue;
|
| 1033 |
+
|
| 1034 |
+
// 5) Bullets expire naturally
|
| 1035 |
if (b.life <= 0 || b.traveled > 2600) bullets.splice(i,1);
|
| 1036 |
}
|
| 1037 |
}
|
|
|
|
| 1053 |
return best;
|
| 1054 |
}
|
| 1055 |
|
| 1056 |
+
// Enemy AI (with medkit use delay)
|
| 1057 |
function updateEnemies(dt, now){
|
| 1058 |
const minSeparation = 20;
|
| 1059 |
for (const e of enemies){
|
| 1060 |
if (e.health <= 0) continue;
|
| 1061 |
if (!e.spawnSafeUntil) e.spawnSafeUntil = performance.now() + SPAWN_PROTECT_MS;
|
| 1062 |
|
| 1063 |
+
// Auto-open chests on proximity
|
|
|
|
| 1064 |
for (const chest of chests){
|
| 1065 |
if (chest.opened) continue;
|
| 1066 |
const distChest = Math.hypot(chest.x - e.x, chest.y - e.y);
|
|
|
|
| 1067 |
if (distChest <= (e.radius + chestRadius() + 6)){
|
| 1068 |
chest.opened = true;
|
| 1069 |
const loot = chest.loot;
|
|
|
|
| 1074 |
} else if (loot.type === 'materials'){
|
| 1075 |
enemyPickupCollect(e, { type:'materials', amount: loot.amount || 10 });
|
| 1076 |
}
|
|
|
|
| 1077 |
enemyEquipBestWeapon(e);
|
| 1078 |
break;
|
| 1079 |
}
|
| 1080 |
}
|
| 1081 |
|
| 1082 |
+
// Handle enemy medkit usage finishing/cancelling
|
| 1083 |
+
if (e.usingMedkit){
|
| 1084 |
+
// if they were attacked while using, cancel
|
| 1085 |
+
if (e.lastAttackedTime > e.usingMedkitStart){
|
| 1086 |
+
e.usingMedkit = false; e.usingMedkitSlot = -1; e.usingMedkitStart = 0; e.usingMedkitUntil = 0;
|
| 1087 |
+
} else if (now >= e.usingMedkitUntil){
|
| 1088 |
+
applyEnemyMedkitUseNow(e);
|
| 1089 |
+
} else {
|
| 1090 |
+
// still using medkit; don't do other heavy actions
|
| 1091 |
+
continue;
|
| 1092 |
+
}
|
| 1093 |
+
}
|
| 1094 |
+
|
| 1095 |
+
// STORM behavior (unchanged)
|
| 1096 |
if (storm.active){
|
| 1097 |
const distToSafeCenter = Math.hypot(e.x - storm.centerX, e.y - storm.centerY);
|
| 1098 |
if (distToSafeCenter > storm.radius){
|
| 1099 |
e.state = 'toSafe';
|
|
|
|
|
|
|
| 1100 |
if (e.tempTarget && now < (e.tempTargetExpiry || 0)){
|
| 1101 |
const td = Math.hypot(e.tempTarget.x - e.x, e.tempTarget.y - e.y);
|
| 1102 |
if (td > 8){
|
| 1103 |
e.angle = Math.atan2(e.tempTarget.y - e.y, e.tempTarget.x - e.x);
|
| 1104 |
moveEntityWithCollision(e, Math.cos(e.angle) * e.speed * dt * 0.95, Math.sin(e.angle) * e.speed * dt * 0.95, e.radius);
|
| 1105 |
continue;
|
| 1106 |
+
} else { e.tempTarget = null; e.tempTargetExpiry = 0; }
|
|
|
|
|
|
|
|
|
|
| 1107 |
}
|
|
|
|
|
|
|
| 1108 |
if (hasLineOfSight(e.x, e.y, storm.centerX, storm.centerY)){
|
| 1109 |
e.angle = Math.atan2(storm.centerY - e.y, storm.centerX - e.x);
|
| 1110 |
moveEntityWithCollision(e, Math.cos(e.angle) * e.speed * dt * 1.0, Math.sin(e.angle) * e.speed * dt * 1.0, e.radius);
|
| 1111 |
continue;
|
| 1112 |
}
|
|
|
|
|
|
|
| 1113 |
const blocker = findBlockingObject(e.x, e.y, storm.centerX, storm.centerY);
|
| 1114 |
if (blocker){
|
| 1115 |
const db = Math.hypot(blocker.x - e.x, blocker.y - e.y);
|
|
|
|
|
|
|
| 1116 |
if (db < 48){
|
|
|
|
| 1117 |
if (e.equippedIndex >= 0){
|
| 1118 |
const eq = e.inventory[e.equippedIndex];
|
| 1119 |
if (eq && eq.type === 'weapon' && eq.ammoInMag > 0 && now - (e.lastShot||0) > (eq.weapon.rate || 300)){
|
|
|
|
| 1121 |
eq.ammoInMag -= 1;
|
| 1122 |
const angle = Math.atan2(blocker.y - e.y, blocker.x - e.x);
|
| 1123 |
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);
|
|
|
|
| 1124 |
e.angle = angle + (Math.random()-0.10)*0.10;
|
| 1125 |
moveEntityWithCollision(e, Math.cos(e.angle) * e.speed * dt * 0.50, Math.sin(e.angle) * e.speed * dt * 0.50, e.radius);
|
| 1126 |
continue;
|
| 1127 |
}
|
| 1128 |
}
|
|
|
|
| 1129 |
blocker.hp -= 28 * dt;
|
| 1130 |
if (blocker.hp <= 0){
|
| 1131 |
blocker.dead = true;
|
| 1132 |
e.materials += (blocker.type === 'wood' ? 3 : 6);
|
| 1133 |
} else {
|
|
|
|
| 1134 |
e.angle = Math.atan2(blocker.y - e.y, blocker.x - e.x);
|
| 1135 |
moveEntityWithCollision(e, Math.cos(e.angle) * e.speed * dt * 0.4, Math.sin(e.angle) * e.speed * dt * 0.4, e.radius);
|
| 1136 |
}
|
| 1137 |
continue;
|
| 1138 |
}
|
|
|
|
|
|
|
| 1139 |
const waypoint = computeDetourWaypoint(e.x, e.y, blocker, storm.centerX, storm.centerY, 12);
|
| 1140 |
if (waypoint){
|
| 1141 |
e.tempTarget = waypoint;
|
|
|
|
| 1145 |
e.angle = Math.atan2(e.tempTarget.y - e.y, e.tempTarget.x - e.x);
|
| 1146 |
moveEntityWithCollision(e, Math.cos(e.angle) * e.speed * dt * 0.95, Math.sin(e.angle) * e.speed * dt * 0.95, e.radius);
|
| 1147 |
continue;
|
| 1148 |
+
} else { e.tempTarget = null; e.tempTargetExpiry = 0; }
|
|
|
|
|
|
|
|
|
|
| 1149 |
}
|
|
|
|
|
|
|
| 1150 |
e.angle = Math.atan2(blocker.y - e.y, blocker.x - e.x);
|
| 1151 |
moveEntityWithCollision(e, Math.cos(e.angle) * e.speed * dt * 0.6, Math.sin(e.angle) * e.speed * dt * 0.6, e.radius);
|
| 1152 |
continue;
|
| 1153 |
}
|
|
|
|
|
|
|
| 1154 |
e.angle = Math.atan2(storm.centerY - e.y, storm.centerX - e.x);
|
| 1155 |
moveEntityWithCollision(e, Math.cos(e.angle) * e.speed * dt * 0.95, Math.sin(e.angle) * e.speed * dt * 0.95, e.radius);
|
| 1156 |
continue;
|
| 1157 |
}
|
| 1158 |
}
|
| 1159 |
|
| 1160 |
+
// NORMAL behavior
|
| 1161 |
if (now - e.lastAttackedTime < 4000) e.state = 'combat';
|
| 1162 |
if (e.state === 'gather') e.gatherTimeLeft -= dt;
|
| 1163 |
|
| 1164 |
+
if (e.health < 60 && now >= (e.nextHealTime || 0) && !e.usingMedkit){
|
| 1165 |
+
// start medkit use (3s) if possible
|
| 1166 |
+
if (enemyStartMedkitUse(e, now)){
|
|
|
|
| 1167 |
continue;
|
| 1168 |
}
|
| 1169 |
}
|
|
|
|
| 1247 |
continue;
|
| 1248 |
}
|
| 1249 |
|
| 1250 |
+
// Combat logic remains (omitted here for brevity but preserved)
|
| 1251 |
if (e.equippedIndex === -1) enemyEquipBestWeapon(e);
|
| 1252 |
if (e.reloadPending){
|
| 1253 |
if (now >= e.reloadingUntil){
|
|
|
|
| 1468 |
while (stormDamageAccumulator >= 1){
|
| 1469 |
stormDamageAccumulator -=1;
|
| 1470 |
player.health -= 1;
|
| 1471 |
+
if (player.usingMedkit) cancelPlayerMedkitUse();
|
| 1472 |
if (player.health <= 0){ player.health = 0; playerDeath(); }
|
| 1473 |
}
|
| 1474 |
} else stormDamageAccumulator = 0;
|
|
|
|
| 1493 |
}
|
| 1494 |
}
|
| 1495 |
|
| 1496 |
+
// Drawing functions (unchanged, omitted here for brevity)...
|
| 1497 |
+
// (All draw functions from the previous code remain unchanged and are present below.)
|
| 1498 |
function drawWorld(){
|
| 1499 |
const TILE = 600;
|
| 1500 |
const cols = Math.ceil(WORLD.width / TILE);
|
|
|
|
| 1620 |
ctx.fillRect(-6,-4,10,4);
|
| 1621 |
ctx.restore();
|
| 1622 |
} else {
|
|
|
|
| 1623 |
const it = e.inventory[e.equippedIndex];
|
| 1624 |
if (it && it.type === 'medkit'){
|
| 1625 |
ctx.save();
|
|
|
|
| 1702 |
|
| 1703 |
function drawCrosshair(){}
|
| 1704 |
|
| 1705 |
+
// Minimap functions (unchanged)
|
| 1706 |
function buildMiniTerrainCache(){
|
| 1707 |
const mw = minimapCanvas.width;
|
| 1708 |
const mh = minimapCanvas.height;
|
|
|
|
| 1733 |
const mw = minimapCanvas.width;
|
| 1734 |
const mh = minimapCanvas.height;
|
| 1735 |
if (!miniTerrainCache) buildMiniTerrainCache();
|
|
|
|
| 1736 |
miniCtx.putImageData(miniTerrainCache, 0, 0);
|
| 1737 |
|
|
|
|
| 1738 |
miniCtx.save();
|
| 1739 |
const scaleX = mw / WORLD.width;
|
| 1740 |
const scaleY = mh / WORLD.height;
|
|
|
|
| 1753 |
miniCtx.fillRect(px-2, py-2, 4, 4);
|
| 1754 |
}
|
| 1755 |
}
|
|
|
|
| 1756 |
for (const p of pickups){
|
| 1757 |
const px = Math.round(p.x * scaleX);
|
| 1758 |
const py = Math.round(p.y * scaleY);
|
|
|
|
| 1767 |
}
|
| 1768 |
}
|
| 1769 |
|
|
|
|
| 1770 |
if (storm.active){
|
|
|
|
| 1771 |
miniCtx.fillStyle = 'rgba(0,0,0,0.35)';
|
| 1772 |
miniCtx.fillRect(0,0,mw,mh);
|
|
|
|
| 1773 |
miniCtx.globalCompositeOperation = 'destination-out';
|
| 1774 |
const cx = storm.centerX * scaleX;
|
| 1775 |
const cy = storm.centerY * scaleY;
|
|
|
|
| 1776 |
const r = storm.radius * ((scaleX + scaleY) / 2);
|
| 1777 |
miniCtx.beginPath();
|
| 1778 |
miniCtx.arc(cx, cy, r, 0, Math.PI*2);
|
| 1779 |
miniCtx.fill();
|
| 1780 |
miniCtx.globalCompositeOperation = 'source-over';
|
|
|
|
| 1781 |
miniCtx.strokeStyle = 'rgba(255,200,80,0.95)';
|
| 1782 |
miniCtx.lineWidth = 2;
|
| 1783 |
miniCtx.beginPath();
|
|
|
|
| 1785 |
miniCtx.stroke();
|
| 1786 |
}
|
| 1787 |
|
|
|
|
| 1788 |
const ppx = Math.round(player.x * (mw / WORLD.width));
|
| 1789 |
const ppy = Math.round(player.y * (mh / WORLD.height));
|
| 1790 |
miniCtx.fillStyle = '#ffff66';
|
|
|
|
| 1822 |
if (isCollidingSolid(player.x, player.y, player.radius)){
|
| 1823 |
player.y = oldY;
|
| 1824 |
}
|
| 1825 |
+
// If player moves while using medkit, cancel use
|
| 1826 |
+
if (player.usingMedkit && (dx !== 0 || dy !== 0)) cancelPlayerMedkitUse();
|
| 1827 |
}
|
| 1828 |
player.x = Math.max(16, Math.min(WORLD.width-16, player.x));
|
| 1829 |
player.y = Math.max(16, Math.min(WORLD.height-16, player.y));
|
|
|
|
| 1855 |
}
|
| 1856 |
|
| 1857 |
if (keys.r) { reloadEquipped(); keys.r = false; }
|
| 1858 |
+
if (keys.e){ attemptUseOrInteract(); keys.e = false; }
|
| 1859 |
if (keys.q){ tryBuild(); keys.q = false; }
|
| 1860 |
|
| 1861 |
updateEnemies(dt, performance.now());
|
|
|
|
| 1875 |
drawWorld(); drawObjects(); drawChests(); drawPickups(); drawEnemies(); drawBullets(); drawPlayer(); drawCrosshair();
|
| 1876 |
updateHUD();
|
| 1877 |
|
|
|
|
| 1878 |
drawMinimap();
|
| 1879 |
|
| 1880 |
if (storm.active && playerInStorm()) stormWarning.classList.remove('hidden'); else stormWarning.classList.add('hidden');
|
|
|
|
| 1916 |
player.health = 100; player.armor = 0; player.kills = 0; player.materials = 0;
|
| 1917 |
player.inventory = [null,null,null,null,null];
|
| 1918 |
player.selectedSlot = 0; player.equippedIndex = -1; player.lastShot = 0; player.lastMelee = 0;
|
| 1919 |
+
player.usingMedkit = false; player.usingMedkitTimeout = null;
|
| 1920 |
|
| 1921 |
populateWorld();
|
| 1922 |
initHUD();
|
|
|
|
| 1935 |
e.tempTarget = null;
|
| 1936 |
e.tempTargetExpiry = 0;
|
| 1937 |
e.prioritizeChestsUntil = e.prioritizeChestsUntil || (performance.now() + 6000 + rand(0,3000));
|
| 1938 |
+
e.usingMedkit = false; e.usingMedkitSlot = -1;
|
| 1939 |
}
|
| 1940 |
|
| 1941 |
gameTime = 300; storm.active = false; storm.radius = storm.maxRadius;
|
|
|
|
| 1962 |
}
|
| 1963 |
|
| 1964 |
function endGame(){ gameActive = false; alert('Match over!'); }
|
| 1965 |
+
function playerDeath(){
|
| 1966 |
+
gameActive = false;
|
| 1967 |
+
// cancel medkit use on death
|
| 1968 |
+
cancelPlayerMedkitUse();
|
| 1969 |
+
deathScreen.classList.remove('hidden');
|
| 1970 |
+
}
|
| 1971 |
|
| 1972 |
document.getElementById('respawnBtn').addEventListener('click', ()=>{ deathScreen.classList.add('hidden'); landingScreen.classList.remove('hidden'); });
|
| 1973 |
goHomeBtn.addEventListener('click', ()=>{ victoryScreen.classList.add('hidden'); gameScreen.classList.add('hidden'); landingScreen.classList.remove('hidden'); });
|
| 1974 |
continueBtn.addEventListener('click', ()=>{ victoryScreen.classList.add('hidden'); startGame(selectedBiome); });
|
| 1975 |
|
| 1976 |
+
// Loading screen logic (unchanged)
|
| 1977 |
const loadingTips = [
|
| 1978 |
"Stick to cover when approaching buildings — open areas get you killed.",
|
| 1979 |
"Loot chests quickly and move — enemies can hear opening animations.",
|
|
|
|
| 1990 |
const LOADING_DURATION = 20000; // 20 seconds
|
| 1991 |
|
| 1992 |
function showLoadingForBiome(biome){
|
|
|
|
| 1993 |
document.querySelectorAll('.biome-selector').forEach(x => { x.classList.add('disabled-pane'); x.style.pointerEvents = 'none'; });
|
| 1994 |
selectedBiome = biome;
|
|
|
|
| 1995 |
loadingTipEl.textContent = loadingTips[Math.floor(Math.random()*loadingTips.length)];
|
| 1996 |
loadingProgressEl.style.width = '0%';
|
| 1997 |
loadingTimerText.textContent = `Landing in ${Math.ceil(LOADING_DURATION/1000)}s`;
|
|
|
|
| 1999 |
loadingScreen.setAttribute('aria-hidden', 'false');
|
| 2000 |
loadingStart = performance.now();
|
| 2001 |
|
|
|
|
| 2002 |
let tipIndex = Math.floor(Math.random()*loadingTips.length);
|
| 2003 |
tipRotateId = setInterval(()=>{
|
| 2004 |
tipIndex = (tipIndex + 1) % loadingTips.length;
|
|
|
|
| 2009 |
}, 180);
|
| 2010 |
}, 4000);
|
| 2011 |
|
|
|
|
| 2012 |
loadingTimerId = setInterval(() => {
|
| 2013 |
const elapsed = performance.now() - loadingStart;
|
| 2014 |
const pct = Math.min(1, elapsed / LOADING_DURATION);
|
| 2015 |
loadingProgressEl.style.width = `${Math.floor(pct*100)}%`;
|
| 2016 |
const remaining = Math.max(0, Math.ceil((LOADING_DURATION - elapsed)/1000));
|
| 2017 |
loadingTimerText.textContent = `Landing in ${remaining}s`;
|
|
|
|
| 2018 |
if (pct >= 1){
|
| 2019 |
clearInterval(loadingTimerId);
|
| 2020 |
clearInterval(tipRotateId);
|
|
|
|
| 2021 |
setTimeout(()=> {
|
| 2022 |
loadingScreen.style.display = 'none';
|
| 2023 |
loadingScreen.setAttribute('aria-hidden', 'true');
|
|
|
|
| 2024 |
document.querySelectorAll('.biome-selector').forEach(x => { x.classList.remove('disabled-pane'); x.style.pointerEvents = ''; });
|
|
|
|
| 2025 |
startGame(biome);
|
| 2026 |
+
}, 220);
|
| 2027 |
}
|
| 2028 |
}, 100);
|
| 2029 |
}
|
|
|
|
| 2035 |
el.classList.add('biome-selected');
|
| 2036 |
const biome = el.dataset.biome;
|
| 2037 |
selectedBiome = biome;
|
|
|
|
| 2038 |
showLoadingForBiome(biome);
|
| 2039 |
});
|
| 2040 |
});
|