bbc123321 commited on
Commit
68e0452
·
verified ·
1 Parent(s): e3a82b5

Manual changes saved

Browse files
Files changed (1) hide show
  1. index.html +132 -481
index.html CHANGED
@@ -3,18 +3,13 @@
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>
10
  html,body { height:100%; margin:0; background:#0b1220; color:#fff; font-family:monospace; }
11
  #canvasContainer { position:relative; flex:1; display:flex; justify-content:center; align-items:center; height:100vh; overflow:hidden; }
12
  #gameCanvas { display:block; user-select:none; cursor:crosshair; box-shadow:0 0 20px rgba(0,0,0,.5); width:100%; height:100%; }
13
-
14
- /* Minimap (top-right) */
15
- #minimap { position: absolute; top:12px; right:12px; width:220px; height:140px; border-radius:8px; background: rgba(0,0,0,0.45); padding:6px; z-index:40; box-shadow: 0 6px 30px rgba(0,0,0,0.6); pointer-events: none; }
16
- #minimapCanvas { width:100%; height:100%; display:block; image-rendering: pixelated; border-radius:6px; background:transparent; }
17
-
18
  /* HUD */
19
  #hudHealth { position:absolute; left:12px; bottom:12px; background:rgba(0,0,0,0.55); padding:6px 8px; border-radius:8px; font-weight:700; font-size:13px; display:flex; align-items:center; gap:8px; z-index:30; }
20
  #hudGearWrap { position:absolute; right:12px; bottom:12px; display:flex; gap:8px; align-items:center; z-index:30; }
@@ -96,11 +91,6 @@
96
  <div id="canvasContainer" class="relative bg-gray-900 border-4 border-gray-700 rounded-xl max-w-full max-h-full">
97
  <canvas id="gameCanvas"></canvas>
98
 
99
- <!-- Minimap (top-right) -->
100
- <div id="minimap">
101
- <canvas id="minimapCanvas" width="220" height="140"></canvas>
102
- </div>
103
-
104
  <div id="stormWarning" class="hidden absolute top-6 left-1/2 transform -translate-x-1/2 bg-red-900 bg-opacity-80 text-white px-6 py-3 rounded-lg flex items-center">
105
  <i data-feather="alert-circle" class="mr-2"></i>
106
  <span>STORM APPROACHING! MOVE TO SAFE ZONE!</span>
@@ -159,11 +149,6 @@
159
  const continueBtn = document.getElementById('continueBtn');
160
  const biomeGrid = document.getElementById('biomeGrid');
161
 
162
- // Minimap elements and cache
163
- const minimapCanvas = document.getElementById('minimapCanvas');
164
- const miniCtx = minimapCanvas.getContext('2d');
165
- let miniTerrainCache = null;
166
-
167
  // World
168
  const WORLD = { width: 6000, height: 4000 };
169
  let camera = { x:0, y:0 };
@@ -172,11 +157,7 @@
172
  const ctn = document.getElementById('canvasContainer');
173
  canvas.width = Math.max(900, Math.floor(ctn.clientWidth));
174
  canvas.height = Math.max(560, Math.floor(ctn.clientHeight));
175
- // keep minimap internal pixel buffer consistent
176
- minimapCanvas.width = 220;
177
- minimapCanvas.height = 140;
178
  cameraUpdate();
179
- miniTerrainCache = null; // rebuild on resize
180
  }
181
  window.addEventListener('resize', resizeCanvas);
182
 
@@ -273,7 +254,7 @@
273
  function generateLootForBiome(b){
274
  const roll = Math.random();
275
  if (roll < 0.35) return { type:'medkit', amount:1 };
276
- if (roll < 0.7) return { type:'materials', amount: 10 };
277
  const weapons = [
278
  { name:'Pistol', dmg:12, rate:320, color:'#ffd86b', magSize:12, startReserve:24 },
279
  { name:'SMG', dmg:6, rate:120, color:'#8ef0ff', magSize:30, startReserve:90 },
@@ -284,61 +265,17 @@
284
  }
285
 
286
  // Behaviour tuning
287
- const VIEW_RANGE = 1200;
288
- const SPAWN_PROTECT_MS = 1200;
289
-
290
- // collision helpers
291
- function getObjectRadius(obj){
292
- if (!obj) return 18;
293
- if (obj.type === 'wall') return 28;
294
- if (obj.type === 'stone') return 18;
295
- if (obj.type === 'wood') return 18;
296
- return 16;
297
- }
298
- function chestRadius(){ return 18; }
299
- function circleOverlap(x1,y1,r1,x2,y2,r2){
300
- return Math.hypot(x1-x2,y1-y2) < (r1 + r2);
301
- }
302
-
303
- function isCollidingSolid(x,y,r){
304
- for (const o of objects){
305
- if (o.dead) continue;
306
- const rr = getObjectRadius(o);
307
- if (circleOverlap(x,y,r,o.x,o.y,rr)) return true;
308
- }
309
- for (const c of chests){
310
- if (c.opened) continue;
311
- if (circleOverlap(x,y,r,c.x,c.y,chestRadius())) return true;
312
- }
313
- return false;
314
- }
315
-
316
- function moveEntityWithCollision(entity, dx, dy, radius){
317
- const oldX = entity.x, oldY = entity.y;
318
- let nx = entity.x + dx;
319
- entity.x = nx;
320
- if (entity.x < radius) entity.x = radius;
321
- if (entity.x > WORLD.width - radius) entity.x = WORLD.width - radius;
322
- if (isCollidingSolid(entity.x, entity.y, radius)){
323
- entity.x = oldX;
324
- }
325
- let ny = entity.y + dy;
326
- entity.y = ny;
327
- if (entity.y < radius) entity.y = radius;
328
- if (entity.y > WORLD.height - radius) entity.y = WORLD.height - radius;
329
- if (isCollidingSolid(entity.x, entity.y, radius)){
330
- entity.y = oldY;
331
- }
332
- }
333
 
334
- // Populate world
335
  function populateWorld(){
336
  chests.length = 0; objects.length = 0; enemies.length = 0; pickups.length = 0;
337
  for (let i=0;i<260;i++){
338
  const x = rand(150, WORLD.width-150);
339
  const y = rand(150, WORLD.height-150);
340
  const loot = generateLootForBiome(biomeAt(x,y));
341
- if (loot.type === 'materials') loot.amount = 10;
342
  chests.push({ x,y, opened:false, loot });
343
  }
344
  for (let i=0;i<700;i++){
@@ -350,6 +287,7 @@
350
  const hp = type==='wood'?40 : (type==='stone'?80:160);
351
  objects.push({ x,y, type, hp, maxHp:hp, dead:false });
352
  }
 
353
  const now = performance.now();
354
  for (let i=0;i<49;i++){
355
  const ex = rand(300, WORLD.width-300);
@@ -358,7 +296,7 @@
358
  id:'e'+i, x:ex, y:ey, radius:14, angle:rand(0,Math.PI*2), speed:110+rand(-20,20),
359
  health: 80 + randInt(0,40), lastMelee:0, meleeRate:800 + randInt(-200,200),
360
  roamTimer: rand(0,3),
361
- inventory: [null,null,null,null,null],
362
  selectedSlot: 0,
363
  equippedIndex: -1,
364
  materials: 0,
@@ -370,10 +308,7 @@
370
  gatherTimeLeft: rand(8,16),
371
  target: null,
372
  nextHealTime: 0,
373
- spawnSafeUntil: now + SPAWN_PROTECT_MS,
374
- tempTarget: null,
375
- tempTargetExpiry: 0,
376
- prioritizeChestsUntil: 0
377
  });
378
  }
379
  updatePlayerCount();
@@ -437,7 +372,7 @@
437
  }
438
  function worldToScreen(wx,wy){ return { x: Math.round(wx - camera.x), y: Math.round(wy - camera.y) }; }
439
 
440
- // Combat utilities (kept same as before)
441
  function shootBullet(originX, originY, targetX, targetY, weaponObj, shooterId){
442
  if (!weaponObj || (typeof weaponObj.ammoInMag === 'number' && weaponObj.ammoInMag <= 0)) return false;
443
  const speed = 1100;
@@ -557,70 +492,37 @@
557
  }
558
  }
559
 
560
- // Enemy helpers
561
  function enemyEquipBestWeapon(e){
562
- // prefer weapons that have ammo loaded or reserve; compute score and prefer loaded ones
563
  let bestIdx = -1;
564
  let bestScore = -Infinity;
565
  for (let i=0;i<5;i++){
566
  const it = e.inventory[i];
567
  if (it && it.type === 'weapon'){
568
- const dmg = (it.weapon && it.weapon.dmg) ? it.weapon.dmg : 1;
569
- const rate = (it.weapon && it.weapon.rate) ? it.weapon.rate : 300;
570
- let score = dmg / rate;
571
- // bonus if currently has ammo in mag
572
- if (typeof it.ammoInMag === 'number' && it.ammoInMag > 0) score += 0.5;
573
- if (typeof it.ammoReserve === 'number' && it.ammoReserve > 0) score += 0.2;
574
  if (score > bestScore){ bestScore = score; bestIdx = i; }
575
  }
576
  }
577
- if (bestIdx !== -1) {
578
- e.equippedIndex = bestIdx;
579
- } else {
580
- e.equippedIndex = -1;
581
- }
582
- }
583
-
584
- function normalizeWeaponPickup(p){
585
- // ensure fields exist and are numbers
586
- const w = p.weapon ? p.weapon : makeWeaponProto(p);
587
- const ammoInMag = (typeof p.ammoInMag === 'number') ? p.ammoInMag : (w.magSize || w.magSize || 12);
588
- const ammoReserve = (typeof p.ammoReserve === 'number') ? p.ammoReserve : (w.startReserve || (w.magSize*2) || 24);
589
- return { type:'weapon', weapon: w, ammoInMag: Math.max(0, Math.floor(ammoInMag)), ammoReserve: Math.max(0, Math.floor(ammoReserve)) };
590
  }
591
 
