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

Manual changes saved

Browse files
Files changed (1) hide show
  1. index.html +277 -214
index.html CHANGED
@@ -3,7 +3,7 @@
3
  <head>
4
  <meta charset="utf-8" />
5
  <meta name="viewport" content="width=device-width, initial-scale=1" />
6
- <title>BattleZone Royale - Minimap Added</title>
7
  <script src="https://cdn.tailwindcss.com"></script>
8
  <script src="https://unpkg.com/feather-icons"></script>
9
  <style>
@@ -26,98 +26,47 @@
26
  .biome-selected { outline: 3px solid rgba(96,165,250,0.9); }
27
 
28
  /* Loading screen */
29
- #loadingScreen {
30
- position: absolute;
31
- inset: 0;
32
- z-index: 80;
33
- display: none;
34
- align-items: center;
35
- justify-content: center;
36
- overflow: hidden;
37
- color: #fff;
38
- padding: 24px;
39
- box-sizing: border-box;
40
- }
41
 
42
- /* animated gradient background */
43
- .loading-bg {
44
  position: absolute;
45
- inset: -20%;
46
- background: linear-gradient(120deg, #0b1220 0%, #123049 30%, #5b2a6a 60%, #2f4b2f 100%);
47
- filter: blur(20px) saturate(1.1);
48
- animation: bgShift 18s linear infinite;
49
- transform: scale(1.1);
50
- opacity: 0.85;
51
- }
52
- @keyframes bgShift {
53
- 0% { filter: hue-rotate(0deg) blur(20px); transform: scale(1.06) rotate(0.01deg); }
54
- 25% { filter: hue-rotate(45deg) blur(18px); transform: scale(1.08) rotate(0.02deg); }
55
- 50% { filter: hue-rotate(90deg) blur(22px); transform: scale(1.04) rotate(-0.01deg); }
56
- 75% { filter: hue-rotate(180deg) blur(20px); transform: scale(1.07) rotate(0.01deg); }
57
- 100% { filter: hue-rotate(360deg) blur(20px); transform: scale(1.06) rotate(-0.02deg); }
58
- }
59
-
60
- .loading-card {
61
- position: relative;
62
- z-index: 2;
63
- max-width: 880px;
64
- width: calc(100% - 48px);
65
- background: rgba(6,10,18,0.55);
66
- border: 1px solid rgba(255,255,255,0.06);
67
- box-shadow: 0 12px 40px rgba(0,0,0,0.7);
68
- border-radius: 12px;
69
- padding: 20px;
70
- display:flex;
71
- gap:18px;
72
- align-items:center;
73
- }
74
-
75
- .loading-left {
76
- flex: 1;
77
- min-width: 260px;
78
- display:flex; flex-direction:column; gap:10px;
79
- }
80
- .loading-right {
81
- width: 260px;
82
- display:flex; flex-direction:column; gap:12px; align-items:center;
83
- }
84
-
85
- .loader-spinner {
86
- width: 84px; height:84px; border-radius:50%; position:relative;
87
- display:flex; align-items:center; justify-content:center;
88
  }
89
- .ring {
90
- position:absolute; inset:0; border-radius:50%; box-shadow: inset 0 0 24px rgba(255,255,255,0.02);
91
- border: 6px solid rgba(255,255,255,0.06);
92
- animation: ringRotate 1.8s linear infinite;
 
93
  }
94
- .ring::after {
95
- content:'';
96
- position:absolute; width:16px; height:16px; right:8px; top:50%; transform: translateY(-50%);
97
- background: linear-gradient(90deg,#ffd86b,#8ef0ff);
98
- border-radius:50%;
 
 
 
 
 
 
 
 
 
 
 
99
  }
100
- @keyframes ringRotate { to { transform: rotate(360deg); } }
101
- .loader-dots { display:flex; gap:6px; }
102
- .dot { width:10px; height:10px; border-radius:50%; background:#ffd86b; opacity:0.6; animation: dotPulse 1.2s infinite; }
103
- .dot:nth-child(2){ animation-delay: 0.15s; background:#8ef0ff; }
104
- .dot:nth-child(3){ animation-delay: 0.3s; background:#ff9fb8; }
105
- @keyframes dotPulse { 0%{ transform: scale(.8); opacity:0.4 } 50%{ transform: scale(1.2); opacity:1 } 100%{ transform: scale(.8); opacity:0.4 } }
106
-
107
- .tips { font-size:14px; color:#e8eef7; background: rgba(0,0,0,0.18); padding:10px; border-radius:8px; line-height:1.3; }
108
- .loading-title { font-size:18px; font-weight:800; color:#ffd86b; display:flex; align-items:center; gap:8px; }
109
-
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){
118
- .loading-card { flex-direction:column; align-items:center; text-align:center; }
119
- .loading-right { width:100%; }
120
- }
121
  </style>
122
  </head>
123
  <body>
@@ -157,7 +106,7 @@
157
  <li><strong>WASD</strong> — Move</li>
158
  <li><strong>Mouse</strong> — Aim</li>
159
  <li><strong>Left Click</strong> — Shoot (if weapon equipped or in selected slot) or pickaxe melee</li>
160
- <li><strong>E</strong> — Use medkit in selected slot OR Interact / Loot</li>
161
  <li><strong>Q</strong> — Build (costs 10 materials)</li>
162
  <li><strong>R</strong> — Reload selected weapon</li>
163
  <li><strong>1-5</strong> — Select inventory slot (also equips it)</li>
@@ -258,6 +207,38 @@
258
  <div id="pickaxeSlot" class="pickaxe-slot" title="Pickaxe (press F to equip)">⛏️</div>
259
  <div id="hudGear" aria-label="gear"></div>
260
  </div>
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
261
  </div>
262
  </div>
263
  </div>
@@ -280,13 +261,22 @@
280
  const continueBtn = document.getElementById('continueBtn');
281
  const biomeGrid = document.getElementById('biomeGrid');
282
 
283
- // Loading screen elements
 
 
 
 
 
 
 
 
 
 
 
284
  const loadingScreen = document.getElementById('loadingScreen');
285
  const loadingTipEl = document.getElementById('loadingTip');
286
  const loadingProgressEl = document.getElementById('loadingProgress');
287
  const loadingTimerText = document.getElementById('loadingTimerText');
288
-
289
- // Minimap elements and cache
290
  const minimapCanvas = document.getElementById('minimapCanvas');
291
  const miniCtx = minimapCanvas.getContext('2d');
292
  let miniTerrainCache = null;
@@ -299,14 +289,22 @@
299
  const ctn = document.getElementById('canvasContainer');
300
  canvas.width = Math.max(900, Math.floor(ctn.clientWidth));
301
  canvas.height = Math.max(560, Math.floor(ctn.clientHeight));
302
- // keep minimap internal pixel buffer consistent
303
  minimapCanvas.width = 220;
304
  minimapCanvas.height = 140;
305
  cameraUpdate();
306
- miniTerrainCache = null; // rebuild on resize
307
  }
308
  window.addEventListener('resize', resizeCanvas);
309
 
 
 
 
 
 
 
 
 
 
310
  // Player
311
  const player = {
312
  id:'player', x: WORLD.width/2, y: WORLD.height/2, radius:16, angle:0, speed:220,
@@ -315,8 +313,9 @@
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
@@ -327,33 +326,42 @@
327
 
328
  window.addEventListener('keydown',(e)=>{
329
  const k = e.key.toLowerCase();
330
- if (gameActive){
331
- if (k in keys) keys[k] = true;
332
- if (['1','2','3','4','5'].includes(k)) {
333
- const idx = parseInt(k)-1;
334
- player.selectedSlot = idx;
335
- equipSlot(idx);
336
- updateHUD();
337
- }
338
- if (k === 'f') {
339
- if (player.equippedIndex === player.selectedSlot) equipSlot(-1);
340
- else equipSlot(player.selectedSlot);
341
- updateHUD();
342
- }
343
- if (k === 'r') keys.r = true;
344
- } else {
345
- if (['1','2','3','4','5'].includes(k)) {
346
- player.selectedSlot = parseInt(k)-1;
347
- updateHUD();
348
- }
349
  }
 
 
 
 
 
 
350
  });
351
 
352
  window.addEventListener('keyup',(e)=>{
353
  const k = e.key.toLowerCase();
354
  if (k in keys) keys[k] = false;
355
- if (k === 'e') keys.e = true;
356
- if (k === 'q') keys.q = true;
 
 
 
 
 
 
 
 
 
 
 
357
  });
358
 
359
  canvas.addEventListener('mousemove',(e)=>{
@@ -375,7 +383,70 @@
375
  });
376
  window.addEventListener('mouseup', ()=> mouse.down = false);
377
 
378
- // Entities
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
379
  const bullets = [];
380
  const chests = [];
381
  const objects = [];
@@ -412,11 +483,9 @@
412
  return { type:'weapon', weapon: weapons[randInt(0, weapons.length)] };
413
  }
414
 
415
- // Behaviour tuning
416
  const VIEW_RANGE = 1200;
417
  const SPAWN_PROTECT_MS = 1200;
418
 
419
- // collision helpers
420
  function getObjectRadius(obj){
421
  if (!obj) return 18;
422
  if (obj.type === 'wall') return 28;
@@ -505,7 +574,6 @@
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
 
@@ -549,9 +617,13 @@
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');
@@ -633,63 +705,10 @@
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;
@@ -718,6 +737,19 @@
718
  }
719
  }
720
 
 
 
 
 
 
 
 
 
 
 
 
 
 
721
  function pickupCollect(p){
722
  if (p.type === 'weapon'){
723
  let merged=false;
@@ -747,7 +779,7 @@
747
  }
748
  }
749
 
750
- // Enemy helpers (medkit usage now takes 3s)
751
  function enemyEquipBestWeapon(e){
752
  let bestIdx = -1;
753
  let bestScore = -Infinity;
@@ -771,7 +803,6 @@
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
  }
@@ -845,7 +876,6 @@
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;
@@ -880,7 +910,6 @@
880
  return true;
881
  }
882
 
883
- // LOS helpers
884
  function hasLineOfSight(x1,y1,x2,y2){
885
  const vx = x2 - x1, vy = y2 - y1;
886
  const vlen2 = vx*vx + vy*vy;
@@ -917,7 +946,6 @@
917
  return closest;
918
  }
919
 
920
- // New helper: compute detour waypoint around blocker
921
  function computeDetourWaypoint(fromX, fromY, blocker, goalX, goalY, padding = 12){
922
  if (!blocker) return null;
923
  let br = 0;
@@ -941,7 +969,7 @@
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];
@@ -954,10 +982,10 @@
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;
@@ -970,12 +998,9 @@
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){
@@ -997,12 +1022,10 @@
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;
@@ -1014,7 +1037,7 @@
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);
@@ -1036,7 +1059,6 @@
1036
  }
1037
  }
