bbc123321 commited on
Commit
2c0205e
·
verified ·
1 Parent(s): ebb0af0

Manual changes saved

Browse files
Files changed (1) hide show
  1. index.html +105 -271
index.html CHANGED
@@ -3,15 +3,13 @@
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</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
- #minimap { position: absolute; top:12px; left:50%; transform:translateX(-50%); 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); }
14
- #minimapCanvas { width:100%; height:100%; display:block; image-rendering: pixelated; border-radius:6px; background:transparent; }
15
  /* HUD */
16
  #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; }
17
  #hudGearWrap { position:absolute; right:12px; bottom:12px; display:flex; gap:8px; align-items:center; z-index:30; }
@@ -93,11 +91,6 @@
93
  <div id="canvasContainer" class="relative bg-gray-900 border-4 border-gray-700 rounded-xl max-w-full max-h-full">
94
  <canvas id="gameCanvas"></canvas>
95
 
96
- <!-- Minimap (top middle) -->
97
- <div id="minimap">
98
- <canvas id="minimapCanvas" width="220" height="140"></canvas>
99
- </div>
100
-
101
  <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">
102
  <i data-feather="alert-circle" class="mr-2"></i>
103
  <span>STORM APPROACHING! MOVE TO SAFE ZONE!</span>
@@ -156,9 +149,6 @@
156
  const continueBtn = document.getElementById('continueBtn');
157
  const biomeGrid = document.getElementById('biomeGrid');
158
 
159
- const minimapCanvas = document.getElementById('minimapCanvas');
160
- const miniCtx = minimapCanvas.getContext('2d');
161
-
162
  // World
163
  const WORLD = { width: 6000, height: 4000 };
164
  let camera = { x:0, y:0 };
@@ -278,55 +268,6 @@
278
  const VIEW_RANGE = 1200; // if player is farther than this, enemies switch target
279
  const SPAWN_PROTECT_MS = 1200; // time after spawn they ignore ground pickups to avoid instant pickup
280
 
281
- // collision helpers
282
- function getObjectRadius(obj){
283
- if (!obj) return 18;
284
- if (obj.type === 'wall') return 28;
285
- if (obj.type === 'stone') return 18;
286
- if (obj.type === 'wood') return 18;
287
- return 16;
288
- }
289
- function chestRadius(){ return 18; }
290
- function circleOverlap(x1,y1,r1,x2,y2,r2){
291
- return Math.hypot(x1-x2,y1-y2) < (r1 + r2);
292
- }
293
-
294
- function isCollidingSolid(x,y,r){
295
- // objects are solid if not dead
296
- for (const o of objects){
297
- if (o.dead) continue;
298
- const rr = getObjectRadius(o);
299
- if (circleOverlap(x,y,r,o.x,o.y,rr)) return true;
300
- }
301
- // chests solid if not opened
302
- for (const c of chests){
303
- if (c.opened) continue;
304
- if (circleOverlap(x,y,r,c.x,c.y,chestRadius())) return true;
305
- }
306
- return false;
307
- }
308
-
309
- // move with axis-based collision (allows sliding)
310
- function moveEntityWithCollision(entity, dx, dy, radius){
311
- // move along x
312
- const oldX = entity.x, oldY = entity.y;
313
- let nx = entity.x + dx;
314
- entity.x = nx;
315
- if (entity.x < radius) entity.x = radius;
316
- if (entity.x > WORLD.width - radius) entity.x = WORLD.width - radius;
317
- if (isCollidingSolid(entity.x, entity.y, radius)){
318
- entity.x = oldX; // revert X
319
- }
320
- // move along y
321
- let ny = entity.y + dy;
322
- entity.y = ny;
323
- if (entity.y < radius) entity.y = radius;
324
- if (entity.y > WORLD.height - radius) entity.y = WORLD.height - radius;
325
- if (isCollidingSolid(entity.x, entity.y, radius)){
326
- entity.y = oldY; // revert Y
327
- }
328
- }
329
-
330
  // Populate world - spawn many objects and enemies
331
  function populateWorld(){
332
  chests.length = 0; objects.length = 0; enemies.length = 0; pickups.length = 0;
@@ -363,7 +304,6 @@
363
  reloadingUntil: 0,
364
  reloadPending: false,
365
  lastAttackedTime: 0,
366
- lastAttackerId: null,
367
  state: 'gather',
368
  gatherTimeLeft: rand(8,16),
369
  target: null,
@@ -665,11 +605,6 @@
665
  return closest;
666
  }
667
 
668
- function findEnemyById(id){
669
- if (!id) return null;
670
- return enemies.find(en => en.id === id) || null;
671
- }
672
-
673
  // Bullets update