592
  function enemyPickupCollect(e, p){
593
- if (!e || !p) return;
594
  if (p.type === 'weapon'){
595
- const normalized = normalizeWeaponPickup(p);
596
- // merge into existing same weapon
597
  for (let s=0;s<5;s++){
598
  const it = e.inventory[s];
599
- if (it && it.type==='weapon' && it.weapon.name === normalized.weapon.name){
600
- it.ammoReserve = (it.ammoReserve || 0) + (normalized.ammoReserve || 0);
601
- it.ammoInMag = Math.min(it.weapon.magSize, (it.ammoInMag || 0) + (normalized.ammoInMag || 0));
602
  enemyEquipBestWeapon(e);
603
- // ensure they equip a loaded weapon if present
604
- if (it.ammoInMag > 0) e.equippedIndex = s;
605
- e.state = 'combat';
606
- e.lastAttackedTime = performance.now();
607
  return;
608
  }
609
  }
610
- // place into empty slot if available
611
  for (let s=0;s<5;s++){
612
- if (!e.inventory[s]){
613
- e.inventory[s] = { type:'weapon', weapon: normalized.weapon, ammoInMag: normalized.ammoInMag, ammoReserve: normalized.ammoReserve };
614
- enemyEquipBestWeapon(e);
615
- // equip this weapon if it has ammo or if no other better weapon
616
- if (e.inventory[s].ammoInMag > 0) e.equippedIndex = s;
617
- e.state = 'combat';
618
- e.lastAttackedTime = performance.now();
619
- return;
620
- }
621
  }
622
- // replace worst weapon if pickup is better
623
  let worstIdx = -1, worstScore = Infinity;
 
624
  for (let s=0;s<5;s++){
625
  const it = e.inventory[s];
626
  if (it && it.type==='weapon'){
@@ -628,14 +530,11 @@
628
  if (score < worstScore){ worstScore = score; worstIdx = s; }
629
  }
630
  }
631
- const pickupScore = (normalized.weapon.dmg || 1) / (normalized.weapon.rate || 300);
632
  if (pickupScore > worstScore && worstIdx !== -1){
633
- e.inventory[worstIdx] = { type:'weapon', weapon: normalized.weapon, ammoInMag: normalized.ammoInMag, ammoReserve: normalized.ammoReserve };
634
  enemyEquipBestWeapon(e);
635
- e.state = 'combat';
636
- e.lastAttackedTime = performance.now();
637
  } else {
638
- e.materials += Math.floor((normalized.ammoReserve || 0) / 2);
639
  }
640
  } else if (p.type === 'medkit'){
641
  for (let s=0;s<5;s++){
@@ -706,34 +605,6 @@
706
  return closest;
707
  }
708
 
709
- // New helper: compute detour waypoint around blocker
710
- function computeDetourWaypoint(fromX, fromY, blocker, goalX, goalY, padding = 12){
711
- if (!blocker) return null;
712
- let br = 0;
713
- if (blocker.type === 'wall') br = 28;
714
- else if (blocker.type === 'stone') br = 22;
715
- else if (blocker.type === 'wood') br = 16;
716
- else br = 18;
717
- // vector from blocker to from
718
- const vx = fromX - blocker.x;
719
- const vy = fromY - blocker.y;
720
- const len = Math.hypot(vx, vy) || 0.0001;
721
- // perp directions
722
- const px = -vy / len;
723
- const py = vx / len;
724
- const radius = br + padding + 8;
725
- const cand1 = { x: blocker.x + px * radius, y: blocker.y + py * radius };
726
- const cand2 = { x: blocker.x - px * radius, y: blocker.y - py * radius };
727
- // choose candidate that's closer to goal and not inside other solids (approx)
728
- const d1 = Math.hypot(cand1.x - goalX, cand1.y - goalY);
729
- const d2 = Math.hypot(cand2.x - goalX, cand2.y - goalY);
730
- const chosen = d1 < d2 ? cand1 : cand2;
731
- // clamp to world
732
- chosen.x = Math.max(16, Math.min(WORLD.width-16, chosen.x));
733
- chosen.y = Math.max(16, Math.min(WORLD.height-16, chosen.y));
734
- return chosen;
735
- }
736
-
737
  // Bullets update
738
  function bulletsUpdate(dt){
739
  for (let i=bullets.length-1;i>=0;i--){
@@ -805,52 +676,41 @@
805
  return best;
806
  }
807
 
808
- // Enemy AI (improved: detour around obstacles and prioritize chest looting early, ensure equipping)
809
  function updateEnemies(dt, now){
810
- const minSeparation = 20;
 
 
811
  for (const e of enemies){
812
  if (e.health <= 0) continue;
813
- if (!e.spawnSafeUntil) e.spawnSafeUntil = performance.now() + SPAWN_PROTECT_MS;
814
 
815
- // clear expired tempTarget
816
- if (e.tempTarget && now > (e.tempTargetExpiry || 0)) { e.tempTarget = null; e.tempTargetExpiry = 0; }
817
 
818
- // If in storm move towards safe zone, using detours around blockers
819
  if (storm.active){
820
  const distToSafeCenter = Math.hypot(e.x - storm.centerX, e.y - storm.centerY);
821
  if (distToSafeCenter > storm.radius){
 
822
  e.state = 'toSafe';
823
- if (e.tempTarget){
824
- const td = Math.hypot(e.tempTarget.x - e.x, e.tempTarget.y - e.y);
825
- if (td > 8){
826
- e.angle = Math.atan2(e.tempTarget.y - e.y, e.tempTarget.x - e.x);
827
- const dx = Math.cos(e.angle) * e.speed * dt * 0.9;
828
- const dy = Math.sin(e.angle) * e.speed * dt * 0.9;
829
- moveEntityWithCollision(e, dx, dy, e.radius);
830
- continue;
831
- } else {
832
- e.tempTarget = null;
833
- e.tempTargetExpiry = 0;
834
- }
835
- }
836
- if (!hasLineOfSight(e.x, e.y, storm.centerX, storm.centerY)){
837
- const blocker = findBlockingObject(e.x, e.y, storm.centerX, storm.centerY);
838
- if (blocker){
839
- e.tempTarget = computeDetourWaypoint(e.x, e.y, blocker, storm.centerX, storm.centerY);
840
- e.tempTargetExpiry = now + 2500;
841
- if (e.tempTarget) { e.angle = Math.atan2(e.tempTarget.y - e.y, e.tempTarget.x - e.x); const dx = Math.cos(e.angle) * e.speed * dt * 0.95; const dy = Math.sin(e.angle) * e.speed * dt * 0.95; moveEntityWithCollision(e, dx, dy, e.radius); continue; }
842
- }
843
- }
844
  e.angle = Math.atan2(storm.centerY - e.y, storm.centerX - e.x);
845
- const dx = Math.cos(e.angle) * e.speed * dt * 0.95;
846
- const dy = Math.sin(e.angle) * e.speed * dt * 0.95;
847
- moveEntityWithCollision(e, dx, dy, e.radius);
848
  if (Math.random() < 0.02) e.angle += (Math.random()-0.5)*0.5;
849
- continue;
 
 
 
850
  }
851
  }
852
 
853
- // Healing logic
 
 
 
 
 
854
  if (e.health < 60 && now >= (e.nextHealTime || 0)){
855
  let medIdx = -1;
856
  for (let s=0;s<5;s++){
@@ -867,106 +727,53 @@
867
  }
868
  }
869
 
870
- // State transitions
871
  const hasUsefulWeapon = e.inventory.some(it => it && it.type === 'weapon' && (it.ammoInMag > 0 || it.ammoReserve > 0));
872
  if ((e.gatherTimeLeft <= 0 || e.materials >= 20 || hasUsefulWeapon) && e.state === 'gather') e.state = 'combat';
873
 
874
- if (e.state === 'gather') e.gatherTimeLeft -= dt;
875
-
876
- // Early-game chest priority: force chest-seeking for first few seconds to equip weapons quickly
877
- const prioritizeChests = (e.prioritizeChestsUntil && now < e.prioritizeChestsUntil);
878
  if (e.state === 'gather'){
 
 
879
  if (now >= (e.spawnSafeUntil || 0)){
880
- // pickups nearby priority (quick ammo/weapon)
881
  let p = findNearestPickup(e, 240);
882
- if (p && !prioritizeChests){
883
  const angle = Math.atan2(p.y - e.y, p.x - e.x);
884
  e.angle = angle;
885
- const dx = Math.cos(e.angle) * e.speed * dt * 0.9;
886
- const dy = Math.sin(e.angle) * e.speed * dt * 0.9;
887
- moveEntityWithCollision(e, dx, dy, e.radius);
888
  if (Math.hypot(p.x - e.x, p.y - e.y) < 18){
889
  enemyPickupCollect(e, p);
890
  const idx = pickups.indexOf(p);
891
  if (idx >= 0) pickups.splice(idx,1);
892
- // equip best weapon immediately if any
893
- enemyEquipBestWeapon(e);
894
  }
895
  continue;
896
  }
897
  }
898
 
899
- // If we don't have a useful weapon yet, prioritize chests strongly (extended radius)
900
- let chestSearchRadius = prioritizeChests || !hasUsefulWeapon ? 1400 : 900;
901
- let chestTarget = findNearestChest(e, chestSearchRadius);
902
  if (chestTarget){
903
  const d = Math.hypot(chestTarget.x - e.x, chestTarget.y - e.y);
904
  if (d > 20){
905
  e.angle = Math.atan2(chestTarget.y - e.y, chestTarget.x - e.x);
906
- // navigate with detour logic if blocked
907
- if (!hasLineOfSight(e.x, e.y, chestTarget.x, chestTarget.y)){
908
- const blocker = findBlockingObject(e.x, e.y, chestTarget.x, chestTarget.y);
909
- if (blocker){
910
- if (!e.tempTarget){
911
- e.tempTarget = computeDetourWaypoint(e.x, e.y, blocker, chestTarget.x, chestTarget.y);
912
- e.tempTargetExpiry = now + 2500;
913
- }
914
- if (e.tempTarget){
915
- const td = Math.hypot(e.tempTarget.x - e.x, e.tempTarget.y - e.y);
916
- if (td > 8){
917
- e.angle = Math.atan2(e.tempTarget.y - e.y, e.tempTarget.x - e.x);
918
- const dx = Math.cos(e.angle) * e.speed * dt * 0.9;
919
- const dy = Math.sin(e.angle) * e.speed * dt * 0.9;
920
- moveEntityWithCollision(e, dx, dy, e.radius);
921
- continue;
922
- } else {
923
- e.tempTarget = null;
924
- e.tempTargetExpiry = 0;
925
- }
926
- }
927
- }
928
- }
929
- // If unobstructed or detour not set/needed, move directly
930
- const dx = Math.cos(e.angle) * e.speed * dt * 0.95;
931
- const dy = Math.sin(e.angle) * e.speed * dt * 0.95;
932
- moveEntityWithCollision(e, dx, dy, e.radius);
933
  } else {
934
- // open chest and collect
935
- if (!chestTarget.opened){
936
- chestTarget.opened = true;
937
- const loot = chestTarget.loot;
938
- if (loot.type === 'weapon') {
939
- const wp = { type:'weapon', weapon: makeWeaponProto(loot.weapon), ammoInMag:loot.weapon.magSize||12, ammoReserve:loot.weapon.startReserve||24 };
940
- enemyPickupCollect(e, wp);
941
- } else if (loot.type === 'medkit') enemyPickupCollect(e, { type:'medkit', amount: loot.amount || 1 });
942
- else if (loot.type === 'materials') enemyPickupCollect(e, { type:'materials', amount: loot.amount || 10 });
943
- // ensure equip
944
- enemyEquipBestWeapon(e);
945
- if (e.equippedIndex !== -1){
946
- const eq = e.inventory[e.equippedIndex];
947
- if (eq && eq.type==='weapon' && eq.ammoInMag <= 0 && eq.ammoReserve > 0){
948
- // immediate reload
949
- reloadItem(eq);
950
- }
951
- }
952
- // after opening chest, prioritize combat if weapon obtained
953
- if (e.inventory.some(it => it && it.type==='weapon')) {
954
- e.state = 'combat';
955
- e.lastAttackedTime = now;
956
- }
957
- }
958
  }
959
  continue;
960
  }
961
 
962
  let objTarget = findNearestHarvestable(e, 700);
963
- if (objTarget && !prioritizeChests){
964
  const d = Math.hypot(objTarget.x - e.x, objTarget.y - e.y);
965
  if (d > 26){
966
  e.angle = Math.atan2(objTarget.y - e.y, objTarget.x - e.x);
967
- const dx = Math.cos(e.angle) * e.speed * dt * 0.8;
968
- const dy = Math.sin(e.angle) * e.speed * dt * 0.8;
969
- moveEntityWithCollision(e, dx, dy, e.radius);
970
  } else {
971
  objTarget.hp -= 40 * dt;
972
  if (objTarget.hp <= 0 && !objTarget.dead){
@@ -983,15 +790,17 @@
983
  continue;
984
  }
985
 
986
- const dx = Math.cos(e.angle) * e.speed * dt * 0.25;
987
- const dy = Math.sin(e.angle) * e.speed * dt * 0.25;
988
- moveEntityWithCollision(e, dx, dy, e.radius);
989
  if (Math.random() < 0.01) e.angle += (Math.random()-0.5)*2;
990
  continue;
991
  }
992
 
993
  // Combat state
994
  if (e.equippedIndex === -1) enemyEquipBestWeapon(e);
 
 
995
  if (e.reloadPending){
996
  if (now >= e.reloadingUntil){
997
  const eq = e.inventory[e.equippedIndex];
@@ -1000,45 +809,21 @@
1000
  e.reloadingUntil = 0;
1001
  }
1002
  }
 
 
1003
  if (e.equippedIndex >= 0){
1004
  const eq = e.inventory[e.equippedIndex];
1005
  if (eq && eq.type === 'weapon' && eq.ammoInMag <= 0 && eq.ammoReserve > 0 && !e.reloadPending && now >= e.reloadingUntil){
1006
  e.reloadPending = true;
1007
- e.reloadingUntil = now + 600 + rand(-100,100);
1008
- }
1009
- }
1010
-
1011
- // If equipped weapon is empty and no reserve, try to swap to another loaded weapon
1012
- if (e.equippedIndex >= 0){
1013
- const eq = e.inventory[e.equippedIndex];
1014
- if (eq && eq.type === 'weapon' && eq.ammoInMag <= 0 && eq.ammoReserve <= 0){
1015
- // try to find another weapon with ammo
1016
- for (let i=0;i<5;i++){
1017
- const it = e.inventory[i];
1018
- if (it && it.type === 'weapon' && it.ammoInMag > 0){
1019
- e.equippedIndex = i;
1020
- break;
1021
- }
1022
- }
1023
- // if still no mag ammo, pick weapon with reserve
1024
- if (e.equippedIndex === -1 || (e.inventory[e.equippedIndex] && e.inventory[e.equippedIndex].ammoInMag <= 0)){
1025
- for (let i=0;i<5;i++){
1026
- const it = e.inventory[i];
1027
- if (it && it.type === 'weapon' && it.ammoReserve > 0){
1028
- e.equippedIndex = i;
1029
- // schedule reload
1030
- e.reloadPending = true;
1031
- e.reloadingUntil = now + 500 + rand(-80,80);
1032
- break;
1033
- }
1034
- }
1035
- }
1036
  }
1037
  }
1038
 
 
1039
  let target = player;
1040
  let bestDist = Math.hypot(player.x - e.x, player.y - e.y);
1041
  if (bestDist > VIEW_RANGE){
 
1042
  const p = findNearestPickup(e, 1200);
1043
  const c = findNearestChest(e, 1200);
1044
  const h = findNearestHarvestable(e, 1200);
@@ -1046,6 +831,7 @@
1046
  if (p){ const d=Math.hypot(p.x-e.x,p.y-e.y); if (d<cd){ candidate=p; cd=d; } }
1047
  if (c){ const d=Math.hypot(c.x-e.x,c.y-e.y); if (d<cd){ candidate=c; cd=d; } }
1048
  if (h){ const d=Math.hypot(h.x-e.x,h.y-e.y); if (d<cd){ candidate=h; cd=d; } }
 
1049
  for (const other of enemies){
1050
  if (other === e || other.health <= 0) continue;
1051
  const d = Math.hypot(other.x - e.x, other.y - e.y);
@@ -1055,9 +841,10 @@
1055
  target = candidate;
1056
  bestDist = cd;
1057
  } else {
1058
- target = null; bestDist = Infinity;
1059
  }
1060
  } else {
 
1061
  for (const other of enemies){
1062
  if (other === e || other.health <= 0) continue;
1063
  const d = Math.hypot(other.x - e.x, other.y - e.y);
@@ -1065,20 +852,20 @@
1065
  }
1066
  }
1067
 
 
1068
  const distToPlayer = Math.hypot(player.x - e.x, player.y - e.y);
1069
  if (distToPlayer < 160 && e.health < 35 && e.materials >= 10 && Math.random() < 0.5){
1070
  enemyTryBuild(e);
1071
  }
1072
 
 
1073
  if (!target){
1074
  e.x += Math.cos(e.angle) * e.speed * dt * 0.7;
1075
  e.y += Math.sin(e.angle) * e.speed * dt * 0.7;
1076
  if (Math.random() < 0.02) e.angle += (Math.random()-0.5)*1.5;
1077
- moveEntityWithCollision(e, 0, 0, e.radius);
1078
  continue;
1079
  }
1080
 
1081
- // If a target exists but blocked, try detour waypoint instead of always breaking/shooting blocker
1082
  const blocked = !hasLineOfSight(e.x, e.y, target.x, target.y);
1083
  if (blocked){
1084
  const blocker = findBlockingObject(e.x, e.y, target.x, target.y);
@@ -1096,34 +883,19 @@
1096
  const angle = Math.atan2(blocker.y - e.y, blocker.x - e.x);
1097
  shootBullet(e.x + Math.cos(angle)*12, e.y + Math.sin(angle)*12, blocker.x + (Math.random()-0.5)*6, blocker.y + (Math.random()-0.5)*6, eq, e.id);
1098
  }
1099
- }
1100
- if (!e.tempTarget){
1101
- const waypoint = computeDetourWaypoint(e.x, e.y, blocker, target.x, target.y);
1102
- if (waypoint){
1103
- e.tempTarget = waypoint;
1104
- e.tempTargetExpiry = now + 3000;
1105
- }
1106
- }
1107
- if (e.tempTarget){
1108
- const td = Math.hypot(e.tempTarget.x - e.x, e.tempTarget.y - e.y);
1109
- if (td > 8){
1110
- e.angle = Math.atan2(e.tempTarget.y - e.y, e.tempTarget.x - e.x);
1111
- moveEntityWithCollision(e, Math.cos(e.angle) * e.speed * dt * 0.8, Math.sin(e.angle) * e.speed * dt * 0.8, e.radius);
1112
- } else {
1113
- e.tempTarget = null;
1114
- e.tempTargetExpiry = 0;
1115
- }
1116
  } else {
1117
  e.angle = Math.atan2(blocker.y - e.y, blocker.x - e.x);
1118
- e.x += Math.cos(e.angle) * e.speed * dt * 0.6;
1119
- e.y += Math.sin(e.angle) * e.speed * dt * 0.6;
1120
  }
1121
  }
1122
  continue;
1123
  }
1124
  }
1125
 
 
1126
  if (target === player){
 
1127
  if (distToPlayer < 36 && now - e.lastMelee > e.meleeRate){
1128
  e.lastMelee = now;
1129
  const dmg = 10 + randInt(0,8);
@@ -1134,14 +906,7 @@
1134
  const eq = e.inventory[e.equippedIndex];
1135
  if (eq && eq.type === 'weapon' && !e.reloadPending && now - (e.lastShot||0) > (eq.weapon.rate || 300)){
1136
  if (eq.ammoInMag <= 0){
1137
- // try reload or swap
1138
- if (eq.ammoReserve > 0 && !e.reloadPending){
1139
- e.reloadPending = true;
1140
- e.reloadingUntil = now + 600 + rand(-100,100);
1141
- } else {
1142
- // swap to other weapon
1143
- enemyEquipBestWeapon(e);
1144
- }
1145
  } else {
1146
  if (hasLineOfSight(e.x, e.y, target.x, target.y)){
1147
  e.lastShot = now;
@@ -1150,57 +915,56 @@
1150
  shootBullet(e.x + Math.cos(angle)*12, e.y + Math.sin(angle)*12, target.x + (Math.random()-0.5)*6, target.y + (Math.random()-0.5)*6, eq, e.id);
1151
  e.lastAttackedTime = now;
1152
  } else {
 
1153
  e.angle = Math.atan2(target.y - e.y, target.x - e.x);
1154
- moveEntityWithCollision(e, Math.cos(e.angle) * e.speed * dt * 0.6, Math.sin(e.angle) * e.speed * dt * 0.6, e.radius);
 
1155
  }
1156
  }
1157
  } else {
 
1158
  e.angle = Math.atan2(target.y - e.y, target.x - e.x);
1159
- moveEntityWithCollision(e, Math.cos(e.angle) * e.speed * dt * 0.9, Math.sin(e.angle) * e.speed * dt * 0.9, e.radius);
 
1160
  }
1161
  } else {
 
1162
  e.angle = Math.atan2(target.y - e.y, target.x - e.x);
1163
- moveEntityWithCollision(e, Math.cos(e.angle) * e.speed * dt * 0.9, Math.sin(e.angle) * e.speed * dt * 0.9, e.radius);
 
1164
  }
1165
  } else {
 
1166
  const td = Math.hypot(target.x - e.x, target.y - e.y);
1167
- if (target.type === 'weapon' || target.type === 'medkit' || target.type === 'materials' || target.type === 'ammo'){
1168
  if (td > 20){
1169
  e.angle = Math.atan2(target.y - e.y, target.x - e.x);
1170
- moveEntityWithCollision(e, Math.cos(e.angle) * e.speed * dt * 0.9, Math.sin(e.angle) * e.speed * dt * 0.9, e.radius);
 
1171
  } else {
1172
  enemyPickupCollect(e, target);
1173
  const idx = pickups.indexOf(target);
1174
  if (idx >= 0) pickups.splice(idx,1);
1175
  e.state = 'gather';
1176
- enemyEquipBestWeapon(e);
1177
  }
1178
- } else if (target.hasOwnProperty('loot')){
1179
  if (td > 20){
1180
  e.angle = Math.atan2(target.y - e.y, target.x - e.x);
1181
- moveEntityWithCollision(e, Math.cos(e.angle) * e.speed * dt * 0.9, Math.sin(e.angle) * e.speed * dt * 0.9, e.radius);
 
1182
  } else {
1183
- if (!target.opened){
1184
- target.opened = true;
1185
- const loot = target.loot;
1186
- if (loot.type === 'weapon') enemyPickupCollect(e, { type:'weapon', weapon: makeWeaponProto(loot.weapon), ammoInMag:loot.weapon.magSize||12, ammoReserve:loot.weapon.startReserve||24 });
1187
- else if (loot.type === 'medkit') enemyPickupCollect(e, { type:'medkit', amount: loot.amount || 1 });
1188
- else if (loot.type === 'materials') enemyPickupCollect(e, { type:'materials', amount: loot.amount || 10 });
1189
- e.state = 'gather';
1190
- enemyEquipBestWeapon(e);
1191
- if (e.equippedIndex !== -1){
1192
- const eq = e.inventory[e.equippedIndex];
1193
- if (eq && eq.type==='weapon' && eq.ammoInMag <= 0 && eq.ammoReserve > 0){
1194
- reloadItem(eq);
1195
- }
1196
- }
1197
- if (e.inventory.some(it => it && it.type === 'weapon')){ e.state = 'combat'; e.lastAttackedTime = now; }
1198
- }
1199
  }
1200
- } else if (target.hasOwnProperty('type') && (target.type === 'wood' || target.type === 'stone')){
1201
  if (td > 26){
1202
  e.angle = Math.atan2(target.y - e.y, target.x - e.x);
1203
- moveEntityWithCollision(e, Math.cos(e.angle) * e.speed * dt * 0.8, Math.sin(e.angle) * e.speed * dt * 0.8, e.radius);
 
1204
  } else {
1205
  target.hp -= 40 * dt;
1206
  if (target.hp <= 0 && !target.dead){
@@ -1210,25 +974,20 @@
1210
  e.state = 'gather';
1211
  }
1212
  } else {
 
1213
  if (td > 40){
1214
  e.angle = Math.atan2(target.y - e.y, target.x - e.x);
1215
- moveEntityWithCollision(e, Math.cos(e.angle) * e.speed * dt * 0.7, Math.sin(e.angle) * e.speed * dt * 0.7, e.radius);
 
1216
  } else {
1217
- if (now - e.lastMelee > e.meleeRate){
1218
- e.lastMelee = now;
1219
- if (target && target.health > 0){
1220
- target.health -= 8 + randInt(0,6);
1221
- target.lastAttackedTime = now;
1222
- target.lastAttackerId = e.id;
1223
- if (target.health <= 0) target.health = 0;
1224
- }
1225
- }
1226
  }
1227
  }
1228
  }
1229
  }
1230
 
1231
- // Separation
1232
  for (let i = 0; i < enemies.length; i++){
1233
  const a = enemies[i];
1234
  if (!a || a.health <= 0) continue;
@@ -1241,12 +1000,14 @@
1241
  if (d < minD){
1242
  const overlap = (minD - d) * 0.5;
1243
  const nx = dx / d, ny = dy / d;
 
1244
  b.x += nx * overlap;
1245
  b.y += ny * overlap;
1246
  a.x -= nx * overlap;
1247
  a.y -= ny * overlap;
1248
  }
1249
  }
 
1250
  const pdx = a.x - player.x, pdy = a.y - player.y;
1251
  const pd = Math.hypot(pdx,pdy) || 0.0001;
1252
  const avoidDist = 24;
@@ -1256,6 +1017,7 @@
1256
  a.x += nx * overlap;
1257
  a.y += ny * overlap;
1258
  }
 
1259
  a.x = Math.max(12, Math.min(WORLD.width-12, a.x));
1260
  a.y = Math.max(12, Math.min(WORLD.height-12, a.y));
1261
  }
@@ -1300,7 +1062,7 @@
1300
  }