1038
 
1039
- // Helpers to find nearest
1040
  function findNearestChest(e, maxDist = 1200){
1041
  let best = null; let bd = Infinity;
1042
  for (const c of chests){ if (c.opened) continue; const d = Math.hypot(c.x - e.x, c.y - e.y); if (d < bd && d <= maxDist){ bd = d; best = c; } }
@@ -1053,14 +1075,12 @@
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);
@@ -1079,20 +1099,16 @@
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){
@@ -1157,12 +1173,10 @@
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
  }
@@ -1247,7 +1261,6 @@
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){
@@ -1453,7 +1466,6 @@
1453
  }
1454
  }
1455
 
1456
- // Storm
1457
  const storm = { maxRadius: 2400, radius:2400, centerX: WORLD.width/2, centerY: WORLD.height/2, damagePerSecond:1, closingSpeed: 0.6, active:false };
1458
  let stormDamageAccumulator = 0;
1459
  function playerInStorm(){ return Math.hypot(player.x - storm.centerX, player.y - storm.centerY) > storm.radius; }
@@ -1468,7 +1480,7 @@
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,8 +1505,7 @@
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);
@@ -1702,7 +1713,7 @@
1702
 
1703
  function drawCrosshair(){}
1704
 
1705
- // Minimap functions (unchanged)
1706
  function buildMiniTerrainCache(){
1707
  const mw = minimapCanvas.width;
1708
  const mh = minimapCanvas.height;
@@ -1795,6 +1806,36 @@
1795
  miniCtx.restore();
1796
  }