674
  function bulletsUpdate(dt){
675
  for (let i=bullets.length-1;i>=0;i--){
@@ -691,7 +626,6 @@
691
  if (Math.hypot(e.x - b.x, e.y - b.y) < 14){
692
  e.health -= b.dmg;
693
  e.lastAttackedTime = performance.now();
694
- e.lastAttackerId = b.shooter; // record who attacked them
695
  if (e.health <= 0){ e.health = 0; if (b.shooter === 'player'){ player.kills++; player.materials += 2; updatePlayerCount(); } }
696
  bullets.splice(i,1); break;
697
  }
@@ -760,11 +694,13 @@
760
  // move towards safe zone center
761
  e.state = 'toSafe';
762
  e.angle = Math.atan2(storm.centerY - e.y, storm.centerX - e.x);
763
- const speedMult = 0.95;
764
- const dx = Math.cos(e.angle) * e.speed * dt * speedMult;
765
- const dy = Math.sin(e.angle) * e.speed * dt * speedMult;
766
- moveEntityWithCollision(e, dx, dy, e.radius);
767
  if (Math.random() < 0.02) e.angle += (Math.random()-0.5)*0.5;
 
 
 
768
  continue; // priority movement to safe zone
769
  }
770
  }
@@ -803,9 +739,8 @@
803
  if (p){
804
  const angle = Math.atan2(p.y - e.y, p.x - e.x);
805
  e.angle = angle;
806
- const dx = Math.cos(e.angle) * e.speed * dt * 0.9;
807
- const dy = Math.sin(e.angle) * e.speed * dt * 0.9;
808
- moveEntityWithCollision(e, dx, dy, e.radius);
809
  if (Math.hypot(p.x - e.x, p.y - e.y) < 18){
810
  enemyPickupCollect(e, p);
811
  const idx = pickups.indexOf(p);
@@ -820,9 +755,8 @@
820
  const d = Math.hypot(chestTarget.x - e.x, chestTarget.y - e.y);
821
  if (d > 20){
822
  e.angle = Math.atan2(chestTarget.y - e.y, chestTarget.x - e.x);
823
- const dx = Math.cos(e.angle) * e.speed * dt * 0.9;
824
- const dy = Math.sin(e.angle) * e.speed * dt * 0.9;
825
- moveEntityWithCollision(e, dx, dy, e.radius);
826
  } else {
827
  chestTarget.opened = true;
828
  const loot = chestTarget.loot;
@@ -838,9 +772,8 @@
838
  const d = Math.hypot(objTarget.x - e.x, objTarget.y - e.y);
839
  if (d > 26){
840
  e.angle = Math.atan2(objTarget.y - e.y, objTarget.x - e.x);
841
- const dx = Math.cos(e.angle) * e.speed * dt * 0.8;
842
- const dy = Math.sin(e.angle) * e.speed * dt * 0.8;
843
- moveEntityWithCollision(e, dx, dy, e.radius);
844
  } else {
845
  objTarget.hp -= 40 * dt;
846
  if (objTarget.hp <= 0 && !objTarget.dead){
@@ -858,9 +791,8 @@
858
  }
859
 
860
  // roam
861
- const dx = Math.cos(e.angle) * e.speed * dt * 0.25;
862
- const dy = Math.sin(e.angle) * e.speed * dt * 0.25;
863
- moveEntityWithCollision(e, dx, dy, e.radius);
864
  if (Math.random() < 0.01) e.angle += (Math.random()-0.5)*2;
865
  continue;
866
  }
@@ -887,63 +819,83 @@
887
  }
888
  }
889
 
890
- // choose a target: respect lastAttackerId first (retaliate), then player if in view range, else nearest meaningful target (including other enemies)
891
- let target = null;
892
- if (e.lastAttackerId && e.lastAttackerId !== 'player'){
893
- const attacker = findEnemyById(e.lastAttackerId);
894
- if (attacker && attacker.health > 0){
895
- target = attacker;
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
896
  } else {
897
- e.lastAttackerId = null;
 
 
 
 
 
 
 
898
  }
899
  }
900
 
901
- if (!target){
902
- const distToPlayer = Math.hypot(player.x - e.x, player.y - e.y);
903
- if (distToPlayer <= VIEW_RANGE){
904
- target = player;
905
- } else {
906
- // look for pickups/chests/harvestables/nearby enemies to fight
907
- const p = findNearestPickup(e, 1200);
908
- const c = findNearestChest(e, 1200);
909
- const h = findNearestHarvestable(e, 1200);
910
- let candidate = null, cd = Infinity;
911
- if (p){ const d=Math.hypot(p.x-e.x,p.y-e.y); if (d<cd){ candidate=p; cd=d; } }
912
- if (c){ const d=Math.hypot(c.x-e.x,c.y-e.y); if (d<cd){ candidate=c; cd=d; } }
913
- if (h){ const d=Math.hypot(h.x-e.x,h.y-e.y); if (d<cd){ candidate=h; cd=d; } }
914
- // consider nearby enemy targets (higher aggression probability)
915
- for (const other of enemies){
916
- if (other === e || other.health <= 0) continue;
917
- const d = Math.hypot(other.x - e.x, other.y - e.y);
918
- if (d < cd && d <= 800){
919
- // preferentially go after enemies if already in combat or if random chance
920
- if (Math.random() < 0.6 || e.state === 'combat'){
921
- candidate = other; cd = d;
922
- }
923
- }
924
- }
925
- if (candidate){
926
- target = candidate;
927
- } else {
928
- target = null;
929
- }
930
- }
931
  }
932
 
933
- // If we still have no target, roam
934
  if (!target){
935
  e.x += Math.cos(e.angle) * e.speed * dt * 0.7;
936
  e.y += Math.sin(e.angle) * e.speed * dt * 0.7;
937
  if (Math.random() < 0.02) e.angle += (Math.random()-0.5)*1.5;
938
- // ensure no collisions after roam
939
- moveEntityWithCollision(e, 0, 0, e.radius);
940
  continue;
941
  }
942
 
943
- // If target is world object (chest/pickup/harvestable/ally), handle accordingly
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
944
  if (target === player){
945
- // Attack player
946
- const distToPlayer = Math.hypot(player.x - e.x, player.y - e.y);
947
  if (distToPlayer < 36 && now - e.lastMelee > e.meleeRate){
948
  e.lastMelee = now;
949
  const dmg = 10 + randInt(0,8);
@@ -963,36 +915,32 @@
963
  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);
964
  e.lastAttackedTime = now;
965
  } else {
966
- // move to get LOS (with collision)
967
  e.angle = Math.atan2(target.y - e.y, target.x - e.x);
968
- const dx = Math.cos(e.angle) * e.speed * dt * 0.6;
969
- const dy = Math.sin(e.angle) * e.speed * dt * 0.6;
970
- moveEntityWithCollision(e, dx, dy, e.radius);
971
  }
972
  }
