Spaces:
Running
Running
Manual changes saved
Browse files- index.html +81 -97
index.html
CHANGED
|
@@ -3,7 +3,7 @@
|
|
| 3 |
<head>
|
| 4 |
<meta charset="utf-8" />
|
| 5 |
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
| 6 |
-
<title>BattleZone Royale -
|
| 7 |
<script src="https://cdn.tailwindcss.com"></script>
|
| 8 |
<script src="https://unpkg.com/feather-icons"></script>
|
| 9 |
<style>
|
|
@@ -190,18 +190,6 @@
|
|
| 190 |
lastShot:0, lastMelee:0
|
| 191 |
};
|
| 192 |
|
| 193 |
-
// Load player sprite from provided URL
|
| 194 |
-
const playerSprite = new Image();
|
| 195 |
-
playerSprite.crossOrigin = "anonymous";
|
| 196 |
-
playerSprite.src = "https://www.vhv.rs/dpng/d/17-179278_fortnite-default-skin-blonde-hair-hd-png-download.png";
|
| 197 |
-
let playerSpriteLoaded = false;
|
| 198 |
-
playerSprite.onload = () => playerSpriteLoaded = true;
|
| 199 |
-
playerSprite.onerror = () => {
|
| 200 |
-
// if CORS blocks or image fails, we keep triangle fallback
|
| 201 |
-
console.warn('Player sprite failed to load — triangle fallback will be used.');
|
| 202 |
-
playerSpriteLoaded = false;
|
| 203 |
-
};
|
| 204 |
-
|
| 205 |
// Input
|
| 206 |
const keys = { w:false,a:false,s:false,d:false,e:false,q:false,r:false,f:false };
|
| 207 |
const mouse = { canvasX:0, canvasY:0, worldX:0, worldY:0, down:false };
|
|
@@ -344,6 +332,7 @@
|
|
| 344 |
}
|
| 345 |
|
| 346 |
// Populate world
|
|
|
|
| 347 |
function populateWorld(){
|
| 348 |
chests.length = 0; objects.length = 0; enemies.length = 0; pickups.length = 0;
|
| 349 |
|
|
@@ -376,6 +365,7 @@
|
|
| 376 |
const ex = rand(300, WORLD.width-300);
|
| 377 |
const ey = rand(300, WORLD.height-300);
|
| 378 |
|
|
|
|
| 379 |
const enemy = {
|
| 380 |
id:'e'+i, x:ex, y:ey, radius:14, angle:rand(0,Math.PI*2), speed:110+rand(-20,20),
|
| 381 |
health: 80 + randInt(0,40), lastMelee:0, meleeRate:800 + randInt(-200,200),
|
|
@@ -398,17 +388,12 @@
|
|
| 398 |
prioritizeChestsUntil: 0
|
| 399 |
};
|
| 400 |
|
| 401 |
-
|
| 402 |
-
|
| 403 |
-
|
| 404 |
-
|
| 405 |
-
|
| 406 |
-
|
| 407 |
-
ammoReserve: proto.startReserve
|
| 408 |
-
};
|
| 409 |
-
enemy.equippedIndex = 0;
|
| 410 |
-
enemy.prioritizeChestsUntil = now + 4000 + rand(0,2000);
|
| 411 |
-
enemy.gatherTimeLeft = Math.min(enemy.gatherTimeLeft, 6);
|
| 412 |
|
| 413 |
enemies.push(enemy);
|
| 414 |
}
|
|
@@ -473,7 +458,7 @@
|
|
| 473 |
}
|
| 474 |
function worldToScreen(wx,wy){ return { x: Math.round(wx - camera.x), y: Math.round(wy - camera.y) }; }
|
| 475 |
|
| 476 |
-
// Combat utilities
|
| 477 |
function shootBullet(originX, originY, targetX, targetY, weaponObj, shooterId){
|
| 478 |
if (!weaponObj || (typeof weaponObj.ammoInMag === 'number' && weaponObj.ammoInMag <= 0)) return false;
|
| 479 |
const speed = 1100;
|
|
@@ -636,6 +621,7 @@
|
|
| 636 |
return;
|
| 637 |
}
|
| 638 |
}
|
|
|
|
| 639 |
let worstIdx = -1, worstScore = Infinity;
|
| 640 |
for (let s=0;s<5;s++){
|
| 641 |
const it = e.inventory[s];
|
|
@@ -730,31 +716,33 @@
|
|
| 730 |
else if (blocker.type === 'stone') br = 22;
|
| 731 |
else if (blocker.type === 'wood') br = 16;
|
| 732 |
else br = 18;
|
|
|
|
| 733 |
const vx = fromX - blocker.x;
|
| 734 |
const vy = fromY - blocker.y;
|
| 735 |
const len = Math.hypot(vx, vy) || 0.0001;
|
|
|
|
| 736 |
const px = -vy / len;
|
| 737 |
const py = vx / len;
|
| 738 |
const radius = br + padding + 8;
|
| 739 |
const cand1 = { x: blocker.x + px * radius, y: blocker.y + py * radius };
|
| 740 |
const cand2 = { x: blocker.x - px * radius, y: blocker.y - py * radius };
|
|
|
|
| 741 |
const d1 = Math.hypot(cand1.x - goalX, cand1.y - goalY);
|
| 742 |
const d2 = Math.hypot(cand2.x - goalX, cand2.y - goalY);
|
| 743 |
const chosen = d1 < d2 ? cand1 : cand2;
|
|
|
|
| 744 |
chosen.x = Math.max(16, Math.min(WORLD.width-16, chosen.x));
|
| 745 |
chosen.y = Math.max(16, Math.min(WORLD.height-16, chosen.y));
|
| 746 |
return chosen;
|
| 747 |
}
|
| 748 |
|
| 749 |
-
// Bullets update
|
| 750 |
function bulletsUpdate(dt){
|
| 751 |
for (let i=bullets.length-1;i>=0;i--){
|
| 752 |
const b = bullets[i];
|
| 753 |
b.x += b.vx * dt; b.y += b.vy * dt;
|
| 754 |
b.traveled += Math.hypot(b.vx*dt, b.vy*dt);
|
| 755 |
b.life -= dt;
|
| 756 |
-
|
| 757 |
-
// Hit player
|
| 758 |
if (b.dmg > 0 && b.shooter !== 'player'){
|
| 759 |
if (Math.hypot(player.x - b.x, player.y - b.y) < 16){
|
| 760 |
player.health -= b.dmg;
|
|
@@ -762,10 +750,7 @@
|
|
| 762 |
bullets.splice(i,1); continue;
|
| 763 |
}
|
| 764 |
}
|
| 765 |
-
|
| 766 |
-
// Hit enemies
|
| 767 |
if (b.dmg > 0){
|
| 768 |
-
let hitSomething = false;
|
| 769 |
for (const e of enemies){
|
| 770 |
if (e.health <= 0) continue;
|
| 771 |
if (b.shooter === e.id) continue;
|
|
@@ -773,50 +758,34 @@
|
|
| 773 |
e.health -= b.dmg;
|
| 774 |
e.lastAttackedTime = performance.now();
|
| 775 |
if (e.health <= 0){ e.health = 0; if (b.shooter === 'player'){ player.kills++; player.materials += 2; updatePlayerCount(); } }
|
| 776 |
-
bullets.splice(i,1);
|
| 777 |
}
|
| 778 |
}
|
| 779 |
-
if (
|
| 780 |
}
|
| 781 |
-
|
| 782 |
-
|
| 783 |
-
|
| 784 |
-
|
| 785 |
-
|
| 786 |
-
|
| 787 |
-
|
| 788 |
-
// Damage the object
|
| 789 |
-
// Stone could be more resistant - scaling factor
|
| 790 |
-
const factor = (obj.type === 'stone') ? 0.8 : 1.0;
|
| 791 |
-
obj.hp -= b.dmg * factor;
|
| 792 |
-
if (obj.hp <= 0 && !obj.dead){
|
| 793 |
-
obj.dead = true;
|
| 794 |
-
// reward player for destroying object if they shot it
|
| 795 |
-
if (b.shooter === 'player') player.materials += (obj.type === 'wood' ? 3 : 6);
|
| 796 |
}
|
| 797 |
-
bullets.splice(i,1);
|
| 798 |
-
hitObject = true;
|
| 799 |
-
break;
|
| 800 |
}
|
| 801 |
-
|
| 802 |
-
|
| 803 |
-
|
| 804 |
-
|
| 805 |
-
|
| 806 |
-
|
| 807 |
-
|
| 808 |
-
|
| 809 |
-
|
| 810 |
-
|
| 811 |
-
|
| 812 |
-
else if (loot.type === 'materials') pickups.push({ x:px, y:py, type:'materials', amount: loot.amount || 10 });
|
| 813 |
-
bullets.splice(i,1);
|
| 814 |
-
hitObject = true;
|
| 815 |
-
break;
|
| 816 |
}
|
|
|
|
| 817 |
}
|
| 818 |
-
if (hitObject) continue;
|
| 819 |
-
|
| 820 |
if (b.life <= 0 || b.traveled > 2600) bullets.splice(i,1);
|
| 821 |
}
|
| 822 |
}
|
|
@@ -838,18 +807,20 @@
|
|
| 838 |
return best;
|
| 839 |
}
|
| 840 |
|
| 841 |
-
// Enemy AI (
|
| 842 |
function updateEnemies(dt, now){
|
| 843 |
const minSeparation = 20;
|
| 844 |
for (const e of enemies){
|
| 845 |
if (e.health <= 0) continue;
|
| 846 |
if (!e.spawnSafeUntil) e.spawnSafeUntil = performance.now() + SPAWN_PROTECT_MS;
|
| 847 |
|
|
|
|
| 848 |
if (storm.active){
|
| 849 |
const distToSafeCenter = Math.hypot(e.x - storm.centerX, e.y - storm.centerY);
|
| 850 |
if (distToSafeCenter > storm.radius){
|
| 851 |
e.state = 'toSafe';
|
| 852 |
|
|
|
|
| 853 |
if (e.tempTarget && now < (e.tempTargetExpiry || 0)){
|
| 854 |
const td = Math.hypot(e.tempTarget.x - e.x, e.tempTarget.y - e.y);
|
| 855 |
if (td > 8){
|
|
@@ -862,17 +833,21 @@
|
|
| 862 |
}
|
| 863 |
}
|
| 864 |
|
|
|
|
| 865 |
if (hasLineOfSight(e.x, e.y, storm.centerX, storm.centerY)){
|
| 866 |
e.angle = Math.atan2(storm.centerY - e.y, storm.centerX - e.x);
|
| 867 |
moveEntityWithCollision(e, Math.cos(e.angle) * e.speed * dt * 1.0, Math.sin(e.angle) * e.speed * dt * 1.0, e.radius);
|
| 868 |
continue;
|
| 869 |
}
|
| 870 |
|
|
|
|
| 871 |
const blocker = findBlockingObject(e.x, e.y, storm.centerX, storm.centerY);
|
| 872 |
if (blocker){
|
| 873 |
const db = Math.hypot(blocker.x - e.x, blocker.y - e.y);
|
| 874 |
|
|
|
|
| 875 |
if (db < 48){
|
|
|
|
| 876 |
if (e.equippedIndex >= 0){
|
| 877 |
const eq = e.inventory[e.equippedIndex];
|
| 878 |
if (eq && eq.type === 'weapon' && eq.ammoInMag > 0 && now - (e.lastShot||0) > (eq.weapon.rate || 300)){
|
|
@@ -880,22 +855,26 @@
|
|
| 880 |
eq.ammoInMag -= 1;
|
| 881 |
const angle = Math.atan2(blocker.y - e.y, blocker.x - e.x);
|
| 882 |
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);
|
| 883 |
-
|
| 884 |
-
|
|
|
|
| 885 |
continue;
|
| 886 |
}
|
| 887 |
}
|
|
|
|
| 888 |
blocker.hp -= 28 * dt;
|
| 889 |
if (blocker.hp <= 0){
|
| 890 |
blocker.dead = true;
|
| 891 |
e.materials += (blocker.type === 'wood' ? 3 : 6);
|
| 892 |
} else {
|
|
|
|
| 893 |
e.angle = Math.atan2(blocker.y - e.y, blocker.x - e.x);
|
| 894 |
moveEntityWithCollision(e, Math.cos(e.angle) * e.speed * dt * 0.4, Math.sin(e.angle) * e.speed * dt * 0.4, e.radius);
|
| 895 |
}
|
| 896 |
continue;
|
| 897 |
}
|
| 898 |
|
|
|
|
| 899 |
const waypoint = computeDetourWaypoint(e.x, e.y, blocker, storm.centerX, storm.centerY, 12);
|
| 900 |
if (waypoint){
|
| 901 |
e.tempTarget = waypoint;
|
|
@@ -911,17 +890,20 @@
|
|
| 911 |
}
|
| 912 |
}
|
| 913 |
|
|
|
|
| 914 |
e.angle = Math.atan2(blocker.y - e.y, blocker.x - e.x);
|
| 915 |
moveEntityWithCollision(e, Math.cos(e.angle) * e.speed * dt * 0.6, Math.sin(e.angle) * e.speed * dt * 0.6, e.radius);
|
| 916 |
continue;
|
| 917 |
}
|
| 918 |
|
|
|
|
| 919 |
e.angle = Math.atan2(storm.centerY - e.y, storm.centerX - e.x);
|
| 920 |
moveEntityWithCollision(e, Math.cos(e.angle) * e.speed * dt * 0.95, Math.sin(e.angle) * e.speed * dt * 0.95, e.radius);
|
| 921 |
continue;
|
| 922 |
}
|
| 923 |
}
|
| 924 |
|
|
|
|
| 925 |
if (now - e.lastAttackedTime < 4000) e.state = 'combat';
|
| 926 |
if (e.state === 'gather') e.gatherTimeLeft -= dt;
|
| 927 |
|
|
@@ -1265,7 +1247,7 @@
|
|
| 1265 |
}
|
| 1266 |
}
|
| 1267 |
|
| 1268 |
-
// Drawing world & entities
|
| 1269 |
function drawWorld(){
|
| 1270 |
const TILE = 600;
|
| 1271 |
const cols = Math.ceil(WORLD.width / TILE);
|
|
@@ -1416,39 +1398,32 @@
|
|
| 1416 |
}
|
| 1417 |
}
|
| 1418 |
|
| 1419 |
-
// draw player using loaded sprite; fallback to triangle if sprite unavailable
|
| 1420 |
function drawPlayer(){
|
| 1421 |
const s = worldToScreen(player.x,player.y);
|
| 1422 |
ctx.save();
|
| 1423 |
-
ctx.translate(s.x,s.y);
|
| 1424 |
-
ctx.
|
| 1425 |
-
|
| 1426 |
-
// shadow
|
| 1427 |
-
ctx.fillStyle='rgba(0,0,0,0.25)'; ctx.beginPath(); ctx.ellipse(0,18,22,8,0,0,Math.PI*2); ctx.fill();
|
| 1428 |
-
|
| 1429 |
-
if (playerSpriteLoaded){
|
| 1430 |
-
// draw sprite centered; scale to fit nicely
|
| 1431 |
-
const w = 48;
|
| 1432 |
-
const h = 64;
|
| 1433 |
-
// optionally flip the sprite so it faces the mouse direction if the image faces right by default.
|
| 1434 |
-
// The provided image faces forward; rotation should be sufficient.
|
| 1435 |
-
ctx.drawImage(playerSprite, -w/2, -h/2, w, h);
|
| 1436 |
-
} else {
|
| 1437 |
-
// fallback triangle
|
| 1438 |
-
ctx.fillStyle='yellow';
|
| 1439 |
-
ctx.beginPath(); ctx.moveTo(18,0); ctx.lineTo(-12,-10); ctx.lineTo(-12,10); ctx.closePath(); ctx.fill();
|
| 1440 |
-
}
|
| 1441 |
|
| 1442 |
-
// draw equipped weapon overlay if any
|
| 1443 |
let activeWeaponItem = null;
|
| 1444 |
if (player.equippedIndex >= 0) activeWeaponItem = player.inventory[player.equippedIndex];
|
| 1445 |
else {
|
| 1446 |
const selected = player.inventory[player.selectedSlot];
|
| 1447 |
if (selected && selected.type === 'weapon') activeWeaponItem = selected;
|
| 1448 |
}
|
| 1449 |
-
|
|
|
|
| 1450 |
ctx.save();
|
| 1451 |
-
ctx.translate(
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1452 |
ctx.rotate(-0.05);
|
| 1453 |
ctx.fillStyle = activeWeaponItem.weapon.color || '#fff';
|
| 1454 |
ctx.fillRect(0,-4,26,8);
|
|
@@ -1456,12 +1431,11 @@
|
|
| 1456 |
ctx.fillRect(18,-2,8,4);
|
| 1457 |
const magPct = Math.max(0, activeWeaponItem.ammoInMag / activeWeaponItem.weapon.magSize);
|
| 1458 |
ctx.fillStyle = 'rgba(0,0,0,0.35)';
|
| 1459 |
-
ctx.fillRect(4,
|
| 1460 |
ctx.fillStyle = '#0f0';
|
| 1461 |
-
ctx.fillRect(4,
|
| 1462 |
ctx.restore();
|
| 1463 |
}
|
| 1464 |
-
|
| 1465 |
ctx.restore();
|
| 1466 |
}
|
| 1467 |
|
|
@@ -1498,8 +1472,10 @@
|
|
| 1498 |
const mw = minimapCanvas.width;
|
| 1499 |
const mh = minimapCanvas.height;
|
| 1500 |
if (!miniTerrainCache) buildMiniTerrainCache();
|
|
|
|
| 1501 |
miniCtx.putImageData(miniTerrainCache, 0, 0);
|
| 1502 |
|
|
|
|
| 1503 |
miniCtx.save();
|
| 1504 |
const scaleX = mw / WORLD.width;
|
| 1505 |
const scaleY = mh / WORLD.height;
|
|
@@ -1518,6 +1494,7 @@
|
|
| 1518 |
miniCtx.fillRect(px-2, py-2, 4, 4);
|
| 1519 |
}
|
| 1520 |
}
|
|
|
|
| 1521 |
for (const p of pickups){
|
| 1522 |
const px = Math.round(p.x * scaleX);
|
| 1523 |
const py = Math.round(p.y * scaleY);
|
|
@@ -1532,17 +1509,22 @@
|
|
| 1532 |
}
|
| 1533 |
}
|
| 1534 |
|
|
|
|
| 1535 |
if (storm.active){
|
|
|
|
| 1536 |
miniCtx.fillStyle = 'rgba(0,0,0,0.35)';
|
| 1537 |
miniCtx.fillRect(0,0,mw,mh);
|
|
|
|
| 1538 |
miniCtx.globalCompositeOperation = 'destination-out';
|
| 1539 |
const cx = storm.centerX * scaleX;
|
| 1540 |
const cy = storm.centerY * scaleY;
|
|
|
|
| 1541 |
const r = storm.radius * ((scaleX + scaleY) / 2);
|
| 1542 |
miniCtx.beginPath();
|
| 1543 |
miniCtx.arc(cx, cy, r, 0, Math.PI*2);
|
| 1544 |
miniCtx.fill();
|
| 1545 |
miniCtx.globalCompositeOperation = 'source-over';
|
|
|
|
| 1546 |
miniCtx.strokeStyle = 'rgba(255,200,80,0.95)';
|
| 1547 |
miniCtx.lineWidth = 2;
|
| 1548 |
miniCtx.beginPath();
|
|
@@ -1550,6 +1532,7 @@
|
|
| 1550 |
miniCtx.stroke();
|
| 1551 |
}
|
| 1552 |
|
|
|
|
| 1553 |
const ppx = Math.round(player.x * (mw / WORLD.width));
|
| 1554 |
const ppy = Math.round(player.y * (mh / WORLD.height));
|
| 1555 |
miniCtx.fillStyle = '#ffff66';
|
|
@@ -1638,6 +1621,7 @@
|
|
| 1638 |
drawWorld(); drawObjects(); drawChests(); drawPickups(); drawEnemies(); drawBullets(); drawPlayer(); drawCrosshair();
|
| 1639 |
updateHUD();
|
| 1640 |
|
|
|
|
| 1641 |
drawMinimap();
|
| 1642 |
|
| 1643 |
if (storm.active && playerInStorm()) stormWarning.classList.remove('hidden'); else stormWarning.classList.add('hidden');
|
|
|
|
| 3 |
<head>
|
| 4 |
<meta charset="utf-8" />
|
| 5 |
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
| 6 |
+
<title>BattleZone Royale - Minimap Added</title>
|
| 7 |
<script src="https://cdn.tailwindcss.com"></script>
|
| 8 |
<script src="https://unpkg.com/feather-icons"></script>
|
| 9 |
<style>
|
|
|
|
| 190 |
lastShot:0, lastMelee:0
|
| 191 |
};
|
| 192 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 193 |
// Input
|
| 194 |
const keys = { w:false,a:false,s:false,d:false,e:false,q:false,r:false,f:false };
|
| 195 |
const mouse = { canvasX:0, canvasY:0, worldX:0, worldY:0, down:false };
|
|
|
|
| 332 |
}
|
| 333 |
|
| 334 |
// Populate world
|
| 335 |
+
// Updated: enemies spawn already equipped with a weapon in inventory slot 0 (equipped)
|
| 336 |
function populateWorld(){
|
| 337 |
chests.length = 0; objects.length = 0; enemies.length = 0; pickups.length = 0;
|
| 338 |
|
|
|
|
| 365 |
const ex = rand(300, WORLD.width-300);
|
| 366 |
const ey = rand(300, WORLD.height-300);
|
| 367 |
|
| 368 |
+
// create enemy base
|
| 369 |
const enemy = {
|
| 370 |
id:'e'+i, x:ex, y:ey, radius:14, angle:rand(0,Math.PI*2), speed:110+rand(-20,20),
|
| 371 |
health: 80 + randInt(0,40), lastMelee:0, meleeRate:800 + randInt(-200,200),
|
|
|
|
| 388 |
prioritizeChestsUntil: 0
|
| 389 |
};
|
| 390 |
|
| 391 |
+
|
| 392 |
+
enemy.equippedIndex = 0; // equip the weapon immediately
|
| 393 |
+
// Slightly bias them to seek chests early so they can refill/reload if needed
|
| 394 |
+
enemy.prioritizeChestsUntil = now + 10000 + rand(0,2000);
|
| 395 |
+
// Reduce gather time so they don't idle unnecessarily
|
| 396 |
+
enemy.gatherTimeLeft = Math.min(enemy.gatherTimeLeft, 10);
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 397 |
|
| 398 |
enemies.push(enemy);
|
| 399 |
}
|
|
|
|
| 458 |
}
|
| 459 |
function worldToScreen(wx,wy){ return { x: Math.round(wx - camera.x), y: Math.round(wy - camera.y) }; }
|
| 460 |
|
| 461 |
+
// Combat utilities (kept same as before)
|
| 462 |
function shootBullet(originX, originY, targetX, targetY, weaponObj, shooterId){
|
| 463 |
if (!weaponObj || (typeof weaponObj.ammoInMag === 'number' && weaponObj.ammoInMag <= 0)) return false;
|
| 464 |
const speed = 1100;
|
|
|
|
| 621 |
return;
|
| 622 |
}
|
| 623 |
}
|
| 624 |
+
// replace worst if pickup is better
|
| 625 |
let worstIdx = -1, worstScore = Infinity;
|
| 626 |
for (let s=0;s<5;s++){
|
| 627 |
const it = e.inventory[s];
|
|
|
|
| 716 |
else if (blocker.type === 'stone') br = 22;
|
| 717 |
else if (blocker.type === 'wood') br = 16;
|
| 718 |
else br = 18;
|
| 719 |
+
// vector from blocker to from
|
| 720 |
const vx = fromX - blocker.x;
|
| 721 |
const vy = fromY - blocker.y;
|
| 722 |
const len = Math.hypot(vx, vy) || 0.0001;
|
| 723 |
+
// perp directions
|
| 724 |
const px = -vy / len;
|
| 725 |
const py = vx / len;
|
| 726 |
const radius = br + padding + 8;
|
| 727 |
const cand1 = { x: blocker.x + px * radius, y: blocker.y + py * radius };
|
| 728 |
const cand2 = { x: blocker.x - px * radius, y: blocker.y - py * radius };
|
| 729 |
+
// choose candidate that's closer to goal and not inside other solids (approx)
|
| 730 |
const d1 = Math.hypot(cand1.x - goalX, cand1.y - goalY);
|
| 731 |
const d2 = Math.hypot(cand2.x - goalX, cand2.y - goalY);
|
| 732 |
const chosen = d1 < d2 ? cand1 : cand2;
|
| 733 |
+
// clamp to world
|
| 734 |
chosen.x = Math.max(16, Math.min(WORLD.width-16, chosen.x));
|
| 735 |
chosen.y = Math.max(16, Math.min(WORLD.height-16, chosen.y));
|
| 736 |
return chosen;
|
| 737 |
}
|
| 738 |
|
| 739 |
+
// Bullets update
|
| 740 |
function bulletsUpdate(dt){
|
| 741 |
for (let i=bullets.length-1;i>=0;i--){
|
| 742 |
const b = bullets[i];
|
| 743 |
b.x += b.vx * dt; b.y += b.vy * dt;
|
| 744 |
b.traveled += Math.hypot(b.vx*dt, b.vy*dt);
|
| 745 |
b.life -= dt;
|
|
|
|
|
|
|
| 746 |
if (b.dmg > 0 && b.shooter !== 'player'){
|
| 747 |
if (Math.hypot(player.x - b.x, player.y - b.y) < 16){
|
| 748 |
player.health -= b.dmg;
|
|
|
|
| 750 |
bullets.splice(i,1); continue;
|
| 751 |
}
|
| 752 |
}
|
|
|
|
|
|
|
| 753 |
if (b.dmg > 0){
|
|
|
|
| 754 |
for (const e of enemies){
|
| 755 |
if (e.health <= 0) continue;
|
| 756 |
if (b.shooter === e.id) continue;
|
|
|
|
| 758 |
e.health -= b.dmg;
|
| 759 |
e.lastAttackedTime = performance.now();
|
| 760 |
if (e.health <= 0){ e.health = 0; if (b.shooter === 'player'){ player.kills++; player.materials += 2; updatePlayerCount(); } }
|
| 761 |
+
bullets.splice(i,1); break;
|
| 762 |
}
|
| 763 |
}
|
| 764 |
+
if (!bullets[i]) continue;
|
| 765 |
}
|
| 766 |
+
if (!b.tracer && b.dmg > 0){
|
| 767 |
+
for (const obj of objects){
|
| 768 |
+
if (obj.dead) continue;
|
| 769 |
+
if (obj.type === 'wall' && Math.hypot(obj.x - b.x, obj.y - b.y) < 18){
|
| 770 |
+
obj.hp -= b.dmg;
|
| 771 |
+
if (obj.hp <= 0 && !obj.dead){ obj.dead = true; if (b.shooter === 'player') player.materials += 6; }
|
| 772 |
+
bullets.splice(i,1); break;
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 773 |
}
|
|
|
|
|
|
|
|
|
|
| 774 |
}
|
| 775 |
+
if (!bullets[i]) continue;
|
| 776 |
+
for (const chest of chests){
|
| 777 |
+
if (!chest.opened && Math.hypot(chest.x - b.x, chest.y - b.y) < 18){
|
| 778 |
+
chest.opened = true;
|
| 779 |
+
const loot = chest.loot;
|
| 780 |
+
const px = chest.x + rand(-20,20), py = chest.y + rand(-20,20);
|
| 781 |
+
if (loot.type === 'weapon') pickups.push({ x:px, y:py, type:'weapon', weapon: makeWeaponProto(loot.weapon), ammoInMag: loot.weapon.magSize || 12, ammoReserve: loot.weapon.startReserve || (loot.weapon.magSize*2 || 24) });
|
| 782 |
+
else if (loot.type === 'medkit') pickups.push({ x:px, y:py, type:'medkit', amount: loot.amount || 1 });
|
| 783 |
+
else if (loot.type === 'materials') pickups.push({ x:px, y:py, type:'materials', amount: loot.amount || 10 });
|
| 784 |
+
bullets.splice(i,1); break;
|
| 785 |
+
}
|
|
|
|
|
|
|
|
|
|
|
|
|
| 786 |
}
|
| 787 |
+
if (!bullets[i]) continue;
|
| 788 |
}
|
|
|
|
|
|
|
| 789 |
if (b.life <= 0 || b.traveled > 2600) bullets.splice(i,1);
|
| 790 |
}
|
| 791 |
}
|
|
|
|
| 807 |
return best;
|
| 808 |
}
|
| 809 |
|
| 810 |
+
// Enemy AI (improved: detour around obstacles and break obstructions to reach safe zone)
|
| 811 |
function updateEnemies(dt, now){
|
| 812 |
const minSeparation = 20;
|
| 813 |
for (const e of enemies){
|
| 814 |
if (e.health <= 0) continue;
|
| 815 |
if (!e.spawnSafeUntil) e.spawnSafeUntil = performance.now() + SPAWN_PROTECT_MS;
|
| 816 |
|
| 817 |
+
// === STORM / MOVE TO SAFE ZONE BEHAVIOR ===
|
| 818 |
if (storm.active){
|
| 819 |
const distToSafeCenter = Math.hypot(e.x - storm.centerX, e.y - storm.centerY);
|
| 820 |
if (distToSafeCenter > storm.radius){
|
| 821 |
e.state = 'toSafe';
|
| 822 |
|
| 823 |
+
// If the enemy already has a temporary waypoint, move toward it until expiry
|
| 824 |
if (e.tempTarget && now < (e.tempTargetExpiry || 0)){
|
| 825 |
const td = Math.hypot(e.tempTarget.x - e.x, e.tempTarget.y - e.y);
|
| 826 |
if (td > 8){
|
|
|
|
| 833 |
}
|
| 834 |
}
|
| 835 |
|
| 836 |
+
// Direct sight to safe center -> run straight
|
| 837 |
if (hasLineOfSight(e.x, e.y, storm.centerX, storm.centerY)){
|
| 838 |
e.angle = Math.atan2(storm.centerY - e.y, storm.centerX - e.x);
|
| 839 |
moveEntityWithCollision(e, Math.cos(e.angle) * e.speed * dt * 1.0, Math.sin(e.angle) * e.speed * dt * 1.0, e.radius);
|
| 840 |
continue;
|
| 841 |
}
|
| 842 |
|
| 843 |
+
// If no direct sight, find the blocking object
|
| 844 |
const blocker = findBlockingObject(e.x, e.y, storm.centerX, storm.centerY);
|
| 845 |
if (blocker){
|
| 846 |
const db = Math.hypot(blocker.x - e.x, blocker.y - e.y);
|
| 847 |
|
| 848 |
+
// If very close to blocker -> attempt to break it physically (melee) or shoot it if has ammo
|
| 849 |
if (db < 48){
|
| 850 |
+
// If has a weapon with ammo, shoot at blocker
|
| 851 |
if (e.equippedIndex >= 0){
|
| 852 |
const eq = e.inventory[e.equippedIndex];
|
| 853 |
if (eq && eq.type === 'weapon' && eq.ammoInMag > 0 && now - (e.lastShot||0) > (eq.weapon.rate || 300)){
|
|
|
|
| 855 |
eq.ammoInMag -= 1;
|
| 856 |
const angle = Math.atan2(blocker.y - e.y, blocker.x - e.x);
|
| 857 |
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);
|
| 858 |
+
// small nudge away/around while shooting
|
| 859 |
+
e.angle = angle + (Math.random()-0.10)*0.10;
|
| 860 |
+
moveEntityWithCollision(e, Math.cos(e.angle) * e.speed * dt * 0.50, Math.sin(e.angle) * e.speed * dt * 0.50, e.radius);
|
| 861 |
continue;
|
| 862 |
}
|
| 863 |
}
|
| 864 |
+
// No ammo or no weapon -> use melee to whack the blocker
|
| 865 |
blocker.hp -= 28 * dt;
|
| 866 |
if (blocker.hp <= 0){
|
| 867 |
blocker.dead = true;
|
| 868 |
e.materials += (blocker.type === 'wood' ? 3 : 6);
|
| 869 |
} else {
|
| 870 |
+
// move closer and keep hitting
|
| 871 |
e.angle = Math.atan2(blocker.y - e.y, blocker.x - e.x);
|
| 872 |
moveEntityWithCollision(e, Math.cos(e.angle) * e.speed * dt * 0.4, Math.sin(e.angle) * e.speed * dt * 0.4, e.radius);
|
| 873 |
}
|
| 874 |
continue;
|
| 875 |
}
|
| 876 |
|
| 877 |
+
// If current blocker is not adjacent, compute a detour waypoint around it
|
| 878 |
const waypoint = computeDetourWaypoint(e.x, e.y, blocker, storm.centerX, storm.centerY, 12);
|
| 879 |
if (waypoint){
|
| 880 |
e.tempTarget = waypoint;
|
|
|
|
| 890 |
}
|
| 891 |
}
|
| 892 |
|
| 893 |
+
// Fallback: try to path straight toward blocker to push through or destroy it
|
| 894 |
e.angle = Math.atan2(blocker.y - e.y, blocker.x - e.x);
|
| 895 |
moveEntityWithCollision(e, Math.cos(e.angle) * e.speed * dt * 0.6, Math.sin(e.angle) * e.speed * dt * 0.6, e.radius);
|
| 896 |
continue;
|
| 897 |
}
|
| 898 |
|
| 899 |
+
// Default: move toward safe center (should not reach here often)
|
| 900 |
e.angle = Math.atan2(storm.centerY - e.y, storm.centerX - e.x);
|
| 901 |
moveEntityWithCollision(e, Math.cos(e.angle) * e.speed * dt * 0.95, Math.sin(e.angle) * e.speed * dt * 0.95, e.radius);
|
| 902 |
continue;
|
| 903 |
}
|
| 904 |
}
|
| 905 |
|
| 906 |
+
// === NORMAL BEHAVIOR (unchanged) ===
|
| 907 |
if (now - e.lastAttackedTime < 4000) e.state = 'combat';
|
| 908 |
if (e.state === 'gather') e.gatherTimeLeft -= dt;
|
| 909 |
|
|
|
|
| 1247 |
}
|
| 1248 |
}
|
| 1249 |
|
| 1250 |
+
// Drawing world & entities (unchanged)
|
| 1251 |
function drawWorld(){
|
| 1252 |
const TILE = 600;
|
| 1253 |
const cols = Math.ceil(WORLD.width / TILE);
|
|
|
|
| 1398 |
}
|
| 1399 |
}
|
| 1400 |
|
|
|
|
| 1401 |
function drawPlayer(){
|
| 1402 |
const s = worldToScreen(player.x,player.y);
|
| 1403 |
ctx.save();
|
| 1404 |
+
ctx.translate(s.x,s.y); ctx.rotate(player.angle);
|
| 1405 |
+
ctx.fillStyle='rgba(0,0,0,0.25)'; ctx.beginPath(); ctx.ellipse(0,14,18,8,0,0,Math.PI*2); ctx.fill();
|
| 1406 |
+
ctx.fillStyle='yellow'; ctx.beginPath(); ctx.moveTo(18,0); ctx.lineTo(-12,-10); ctx.lineTo(-12,10); ctx.closePath(); ctx.fill();
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1407 |
|
|
|
|
| 1408 |
let activeWeaponItem = null;
|
| 1409 |
if (player.equippedIndex >= 0) activeWeaponItem = player.inventory[player.equippedIndex];
|
| 1410 |
else {
|
| 1411 |
const selected = player.inventory[player.selectedSlot];
|
| 1412 |
if (selected && selected.type === 'weapon') activeWeaponItem = selected;
|
| 1413 |
}
|
| 1414 |
+
|
| 1415 |
+
if (player.equippedIndex === -1){
|
| 1416 |
ctx.save();
|
| 1417 |
+
ctx.translate(12, 6);
|
| 1418 |
+
ctx.rotate(-0.2);
|
| 1419 |
+
ctx.fillStyle = '#8b6b4a';
|
| 1420 |
+
ctx.fillRect(-2,0,4,18);
|
| 1421 |
+
ctx.fillStyle = '#cfcfcf';
|
| 1422 |
+
ctx.fillRect(-8,-6,12,6);
|
| 1423 |
+
ctx.restore();
|
| 1424 |
+
} else if (activeWeaponItem && activeWeaponItem.type === 'weapon'){
|
| 1425 |
+
ctx.save();
|
| 1426 |
+
ctx.translate(12, 2);
|
| 1427 |
ctx.rotate(-0.05);
|
| 1428 |
ctx.fillStyle = activeWeaponItem.weapon.color || '#fff';
|
| 1429 |
ctx.fillRect(0,-4,26,8);
|
|
|
|
| 1431 |
ctx.fillRect(18,-2,8,4);
|
| 1432 |
const magPct = Math.max(0, activeWeaponItem.ammoInMag / activeWeaponItem.weapon.magSize);
|
| 1433 |
ctx.fillStyle = 'rgba(0,0,0,0.35)';
|
| 1434 |
+
ctx.fillRect(4,6,18,4);
|
| 1435 |
ctx.fillStyle = '#0f0';
|
| 1436 |
+
ctx.fillRect(4,6,18*magPct,4);
|
| 1437 |
ctx.restore();
|
| 1438 |
}
|
|
|
|
| 1439 |
ctx.restore();
|
| 1440 |
}
|
| 1441 |
|
|
|
|
| 1472 |
const mw = minimapCanvas.width;
|
| 1473 |
const mh = minimapCanvas.height;
|
| 1474 |
if (!miniTerrainCache) buildMiniTerrainCache();
|
| 1475 |
+
// draw base terrain
|
| 1476 |
miniCtx.putImageData(miniTerrainCache, 0, 0);
|
| 1477 |
|
| 1478 |
+
// draw world objects (trees/stones/walls) as tiny marks - chests excluded per request
|
| 1479 |
miniCtx.save();
|
| 1480 |
const scaleX = mw / WORLD.width;
|
| 1481 |
const scaleY = mh / WORLD.height;
|
|
|
|
| 1494 |
miniCtx.fillRect(px-2, py-2, 4, 4);
|
| 1495 |
}
|
| 1496 |
}
|
| 1497 |
+
// optionally show pickups (not enemies/chests) - small blue/green dots
|
| 1498 |
for (const p of pickups){
|
| 1499 |
const px = Math.round(p.x * scaleX);
|
| 1500 |
const py = Math.round(p.y * scaleY);
|
|
|
|
| 1509 |
}
|
| 1510 |
}
|
| 1511 |
|
| 1512 |
+
// Storm safe zone: draw overlay darkening outside safe zone and stroke the safe circle
|
| 1513 |
if (storm.active){
|
| 1514 |
+
// darken everything
|
| 1515 |
miniCtx.fillStyle = 'rgba(0,0,0,0.35)';
|
| 1516 |
miniCtx.fillRect(0,0,mw,mh);
|
| 1517 |
+
// carve out safe zone
|
| 1518 |
miniCtx.globalCompositeOperation = 'destination-out';
|
| 1519 |
const cx = storm.centerX * scaleX;
|
| 1520 |
const cy = storm.centerY * scaleY;
|
| 1521 |
+
// radius scaled by average axis to keep circle shape close
|
| 1522 |
const r = storm.radius * ((scaleX + scaleY) / 2);
|
| 1523 |
miniCtx.beginPath();
|
| 1524 |
miniCtx.arc(cx, cy, r, 0, Math.PI*2);
|
| 1525 |
miniCtx.fill();
|
| 1526 |
miniCtx.globalCompositeOperation = 'source-over';
|
| 1527 |
+
// border
|
| 1528 |
miniCtx.strokeStyle = 'rgba(255,200,80,0.95)';
|
| 1529 |
miniCtx.lineWidth = 2;
|
| 1530 |
miniCtx.beginPath();
|
|
|
|
| 1532 |
miniCtx.stroke();
|
| 1533 |
}
|
| 1534 |
|
| 1535 |
+
// draw player dot (always visible)
|
| 1536 |
const ppx = Math.round(player.x * (mw / WORLD.width));
|
| 1537 |
const ppy = Math.round(player.y * (mh / WORLD.height));
|
| 1538 |
miniCtx.fillStyle = '#ffff66';
|
|
|
|
| 1621 |
drawWorld(); drawObjects(); drawChests(); drawPickups(); drawEnemies(); drawBullets(); drawPlayer(); drawCrosshair();
|
| 1622 |
updateHUD();
|
| 1623 |
|
| 1624 |
+
// Draw the minimap last so it reflects the latest world state
|
| 1625 |
drawMinimap();
|
| 1626 |
|
| 1627 |
if (storm.active && playerInStorm()) stormWarning.classList.remove('hidden'); else stormWarning.classList.add('hidden');
|