bbc123321 commited on
Commit
bb226c2
·
verified ·
1 Parent(s): beadbb6

Manual changes saved

Browse files
Files changed (1) hide show
  1. index.html +163 -271
index.html CHANGED
@@ -3,24 +3,18 @@
3
  <head>
4
  <meta charset="utf-8" />
5
  <meta name="viewport" content="width=device-width, initial-scale=1" />
6
- <title>BattleZone Royale - Collisions, AI & Minimap (fixed)</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 moved to top-right and won't block input */
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
- /* allow visuals (but pointer-events stays off for whole minimap so it doesn't capture mouse) */
17
- #minimapCanvas { width:100%; height:100%; display:block; image-rendering: pixelated; border-radius:6px; background:transparent; }
18
-
19
  /* HUD */
20
  #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; }
21
  #hudGearWrap { position:absolute; right:12px; bottom:12px; display:flex; gap:8px; align-items:center; z-index:30; }
22
- .pickaxe-slot { width:46px; height:46px; background: rgba(255,255,255,0.03); border-radius:6px; display:flex; align-items:center; justify-content:center; cursor:pointer; pointer-events:auto; }
23
- .gear-slot { min-width:46px; height:36px; background: rgba(255,255,255,0.03); border-radius:6px; display:flex; align-items:center; justify-content:center; font-size:11px; color:#ddd; padding:4px; position:relative; cursor:pointer; pointer-events:auto; }
24
  .selected { outline: 2px solid rgba(255,215,0,0.9); box-shadow: 0 0 6px rgba(255,215,0,0.12); }
25
  .equipped { box-shadow: inset 0 -6px 14px rgba(255,255,255,0.03); border:1px solid rgba(255,255,255,0.04); }
26
  .medkit-count { position:absolute; right:4px; bottom:2px; font-size:10px; color:#ffd; }
@@ -97,11 +91,6 @@
97
  <div id="canvasContainer" class="relative bg-gray-900 border-4 border-gray-700 rounded-xl max-w-full max-h-full">
98
  <canvas id="gameCanvas"></canvas>
99
 
100
- <!-- Minimap (top-right) -->
101
- <div id="minimap">
102
- <canvas id="minimapCanvas" width="220" height="140"></canvas>
103
- </div>
104
-
105
  <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">
106
  <i data-feather="alert-circle" class="mr-2"></i>
107
  <span>STORM APPROACHING! MOVE TO SAFE ZONE!</span>
@@ -160,9 +149,6 @@
160
  const continueBtn = document.getElementById('continueBtn');
161
  const biomeGrid = document.getElementById('biomeGrid');
162
 
163
- const minimapCanvas = document.getElementById('minimapCanvas');
164
- const miniCtx = minimapCanvas.getContext('2d');
165
-
166
  // World
167
  const WORLD = { width: 6000, height: 4000 };
168
  let camera = { x:0, y:0 };
@@ -171,12 +157,7 @@
171
  const ctn = document.getElementById('canvasContainer');
172
  canvas.width = Math.max(900, Math.floor(ctn.clientWidth));
173
  canvas.height = Math.max(560, Math.floor(ctn.clientHeight));
174
- // keep minimap internal pixel buffer consistent
175
- minimapCanvas.width = 220;
176
- minimapCanvas.height = 140;
177
  cameraUpdate();
178
- // invalidate minimap terrain cache to adjust if world/scale changed
179
- miniTerrainCache = null;
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,60 +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
  function populateWorld(){
335
  chests.length = 0; objects.length = 0; enemies.length = 0; pickups.length = 0;
336
  for (let i=0;i<260;i++){
337
  const x = rand(150, WORLD.width-150);
338
  const y = rand(150, WORLD.height-150);
339
  const loot = generateLootForBiome(biomeAt(x,y));
340
- if (loot.type === 'materials') loot.amount = 10;
341
  chests.push({ x,y, opened:false, loot });
342
  }
343
  for (let i=0;i<700;i++){
@@ -349,6 +287,7 @@
349
  const hp = type==='wood'?40 : (type==='stone'?80:160);
350
  objects.push({ x,y, type, hp, maxHp:hp, dead:false });
351
  }
 
352
  const now = performance.now();
353
  for (let i=0;i<49;i++){
354
  const ex = rand(300, WORLD.width-300);
@@ -357,7 +296,7 @@
357
  id:'e'+i, x:ex, y:ey, radius:14, angle:rand(0,Math.PI*2), speed:110+rand(-20,20),
358
  health: 80 + randInt(0,40), lastMelee:0, meleeRate:800 + randInt(-200,200),
359
  roamTimer: rand(0,3),
360
- inventory: [null,null,null,null,null],
361
  selectedSlot: 0,
362
  equippedIndex: -1,
363
  materials: 0,
@@ -365,12 +304,11 @@
365
  reloadingUntil: 0,
366
  reloadPending: false,
367
  lastAttackedTime: 0,
368
- lastAttackerId: null,
369
  state: 'gather',
370
  gatherTimeLeft: rand(8,16),
371
  target: null,
372
  nextHealTime: 0,
373
- spawnSafeUntil: now + SPAWN_PROTECT_MS
374
  });
375
  }
376
  updatePlayerCount();
@@ -667,11 +605,6 @@
667
  return closest;
668
  }