1797
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1798
  // Main loop
1799
  let lastTime = 0;
1800
  function gameLoop(ts){
@@ -1822,8 +1863,11 @@
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,9 +1899,25 @@
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());
1862
  bulletsUpdate(dt);
1863
 
@@ -1879,6 +1939,10 @@
1879
 
1880
  if (storm.active && playerInStorm()) stormWarning.classList.remove('hidden'); else stormWarning.classList.add('hidden');
1881
 
 
 
 
 
1882
  requestAnimationFrame(gameLoop);
1883
  }
1884
 
@@ -1916,7 +1980,7 @@
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();
@@ -1964,8 +2028,7 @@
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
 
@@ -1973,7 +2036,7 @@
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.",
 
3
  <head>
4
  <meta charset="utf-8" />
5
  <meta name="viewport" content="width=device-width, initial-scale=1" />
6
+ <title>BattleZone Royale - Mobile Controls & Medkit Hold</title>
7
  <script src="https://cdn.tailwindcss.com"></script>
8
  <script src="https://unpkg.com/feather-icons"></script>
9
  <style>
 
26
  .biome-selected { outline: 3px solid rgba(96,165,250,0.9); }
27
 
28
  /* Loading screen */
29
+ #loadingScreen { position: absolute; inset: 0; z-index: 80; display: none; align-items: center; justify-content: center; overflow: hidden; color: #fff; padding: 24px; box-sizing: border-box; }
 
 
 
 
 
 
 
 
 
 
 
30
 
31
+ /* Mobile controls */
32
+ #mobileControls {
33
  position: absolute;
34
+ bottom: 14px;
35
+ left: 14px;
36
+ right: 14px;
37
+ pointer-events: auto;
38
+ z-index: 60;
39
+ display: none;
40
+ touch-action: none;
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
41
  }
42
+ .mobile-left, .mobile-right { display:flex; gap:10px; align-items:center; }
43
+ .dpad { width: 140px; height: 140px; position: relative; }
44
+ .dpad button {
45
+ position: absolute; width: 46px; height: 46px; border-radius: 10px; background: rgba(255,255,255,0.06); border: 1px solid rgba(255,255,255,0.04); color:#fff; font-weight:700;
46
+ display:flex; align-items:center; justify-content:center; -webkit-user-select:none; user-select:none;
47
  }