973
  } else {
974
  // unarmed behavior: rush to melee
975
  e.angle = Math.atan2(target.y - e.y, target.x - e.x);
976
- const dx = Math.cos(e.angle) * e.speed * dt * 0.9;
977
- const dy = Math.sin(e.angle) * e.speed * dt * 0.9;
978
- moveEntityWithCollision(e, dx, dy, e.radius);
979
  }
980
  } else {
981
  // no weapon: rush in
982
  e.angle = Math.atan2(target.y - e.y, target.x - e.x);
983
- const dx = Math.cos(e.angle) * e.speed * dt * 0.9;
984
- const dy = Math.sin(e.angle) * e.speed * dt * 0.9;
985
- moveEntityWithCollision(e, dx, dy, e.radius);
986
  }
987
  } else {
988
- // Non-player target (pickup/chest/harvestable/other enemy)
989
  const td = Math.hypot(target.x - e.x, target.y - e.y);
990
  if (target.type === 'weapon' || target.type === 'medkit' || target.type === 'materials' || target.type === 'ammo'){ // a pickup
991
  if (td > 20){
992
  e.angle = Math.atan2(target.y - e.y, target.x - e.x);
993
- const dx = Math.cos(e.angle) * e.speed * dt * 0.9;
994
- const dy = Math.sin(e.angle) * e.speed * dt * 0.9;
995
- moveEntityWithCollision(e, dx, dy, e.radius);
996
  } else {
997
  enemyPickupCollect(e, target);
998
  const idx = pickups.indexOf(target);
@@ -1002,9 +950,8 @@
1002
  } else if (target.hasOwnProperty('loot')){ // chest
1003
  if (td > 20){
1004
  e.angle = Math.atan2(target.y - e.y, target.x - e.x);
1005
- const dx = Math.cos(e.angle) * e.speed * dt * 0.9;
1006
- const dy = Math.sin(e.angle) * e.speed * dt * 0.9;
1007
- moveEntityWithCollision(e, dx, dy, e.radius);
1008
  } else {
1009
  target.opened = true;
1010
  const loot = target.loot;
@@ -1016,9 +963,8 @@
1016
  } else if (target.hasOwnProperty('type') && (target.type === 'wood' || target.type === 'stone')){ // harvestable object
1017
  if (td > 26){
1018
  e.angle = Math.atan2(target.y - e.y, target.x - e.x);
1019
- const dx = Math.cos(e.angle) * e.speed * dt * 0.8;
1020
- const dy = Math.sin(e.angle) * e.speed * dt * 0.8;
1021
- moveEntityWithCollision(e, dx, dy, e.radius);
1022
  } else {
1023
  target.hp -= 40 * dt;
1024
  if (target.hp <= 0 && !target.dead){
@@ -1028,27 +974,14 @@
1028
  e.state = 'gather';
1029
  }
1030
  } else {
1031
- // target is another enemy -> fight them
1032
  if (td > 40){
1033
  e.angle = Math.atan2(target.y - e.y, target.x - e.x);
1034
- const dx = Math.cos(e.angle) * e.speed * dt * 0.7;
1035
- const dy = Math.sin(e.angle) * e.speed * dt * 0.7;
1036
- moveEntityWithCollision(e, dx, dy, e.radius);
1037
  } else {
1038
- // in melee range, do melee attacks
1039
- if (now - e.lastMelee > e.meleeRate){
1040
- e.lastMelee = now;
1041
- // damage target if it's an enemy
1042
- if (target && target.health > 0){
1043
- target.health -= 8 + randInt(0,6);
1044
- target.lastAttackedTime = now;
1045
- target.lastAttackerId = e.id;
1046
- if (target.health <= 0){
1047
- target.health = 0;
1048
- // don't credit player
1049
- }
1050
- }
1051
- }
1052
  }
1053
  }
1054
  }
@@ -1147,25 +1080,13 @@
1147
  ctx.strokeStyle = 'rgba(0,0,0,0.08)'; ctx.strokeRect(s.x,s.y,TILE,TILE);
1148
  }
1149
  }
1150
- // Storm effect: color outside safe zone (overlay) and leave inside uncolored
1151
  if (storm.active){
1152
- // fill whole screen with overlay
1153
- ctx.save();
1154
- ctx.fillStyle = 'rgba(10,30,80,0.45)';
1155
- ctx.fillRect(0,0,canvas.width,canvas.height);
1156
- // Clear the safe circle area by using destination-out
1157
  const sc = worldToScreen(storm.centerX, storm.centerY);
1158
- ctx.globalCompositeOperation = 'destination-out';
1159
- ctx.beginPath();
1160
- ctx.arc(sc.x, sc.y, storm.radius, 0, Math.PI*2);
1161
- ctx.fill();
1162
- // restore to draw border
1163
- ctx.globalCompositeOperation = 'source-over';
1164
- ctx.strokeStyle = 'rgba(255,200,80,0.9)';
1165
- ctx.lineWidth = 3;
1166
- ctx.beginPath();
1167
- ctx.arc(sc.x, sc.y, storm.radius, 0, Math.PI*2);
1168
- ctx.stroke();
1169
  ctx.restore();
1170
  }
1171
  }
