bbc123321 commited on
Commit
b72bd6e
·
verified ·
1 Parent(s): c4de6b1

Manual changes saved

Browse files
Files changed (1) hide show
  1. 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; opacity:0.95; }
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
- hudHealthText.textContent = `${Math.max(0,Math.floor(player.health))}%`;
 
 
 
 
 
 
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 (kept same as before)
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 interactNearby(){
642
  const sel = player.selectedSlot;
643
  const selItem = player.inventory[sel];
644
  if (selItem && selItem.type === 'medkit'){
645
- selItem.amount -= 1;
646
- player.health = Math.min(100, player.health + 50);
647
- if (selItem.amount <= 0) player.inventory[sel] = null;
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
648
  updateHUD();
649
- return;
 
 
 
 
 
 
 
 
 
 
 
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 enemyUseMedkit(e, now){
 
724
  for (let s=0;s<5;s++){
725
  const it = e.inventory[s];
726
  if (it && it.type === 'medkit' && it.amount > 0){
727
- // visually switch to medkit slot
728
- e.equippedIndex = s;
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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
- return false;
 
 
 
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
- // if low health, immediately use medkit (and "swap to heals")
792
- if (e.health < 60) enemyUseMedkit(e, performance.now());
 
 
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) enemyUseMedkit(e, performance.now());
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
- if (b.dmg > 0 && b.shooter !== 'player'){
898
- if (Math.hypot(player.x - b.x, player.y - b.y) < 16){
 
 
 
 
 
899
  player.health -= b.dmg;
 
 
900
  if (player.health <= 0){ player.health = 0; playerDeath(); }
901
- bullets.splice(i,1); continue;
 
902
  }
903
  }
904
- if (b.dmg > 0){
905
- for (const e of enemies){
906
- if (e.health <= 0) continue;
907
- if (b.shooter === e.id) continue;
908
- if (Math.hypot(e.x - b.x, e.y - b.y) < 14){
909
- e.health -= b.dmg;
910
- e.lastAttackedTime = performance.now();
911
- if (e.health <= 0){ e.health = 0; if (b.shooter === 'player'){ player.kills++; player.materials += 2; updatePlayerCount(); } }
912
- bullets.splice(i,1); break;
 
 
 
 
 
913
  }
 
 
 
 
 
 
914
  }
915
- if (!bullets[i]) continue;
916
  }
917
- if (!b.tracer && b.dmg > 0){
918
- for (const obj of objects){
919
- if (obj.dead) continue;
920
- if (obj.type === 'wall' && Math.hypot(obj.x - b.x, obj.y - b.y) < 18){
921
- obj.hp -= b.dmg;
922
- if (obj.hp <= 0 && !obj.dead){ obj.dead = true; if (b.shooter === 'player') player.materials += 6; }
923
- bullets.splice(i,1); break;
 
 
 
 
 
 
 
 
 
 
 
924
  }
 
 
925
  }
926
- if (!bullets[i]) continue;
927
- for (const chest of chests){
928
- if (!chest.opened && Math.hypot(chest.x - b.x, chest.y - b.y) < 18){
929
- chest.opened = true;
930
- const loot = chest.loot;
931
- const px = chest.x + rand(-20,20), py = chest.y + rand(-20,20);
932
- 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) });
933
- else if (loot.type === 'medkit') pickups.push({ x:px, y:py, type:'medkit', amount: loot.amount || 1 });
934
- else if (loot.type === 'materials') pickups.push({ x:px, y:py, type:'materials', amount: loot.amount || 10 });
935
- bullets.splice(i,1); break;
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 (improved: detour around obstacles and break obstructions to reach safe zone)
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
- // --- NEW: Auto-open chests on proximity ---
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
- // === STORM / MOVE TO SAFE ZONE BEHAVIOR ===
 
 
 
 
 
 
 
 
 
 
 
 
 
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
- // === NORMAL BEHAVIOR (unchanged) ===
1080
  if (now - e.lastAttackedTime < 4000) e.state = 'combat';
1081
  if (e.state === 'gather') e.gatherTimeLeft -= dt;
1082
 
1083
- // Enhanced heal handling: swap to medkit and consume when needed
1084
- if (e.health < 60 && now >= (e.nextHealTime || 0)){
1085
- // Attempt to use medkit (this will "swap" to medkit slot and then back to weapons)
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 world & entities (unchanged)
 
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: build terrain cache and draw overlay (EXCLUDES enemies & chests)
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){ interactNearby(); keys.e = false; }
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(){ gameActive = false; deathScreen.classList.remove('hidden'); }
 
 
 
 
 
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
- // ---- Loading screen logic ----
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); // brief fade window
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
  });