1301
  }
1302
 
1303
- // Drawing world & entities
1304
  function drawWorld(){
1305
  const TILE = 600;
1306
  const cols = Math.ceil(WORLD.width / TILE);
@@ -1399,6 +1161,7 @@
1399
  const hpPct = Math.max(0, Math.min(1, e.health/120));
1400
  ctx.fillStyle='#ff6b6b'; ctx.fillRect(-18,-22,36*hpPct,6);
1401
 
 
1402
  if (e.equippedIndex >= 0 && e.inventory[e.equippedIndex] && e.inventory[e.equippedIndex].type === 'weapon'){
1403
  const we = e.inventory[e.equippedIndex];
1404
  const color = we.weapon.color || '#ddd';
@@ -1428,6 +1191,7 @@
1428
  }
1429
  }
1430
 
 
1431
  ctx.fillStyle = e.state === 'gather' ? 'rgba(0,200,200,0.9)' : (e.state === 'combat' ? 'rgba(255,80,80,0.95)' : 'rgba(255,200,80,0.9)');
1432
  ctx.beginPath(); ctx.arc(18, -18, 5, 0, Math.PI*2); ctx.fill();
1433
  ctx.restore();
@@ -1494,108 +1258,6 @@
1494
 
1495
  function drawCrosshair(){}