48
+ .dpad button:active { background: rgba(255,255,255,0.10); }
49
+ .dpad .up { left: 50%; transform: translateX(-50%); top: 6px; }
50
+ .dpad .down { left: 50%; transform: translateX(-50%); bottom: 6px; }
51
+ .dpad .left { left: 6px; top: 50%; transform: translateY(-50%); }
52
+ .dpad .right { right: 6px; top: 50%; transform: translateY(-50%); }
53
+
54
+ .mobile-actions { position:absolute; right:14px; bottom:14px; display:flex; flex-direction:column; gap:10px; align-items:flex-end; z-index:61; }
55
+ .action-btn { width:64px; height:64px; border-radius:12px; background: rgba(255,255,255,0.06); border:1px solid rgba(255,255,255,0.04); display:flex; align-items:center; justify-content:center; color:#fff; font-weight:700; }
56
+ .action-btn:active { background: rgba(255,255,255,0.10); }
57
+ .small-actions { display:flex; gap:8px; margin-top:8px; }
58
+
59
+ /* progress ring for medkit button */
60
+ .medkit-wrap { position:relative; width:64px; height:64px; display:flex; align-items:center; justify-content:center; }
61
+ .medkit-progress {
62
+ position:absolute; top:0; left:0; width:100%; height:100%; pointer-events:none;
63
+ opacity:0.95;
64
  }
 
 
 
 
 
 
 
 
 
 
 
 
65
 
66
+ /* Loading card styles (from previous) */
67
+ .loading-bg { position: absolute; inset: -20%; background: linear-gradient(120deg, #0b1220 0%, #123049 30%, #5b2a6a 60%, #2f4b2f 100%); filter: blur(20px) saturate(1.1); animation: bgShift 18s linear infinite; transform: scale(1.1); opacity: 0.85; }
68
+ @keyframes bgShift { 0% { filter: hue-rotate(0deg) blur(20px); transform: scale(1.06) rotate(0.01deg); } 25% { filter: hue-rotate(45deg) blur(18px); transform: scale(1.08) rotate(0.02deg); } 50% { filter: hue-rotate(90deg) blur(22px); transform: scale(1.04) rotate(-0.01deg); } 75% { filter: hue-rotate(180deg) blur(20px); transform: scale(1.07) rotate(0.01deg); } 100% { filter: hue-rotate(360deg) blur(20px); transform: scale(1.06) rotate(-0.02deg); } }
69
+ .loading-card { position: relative; z-index: 2; max-width: 880px; width: calc(100% - 48px); background: rgba(6,10,18,0.55); border: 1px solid rgba(255,255,255,0.06); box-shadow: 0 12px 40px rgba(0,0,0,0.7); border-radius: 12px; padding: 20px; display:flex; gap:18px; align-items:center; }
 
 
 
 
70
  </style>
71
  </head>
72
  <body>
 
106
  <li><strong>WASD</strong> — Move</li>
107
  <li><strong>Mouse</strong> — Aim</li>
108
  <li><strong>Left Click</strong> — Shoot (if weapon equipped or in selected slot) or pickaxe melee</li>
109
+ <li><strong>E</strong> — Hold to use medkit (3s) OR tap to Interact / Loot</li>
110
  <li><strong>Q</strong> — Build (costs 10 materials)</li>
111
  <li><strong>R</strong> — Reload selected weapon</li>
112
  <li><strong>1-5</strong> — Select inventory slot (also equips it)</li>
 
207
  <div id="pickaxeSlot" class="pickaxe-slot" title="Pickaxe (press F to equip)">⛏️</div>
208
  <div id="hudGear" aria-label="gear"></div>
209
  </div>
210
+
211
+ <!-- Mobile Controls -->
212
+ <div id="mobileControls" aria-hidden="true">
213
+ <div style="display:flex; justify-content:space-between;">
214
+ <div class="mobile-left">
215
+ <div class="dpad" id="dpad">
216
+ <button class="up" data-dir="up">▲</button>
217
+ <button class="down" data-dir="down">▼</button>
218
+ <button class="left" data-dir="left">◀</button>
219
+ <button class="right" data-dir="right">▶</button>
220
+ </div>
221
+ </div>
222
+
223
+ <div class="mobile-right" style="align-items:flex-end; gap:14px;">
224
+ <div style="display:flex; flex-direction:column; align-items:flex-end;">
225
+ <div class="mobile-actions" id="mobileActions">
226
+ <div class="medkit-wrap">
227
+ <canvas class="medkit-progress" id="medkitProgress" width="64" height="64" style="border-radius:12px;"></canvas>
228
+ <button id="fireBtn" class="action-btn" style="width:64px;height:64px;">FIRE</button>
229
+ </div>
230
+ <div class="small-actions">
231
+ <button id="interactBtn" class="action-btn" style="width:64px;height:44px;">USE</button>
232
+ <button id="buildBtn" class="action-btn" style="width:64px;height:44px;">BUILD</button>
233
+ <button id="reloadBtn" class="action-btn" style="width:64px;height:44px;">RLD</button>
234
+ <button id="equipBtn" class="action-btn" style="width:64px;height:44px;">EQ</button>
235
+ </div>
236
+ </div>
237
+ </div>
238
+ </div>
239
+ </div>
240
+ </div>
241
+
242
  </div>
243
  </div>
244
  </div>
 
261
  const continueBtn = document.getElementById('continueBtn');
262
  const biomeGrid = document.getElementById('biomeGrid');
263
 
264
+ // Mobile controls elements
265
+ const mobileControls = document.getElementById('mobileControls');
266
+ const dpad = document.getElementById('dpad');
267
+ const fireBtn = document.getElementById('fireBtn');
268
+ const interactBtn = document.getElementById('interactBtn');
269
+ const buildBtn = document.getElementById('buildBtn');
270
+ const reloadBtn = document.getElementById('reloadBtn');
271
+ const equipBtn = document.getElementById('equipBtn');
272
+ const medkitCanvas = document.getElementById('medkitProgress');
273
+ const medkitCtx = medkitCanvas.getContext('2d');
274
+
275
+ // Loading screen elements and minimap
276
  const loadingScreen = document.getElementById('loadingScreen');
277
  const loadingTipEl = document.getElementById('loadingTip');
278
  const loadingProgressEl = document.getElementById('loadingProgress');
279
  const loadingTimerText = document.getElementById('loadingTimerText');
 
 
280
  const minimapCanvas = document.getElementById('minimapCanvas');
281
  const miniCtx = minimapCanvas.getContext('2d');
282
  let miniTerrainCache = null;
 
289
  const ctn = document.getElementById('canvasContainer');
290
  canvas.width = Math.max(900, Math.floor(ctn.clientWidth));
291
  canvas.height = Math.max(560, Math.floor(ctn.clientHeight));
 
292
  minimapCanvas.width = 220;
293
  minimapCanvas.height = 140;
294
  cameraUpdate();
295
+ miniTerrainCache = null;
296
  }
297
  window.addEventListener('resize', resizeCanvas);
298
 
299
+ // Detect mobile/touch
300
+ const isMobile = ('ontouchstart' in window) || navigator.maxTouchPoints > 0;
301
+ if (isMobile){
302
+ mobileControls.style.display = 'block';
303
+ mobileControls.setAttribute('aria-hidden', 'false');
304
+ // hide cursor for better mobile feel
305
+ canvas.style.cursor = 'none';
306
+ }
307
+
308
  // Player
309
  const player = {
310
  id:'player', x: WORLD.width/2, y: WORLD.height/2, radius:16, angle:0, speed:220,
 
313
  selectedSlot:0,
314
  equippedIndex: -1,
315
  lastShot:0, lastMelee:0,
316
+ // medkit hold state
317
+ ePressStart: 0, // timestamp when E/use started (keyboard or touch)
318
+ ePressApplied: false // whether medkit has been applied for current press
319
  };
320
 
321
  // Input
 
326
 
327
  window.addEventListener('keydown',(e)=>{
328
  const k = e.key.toLowerCase();
329
+ if (k in keys) keys[k] = true;
330
+ // handle medkit hold start on keydown
331
+ if (k === 'e'){
332
+ player.ePressStart = performance.now();
333
+ player.ePressApplied = false;
334
+ }
335
+ if (['1','2','3','4','5'].includes(k)) {
336
+ const idx = parseInt(k)-1;
337
+ player.selectedSlot = idx;
338
+ equipSlot(idx);
339
+ updateHUD();
 
 
 
 
 
 
 
 
340
  }
341
+ if (k === 'f') {
342
+ if (player.equippedIndex === player.selectedSlot) equipSlot(-1);
343
+ else equipSlot(player.selectedSlot);
344
+ updateHUD();
345
+ }
346
+ if (k === 'r') keys.r = true;
347
  });
348
 
349
  window.addEventListener('keyup',(e)=>{
350
  const k = e.key.toLowerCase();
351
  if (k in keys) keys[k] = false;
352
+ // medkit / interact: if released quickly treat as tap interact, else if held long enough medkit already applied
353
+ if (k === 'e'){
354
+ const now = performance.now();
355
+ const held = player.ePressStart ? (now - player.ePressStart) : 0;
356
+ if (!player.ePressApplied){
357
+ if (held < 3000){
358
+ interactNearby();
359
+ } // else medkit applied automatically earlier
360
+ }
361
+ player.ePressStart = 0;
362
+ player.ePressApplied = false;
363
+ }
364
+ if (k === 'q') { keys.q = false; }
365
  });
366
 
367
  canvas.addEventListener('mousemove',(e)=>{
 
383
  });
384
  window.addEventListener('mouseup', ()=> mouse.down = false);
385
 
386
+ // Mobile controls touch handlers
387
+ function bindTouchButton(el, onStart, onEnd){
388
+ if (!el) return;
389
+ el.addEventListener('touchstart', (ev) => { ev.preventDefault(); onStart && onStart(ev); }, { passive:false });
390
+ el.addEventListener('touchend', (ev) => { ev.preventDefault(); onEnd && onEnd(ev); }, { passive:false });
391
+ el.addEventListener('touchcancel', (ev) => { ev.preventDefault(); onEnd && onEnd(ev); }, { passive:false });
392
+ }
393
+
394
+ // D-pad buttons
395
+ const dirButtons = dpad.querySelectorAll('button');
396
+ dirButtons.forEach(btn => {
397
+ const dir = btn.dataset.dir;
398
+ bindTouchButton(btn,
399
+ () => { // start
400
+ if (dir === 'up') keys.w = true;
401
+ if (dir === 'down') keys.s = true;
402
+ if (dir === 'left') keys.a = true;
403
+ if (dir === 'right') keys.d = true;
404
+ },
405
+ () => { // end
406
+ if (dir === 'up') keys.w = false;
407
+ if (dir === 'down') keys.s = false;
408
+ if (dir === 'left') keys.a = false;
409
+ if (dir === 'right') keys.d = false;
410
+ }
411
+ );
412
+ });
413
+
414
+ // FIRE: hold to fire (maps to mouse.down)
415
+ bindTouchButton(fireBtn, () => { mouse.down = true; }, () => { mouse.down = false; });
416
+
417
+ // INTERACT / USE: tap to interact, hold to use medkit
418
+ bindTouchButton(interactBtn,
419
+ (ev) => {
420
+ player.ePressStart = performance.now();
421
+ player.ePressApplied = false;
422
+ },
423
+ (ev) => {
424
+ const now = performance.now();
425
+ const held = player.ePressStart ? (now - player.ePressStart) : 0;
426
+ if (!player.ePressApplied){
427
+ if (held < 3000){
428
+ interactNearby();
429
+ }
430
+ }
431
+ player.ePressStart = 0;
432
+ player.ePressApplied = false;
433
+ }
434
+ );
435
+
436
+ // BUILD (Q)
437
+ bindTouchButton(buildBtn, null, () => { tryBuild(); });
438
+
439
+ // RELOAD (R)
440
+ bindTouchButton(reloadBtn, null, () => { reloadEquipped(); });
441
+
442
+ // EQUIP toggle (F)
443
+ bindTouchButton(equipBtn, null, () => {
444
+ if (player.equippedIndex === player.selectedSlot) equipSlot(-1);
445
+ else equipSlot(player.selectedSlot);
446
+ updateHUD();
447
+ });
448
+
449
+ // Entities and world arrays
450
  const bullets = [];
451
  const chests = [];
452
  const objects = [];
 
483
  return { type:'weapon', weapon: weapons[randInt(0, weapons.length)] };
484
  }
485
 
 
486
  const VIEW_RANGE = 1200;
487
  const SPAWN_PROTECT_MS = 1200;
488
 
 
489
  function getObjectRadius(obj){
490
  if (!obj) return 18;
491
  if (obj.type === 'wall') return 28;
 
574
  tempTarget: null,
575
  tempTargetExpiry: 0,
576
  prioritizeChestsUntil: 0,
 
577
  usingMedkit: false, usingMedkitStart: 0, usingMedkitUntil: 0, usingMedkitSlot: -1
578
  };
579
 
 
617
  function updateHUD(){
618
  const now = performance.now();
619
  let healthText = `${Math.max(0,Math.floor(player.health))}%`;
620
+ // show medkit hold progress on HUD for keyboard or show "Healed" when applied
621
+ if (player.ePressStart && !player.ePressApplied){
622
+ const elapsed = Math.min(3000, now - player.ePressStart);
623
+ const remaining = Math.ceil((3000 - elapsed)/1000);
624
+ healthText += ` (Holding to use medkit: ${Math.max(0, (3000 - Math.floor(elapsed))/1000).toFixed(1)}s)`;
625
+ } else if (player.ePressApplied){
626
+ healthText += ' (Medkit used)';
627
  }
628
  hudHealthText.textContent = healthText;
629
  const slots = hudGear.querySelectorAll('.gear-slot');
 
705
  }
706
  }