@@ -1337,74 +1258,6 @@
1337
 
1338
  function drawCrosshair(){}
1339
 
1340
- // Minimap drawing
1341
- function drawMinimap(){
1342
- const mw = minimapCanvas.width;
1343
- const mh = minimapCanvas.height;
1344
- const scaleX = WORLD.width / mw;
1345
- const scaleY = WORLD.height / mh;
1346
- // draw terrain by per-pixel sampling of biomeAt (cheap for 220x140)
1347
- const img = miniCtx.createImageData(mw, mh);
1348
- for (let my=0; my<mh; my++){
1349
- for (let mx=0; mx<mw; mx++){
1350
- const wx = Math.floor(mx * scaleX + scaleX/2);
1351
- const wy = Math.floor(my * scaleY + scaleY/2);
1352
- const b = biomeAt(wx, wy);
1353
- let col = [32,58,43]; // default
1354
- if (b==='desert') col = [203,183,139];
1355
- else if (b==='forest') col = [22,65,31];
1356
- else if (b==='oasis') col = [39,75,82];
1357
- else if (b==='ruins') col = [74,59,59];
1358
- const idx = (my*mw + mx)*4;
1359
- img.data[idx] = col[0];
1360
- img.data[idx+1] = col[1];
1361
- img.data[idx+2] = col[2];
1362
- img.data[idx+3] = 255;
1363
- }
1364
- }
1365
- miniCtx.putImageData(img, 0, 0);
1366
-
1367
- // draw storm overlay: color outside safe zone, leave inside transparent
1368
- if (storm.active){
1369
- miniCtx.save();
1370
- miniCtx.fillStyle = 'rgba(10,30,80,0.55)';
1371
- miniCtx.fillRect(0,0,mw,mh);
1372
- miniCtx.globalCompositeOperation = 'destination-out';
1373
- const cx = (storm.centerX) / WORLD.width * mw;
1374
- const cy = (storm.centerY) / WORLD.height * mh;
1375
- const r = storm.radius / WORLD.width * mw; // approximate scale (keep aspect roughly)
1376
- miniCtx.beginPath();
1377
- miniCtx.arc(cx, cy, r, 0, Math.PI*2);
1378
- miniCtx.fill();
1379
- miniCtx.globalCompositeOperation = 'source-over';
1380
- // border
1381
- miniCtx.strokeStyle = 'rgba(255,200,80,0.9)';
1382
- miniCtx.lineWidth = 2;
1383
- miniCtx.beginPath();
1384
- miniCtx.arc(cx, cy, r, 0, Math.PI*2);
1385
- miniCtx.stroke();
1386
- miniCtx.restore();
1387
- }
1388
-
1389
- // draw player dot
1390
- const px = player.x / WORLD.width * mw;
1391
- const py = player.y / WORLD.height * mh;
1392
- miniCtx.fillStyle = '#ffff66';
1393
- miniCtx.beginPath();
1394
- miniCtx.arc(px, py, 3, 0, Math.PI*2);
1395
- miniCtx.fill();
1396
-
1397
- // optionally draw safe-zone center dot
1398
- if (storm.active){
1399
- const cx = (storm.centerX) / WORLD.width * mw;
1400
- const cy = (storm.centerY) / WORLD.height * mh;
1401
- miniCtx.fillStyle = 'rgba(255,200,80,0.9)';
1402
- miniCtx.beginPath();
1403
- miniCtx.arc(cx, cy, 2, 0, Math.PI*2);
1404
- miniCtx.fill();
1405
- }
1406
- }
1407
-
1408
  // Main loop