1496
 
1497
- // Minimap: build terrain cache and draw overlay (EXCLUDES enemies & chests)
1498
- function buildMiniTerrainCache(){
1499
- const mw = minimapCanvas.width;
1500
- const mh = minimapCanvas.height;
1501
- const scaleX = WORLD.width / mw;
1502
- const scaleY = WORLD.height / mh;
1503
- const img = miniCtx.createImageData(mw, mh);
1504
- for (let my=0; my<mh; my++){
1505
- for (let mx=0; mx<mw; mx++){
1506
- const wx = Math.floor(mx * scaleX + scaleX/2);
1507
- const wy = Math.floor(my * scaleY + scaleY/2);
1508
- const b = biomeAt(wx, wy);
1509
- let col = [32,58,43];
1510
- if (b==='desert') col = [203,183,139];
1511
- else if (b==='forest') col = [22,65,31];
1512
- else if (b==='oasis') col = [39,75,82];
1513
- else if (b==='ruins') col = [74,59,59];
1514
- const idx = (my*mw + mx)*4;
1515
- img.data[idx] = col[0];
1516
- img.data[idx+1] = col[1];
1517
- img.data[idx+2] = col[2];
1518
- img.data[idx+3] = 255;
1519
- }
1520
- }
1521
- miniTerrainCache = img;
1522
- }
1523
-
1524
- function drawMinimap(){
1525
- const mw = minimapCanvas.width;
1526
- const mh = minimapCanvas.height;
1527
- if (!miniTerrainCache) buildMiniTerrainCache();
1528
- // draw base terrain
1529
- miniCtx.putImageData(miniTerrainCache, 0, 0);
1530
-
1531
- // draw world objects (trees/stones/walls) as tiny marks - chests excluded per request
1532
- miniCtx.save();
1533
- const scaleX = mw / WORLD.width;
1534
- const scaleY = mh / WORLD.height;
1535
- for (const obj of objects){
1536
- if (obj.dead) continue;
1537
- const px = Math.round(obj.x * scaleX);
1538
- const py = Math.round(obj.y * scaleY);
1539
- if (obj.type === 'wood'){
1540
- miniCtx.fillStyle = '#3f210f';
1541
- miniCtx.fillRect(px-1, py-1, 2, 2);
1542
- } else if (obj.type === 'stone'){
1543
- miniCtx.fillStyle = '#666';
1544
- miniCtx.fillRect(px-1, py-1, 2, 2);
1545
- } else if (obj.type === 'wall'){
1546
- miniCtx.fillStyle = '#8b5a32';
1547
- miniCtx.fillRect(px-2, py-2, 4, 4);
1548
- }
1549
- }
1550
- // optionally show pickups (not enemies/chests) - small blue/green dots
1551
- for (const p of pickups){
1552
- const px = Math.round(p.x * scaleX);
1553
- const py = Math.round(p.y * scaleY);
1554
- if (p.type === 'weapon'){
1555
- miniCtx.fillStyle = '#ffd86b'; miniCtx.fillRect(px-1, py-1, 2, 2);
1556
- } else if (p.type === 'medkit'){
1557
- miniCtx.fillStyle = '#ff6b6b'; miniCtx.fillRect(px-1, py-1, 2, 2);
1558
- } else if (p.type === 'materials'){
1559
- miniCtx.fillStyle = '#cfe0a6'; miniCtx.fillRect(px-1, py-1, 2, 2);
1560
- } else if (p.type === 'ammo'){
1561
- miniCtx.fillStyle = '#e6e6e6'; miniCtx.fillRect(px-1, py-1, 2, 2);
1562
- }
1563
- }
1564
-
1565
- // Storm safe zone: draw overlay darkening outside safe zone and stroke the safe circle
1566
- if (storm.active){
1567
- // darken everything
1568
- miniCtx.fillStyle = 'rgba(0,0,0,0.35)';
1569
- miniCtx.fillRect(0,0,mw,mh);
1570
- // carve out safe zone
1571
- miniCtx.globalCompositeOperation = 'destination-out';
1572
- const cx = storm.centerX * scaleX;
1573
- const cy = storm.centerY * scaleY;
1574
- // radius scaled by average axis to keep circle shape close
1575
- const r = storm.radius * ((scaleX + scaleY) / 2);
1576
- miniCtx.beginPath();
1577
- miniCtx.arc(cx, cy, r, 0, Math.PI*2);
1578
- miniCtx.fill();
1579
- miniCtx.globalCompositeOperation = 'source-over';
1580
- // border
1581
- miniCtx.strokeStyle = 'rgba(255,200,80,0.95)';
1582
- miniCtx.lineWidth = 2;
1583
- miniCtx.beginPath();
1584
- miniCtx.arc(cx, cy, r, 0, Math.PI*2);
1585
- miniCtx.stroke();
1586
- }
1587
-
1588
- // draw player dot (always visible)
1589
- const ppx = Math.round(player.x * (mw / WORLD.width));
1590
- const ppy = Math.round(player.y * (mh / WORLD.height));
1591
- miniCtx.fillStyle = '#ffff66';
1592
- miniCtx.beginPath();
1593
- miniCtx.arc(ppx, ppy, 3, 0, Math.PI*2);
1594
- miniCtx.fill();
1595
-
1596
- miniCtx.restore();
1597
- }
1598
-
1599
  // Main loop