707
 
708
+ // Player interact & medkit usage (refactored: hold to use for 3s; tap to interact)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
709
  function interactNearby(){
710
  const sel = player.selectedSlot;
711
  const selItem = player.inventory[sel];
 
712
  const range = 56;
713
  for (const chest of chests){
714
  if (chest.opened) continue;
 
737
  }
738
  }
739
 
740
+ function applyPlayerMedkitNow(){
741
+ const sel = player.selectedSlot;
742
+ const it = player.inventory[sel];
743
+ if (!it || it.type !== 'medkit' || it.amount <= 0) return false;
744
+ it.amount -= 1;
745
+ player.health = Math.min(100, player.health + 50);
746
+ if (it.amount <= 0) player.inventory[sel] = null;
747
+ player.ePressApplied = true;
748
+ player.ePressStart = 0;
749
+ updateHUD();
750
+ return true;
751
+ }
752
+
753
  function pickupCollect(p){
754
  if (p.type === 'weapon'){
755
  let merged=false;
 
779
  }
780
  }
781
 
782
+ // Enemy medkit helpers (unchanged)
783
  function enemyEquipBestWeapon(e){
784
  let bestIdx = -1;
785
  let bestScore = -Infinity;
 
803
  e.usingMedkitStart = now;
804
  e.usingMedkitUntil = now + 3000;
805
  e.usingMedkitSlot = s;
 
806
  e.nextHealTime = now + 5000;
807
  return true;
808
  }
 