669
 
670
- function findEnemyById(id){
671
- if (!id) return null;
672
- return enemies.find(en => en.id === id) || null;
673
- }
674
-
675
  // Bullets update
676
  function bulletsUpdate(dt){
677
  for (let i=bullets.length-1;i>=0;i--){
@@ -693,7 +626,6 @@
693
  if (Math.hypot(e.x - b.x, e.y - b.y) < 14){
694
  e.health -= b.dmg;
695
  e.lastAttackedTime = performance.now();
696
- e.lastAttackerId = b.shooter;
697
  if (e.health <= 0){ e.health = 0; if (b.shooter === 'player'){ player.kills++; player.materials += 2; updatePlayerCount(); } }
698
  bullets.splice(i,1); break;
699
  }
@@ -746,29 +678,39 @@
746
 
747
  // Enemy AI with gather-first + separation + reload with delay
748
  function updateEnemies(dt, now){
749
- const minSeparation = 20;
 
 
750
  for (const e of enemies){
751
  if (e.health <= 0) continue;
 
 
752
  if (!e.spawnSafeUntil) e.spawnSafeUntil = performance.now() + SPAWN_PROTECT_MS;
753
 
754
- // storm handling (move to safe zone)
755
  if (storm.active){
756
  const distToSafeCenter = Math.hypot(e.x - storm.centerX, e.y - storm.centerY);
757
  if (distToSafeCenter > storm.radius){
 
758
  e.state = 'toSafe';
759
  e.angle = Math.atan2(storm.centerY - e.y, storm.centerX - e.x);
760
- const speedMult = 0.95;
761
- const dx = Math.cos(e.angle) * e.speed * dt * speedMult;
762
- const dy = Math.sin(e.angle) * e.speed * dt * speedMult;
763
- moveEntityWithCollision(e, dx, dy, e.radius);
764
  if (Math.random() < 0.02) e.angle += (Math.random()-0.5)*0.5;
765
- continue;
 
 
 
766
  }
767
  }
768
 
 
769
  if (now - e.lastAttackedTime < 4000) e.state = 'combat';
 
770
  if (e.state === 'gather') e.gatherTimeLeft -= dt;
771
 
 
772
  if (e.health < 60 && now >= (e.nextHealTime || 0)){
773
  let medIdx = -1;
774
  for (let s=0;s<5;s++){
@@ -785,18 +727,20 @@
785
  }
786
  }
787
 
 
788
  const hasUsefulWeapon = e.inventory.some(it => it && it.type === 'weapon' && (it.ammoInMag > 0 || it.ammoReserve > 0));
789
  if ((e.gatherTimeLeft <= 0 || e.materials >= 20 || hasUsefulWeapon) && e.state === 'gather') e.state = 'combat';
790
 
791
  if (e.state === 'gather'){
 
 
792
  if (now >= (e.spawnSafeUntil || 0)){
793
  let p = findNearestPickup(e, 240);
794
  if (p){
795
  const angle = Math.atan2(p.y - e.y, p.x - e.x);
796
  e.angle = angle;
797
- const dx = Math.cos(e.angle) * e.speed * dt * 0.9;
798
- const dy = Math.sin(e.angle) * e.speed * dt * 0.9;
799
- moveEntityWithCollision(e, dx, dy, e.radius);
800
  if (Math.hypot(p.x - e.x, p.y - e.y) < 18){
801
  enemyPickupCollect(e, p);
802
  const idx = pickups.indexOf(p);
@@ -811,9 +755,8 @@
811
  const d = Math.hypot(chestTarget.x - e.x, chestTarget.y - e.y);
812
  if (d > 20){
813
  e.angle = Math.atan2(chestTarget.y - e.y, chestTarget.x - e.x);
814
- const dx = Math.cos(e.angle) * e.speed * dt * 0.9;
815
- const dy = Math.sin(e.angle) * e.speed * dt * 0.9;
816
- moveEntityWithCollision(e, dx, dy, e.radius);
817
  } else {
818
  chestTarget.opened = true;
819
  const loot = chestTarget.loot;
@@ -829,9 +772,8 @@
829
  const d = Math.hypot(objTarget.x - e.x, objTarget.y - e.y);
830
  if (d > 26){
831
  e.angle = Math.atan2(objTarget.y - e.y, objTarget.x - e.x);
832
- const dx = Math.cos(e.angle) * e.speed * dt * 0.8;
833
- const dy = Math.sin(e.angle) * e.speed * dt * 0.8;
834
- moveEntityWithCollision(e, dx, dy, e.radius);
835
  } else {
836
  objTarget.hp -= 40 * dt;
837
  if (objTarget.hp <= 0 && !objTarget.dead){
@@ -848,15 +790,17 @@
848
  continue;
849
  }
850
 
851
- const dx = Math.cos(e.angle) * e.speed * dt * 0.25;
852
- const dy = Math.sin(e.angle) * e.speed * dt * 0.25;
853
- moveEntityWithCollision(e, dx, dy, e.radius);
854
  if (Math.random() < 0.01) e.angle += (Math.random()-0.5)*2;
855
  continue;
856
  }
857
 
 
858
  if (e.equippedIndex === -1) enemyEquipBestWeapon(e);
859
 
 
860
  if (e.reloadPending){
861
  if (now >= e.reloadingUntil){
862
  const eq = e.inventory[e.equippedIndex];
@@ -866,56 +810,92 @@
866
  }
867
  }
868
 
 
869
  if (e.equippedIndex >= 0){
870
  const eq = e.inventory[e.equippedIndex];
871
  if (eq && eq.type === 'weapon' && eq.ammoInMag <= 0 && eq.ammoReserve > 0 && !e.reloadPending && now >= e.reloadingUntil){
872
  e.reloadPending = true;
873
- e.reloadingUntil = now + 600 + rand(-100,100);
874
  }
875
  }
876
 
877
- let target = null;
878
- if (e.lastAttackerId && e.lastAttackerId !== 'player'){
879
- const attacker = findEnemyById(e.lastAttackerId);
880
- if (attacker && attacker.health > 0){ target = attacker; }
881
- else { e.lastAttackerId = null; }
882
- }
883
-
884
- if (!target){
885
- const distToPlayer = Math.hypot(player.x - e.x, player.y - e.y);
886
- if (distToPlayer <= VIEW_RANGE){
887
- target = player;
 
 
 
 
 
 
 
 
 
 
888
  } else {
889
- const p = findNearestPickup(e, 1200);
890
- const c = findNearestChest(e, 1200);
891
- const h = findNearestHarvestable(e, 1200);
892
- let candidate = null, cd = Infinity;
893
- if (p){ const d=Math.hypot(p.x-e.x,p.y-e.y); if (d<cd){ candidate=p; cd=d; } }
894
- if (c){ const d=Math.hypot(c.x-e.x,c.y-e.y); if (d<cd){ candidate=c; cd=d; } }
895
- if (h){ const d=Math.hypot(h.x-e.x,h.y-e.y); if (d<cd){ candidate=h; cd=d; } }
896
- for (const other of enemies){
897
- if (other === e || other.health <= 0) continue;
898
- const d = Math.hypot(other.x - e.x, other.y - e.y);
899
- if (d < cd && d <= 800){
900
- if (Math.random() < 0.6 || e.state === 'combat'){
901
- candidate = other; cd = d;
902
- }
903
- }
904
- }
905
- if (candidate) target = candidate;
906
  }
 
 
 
 
 
 
 
 
 
 
 
 
 
907
  }
908
 
 
909
  if (!target){
910
  e.x += Math.cos(e.angle) * e.speed * dt * 0.7;
911
  e.y += Math.sin(e.angle) * e.speed * dt * 0.7;
912
  if (Math.random() < 0.02) e.angle += (Math.random()-0.5)*1.5;
913
- moveEntityWithCollision(e, 0, 0, e.radius);
914
  continue;
915
  }
916
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
917
  if (target === player){
918
- const distToPlayer = Math.hypot(player.x - e.x, player.y - e.y);
919
  if (distToPlayer < 36 && now - e.lastMelee > e.meleeRate){
920
  e.lastMelee = now;
921
  const dmg = 10 + randInt(0,8);
@@ -926,7 +906,7 @@
926
  const eq = e.inventory[e.equippedIndex];
927
  if (eq && eq.type === 'weapon' && !e.reloadPending && now - (e.lastShot||0) > (eq.weapon.rate || 300)){
928
  if (eq.ammoInMag <= 0){
929
- // reload handled above
930
  } else {
931
  if (hasLineOfSight(e.x, e.y, target.x, target.y)){
932
  e.lastShot = now;
@@ -935,44 +915,43 @@
935
  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);
936
  e.lastAttackedTime = now;
937
  } else {
 
938
  e.angle = Math.atan2(target.y - e.y, target.x - e.x);
939
- const dx = Math.cos(e.angle) * e.speed * dt * 0.6;
940
- const dy = Math.sin(e.angle) * e.speed * dt * 0.6;
941
- moveEntityWithCollision(e, dx, dy, e.radius);
942
  }
943
  }
944
  } else {
 
945
  e.angle = Math.atan2(target.y - e.y, target.x - e.x);
946
- const dx = Math.cos(e.angle) * e.speed * dt * 0.9;
947
- const dy = Math.sin(e.angle) * e.speed * dt * 0.9;
948
- moveEntityWithCollision(e, dx, dy, e.radius);
949
  }
950
  } else {
 
951
  e.angle = Math.atan2(target.y - e.y, target.x - e.x);
952
- const dx = Math.cos(e.angle) * e.speed * dt * 0.9;
953
- const dy = Math.sin(e.angle) * e.speed * dt * 0.9;
954
- moveEntityWithCollision(e, dx, dy, e.radius);
955
  }
956
  } else {
 
957
  const td = Math.hypot(target.x - e.x, target.y - e.y);
958
- if (target.type === 'weapon' || target.type === 'medkit' || target.type === 'materials' || target.type === 'ammo'){
959
  if (td > 20){
960
  e.angle = Math.atan2(target.y - e.y, target.x - e.x);
961
- const dx = Math.cos(e.angle) * e.speed * dt * 0.9;
962
- const dy = Math.sin(e.angle) * e.speed * dt * 0.9;
963
- moveEntityWithCollision(e, dx, dy, e.radius);
964
  } else {
965
  enemyPickupCollect(e, target);
966
  const idx = pickups.indexOf(target);
967
  if (idx >= 0) pickups.splice(idx,1);
968
  e.state = 'gather';
969
  }
970
- } else if (target.hasOwnProperty('loot')){
971
  if (td > 20){
972
  e.angle = Math.atan2(target.y - e.y, target.x - e.x);
973
- const dx = Math.cos(e.angle) * e.speed * dt * 0.9;
974
- const dy = Math.sin(e.angle) * e.speed * dt * 0.9;
975
- moveEntityWithCollision(e, dx, dy, e.radius);
976
  } else {
977
  target.opened = true;
978
  const loot = target.loot;
@@ -981,12 +960,11 @@
981
  else if (loot.type === 'materials') enemyPickupCollect(e, { type:'materials', amount: loot.amount || 10 });
982
  e.state = 'gather';
983
  }
984
- } else if (target.hasOwnProperty('type') && (target.type === 'wood' || target.type === 'stone')){
985
  if (td > 26){
986
  e.angle = Math.atan2(target.y - e.y, target.x - e.x);
987
- const dx = Math.cos(e.angle) * e.speed * dt * 0.8;
988
- const dy = Math.sin(e.angle) * e.speed * dt * 0.8;
989
- moveEntityWithCollision(e, dx, dy, e.radius);
990
  } else {
991
  target.hp -= 40 * dt;
992
  if (target.hp <= 0 && !target.dead){
@@ -996,27 +974,20 @@
996
  e.state = 'gather';
997
  }
998
  } else {
 
999
  if (td > 40){
1000
  e.angle = Math.atan2(target.y - e.y, target.x - e.x);
1001
- const dx = Math.cos(e.angle) * e.speed * dt * 0.7;
1002
- const dy = Math.sin(e.angle) * e.speed * dt * 0.7;
1003
- moveEntityWithCollision(e, dx, dy, e.radius);
1004
  } else {
1005
- if (now - e.lastMelee > e.meleeRate){
1006
- e.lastMelee = now;
1007
- if (target && target.health > 0){
1008
- target.health -= 8 + randInt(0,6);
1009
- target.lastAttackedTime = now;
1010
- target.lastAttackerId = e.id;
1011
- if (target.health <= 0) target.health = 0;
1012
- }
1013
- }
1014
  }
1015
  }
1016
  }
1017
  }
1018
 
1019
- // Separation and simple repulsion
1020
  for (let i = 0; i < enemies.length; i++){
1021
  const a = enemies[i];
1022
  if (!a || a.health <= 0) continue;
@@ -1029,12 +1000,14 @@
1029
  if (d < minD){
1030
  const overlap = (minD - d) * 0.5;
1031
  const nx = dx / d, ny = dy / d;
 
1032
  b.x += nx * overlap;
1033
  b.y += ny * overlap;
1034
  a.x -= nx * overlap;
1035
  a.y -= ny * overlap;
1036
  }
1037
  }
 
1038
  const pdx = a.x - player.x, pdy = a.y - player.y;
1039
  const pd = Math.hypot(pdx,pdy) || 0.0001;
1040
  const avoidDist = 24;
@@ -1044,6 +1017,7 @@
1044
  a.x += nx * overlap;
1045
  a.y += ny * overlap;
1046
  }
 
1047
  a.x = Math.max(12, Math.min(WORLD.width-12, a.x));
1048
  a.y = Math.max(12, Math.min(WORLD.height-12, a.y));
1049
  }
@@ -1107,20 +1081,12 @@
1107
  }
1108
  }
1109
  if (storm.active){
1110
- ctx.save();
1111
- ctx.fillStyle = 'rgba(10,30,80,0.45)';
1112
- ctx.fillRect(0,0,canvas.width,canvas.height);
1113
  const sc = worldToScreen(storm.centerX, storm.centerY);
1114
- ctx.globalCompositeOperation = 'destination-out';
1115
- ctx.beginPath();
1116
- ctx.arc(sc.x, sc.y, storm.radius, 0, Math.PI*2);
1117
- ctx.fill();
1118
- ctx.globalCompositeOperation = 'source-over';
1119
- ctx.strokeStyle = 'rgba(255,200,80,0.9)';
1120
- ctx.lineWidth = 3;
1121
- ctx.beginPath();
1122
- ctx.arc(sc.x, sc.y, storm.radius, 0, Math.PI*2);
1123
- ctx.stroke();
1124
  ctx.restore();
1125
  }
1126
  }
@@ -1195,6 +1161,7 @@
1195
  const hpPct = Math.max(0, Math.min(1, e.health/120));
1196
  ctx.fillStyle='#ff6b6b'; ctx.fillRect(-18,-22,36*hpPct,6);
1197
 
 
1198
  if (e.equippedIndex >= 0 && e.inventory[e.equippedIndex] && e.inventory[e.equippedIndex].type === 'weapon'){
1199
  const we = e.inventory[e.equippedIndex];
1200
  const color = we.weapon.color || '#ddd';
@@ -1224,6 +1191,7 @@
1224
  }
1225
  }
1226
 
 
1227
  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)');
1228
  ctx.beginPath(); ctx.arc(18, -18, 5, 0, Math.PI*2); ctx.fill();
1229
  ctx.restore();
@@ -1288,79 +1256,7 @@
1288
  ctx.restore();
1289
  }