1600
  let lastTime = 0;
1601
  function gameLoop(ts){
@@ -1604,25 +1266,13 @@
1604
  const dt = Math.min(0.05, (ts - lastTime)/1000);
1605
  lastTime = ts;
1606
 
 
1607
  let dx=0, dy=0;
1608
  if (keys.w) dy -= 1; if (keys.s) dy += 1; if (keys.a) dx -= 1; if (keys.d) dx += 1;
1609
  if (dx !== 0 || dy !== 0){
1610
  const len = Math.hypot(dx,dy) || 1;
1611
- const mvx = (dx/len) * player.speed * dt;
1612
- const mvy = (dy/len) * player.speed * dt;
1613
- const oldX = player.x, oldY = player.y;
1614
- player.x += mvx;
1615
- if (player.x < player.radius) player.x = player.radius;
1616
- if (player.x > WORLD.width - player.radius) player.x = WORLD.width - player.radius;
1617
- if (isCollidingSolid(player.x, player.y, player.radius)){
1618
- player.x = oldX;
1619
- }
1620
- player.y += mvy;
1621
- if (player.y < player.radius) player.y = player.radius;
1622
- if (player.y > WORLD.height - player.radius) player.y = WORLD.height - player.radius;
1623
- if (isCollidingSolid(player.x, player.y, player.radius)){
1624
- player.y = oldY;
1625
- }
1626
  }
1627
  player.x = Math.max(16, Math.min(WORLD.width-16, player.x));
1628
  player.y = Math.max(16, Math.min(WORLD.height-16, player.y));
@@ -1639,6 +1289,7 @@
1639
  if (selected && selected.type === 'weapon') activeWeaponItem = selected;
1640
  }