1409
  let lastTime = 0;
1410
  function gameLoop(ts){
@@ -1413,29 +1266,13 @@
1413
  const dt = Math.min(0.05, (ts - lastTime)/1000);
1414
  lastTime = ts;
1415
 
1416
- // player movement - axis separated to allow sliding against solids
1417
  let dx=0, dy=0;
1418
  if (keys.w) dy -= 1; if (keys.s) dy += 1; if (keys.a) dx -= 1; if (keys.d) dx += 1;
1419
  if (dx !== 0 || dy !== 0){
1420
  const len = Math.hypot(dx,dy) || 1;
1421
- const mvx = (dx/len) * player.speed * dt;
1422
- const mvy = (dy/len) * player.speed * dt;
1423
- // axis movement with collision
1424
- // move x first
1425
- const oldX = player.x, oldY = player.y;
1426
- player.x += mvx;
1427
- if (player.x < player.radius) player.x = player.radius;
1428
- if (player.x > WORLD.width - player.radius) player.x = WORLD.width - player.radius;
1429
- if (isCollidingSolid(player.x, player.y, player.radius)){
1430
- player.x = oldX;
1431
- }
1432
- // then y
1433
- player.y += mvy;
1434
- if (player.y < player.radius) player.y = player.radius;
1435
- if (player.y > WORLD.height - player.radius) player.y = WORLD.height - player.radius;
1436
- if (isCollidingSolid(player.x, player.y, player.radius)){
1437
- player.y = oldY;
1438
- }
1439
  }
1440
  player.x = Math.max(16, Math.min(WORLD.width-16, player.x));
1441
  player.y = Math.max(16, Math.min(WORLD.height-16, player.y));
@@ -1492,9 +1329,6 @@
1492
  drawWorld(); drawObjects(); drawChests(); drawPickups(); drawEnemies(); drawBullets(); drawPlayer(); drawCrosshair();
1493
  updateHUD();
1494
 
1495
- // minimap draw
1496
- drawMinimap();
1497
-
1498
  if (storm.active && playerInStorm()) stormWarning.classList.remove('hidden'); else stormWarning.classList.add('hidden');
1499
 
1500
  requestAnimationFrame(gameLoop);
 
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 };
 
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;
 
304
  reloadingUntil: 0,
305
  reloadPending: false,
306
  lastAttackedTime: 0,
 
307
  state: 'gather',
308
  gatherTimeLeft: rand(8,16),
309
  target: null,
 
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
  }
 
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
  }
 
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){
 
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
  }
 
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);
 
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);
 
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;
 
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
  }
 
1080
  ctx.strokeStyle = 'rgba(0,0,0,0.08)'; ctx.strokeRect(s.x,s.y,TILE,TILE);
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
  }
 
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));
 
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);