1290
 
1291
- // Minimap caching to avoid heavy per-frame terrain rebuild
1292
- let miniTerrainCache = null;
1293
- function buildMiniTerrainCache(){
1294
- const mw = minimapCanvas.width;
1295
- const mh = minimapCanvas.height;
1296
- const scaleX = WORLD.width / mw;
1297
- const scaleY = WORLD.height / mh;
1298
- const img = miniCtx.createImageData(mw, mh);
1299
- for (let my=0; my<mh; my++){
1300
- for (let mx=0; mx<mw; mx++){
1301
- const wx = Math.floor(mx * scaleX + scaleX/2);
1302
- const wy = Math.floor(my * scaleY + scaleY/2);
1303
- const b = biomeAt(wx, wy);
1304
- let col = [32,58,43];
1305
- if (b==='desert') col = [203,183,139];
1306
- else if (b==='forest') col = [22,65,31];
1307
- else if (b==='oasis') col = [39,75,82];
1308
- else if (b==='ruins') col = [74,59,59];
1309
- const idx = (my*mw + mx)*4;
1310
- img.data[idx] = col[0];
1311
- img.data[idx+1] = col[1];
1312
- img.data[idx+2] = col[2];
1313
- img.data[idx+3] = 255;
1314
- }
1315
- }
1316
- miniTerrainCache = img;
1317
- }
1318
-
1319
- function drawMinimap(){
1320
- const mw = minimapCanvas.width;
1321
- const mh = minimapCanvas.height;
1322
- if (!miniTerrainCache) buildMiniTerrainCache();
1323
- miniCtx.putImageData(miniTerrainCache, 0, 0);
1324
-
1325
- // draw storm overlay: color outside safe zone, leave inside transparent
1326
- if (storm.active){
1327
- miniCtx.save();
1328
- miniCtx.fillStyle = 'rgba(10,30,80,0.55)';
1329
- miniCtx.fillRect(0,0,mw,mh);
1330
- miniCtx.globalCompositeOperation = 'destination-out';
1331
- const cx = (storm.centerX) / WORLD.width * mw;
1332
- const cy = (storm.centerY) / WORLD.height * mh;
1333
- const r = storm.radius / WORLD.width * mw;
1334
- miniCtx.beginPath();
1335
- miniCtx.arc(cx, cy, r, 0, Math.PI*2);
1336
- miniCtx.fill();
1337
- miniCtx.globalCompositeOperation = 'source-over';
1338
- miniCtx.strokeStyle = 'rgba(255,200,80,0.9)';
1339
- miniCtx.lineWidth = 2;
1340
- miniCtx.beginPath();
1341
- miniCtx.arc(cx, cy, r, 0, Math.PI*2);
1342
- miniCtx.stroke();
1343
- miniCtx.restore();
1344
- }
1345
-
1346
- // draw player dot
1347
- const px = player.x / WORLD.width * mw;
1348
- const py = player.y / WORLD.height * mh;
1349
- miniCtx.fillStyle = '#ffff66';
1350
- miniCtx.beginPath();
1351
- miniCtx.arc(px, py, 3, 0, Math.PI*2);
1352
- miniCtx.fill();
1353
-
1354
- // draw safe-zone center dot
1355
- if (storm.active){
1356
- const cx = (storm.centerX) / WORLD.width * mw;
1357
- const cy = (storm.centerY) / WORLD.height * mh;
1358
- miniCtx.fillStyle = 'rgba(255,200,80,0.9)';
1359
- miniCtx.beginPath();
1360
- miniCtx.arc(cx, cy, 2, 0, Math.PI*2);
1361
- miniCtx.fill();
1362
- }
1363
- }
1364
 