1641
 
 
1642
  if (mouse.down){
1643
  if (player.equippedIndex === -1) playerMeleeHit();
1644
  else if (activeWeaponItem && activeWeaponItem.type === 'weapon'){
@@ -1657,26 +1308,27 @@
1657
  if (keys.e){ interactNearby(); keys.e = false; }
1658
  if (keys.q){ tryBuild(); keys.q = false; }
1659
 
 
1660
  updateEnemies(dt, performance.now());
1661
  bulletsUpdate(dt);
1662
 
 
1663
  for (let i=pickups.length-1;i>=0;i--){
1664
  const p = pickups[i];
1665
  if (Math.hypot(p.x - player.x, p.y - player.y) < 18){ pickupCollect(p); pickups.splice(i,1); updateHUD(); }
1666
  }
1667
 
 
1668
  for (let i=objects.length-1;i>=0;i--) if (objects[i].dead) objects.splice(i,1);
1669
 
1670
  updatePlayerCount();
1671
  updateStorm(dt);
1672
 
 
1673
  ctx.clearRect(0,0,canvas.width,canvas.height);
1674
  drawWorld(); drawObjects(); drawChests(); drawPickups(); drawEnemies(); drawBullets(); drawPlayer(); drawCrosshair();
1675
  updateHUD();
1676
 
1677
- // Draw the minimap last so it reflects the latest world state
1678
- drawMinimap();
1679
-
1680
  if (storm.active && playerInStorm()) stormWarning.classList.remove('hidden'); else stormWarning.classList.add('hidden');
1681
 
1682
  requestAnimationFrame(gameLoop);
@@ -1685,6 +1337,7 @@
1685
  // Landing -> spawn selection
1686
  let selectedBiome = null;
1687
  function getSpawnForBiome(b){
 
1688
  if (!b) return { x: WORLD.width/2 + (Math.random()-0.5)*400, y: WORLD.height/2 + (Math.random()-0.5)*400 };
1689
  if (b === 'desert') return { x: rand(200, WORLD.width*0.3), y: rand(200, WORLD.height*0.3) };
1690
  if (b === 'forest') return { x: rand(WORLD.width*0.7, WORLD.width-200), y: rand(200, WORLD.height*0.3) };
@@ -1699,6 +1352,7 @@
1699
  let gameActive = false;
1700
 
1701
  function startGame(biome){
 
1702
  selectedBiome = biome || selectedBiome;
1703
  const spawn = getSpawnForBiome(selectedBiome);
1704
  document.getElementById('currentBiome').textContent = selectedBiome ? selectedBiome.toUpperCase() : 'CENTER';
@@ -1721,23 +1375,17 @@
1721
  initHUD();
1722
  cameraUpdate();
1723
 
1724
- // Set each enemy to prioritize chests for a short time so they grab weapons early, and reposition away from player spawn
1725
  for (const e of enemies){
1726
  if (Math.hypot(e.x - player.x, e.y - player.y) < 180){
1727
  e.x += (Math.random()<0.5? -1:1) * rand(160,260);
1728
  e.y += (Math.random()<0.5? -1:1) * rand(160,260);
1729
  }
 
1730
  e.inventory = [null,null,null,null,null];
1731
  e.equippedIndex = -1;
1732
  e.spawnSafeUntil = performance.now() + SPAWN_PROTECT_MS;
1733
  e.state = 'gather';
1734
- e.nextHealTime = 0;
1735
- e.tempTarget = null;
1736
- e.tempTargetExpiry = 0;
1737
- // this flag makes them aggressively seek chests in the first few seconds
1738
- e.prioritizeChestsUntil = performance.now() + 6000 + rand(0,3000);
1739
- // also slightly reduce gatherTimeLeft so they don't linger on harvesting
1740
- e.gatherTimeLeft = Math.min(e.gatherTimeLeft, 6);
1741
  }
1742
 
1743
  gameTime = 300; storm.active = false; storm.radius = storm.maxRadius;
@@ -1770,17 +1418,20 @@
1770
  goHomeBtn.addEventListener('click', ()=>{ victoryScreen.classList.add('hidden'); gameScreen.classList.add('hidden'); landingScreen.classList.remove('hidden'); });
1771
  continueBtn.addEventListener('click', ()=>{ victoryScreen.classList.add('hidden'); startGame(selectedBiome); });
1772
 
 
1773
  document.querySelectorAll('.biome-selector').forEach(el => {
1774
  el.addEventListener('click', (ev)=>{
 
1775
  document.querySelectorAll('.biome-selector').forEach(x=>x.classList.remove('biome-selected'));
1776
  el.classList.add('biome-selected');
1777
  const biome = el.dataset.biome;
1778
  selectedBiome = biome;
 
1779
  startGame(biome);
1780
  });
1781
  });
1782
 
1783
- // initialization
1784
  resizeCanvas();
1785
  populateWorld();
1786
  feather.replace();
 
3
  <head>
4
  <meta charset="utf-8" />
5
  <meta name="viewport" content="width=device-width, initial-scale=1" />
6
+ <title>BattleZone Royale - Spawn & AI Fixes</title>
7
  <script src="https://cdn.tailwindcss.com"></script>
8
  <script src="https://unpkg.com/feather-icons"></script>
9
  <style>
10
  html,body { height:100%; margin:0; background:#0b1220; color:#fff; font-family:monospace; }
11
  #canvasContainer { position:relative; flex:1; display:flex; justify-content:center; align-items:center; height:100vh; overflow:hidden; }
12
  #gameCanvas { display:block; user-select:none; cursor:crosshair; box-shadow:0 0 20px rgba(0,0,0,.5); width:100%; height:100%; }
 
 
 
 
 
13
  /* HUD */
14
  #hudHealth { position:absolute; left:12px; bottom:12px; background:rgba(0,0,0,0.55); padding:6px 8px; border-radius:8px; font-weight:700; font-size:13px; display:flex; align-items:center; gap:8px; z-index:30; }
15
  #hudGearWrap { position:absolute; right:12px; bottom:12px; display:flex; gap:8px; align-items:center; z-index:30; }
 
91
  <div id="canvasContainer" class="relative bg-gray-900 border-4 border-gray-700 rounded-xl max-w-full max-h-full">
92
  <canvas id="gameCanvas"></canvas>
93
 
 
 
 
 
 
94
  <div id="stormWarning" class="hidden absolute top-6 left-1/2 transform -translate-x-1/2 bg-red-900 bg-opacity-80 text-white px-6 py-3 rounded-lg flex items-center">
95
  <i data-feather="alert-circle" class="mr-2"></i>
96
  <span>STORM APPROACHING! MOVE TO SAFE ZONE!</span>
 
149
  const continueBtn = document.getElementById('continueBtn');
150
  const biomeGrid = document.getElementById('biomeGrid');
151
 
 
 
 
 
 
152
  // World
153
  const WORLD = { width: 6000, height: 4000 };
154
  let camera = { x:0, y:0 };
 
157
  const ctn = document.getElementById('canvasContainer');
158
  canvas.width = Math.max(900, Math.floor(ctn.clientWidth));
159
  canvas.height = Math.max(560, Math.floor(ctn.clientHeight));
 
 
 
160
  cameraUpdate();
 
161
  }
162
  window.addEventListener('resize', resizeCanvas);
163
 
 
254
  function generateLootForBiome(b){
255
  const roll = Math.random();
256
  if (roll < 0.35) return { type:'medkit', amount:1 };
257
+ if (roll < 0.7) return { type:'materials', amount: 10 }; // always 10
258
  const weapons = [
259
  { name:'Pistol', dmg:12, rate:320, color:'#ffd86b', magSize:12, startReserve:24 },
260
  { name:'SMG', dmg:6, rate:120, color:'#8ef0ff', magSize:30, startReserve:90 },
 
265
  }
266
 
267
  // Behaviour tuning
268
+ const VIEW_RANGE = 1200; // if player is farther than this, enemies switch target
269
+ const SPAWN_PROTECT_MS = 1200; // time after spawn they ignore ground pickups to avoid instant pickup
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
270
 
271
+ // Populate world - spawn many objects and enemies
272
  function populateWorld(){
273
  chests.length = 0; objects.length = 0; enemies.length = 0; pickups.length = 0;
274
  for (let i=0;i<260;i++){
275
  const x = rand(150, WORLD.width-150);
276
  const y = rand(150, WORLD.height-150);
277
  const loot = generateLootForBiome(biomeAt(x,y));
278
+ if (loot.type === 'materials') loot.amount = 10; // ensure 10
279
  chests.push({ x,y, opened:false, loot });
280
  }
281
  for (let i=0;i<700;i++){
 
287
  const hp = type==='wood'?40 : (type==='stone'?80:160);
288
  objects.push({ x,y, type, hp, maxHp:hp, dead:false });
289
  }
290
+ // spawn enemies - ensure they start unarmed and must loot to obtain weapons
291
  const now = performance.now();
292
  for (let i=0;i<49;i++){
293
  const ex = rand(300, WORLD.width-300);
 
296
  id:'e'+i, x:ex, y:ey, radius:14, angle:rand(0,Math.PI*2), speed:110+rand(-20,20),
297
  health: 80 + randInt(0,40), lastMelee:0, meleeRate:800 + randInt(-200,200),
298
  roamTimer: rand(0,3),
299
+ inventory: [null,null,null,null,null], // explicitly no weapons at spawn
300
  selectedSlot: 0,
301
  equippedIndex: -1,
302
  materials: 0,
 
308
  gatherTimeLeft: rand(8,16),
309
  target: null,
310
  nextHealTime: 0,
311
+ spawnSafeUntil: now + SPAWN_PROTECT_MS // cooldown: ignore nearby ground pickups for short time
 
 
 
312
  });
313
  }
314
  updatePlayerCount();
 
372
  }
373
  function worldToScreen(wx,wy){ return { x: Math.round(wx - camera.x), y: Math.round(wy - camera.y) }; }
374
 
375
+ // Combat utilities
376
  function shootBullet(originX, originY, targetX, targetY, weaponObj, shooterId){
377
  if (!weaponObj || (typeof weaponObj.ammoInMag === 'number' && weaponObj.ammoInMag <= 0)) return false;
378
  const speed = 1100;
 
492
  }
493
  }
494
 
495
+ // Enemy helpers (equip / collect)
496
  function enemyEquipBestWeapon(e){
 
497
  let bestIdx = -1;
498
  let bestScore = -Infinity;
499
  for (let i=0;i<5;i++){
500
  const it = e.inventory[i];
501
  if (it && it.type === 'weapon'){
502
+ const score = (it.weapon.dmg || 1) / (it.weapon.rate || 300);
 
 
 
 
 
503
  if (score > bestScore){ bestScore = score; bestIdx = i; }
504
  }
505
  }
506
+ if (bestIdx !== -1) e.equippedIndex = bestIdx;
507
+ else e.equippedIndex = -1;
 
 
 
 
 
 
 
 
 
 
 
508
  }
509
 
510
  function enemyPickupCollect(e, p){
 
511
  if (p.type === 'weapon'){
 
 
512
  for (let s=0;s<5;s++){
513
  const it = e.inventory[s];
514
+ if (it && it.type==='weapon' && it.weapon.name === p.weapon.name){
515
+ it.ammoReserve += p.ammoReserve;
516
+ it.ammoInMag = Math.min(it.weapon.magSize, it.ammoInMag + p.ammoInMag);
517
  enemyEquipBestWeapon(e);
 
 
 
 
518
  return;
519
  }
520
  }
 
521
  for (let s=0;s<5;s++){
522
+ if (!e.inventory[s]) { e.inventory[s] = { type:'weapon', weapon:p.weapon, ammoInMag:p.ammoInMag, ammoReserve:p.ammoReserve }; enemyEquipBestWeapon(e); return; }
 
 
 
 
 
 
 
 
523
  }
 
524
  let worstIdx = -1, worstScore = Infinity;
525
+ const pickupScore = (p.weapon.dmg || 1) / (p.weapon.rate || 300);
526
  for (let s=0;s<5;s++){
527
  const it = e.inventory[s];
528
  if (it && it.type==='weapon'){
 
530
  if (score < worstScore){ worstScore = score; worstIdx = s; }
531
  }
532
  }
 
533
  if (pickupScore > worstScore && worstIdx !== -1){
534
+ e.inventory[worstIdx] = { type:'weapon', weapon:p.weapon, ammoInMag:p.ammoInMag, ammoReserve:p.ammoReserve };
535
  enemyEquipBestWeapon(e);
 
 
536
  } else {
537
+ e.materials += Math.floor((p.ammoReserve || 0) / 2);
538
  }
539
  } else if (p.type === 'medkit'){
540
  for (let s=0;s<5;s++){
 
605
  return closest;
606
  }
607
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
608
  // Bullets update
609
  function bulletsUpdate(dt){
610
  for (let i=bullets.length-1;i>=0;i--){
 
676
  return best;
677
  }
678
 
679
+ // Enemy AI with gather-first + separation + reload with delay
680
  function updateEnemies(dt, now){
681
+ // basic separation step variables
682
+ const minSeparation = 20; // minimal distance between enemies
683
+ // Update each enemy movement and actions
684
  for (const e of enemies){
685
  if (e.health <= 0) continue;
 
686
 
687
+ // ensure spawn protection exists
688
+ if (!e.spawnSafeUntil) e.spawnSafeUntil = performance.now() + SPAWN_PROTECT_MS;
689
 
690
+ // If storm is active and enemy is outside safe zone, move to safe zone first
691
  if (storm.active){
692
  const distToSafeCenter = Math.hypot(e.x - storm.centerX, e.y - storm.centerY);
693
  if (distToSafeCenter > storm.radius){
694
+ // move towards safe zone center
695
  e.state = 'toSafe';
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
696
  e.angle = Math.atan2(storm.centerY - e.y, storm.centerX - e.x);
697
+ e.x += Math.cos(e.angle) * e.speed * dt * 0.95;
698
+ e.y += Math.sin(e.angle) * e.speed * dt * 0.95;
699
+ // avoid getting stuck: allow minor wandering
700
  if (Math.random() < 0.02) e.angle += (Math.random()-0.5)*0.5;
701
+ // clamp
702
+ e.x = Math.max(12, Math.min(WORLD.width-12, e.x));
703
+ e.y = Math.max(12, Math.min(WORLD.height-12, e.y));
704
+ continue; // priority movement to safe zone
705
  }
706
  }
707
 
708
+ // if recently attacked go combat
709
+ if (now - e.lastAttackedTime < 4000) e.state = 'combat';
710
+
711
+ if (e.state === 'gather') e.gatherTimeLeft -= dt;
712
+
713
+ // Medkit-only heal (consume medkit, with cooldown)
714
  if (e.health < 60 && now >= (e.nextHealTime || 0)){
715
  let medIdx = -1;
716
  for (let s=0;s<5;s++){
 
727
  }
728
  }
729
 
730
+ // Transition gather->combat only if they actually have weapons/ammo or enough materials or time expired
731
  const hasUsefulWeapon = e.inventory.some(it => it && it.type === 'weapon' && (it.ammoInMag > 0 || it.ammoReserve > 0));
732
  if ((e.gatherTimeLeft <= 0 || e.materials >= 20 || hasUsefulWeapon) && e.state === 'gather') e.state = 'combat';
733
 
 
 
 
 
734
  if (e.state === 'gather'){
735
+ // Prioritize pickup -> chest -> harvest -> roam
736
+ // Respect spawn protection so they don't instantly pick up something they spawned on
737
  if (now >= (e.spawnSafeUntil || 0)){
 
738
  let p = findNearestPickup(e, 240);
739
+ if (p){
740
  const angle = Math.atan2(p.y - e.y, p.x - e.x);
741
  e.angle = angle;
742
+ e.x += Math.cos(e.angle) * e.speed * dt * 0.9;
743
+ e.y += Math.sin(e.angle) * e.speed * dt * 0.9;
 
744
  if (Math.hypot(p.x - e.x, p.y - e.y) < 18){
745
  enemyPickupCollect(e, p);
746
  const idx = pickups.indexOf(p);
747
  if (idx >= 0) pickups.splice(idx,1);
 
 
748
  }
749
  continue;
750
  }
751
  }
752
 
753
+ let chestTarget = findNearestChest(e, 900);
 
 
754
  if (chestTarget){
755
  const d = Math.hypot(chestTarget.x - e.x, chestTarget.y - e.y);
756
  if (d > 20){
757
  e.angle = Math.atan2(chestTarget.y - e.y, chestTarget.x - e.x);
758
+ e.x += Math.cos(e.angle) * e.speed * dt * 0.9;
759
+ e.y += Math.sin(e.angle) * e.speed * dt * 0.9;
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
760
  } else {
761
+ chestTarget.opened = true;
762
+ const loot = chestTarget.loot;
763
+ if (loot.type === 'weapon') enemyPickupCollect(e, { type:'weapon', weapon: makeWeaponProto(loot.weapon), ammoInMag:loot.weapon.magSize||12, ammoReserve:loot.weapon.startReserve||24 });
764
+ else if (loot.type === 'medkit') enemyPickupCollect(e, { type:'medkit', amount: loot.amount || 1 });
765
+ else if (loot.type === 'materials') enemyPickupCollect(e, { type:'materials', amount: loot.amount || 10 });
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
766
  }
767
  continue;
768
  }
769
 
770
  let objTarget = findNearestHarvestable(e, 700);
771
+ if (objTarget){
772
  const d = Math.hypot(objTarget.x - e.x, objTarget.y - e.y);
773
  if (d > 26){
774
  e.angle = Math.atan2(objTarget.y - e.y, objTarget.x - e.x);
775
+ e.x += Math.cos(e.angle) * e.speed * dt * 0.8;
776
+ e.y += Math.sin(e.angle) * e.speed * dt * 0.8;
 
777
  } else {
778
  objTarget.hp -= 40 * dt;
779
  if (objTarget.hp <= 0 && !objTarget.dead){
 
790
  continue;
791
  }
792
 
793
+ // roam
794
+ e.x += Math.cos(e.angle) * e.speed * dt * 0.25;
795
+ e.y += Math.sin(e.angle) * e.speed * dt * 0.25;
796
  if (Math.random() < 0.01) e.angle += (Math.random()-0.5)*2;
797
  continue;
798
  }
799
 
800
  // Combat state
801
  if (e.equippedIndex === -1) enemyEquipBestWeapon(e);
802
+
803
+ // Handle reload pending (delayed reload to simulate player reload)
804
  if (e.reloadPending){
805
  if (now >= e.reloadingUntil){
806
  const eq = e.inventory[e.equippedIndex];
 
809
  e.reloadingUntil = 0;
810
  }
811
  }
812
+
813
+ // If equipped weapon empty and reserve present, start reload (delayed)
814
  if (e.equippedIndex >= 0){
815
  const eq = e.inventory[e.equippedIndex];
816
  if (eq && eq.type === 'weapon' && eq.ammoInMag <= 0 && eq.ammoReserve > 0 && !e.reloadPending && now >= e.reloadingUntil){
817
  e.reloadPending = true;
818
+ e.reloadingUntil = now + 600 + rand(-100,100); // ~600ms reload time
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
819
  }
820
  }
821
 
822
+ // choose a target: prefer player if within view range, otherwise pick something else (pickup/chest/harvest/nearest enemy)
823
  let target = player;
824
  let bestDist = Math.hypot(player.x - e.x, player.y - e.y);
825
  if (bestDist > VIEW_RANGE){
826
+ // player is too far: find the nearest meaningful target
827
  const p = findNearestPickup(e, 1200);
828
  const c = findNearestChest(e, 1200);
829
  const h = findNearestHarvestable(e, 1200);
 
831
  if (p){ const d=Math.hypot(p.x-e.x,p.y-e.y); if (d<cd){ candidate=p; cd=d; } }
832
  if (c){ const d=Math.hypot(c.x-e.x,c.y-e.y); if (d<cd){ candidate=c; cd=d; } }
833
  if (h){ const d=Math.hypot(h.x-e.x,h.y-e.y); if (d<cd){ candidate=h; cd=d; } }
834
+ // try target nearest enemy (tend to form small groups) but don't go after a dead one
835
  for (const other of enemies){
836
  if (other === e || other.health <= 0) continue;
837
  const d = Math.hypot(other.x - e.x, other.y - e.y);
 
841
  target = candidate;
842
  bestDist = cd;
843
  } else {
844
+ target = null; bestDist = Infinity; // no specific target - roam
845
  }
846
  } else {
847
+ // still consider switching to another close enemy sometimes
848
  for (const other of enemies){
849
  if (other === e || other.health <= 0) continue;
850
  const d = Math.hypot(other.x - e.x, other.y - e.y);
 
852
  }
853
  }
854
 
855
+ // retreat/build if low HP and has materials
856
  const distToPlayer = Math.hypot(player.x - e.x, player.y - e.y);
857
  if (distToPlayer < 160 && e.health < 35 && e.materials >= 10 && Math.random() < 0.5){
858
  enemyTryBuild(e);
859
  }
860
 
861
+ // If no target (roaming), do simple roam
862
  if (!target){
863
  e.x += Math.cos(e.angle) * e.speed * dt * 0.7;
864
  e.y += Math.sin(e.angle) * e.speed * dt * 0.7;
865
  if (Math.random() < 0.02) e.angle += (Math.random()-0.5)*1.5;
 
866
  continue;
867
  }
868
 
 
869
  const blocked = !hasLineOfSight(e.x, e.y, target.x, target.y);
870
  if (blocked){
871
  const blocker = findBlockingObject(e.x, e.y, target.x, target.y);
 
883
  const angle = Math.atan2(blocker.y - e.y, blocker.x - e.x);
884
  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);
885
  }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
886
  } else {
887
  e.angle = Math.atan2(blocker.y - e.y, blocker.x - e.x);
888
+ e.x += Math.cos(e.angle) * e.speed * dt * 0.8;
889
+ e.y += Math.sin(e.angle) * e.speed * dt * 0.8;
890
  }
891
  }
892
  continue;
893
  }
894
  }
895
 
896
+ // If target is player, perform attack logic; if target is other entity/object, adapt accordingly
897
  if (target === player){
898
+ // Attack: if melee range
899
  if (distToPlayer < 36 && now - e.lastMelee > e.meleeRate){
900
  e.lastMelee = now;
901
  const dmg = 10 + randInt(0,8);
 
906
  const eq = e.inventory[e.equippedIndex];
907
  if (eq && eq.type === 'weapon' && !e.reloadPending && now - (e.lastShot||0) > (eq.weapon.rate || 300)){
908
  if (eq.ammoInMag <= 0){
909
+ // reload will be handled above
 
 
 
 
 
 
 
910
  } else {
911
  if (hasLineOfSight(e.x, e.y, target.x, target.y)){
912
  e.lastShot = now;
 
915
  shootBullet(e.x + Math.cos(angle)*12, e.y + Math.sin(angle)*12, target.x + (Math.random()-0.5)*6, target.y + (Math.random()-0.5)*6, eq, e.id);
916
  e.lastAttackedTime = now;
917
  } else {
918
+ // move to get LOS
919
  e.angle = Math.atan2(target.y - e.y, target.x - e.x);
920
+ e.x += Math.cos(e.angle) * e.speed * dt * 0.6;
921
+ e.y += Math.sin(e.angle) * e.speed * dt * 0.6;
922
  }
923
  }
924
  } else {
925
+ // unarmed behavior: rush to melee
926
  e.angle = Math.atan2(target.y - e.y, target.x - e.x);
927
+ e.x += Math.cos(e.angle) * e.speed * dt * 0.9;
928
+ e.y += Math.sin(e.angle) * e.speed * dt * 0.9;
929
  }
930
  } else {
931
+ // no weapon: rush in
932
  e.angle = Math.atan2(target.y - e.y, target.x - e.x);
933
+ e.x += Math.cos(e.angle) * e.speed * dt * 0.9;
934
+ e.y += Math.sin(e.angle) * e.speed * dt * 0.9;
935
  }
936
  } else {
937
+ // Target is a chest/pickup/harvestable/other enemy
938
  const td = Math.hypot(target.x - e.x, target.y - e.y);
939
+ if (target.type === 'weapon' || target.type === 'medkit' || target.type === 'materials' || target.type === 'ammo'){ // a pickup
940
  if (td > 20){
941
  e.angle = Math.atan2(target.y - e.y, target.x - e.x);
942
+ e.x += Math.cos(e.angle) * e.speed * dt * 0.9;
943
+ e.y += Math.sin(e.angle) * e.speed * dt * 0.9;
944
  } else {
945
  enemyPickupCollect(e, target);
946
  const idx = pickups.indexOf(target);
947
  if (idx >= 0) pickups.splice(idx,1);
948
  e.state = 'gather';
 
949
  }
950
+ } else if (target.hasOwnProperty('loot')){ // chest
951
  if (td > 20){
952
  e.angle = Math.atan2(target.y - e.y, target.x - e.x);
953
+ e.x += Math.cos(e.angle) * e.speed * dt * 0.9;
954
+ e.y += Math.sin(e.angle) * e.speed * dt * 0.9;
955
  } else {
956
+ target.opened = true;
957
+ const loot = target.loot;
958
+ if (loot.type === 'weapon') enemyPickupCollect(e, { type:'weapon', weapon: makeWeaponProto(loot.weapon), ammoInMag:loot.weapon.magSize||12, ammoReserve:loot.weapon.startReserve||24 });
959
+ else if (loot.type === 'medkit') enemyPickupCollect(e, { type:'medkit', amount: loot.amount || 1 });
960
+ else if (loot.type === 'materials') enemyPickupCollect(e, { type:'materials', amount: loot.amount || 10 });
961
+ e.state = 'gather';
 
 
 
 
 
 
 
 
 
 
962
  }
963
+ } else if (target.hasOwnProperty('type') && (target.type === 'wood' || target.type === 'stone')){ // harvestable object
964
  if (td > 26){
965
  e.angle = Math.atan2(target.y - e.y, target.x - e.x);
966
+ e.x += Math.cos(e.angle) * e.speed * dt * 0.8;
967
+ e.y += Math.sin(e.angle) * e.speed * dt * 0.8;
968
  } else {
969
  target.hp -= 40 * dt;
970
  if (target.hp <= 0 && !target.dead){
 
974
  e.state = 'gather';
975
  }
976
  } else {
977
+ // target is another enemy (group up / follow)
978
  if (td > 40){
979
  e.angle = Math.atan2(target.y - e.y, target.x - e.x);
980
+ e.x += Math.cos(e.angle) * e.speed * dt * 0.7;
981
+ e.y += Math.sin(e.angle) * e.speed * dt * 0.7;
982
  } else {
983
+ // stay near ally
984
+ if (Math.random() < 0.02) e.angle += (Math.random()-0.5)*0.5;
 
 
 
 
 
 
 
985
  }
986
  }
987
  }
988
  }
989
 
990
+ // Separation pass to avoid clumping (simple repulsion)
991
  for (let i = 0; i < enemies.length; i++){
992
  const a = enemies[i];
993
  if (!a || a.health <= 0) continue;
 
1000
  if (d < minD){
1001
  const overlap = (minD - d) * 0.5;
1002
  const nx = dx / d, ny = dy / d;
1003
+ // push both away proportional to overlap
1004
  b.x += nx * overlap;
1005
  b.y += ny * overlap;
1006
  a.x -= nx * overlap;
1007
  a.y -= ny * overlap;
1008
  }
1009
  }
1010
+ // also avoid getting on top of player
1011
  const pdx = a.x - player.x, pdy = a.y - player.y;
1012
  const pd = Math.hypot(pdx,pdy) || 0.0001;
1013
  const avoidDist = 24;
 
1017
  a.x += nx * overlap;
1018
  a.y += ny * overlap;
1019
  }
1020
+ // clamp to world
1021
  a.x = Math.max(12, Math.min(WORLD.width-12, a.x));
1022
  a.y = Math.max(12, Math.min(WORLD.height-12, a.y));
1023
  }
 
1062
  }
1063
  }