876
  const it = e.inventory[s];
877
  if (it && it.type==='medkit'){ it.amount += p.amount;
878
  if (e.health < 60) {
 
879
  if (!e.usingMedkit) enemyStartMedkitUse(e, performance.now());
880
  }
881
  return;
 
910
  return true;
911
  }
912
 
 
913
  function hasLineOfSight(x1,y1,x2,y2){
914
  const vx = x2 - x1, vy = y2 - y1;
915
  const vlen2 = vx*vx + vy*vy;
 
946
  return closest;
947
  }
948
 
 
949
  function computeDetourWaypoint(fromX, fromY, blocker, goalX, goalY, padding = 12){
950
  if (!blocker) return null;
951
  let br = 0;
 
969
  return chosen;
970
  }
971
 
972
+ // Bullets update with collisions (player & enemy & terrain)
973
  function bulletsUpdate(dt){
974
  for (let i=bullets.length-1;i>=0;i--){
975
  const b = bullets[i];
 
982
  if (b.shooter !== 'player'){
983
  const dPlayer = Math.hypot(player.x - b.x, player.y - b.y);
984
  if (dPlayer < hitRadiusPlayer){
 
985
  player.health -= b.dmg;
986
+ // cancel medkit hold on taking damage
987
+ player.ePressStart = 0;
988
+ player.ePressApplied = false;
989
  if (player.health <= 0){ player.health = 0; playerDeath(); }
990
  bullets.splice(i,1);
991
  continue;
 
998
  if (e.health <= 0) continue;
999
  const d = Math.hypot(e.x - b.x, e.y - b.y);
1000
  if (d < 14 && b.shooter !== e.id){
 
1001
  e.health -= b.dmg;
1002
  e.lastAttackedTime = performance.now();
 
1003
  if (e.usingMedkit){
 
1004
  e.usingMedkit = false; e.usingMedkitSlot = -1; e.usingMedkitStart = 0; e.usingMedkitUntil = 0;
1005
  }
1006
  if (e.health <= 0){
 
1022
  if (obj.dead) continue;
1023
  const rr = getObjectRadius(obj);
1024
  const d = Math.hypot(obj.x - b.x, obj.y - b.y);
 
1025
  if (d < rr + 2){
1026
  obj.hp -= b.dmg;
1027
  if (obj.hp <= 0){
1028
  obj.dead = true;
 
1029
  if (b.shooter === 'player') player.materials += (obj.type === 'wood' ? 3 : 6);
1030
  }
1031
  hitObj = obj;
 
1037
  continue;
1038
  }
1039
 
1040
+ // 4) Hit chests
1041
  for (const chest of chests){
1042
  if (chest.opened) continue;
1043
  const d = Math.hypot(chest.x - b.x, chest.y - b.y);
 
1059
  }
1060
  }
1061
 
 
1062
  function findNearestChest(e, maxDist = 1200){
1063
  let best = null; let bd = Infinity;
1064
  for (const c of chests){ if (c.opened) continue; const d = Math.hypot(c.x - e.x, c.y - e.y); if (d < bd && d <= maxDist){ bd = d; best = c; } }
 
1075
  return best;
1076
  }
1077
 
 
1078
  function updateEnemies(dt, now){
1079
  const minSeparation = 20;
1080
  for (const e of enemies){
1081
  if (e.health <= 0) continue;
1082
  if (!e.spawnSafeUntil) e.spawnSafeUntil = performance.now() + SPAWN_PROTECT_MS;
1083
 
 
1084
  for (const chest of chests){
1085
  if (chest.opened) continue;
1086
  const distChest = Math.hypot(chest.x - e.x, chest.y - e.y);
 
1099
  }
1100
  }
1101
 
 
1102
  if (e.usingMedkit){
 
1103
  if (e.lastAttackedTime > e.usingMedkitStart){
1104
  e.usingMedkit = false; e.usingMedkitSlot = -1; e.usingMedkitStart = 0; e.usingMedkitUntil = 0;
1105
  } else if (now >= e.usingMedkitUntil){
1106
  applyEnemyMedkitUseNow(e);
1107
  } else {
 
1108
  continue;
1109
  }
1110
  }
1111
 
 
1112
  if (storm.active){
1113
  const distToSafeCenter = Math.hypot(e.x - storm.centerX, e.y - storm.centerY);
1114
  if (distToSafeCenter > storm.radius){
 
1173
  }
1174
  }
1175
 
 
1176
  if (now - e.lastAttackedTime < 4000) e.state = 'combat';
1177
  if (e.state === 'gather') e.gatherTimeLeft -= dt;
1178
 
1179
  if (e.health < 60 && now >= (e.nextHealTime || 0) && !e.usingMedkit){
 
1180
  if (enemyStartMedkitUse(e, now)){
1181
  continue;
1182
  }
 
1261
  continue;
1262
  }
1263
 
 
1264
  if (e.equippedIndex === -1) enemyEquipBestWeapon(e);
1265
  if (e.reloadPending){
1266
  if (now >= e.reloadingUntil){
 
1466
  }
1467
  }
1468
 
 
1469
  const storm = { maxRadius: 2400, radius:2400, centerX: WORLD.width/2, centerY: WORLD.height/2, damagePerSecond:1, closingSpeed: 0.6, active:false };
1470
  let stormDamageAccumulator = 0;
1471
  function playerInStorm(){ return Math.hypot(player.x - storm.centerX, player.y - storm.centerY) > storm.radius; }
 
1480
  while (stormDamageAccumulator >= 1){
1481
  stormDamageAccumulator -=1;
1482
  player.health -= 1;
1483
+ player.ePressStart = 0; player.ePressApplied = false;
1484
  if (player.health <= 0){ player.health = 0; playerDeath(); }
1485
  }
1486
  } else stormDamageAccumulator = 0;
 
1505
  }
1506
  }
1507
 
1508
+ // Drawing functions (kept from prior code)
 
1509
  function drawWorld(){
1510
  const TILE = 600;
1511
  const cols = Math.ceil(WORLD.width / TILE);
 
1713
 
1714
  function drawCrosshair(){}
1715
 
1716
+ // Minimap functions
1717
  function buildMiniTerrainCache(){
1718
  const mw = minimapCanvas.width;
1719
  const mh = minimapCanvas.height;
 
1806
  miniCtx.restore();
1807
  }
1808
 
1809
+ // Medkit progress draw (mobile ring)
1810
+ function drawMedkitProgress(){
1811
+ const ctxm = medkitCtx;
1812
+ ctxm.clearRect(0,0,64,64);
1813
+ const now = performance.now();
1814
+ let progress = 0;
1815
+ if (player.ePressStart && !player.ePressApplied){
1816
+ progress = Math.min(1, (now - player.ePressStart) / 3000);
1817
+ } else if (player.ePressApplied){
1818
+ progress = 1;
1819
+ }
1820
+ // background circle
1821
+ ctxm.beginPath();
1822
+ ctxm.arc(32,32,28,0,Math.PI*2);
1823
+ ctxm.fillStyle = 'rgba(255,255,255,0.02)';
1824
+ ctxm.fill();
1825
+ // progress arc
1826
+ ctxm.beginPath();
1827
+ ctxm.strokeStyle = 'rgba(255,107,107,0.95)';
1828
+ ctxm.lineWidth = 6;
1829
+ ctxm.lineCap = 'round';
1830
+ ctxm.arc(32,32,24,-Math.PI/2, -Math.PI/2 + (Math.PI*2*progress));
1831
+ ctxm.stroke();
1832
+ // center label
1833
+ ctxm.fillStyle = '#fff';
1834
+ ctxm.font = '10px monospace';
1835
+ ctxm.textAlign = 'center';
1836
+ ctxm.fillText('USE', 32, 36);
1837
+ }
1838
+
1839
  // Main loop
1840
  let lastTime = 0;
1841
  function gameLoop(ts){
 
1863
  if (isCollidingSolid(player.x, player.y, player.radius)){
1864
  player.y = oldY;
1865
  }
1866
+ // If player moves while holding medkit, cancel the hold
1867
+ if (player.ePressStart){
1868
+ player.ePressStart = 0;
1869
+ player.ePressApplied = false;
1870
+ }
1871
  }
1872
  player.x = Math.max(16, Math.min(WORLD.width-16, player.x));
1873
  player.y = Math.max(16, Math.min(WORLD.height-16, player.y));
 
1899
  }
1900
 
1901
  if (keys.r) { reloadEquipped(); keys.r = false; }
 
1902
  if (keys.q){ tryBuild(); keys.q = false; }
1903
 
1904
+ // Handle player medkit hold: if holding and not applied and reached duration, apply automatically
1905
+ if (player.ePressStart && !player.ePressApplied){
1906
+ const now = performance.now();
1907
+ const elapsed = now - player.ePressStart;
1908
+ const sel = player.selectedSlot;
1909
+ const it = player.inventory[sel];
1910
+ if (it && it.type === 'medkit'){
1911
+ if (elapsed >= 3000){
1912
+ // apply medkit
1913
+ applyPlayerMedkitNow();
1914
+ }
1915
+ } else {
1916
+ // if not medkit in slot, cancel hold and treat as simple interact on release
1917
+ // (we don't auto-interact here)
1918
+ }
1919
+ }
1920
+
1921
  updateEnemies(dt, performance.now());
1922
  bulletsUpdate(dt);
1923
 
 
1939
 
1940
  if (storm.active && playerInStorm()) stormWarning.classList.remove('hidden'); else stormWarning.classList.add('hidden');
1941
 
1942
+ if (isMobile){
1943
+ drawMedkitProgress();
1944
+ }
1945
+
1946
  requestAnimationFrame(gameLoop);
1947
  }
1948
 
 
1980
  player.health = 100; player.armor = 0; player.kills = 0; player.materials = 0;
1981
  player.inventory = [null,null,null,null,null];
1982
  player.selectedSlot = 0; player.equippedIndex = -1; player.lastShot = 0; player.lastMelee = 0;
1983
+ player.ePressStart = 0; player.ePressApplied = false;
1984
 
1985
  populateWorld();
1986
  initHUD();
 
2028
  function endGame(){ gameActive = false; alert('Match over!'); }
2029
  function playerDeath(){
2030
  gameActive = false;
2031
+ player.ePressStart = 0; player.ePressApplied = false;
 
2032
  deathScreen.classList.remove('hidden');
2033
  }
2034
 
 
2036
  goHomeBtn.addEventListener('click', ()=>{ victoryScreen.classList.add('hidden'); gameScreen.classList.add('hidden'); landingScreen.classList.remove('hidden'); });
2037
  continueBtn.addEventListener('click', ()=>{ victoryScreen.classList.add('hidden'); startGame(selectedBiome); });
2038
 
2039
+ // Loading tips
2040
  const loadingTips = [
2041
  "Stick to cover when approaching buildings — open areas get you killed.",
2042
  "Loot chests quickly and move — enemies can hear opening animations.",