1365
  // Main loop
1366
  let lastTime = 0;
@@ -1370,25 +1266,13 @@
1370
  const dt = Math.min(0.05, (ts - lastTime)/1000);
1371
  lastTime = ts;
1372
 
 
1373
  let dx=0, dy=0;
1374
  if (keys.w) dy -= 1; if (keys.s) dy += 1; if (keys.a) dx -= 1; if (keys.d) dx += 1;
1375
  if (dx !== 0 || dy !== 0){
1376
  const len = Math.hypot(dx,dy) || 1;
1377
- const mvx = (dx/len) * player.speed * dt;
1378
- const mvy = (dy/len) * player.speed * dt;
1379
- const oldX = player.x, oldY = player.y;
1380
- player.x += mvx;
1381
- if (player.x < player.radius) player.x = player.radius;
1382
- if (player.x > WORLD.width - player.radius) player.x = WORLD.width - player.radius;
1383
- if (isCollidingSolid(player.x, player.y, player.radius)){
1384
- player.x = oldX;
1385
- }
1386
- player.y += mvy;
1387
- if (player.y < player.radius) player.y = player.radius;
1388
- if (player.y > WORLD.height - player.radius) player.y = WORLD.height - player.radius;
1389
- if (isCollidingSolid(player.x, player.y, player.radius)){
1390
- player.y = oldY;
1391
- }
1392
  }