1064
 
1065
+ // Drawing
1066
  function drawWorld(){
1067
  const TILE = 600;
1068
  const cols = Math.ceil(WORLD.width / TILE);
 
1161
  const hpPct = Math.max(0, Math.min(1, e.health/120));
1162
  ctx.fillStyle='#ff6b6b'; ctx.fillRect(-18,-22,36*hpPct,6);
1163
 
1164
+ // visible equipped weapon
1165
  if (e.equippedIndex >= 0 && e.inventory[e.equippedIndex] && e.inventory[e.equippedIndex].type === 'weapon'){
1166
  const we = e.inventory[e.equippedIndex];
1167
  const color = we.weapon.color || '#ddd';
 
1191
  }
1192
  }
1193
 
1194
+ // state marker
1195
  ctx.fillStyle = e.state === 'gather' ? 'rgba(0,200,200,0.9)' : (e.state === 'combat' ? 'rgba(255,80,80,0.95)' : 'rgba(255,200,80,0.9)');
1196
  ctx.beginPath(); ctx.arc(18, -18, 5, 0, Math.PI*2); ctx.fill();
1197
  ctx.restore();
 
1258
 
1259
  function drawCrosshair(){}
1260
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1261
  // Main loop
1262
  let lastTime = 0;
1263
  function gameLoop(ts){
 
1266
  const dt = Math.min(0.05, (ts - lastTime)/1000);
1267
  lastTime = ts;
1268
 
1269
+ // player movement
1270
  let dx=0, dy=0;
1271
  if (keys.w) dy -= 1; if (keys.s) dy += 1; if (keys.a) dx -= 1; if (keys.d) dx += 1;
1272
  if (dx !== 0 || dy !== 0){
1273
  const len = Math.hypot(dx,dy) || 1;
1274
+ player.x += (dx/len) * player.speed * dt;
1275
+ player.y += (dy/len) * player.speed * dt;
 
 
 
 
 
 
 
 
 
 
 
 
 
1276
  }
1277
  player.x = Math.max(16, Math.min(WORLD.width-16, player.x));
1278
  player.y = Math.max(16, Math.min(WORLD.height-16, player.y));
 
1289
  if (selected && selected.type === 'weapon') activeWeaponItem = selected;
1290
  }
