Spaces:
Running
Running
Manual changes saved
Browse files- 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 -
|
| 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 |
-
|
| 764 |
-
|
| 765 |
-
|
| 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 |
-
|
| 807 |
-
|
| 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 |
-
|
| 824 |
-
|
| 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 |
-
|
| 842 |
-
|
| 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 |
-
|
| 862 |
-
|
| 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:
|
| 891 |
-
let target =
|
| 892 |
-
|
| 893 |
-
|
| 894 |
-
|
| 895 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 896 |
} else {
|
| 897 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 898 |
}
|
| 899 |
}
|
| 900 |
|
| 901 |
-
if
|
| 902 |
-
|
| 903 |
-
|
| 904 |
-
|
| 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
|
| 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 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 944 |
if (target === player){
|
| 945 |
-
// Attack
|
| 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
|
| 967 |
e.angle = Math.atan2(target.y - e.y, target.x - e.x);
|
| 968 |
-
|
| 969 |
-
|
| 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 |
-
|
| 977 |
-
|
| 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 |
-
|
| 984 |
-
|
| 985 |
-
moveEntityWithCollision(e, dx, dy, e.radius);
|
| 986 |
}
|
| 987 |
} else {
|
| 988 |
-
//
|
| 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 |
-
|
| 994 |
-
|
| 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 |
-
|
| 1006 |
-
|
| 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 |
-
|
| 1020 |
-
|
| 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
|
| 1032 |
if (td > 40){
|
| 1033 |
e.angle = Math.atan2(target.y - e.y, target.x - e.x);
|
| 1034 |
-
|
| 1035 |
-
|
| 1036 |
-
moveEntityWithCollision(e, dx, dy, e.radius);
|
| 1037 |
} else {
|
| 1038 |
-
//
|
| 1039 |
-
if (
|
| 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.
|
| 1159 |
-
ctx.
|
| 1160 |
-
|
| 1161 |
-
ctx.fill();
|
| 1162 |
-
|
| 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
|
| 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 |
-
|
| 1422 |
-
|
| 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);
|