1393
  player.x = Math.max(16, Math.min(WORLD.width-16, player.x));
1394
  player.y = Math.max(16, Math.min(WORLD.height-16, player.y));
@@ -1405,6 +1289,7 @@
1405
  if (selected && selected.type === 'weapon') activeWeaponItem = selected;
1406
  }
1407
 
 
1408
  if (mouse.down){
1409
  if (player.equippedIndex === -1) playerMeleeHit();
1410
  else if (activeWeaponItem && activeWeaponItem.type === 'weapon'){
@@ -1423,26 +1308,27 @@
1423
  if (keys.e){ interactNearby(); keys.e = false; }
1424
  if (keys.q){ tryBuild(); keys.q = false; }
1425
 
 
1426
  updateEnemies(dt, performance.now());
1427
  bulletsUpdate(dt);
1428
 
 
1429
  for (let i=pickups.length-1;i>=0;i--){
1430
  const p = pickups[i];
1431
  if (Math.hypot(p.x - player.x, p.y - player.y) < 18){ pickupCollect(p); pickups.splice(i,1); updateHUD(); }
1432
  }
1433
 
 
1434
  for (let i=objects.length-1;i>=0;i--) if (objects[i].dead) objects.splice(i,1);
1435
 
1436
  updatePlayerCount();
1437
  updateStorm(dt);
1438
 
 
1439
  ctx.clearRect(0,0,canvas.width,canvas.height);
1440
- drawWorld(); drawObjects(); drawChests(); drawPickups(); drawEnemies(); drawBullets(); drawPlayer();
1441
  updateHUD();
1442
 
1443
- // minimap draw (cached terrain + overlays) - cheap each frame now
1444
- drawMinimap();
1445
-
1446
  if (storm.active && playerInStorm()) stormWarning.classList.remove('hidden'); else stormWarning.classList.add('hidden');
1447
 
1448
  requestAnimationFrame(gameLoop);
@@ -1451,6 +1337,7 @@
1451
  // Landing -> spawn selection
1452
  let selectedBiome = null;
1453
  function getSpawnForBiome(b){
 
1454
  if (!b) return { x: WORLD.width/2 + (Math.random()-0.5)*400, y: WORLD.height/2 + (Math.random()-0.5)*400 };
1455
  if (b === 'desert') return { x: rand(200, WORLD.width*0.3), y: rand(200, WORLD.height*0.3) };
1456
  if (b === 'forest') return { x: rand(WORLD.width*0.7, WORLD.width-200), y: rand(200, WORLD.height*0.3) };
@@ -1465,6 +1352,7 @@
1465
  let gameActive = false;
1466
 
1467
  function startGame(biome){
 
1468
  selectedBiome = biome || selectedBiome;
1469
  const spawn = getSpawnForBiome(selectedBiome);
1470
  document.getElementById('currentBiome').textContent = selectedBiome ? selectedBiome.toUpperCase() : 'CENTER';
@@ -1487,16 +1375,17 @@
1487
  initHUD();
1488
  cameraUpdate();
1489
 
 
1490
  for (const e of enemies){
1491
  if (Math.hypot(e.x - player.x, e.y - player.y) < 180){
1492
  e.x += (Math.random()<0.5? -1:1) * rand(160,260);
1493
  e.y += (Math.random()<0.5? -1:1) * rand(160,260);
1494
  }
 
1495
  e.inventory = [null,null,null,null,null];
1496
  e.equippedIndex = -1;
1497
  e.spawnSafeUntil = performance.now() + SPAWN_PROTECT_MS;
1498
  e.state = 'gather';
1499
- e.nextHealTime = 0;
1500
  }
1501
 
1502
  gameTime = 300; storm.active = false; storm.radius = storm.maxRadius;
@@ -1529,17 +1418,20 @@
1529
  goHomeBtn.addEventListener('click', ()=>{ victoryScreen.classList.add('hidden'); gameScreen.classList.add('hidden'); landingScreen.classList.remove('hidden'); });
1530
  continueBtn.addEventListener('click', ()=>{ victoryScreen.classList.add('hidden'); startGame(selectedBiome); });
1531
 
 
1532
  document.querySelectorAll('.biome-selector').forEach(el => {
1533
  el.addEventListener('click', (ev)=>{
 
1534
  document.querySelectorAll('.biome-selector').forEach(x=>x.classList.remove('biome-selected'));
1535
  el.classList.add('biome-selected');
1536
  const biome = el.dataset.biome;
1537
  selectedBiome = biome;
 
1538
  startGame(biome);
1539
  });
1540
  });
1541
 
1542
- // initialization
1543
  resizeCanvas();
1544
  populateWorld();
1545
  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; }
16
+ .pickaxe-slot { width:46px; height:46px; background: rgba(255,255,255,0.03); border-radius:6px; display:flex; align-items:center; justify-content:center; cursor:pointer; }
17
+ .gear-slot { min-width:46px; height:36px; background: rgba(255,255,255,0.03); border-radius:6px; display:flex; align-items:center; justify-content:center; font-size:11px; color:#ddd; padding:4px; position:relative; cursor:pointer; }
18
  .selected { outline: 2px solid rgba(255,215,0,0.9); box-shadow: 0 0 6px rgba(255,215,0,0.12); }
19
  .equipped { box-shadow: inset 0 -6px 14px rgba(255,255,255,0.03); border:1px solid rgba(255,255,255,0.04); }
20
  .medkit-count { position:absolute; right:4px; bottom:2px; font-size:10px; color:#ffd; }
 
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,
 
304
  reloadingUntil: 0,
305
  reloadPending: false,
306
  lastAttackedTime: 0,
 
307
  state: 'gather',
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();
 
605
  return closest;
606
  }
607
 
 
 
 
 
 
608
  // Bullets update
609
  function bulletsUpdate(dt){
610
  for (let i=bullets.length-1;i>=0;i--){
 
626
  if (Math.hypot(e.x - b.x, e.y - b.y) < 14){
627
  e.health -= b.dmg;
628
  e.lastAttackedTime = performance.now();
 
629
  if (e.health <= 0){ e.health = 0; if (b.shooter === 'player'){ player.kills++; player.materials += 2; updatePlayerCount(); } }
630
  bullets.splice(i,1); break;
631
  }
 
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);
 
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;
 
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];
 
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);
830
+ let candidate = null, cd = Infinity;
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);
838
+ if (d < cd && d <= 800){ candidate = other; cd = d; }
839
+ }
840
+ if (candidate){
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);
851
+ if (d < bestDist && Math.random() < 0.6){ bestDist = d; target = other; }
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);
872
+ if (blocker){
873
+ const db = Math.hypot(blocker.x - e.x, blocker.y - e.y);
874
+ if (db < 36){
875
+ blocker.hp -= 18 * dt * 2;
876
+ if (blocker.hp <= 0){ blocker.dead = true; e.materials += (blocker.type === 'wood' ? 3 : 6); }
877
+ } else {
878
+ if (e.equippedIndex >= 0){
879
+ const eq = e.inventory[e.equippedIndex];
880
+ if (eq && eq.type === 'weapon' && eq.ammoInMag > 0 && !e.reloadPending && now - e.lastShot > (eq.weapon.rate || 300)){
881
+ e.lastShot = now;
882
+ eq.ammoInMag -= 1;
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;
 
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
  }
 
1081
  }
1082
  }
1083
  if (storm.active){
 
 
 
1084
  const sc = worldToScreen(storm.centerX, storm.centerY);
1085
+ ctx.save();
1086
+ const grad = ctx.createRadialGradient(sc.x, sc.y, storm.radius*0.15, sc.x, sc.y, storm.radius);
1087
+ grad.addColorStop(0,'rgba(100,149,237,0.02)'); grad.addColorStop(1,'rgba(100,149,237,0.45)');
1088
+ ctx.fillStyle = grad; ctx.beginPath(); ctx.arc(sc.x, sc.y, storm.radius, 0, Math.PI*2); ctx.fill();
1089
+ ctx.strokeStyle = 'rgba(255,200,80,0.9)'; ctx.lineWidth=4; ctx.beginPath(); ctx.arc(sc.x, sc.y, storm.radius, 0, Math.PI*2); ctx.stroke();
 
 
 
 
 
1090
  ctx.restore();
1091
  }
1092
  }
 
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();
 
1256
  ctx.restore();
1257
  }
1258
 
1259
+ function drawCrosshair(){}
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1260
 
1261
  // Main loop
1262
  let lastTime = 0;
 
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();