1291
 
1292
+ // player attack
1293
  if (mouse.down){
1294
  if (player.equippedIndex === -1) playerMeleeHit();
1295
  else if (activeWeaponItem && activeWeaponItem.type === 'weapon'){
 
1308
  if (keys.e){ interactNearby(); keys.e = false; }
1309
  if (keys.q){ tryBuild(); keys.q = false; }
1310
 
1311
+ // update
1312
  updateEnemies(dt, performance.now());
1313
  bulletsUpdate(dt);
1314
 
1315
+ // player pickup auto-collect
1316
  for (let i=pickups.length-1;i>=0;i--){
1317
  const p = pickups[i];
1318
  if (Math.hypot(p.x - player.x, p.y - player.y) < 18){ pickupCollect(p); pickups.splice(i,1); updateHUD(); }
1319
  }
1320
 
1321
+ // clean dead objects
1322
  for (let i=objects.length-1;i>=0;i--) if (objects[i].dead) objects.splice(i,1);
1323
 
1324
  updatePlayerCount();
1325
  updateStorm(dt);
1326
 
1327
+ // render
1328
  ctx.clearRect(0,0,canvas.width,canvas.height);
1329
  drawWorld(); drawObjects(); drawChests(); drawPickups(); drawEnemies(); drawBullets(); drawPlayer(); drawCrosshair();
1330
  updateHUD();
1331
 
 
 
 
1332
  if (storm.active && playerInStorm()) stormWarning.classList.remove('hidden'); else stormWarning.classList.add('hidden');
1333
 
1334
  requestAnimationFrame(gameLoop);
 
1337
  // Landing -> spawn selection
1338
  let selectedBiome = null;
1339
  function getSpawnForBiome(b){
1340
+ // choose a spawn area region for each biome
1341
  if (!b) return { x: WORLD.width/2 + (Math.random()-0.5)*400, y: WORLD.height/2 + (Math.random()-0.5)*400 };
1342
  if (b === 'desert') return { x: rand(200, WORLD.width*0.3), y: rand(200, WORLD.height*0.3) };
1343
  if (b === 'forest') return { x: rand(WORLD.width*0.7, WORLD.width-200), y: rand(200, WORLD.height*0.3) };
 
1352
  let gameActive = false;
1353
 
1354
  function startGame(biome){
1355
+ // set current biome & spawn where selected
1356
  selectedBiome = biome || selectedBiome;
1357
  const spawn = getSpawnForBiome(selectedBiome);
1358
  document.getElementById('currentBiome').textContent = selectedBiome ? selectedBiome.toUpperCase() : 'CENTER';
 
1375
  initHUD();
1376
  cameraUpdate();
1377
 
1378
+ // small safety: ensure enemies don't spawn right on player
1379
  for (const e of enemies){
1380
  if (Math.hypot(e.x - player.x, e.y - player.y) < 180){
1381
  e.x += (Math.random()<0.5? -1:1) * rand(160,260);
1382
  e.y += (Math.random()<0.5? -1:1) * rand(160,260);
1383
  }
1384
+ // ensure they are unarmed at start
1385
  e.inventory = [null,null,null,null,null];
1386
  e.equippedIndex = -1;
1387
  e.spawnSafeUntil = performance.now() + SPAWN_PROTECT_MS;
1388
  e.state = 'gather';
 
 
 
 
 
 
 
1389
  }
1390
 
1391
  gameTime = 300; storm.active = false; storm.radius = storm.maxRadius;
 
1418
  goHomeBtn.addEventListener('click', ()=>{ victoryScreen.classList.add('hidden'); gameScreen.classList.add('hidden'); landingScreen.classList.remove('hidden'); });
1419
  continueBtn.addEventListener('click', ()=>{ victoryScreen.classList.add('hidden'); startGame(selectedBiome); });
1420
 
1421
+ // Biome click behavior: select & start spawn there
1422
  document.querySelectorAll('.biome-selector').forEach(el => {
1423
  el.addEventListener('click', (ev)=>{
1424
+ // visual selection feedback
1425
  document.querySelectorAll('.biome-selector').forEach(x=>x.classList.remove('biome-selected'));
1426
  el.classList.add('biome-selected');
1427
  const biome = el.dataset.biome;
1428
  selectedBiome = biome;
1429
+ // start game immediately with the selected biome spawn region
1430
  startGame(biome);
1431
  });
1432
  });
1433
 
1434
+ // initialisation
1435
  resizeCanvas();
1436
  populateWorld();
1437
  feather.replace();