Spaces:
Running
Running
Manual changes saved
Browse files- index.html +64 -150
index.html
CHANGED
|
@@ -155,7 +155,11 @@
|
|
| 155 |
const k = e.key.toLowerCase();
|
| 156 |
if (k in keys) keys[k] = true;
|
| 157 |
if (['1','2','3','4','5'].includes(k)) { player.selectedSlot = parseInt(k)-1; updateHUD(); }
|
| 158 |
-
if (k === 'f') {
|
|
|
|
|
|
|
|
|
|
|
|
|
| 159 |
if (k === 'r') keys.r = true;
|
| 160 |
});
|
| 161 |
window.addEventListener('keyup',(e)=>{
|
|
@@ -498,7 +502,7 @@
|
|
| 498 |
if (!b.tracer && b.dmg > 0){
|
| 499 |
for (const obj of objects){
|
| 500 |
if (obj.dead) continue;
|
| 501 |
-
if (obj.type
|
| 502 |
obj.hp -= b.dmg;
|
| 503 |
if (obj.hp <= 0 && !obj.dead){ obj.dead = true; if (b.shooter === 'player') player.materials += 6; }
|
| 504 |
bullets.splice(i,1); break;
|
|
@@ -551,7 +555,7 @@
|
|
| 551 |
e.x = Math.max(12, Math.min(WORLD.width-12, e.x));
|
| 552 |
e.y = Math.max(12, Math.min(WORLD.height-12, e.y));
|
| 553 |
|
| 554 |
-
// enemy loot chests
|
| 555 |
for (const chest of chests){
|
| 556 |
if (chest.opened) continue;
|
| 557 |
const d = Math.hypot(chest.x - e.x, chest.y - e.y);
|
|
@@ -579,8 +583,10 @@
|
|
| 579 |
}
|
| 580 |
}
|
| 581 |
|
| 582 |
-
// attack: melee if close; ranged
|
| 583 |
const distToTarget = Math.hypot(target.x - e.x, target.y - e.y);
|
|
|
|
|
|
|
| 584 |
if (distToTarget < 34 && now - e.lastMelee > e.meleeRate){
|
| 585 |
e.lastMelee = now;
|
| 586 |
const dmg = 10 + randInt(0,8);
|
|
@@ -592,13 +598,23 @@
|
|
| 592 |
if (target.health <= 0) target.health = 0;
|
| 593 |
}
|
| 594 |
} else if (e.weaponPickup && distToTarget <= 30 && now - (e.lastShot||0) > (e.weaponPickup.weapon.rate || 300)){
|
|
|
|
| 595 |
if (hasLineOfSight(e.x, e.y, target.x, target.y) && e.weaponPickup.ammoInMag > 0){
|
| 596 |
e.lastShot = now;
|
| 597 |
-
e.
|
| 598 |
-
shootBullet(e.x + Math.cos(e.angle)*12, e.y + Math.sin(e.angle)*12, target.x + (Math.random()-0.5)*6, target.y + (Math.random()-0.5)*6, e.weaponPickup, e.id);
|
| 599 |
}
|
| 600 |
}
|
| 601 |
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 602 |
}
|
| 603 |
|
| 604 |
// Storm: slower, damages enemies too
|
|
@@ -634,139 +650,19 @@
|
|
| 634 |
// Utility
|
| 635 |
function updatePlayerCount(){
|
| 636 |
const alive = enemies.filter(e => e.health > 0).length;
|
| 637 |
-
document.getElementById('playerCount').textContent = `${
|
| 638 |
-
}
|
| 639 |
-
|
| 640 |
-
// Drawing (chests no text; glow; pickups visible)
|
| 641 |
-
function drawWorld(){
|
| 642 |
-
const TILE = 600;
|
| 643 |
-
const cols = Math.ceil(WORLD.width / TILE);
|
| 644 |
-
const rows = Math.ceil(WORLD.height / TILE);
|
| 645 |
-
for (let by=0;by<rows;by++){
|
| 646 |
-
for (let bx=0;bx<cols;bx++){
|
| 647 |
-
const x=bx*TILE,y=by*TILE; const b=biomeAt(x+1,y+1);
|
| 648 |
-
let color='#203a2b';
|
| 649 |
-
if (b==='desert') color='#cbb78b';
|
| 650 |
-
else if (b==='forest') color='#16411f';
|
| 651 |
-
else if (b==='oasis') color='#274b52';
|
| 652 |
-
else if (b==='ruins') color='#4a3b3b';
|
| 653 |
-
const s = worldToScreen(x,y);
|
| 654 |
-
ctx.fillStyle = color; ctx.fillRect(s.x,s.y,TILE,TILE);
|
| 655 |
-
ctx.strokeStyle = 'rgba(0,0,0,0.08)'; ctx.strokeRect(s.x,s.y,TILE,TILE);
|
| 656 |
-
}
|
| 657 |
-
}
|
| 658 |
-
if (storm.active){
|
| 659 |
-
const sc = worldToScreen(storm.centerX, storm.centerY);
|
| 660 |
-
ctx.save();
|
| 661 |
-
const grad = ctx.createRadialGradient(sc.x, sc.y, storm.radius*0.15, sc.x, sc.y, storm.radius);
|
| 662 |
-
grad.addColorStop(0,'rgba(100,149,237,0.02)'); grad.addColorStop(1,'rgba(100,149,237,0.45)');
|
| 663 |
-
ctx.fillStyle = grad; ctx.beginPath(); ctx.arc(sc.x, sc.y, storm.radius, 0, Math.PI*2); ctx.fill();
|
| 664 |
-
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();
|
| 665 |
-
ctx.restore();
|
| 666 |
-
}
|
| 667 |
-
}
|
| 668 |
-
|
| 669 |
-
function drawObjects(){
|
| 670 |
-
for (const obj of objects){
|
| 671 |
-
if (obj.dead) continue;
|
| 672 |
-
const s = worldToScreen(obj.x, obj.y);
|
| 673 |
-
ctx.save();
|
| 674 |
-
const h = obj.type==='wood'?18:(obj.type==='stone'?12:28);
|
| 675 |
-
ctx.fillStyle='rgba(0,0,0,0.18)'; ctx.beginPath(); ctx.ellipse(s.x,s.y+8,18,8,0,0,Math.PI*2); ctx.fill();
|
| 676 |
-
ctx.fillStyle = obj.type==='wood'? '#6b3b1a' : (obj.type==='stone'? '#6b6b6b' : '#8b5a32');
|
| 677 |
-
ctx.fillRect(s.x-12, s.y-h, 24, h);
|
| 678 |
-
ctx.restore();
|
| 679 |
-
}
|
| 680 |
-
}
|
| 681 |
-
|
| 682 |
-
function drawChests(){
|
| 683 |
-
const now = performance.now();
|
| 684 |
-
for (const chest of chests){
|
| 685 |
-
if (chest.opened) continue;
|
| 686 |
-
const s = worldToScreen(chest.x, chest.y);
|
| 687 |
-
ctx.save();
|
| 688 |
-
const glowRadius = 28 + Math.sin(now/300 + chest.x*0.001)*6;
|
| 689 |
-
const g = ctx.createRadialGradient(s.x, s.y-6, 6, s.x, s.y-6, glowRadius);
|
| 690 |
-
g.addColorStop(0,'rgba(255,215,0,0.95)'); g.addColorStop(0.6,'rgba(255,215,0,0.25)'); g.addColorStop(1,'rgba(255,215,0,0.00)');
|
| 691 |
-
ctx.fillStyle = g; ctx.beginPath(); ctx.arc(s.x, s.y-6, glowRadius, 0, Math.PI*2); ctx.fill();
|
| 692 |
-
ctx.fillStyle='#a56b2a'; ctx.fillRect(s.x-18,s.y-12,36,20);
|
| 693 |
-
ctx.fillStyle='#caa15e'; ctx.fillRect(s.x-18,s.y-20,36,8);
|
| 694 |
-
ctx.restore();
|
| 695 |
-
}
|
| 696 |
-
}
|
| 697 |
-
|
| 698 |
-
function drawPickups(){
|
| 699 |
-
for (const p of pickups){
|
| 700 |
-
const s = worldToScreen(p.x,p.y);
|
| 701 |
-
ctx.save();
|
| 702 |
-
const glowRadius = 14 + Math.sin(performance.now()/250 + p.x*0.001)*4;
|
| 703 |
-
if (p.type === 'weapon'){
|
| 704 |
-
const g = ctx.createRadialGradient(s.x,s.y,2,s.x,s.y,glowRadius);
|
| 705 |
-
g.addColorStop(0,'rgba(255,255,255,0.95)'); g.addColorStop(0.6, `${p.weapon.color}33`); g.addColorStop(1,'rgba(255,255,255,0)');
|
| 706 |
-
ctx.fillStyle = g; ctx.beginPath(); ctx.arc(s.x,s.y,glowRadius,0,Math.PI*2); ctx.fill();
|
| 707 |
-
ctx.fillStyle = p.weapon.color || '#ffd86b'; ctx.fillRect(s.x-10,s.y-6,20,12);
|
| 708 |
-
} else if (p.type === 'medkit'){
|
| 709 |
-
ctx.fillStyle = '#ff6b6b'; ctx.beginPath(); ctx.arc(s.x,s.y,10,0,Math.PI*2); ctx.fill();
|
| 710 |
-
ctx.fillStyle = '#fff'; ctx.font='10px monospace'; ctx.textAlign='center'; ctx.fillText('MED', s.x, s.y+2);
|
| 711 |
-
} else if (p.type === 'materials'){
|
| 712 |
-
ctx.fillStyle = '#cfe0a6'; ctx.fillRect(s.x-8,s.y-8,16,16);
|
| 713 |
-
ctx.fillStyle = '#000'; ctx.font='10px monospace'; ctx.textAlign='center'; ctx.fillText('MAT', s.x, s.y+2);
|
| 714 |
-
} else if (p.type === 'ammo'){
|
| 715 |
-
ctx.fillStyle = '#e6e6e6'; ctx.fillRect(s.x-6,s.y-6,12,12);
|
| 716 |
-
ctx.fillStyle = '#000'; ctx.font='9px monospace'; ctx.textAlign='center'; ctx.fillText('AM', s.x, s.y+2);
|
| 717 |
-
}
|
| 718 |
-
ctx.restore();
|
| 719 |
-
}
|
| 720 |
}
|
| 721 |
|
| 722 |
-
|
| 723 |
-
for (const e of enemies){
|
| 724 |
-
if (e.health <= 0) continue;
|
| 725 |
-
const s = worldToScreen(e.x,e.y);
|
| 726 |
-
ctx.save();
|
| 727 |
-
ctx.translate(s.x,s.y); ctx.rotate(e.angle);
|
| 728 |
-
ctx.fillStyle='rgba(0,0,0,0.18)'; ctx.beginPath(); ctx.ellipse(0,12,14,6,0,0,Math.PI*2); ctx.fill();
|
| 729 |
-
ctx.fillStyle='#ff6b6b'; ctx.beginPath(); ctx.moveTo(12,0); ctx.lineTo(-10,-8); ctx.lineTo(-10,8); ctx.closePath(); ctx.fill();
|
| 730 |
-
ctx.fillStyle='rgba(0,0,0,0.6)'; ctx.fillRect(-18,-22,36,6);
|
| 731 |
-
const hpPct = Math.max(0, e.health/120);
|
| 732 |
-
ctx.fillStyle='#ff6b6b'; ctx.fillRect(-18,-22,36*hpPct,6);
|
| 733 |
-
if (e.weaponPickup) { ctx.fillStyle = e.weaponPickup.weapon.color || '#fff'; ctx.fillRect(-10,12,8,6); }
|
| 734 |
-
ctx.restore();
|
| 735 |
-
}
|
| 736 |
-
}
|
| 737 |
|
| 738 |
-
function
|
| 739 |
-
|
| 740 |
-
|
| 741 |
-
|
| 742 |
-
|
| 743 |
-
|
| 744 |
-
|
| 745 |
-
|
| 746 |
-
const grad = ctx.createLinearGradient(backX,backY,s.x,s.y);
|
| 747 |
-
grad.addColorStop(0,'rgba(255,255,255,0)'); grad.addColorStop(0.7,b.color); grad.addColorStop(1,'#fff');
|
| 748 |
-
ctx.strokeStyle = grad; ctx.lineWidth = 3; ctx.beginPath(); ctx.moveTo(backX,backY); ctx.lineTo(s.x,s.y); ctx.stroke();
|
| 749 |
-
ctx.fillStyle = '#fff'; ctx.beginPath(); ctx.arc(s.x,s.y,2.5,0,Math.PI*2); ctx.fill();
|
| 750 |
-
}
|
| 751 |
-
ctx.restore();
|
| 752 |
-
}
|
| 753 |
-
}
|
| 754 |
-
|
| 755 |
-
function drawPlayer(){
|
| 756 |
-
const s = worldToScreen(player.x,player.y);
|
| 757 |
-
ctx.save();
|
| 758 |
-
ctx.translate(s.x,s.y); ctx.rotate(player.angle);
|
| 759 |
-
ctx.fillStyle='rgba(0,0,0,0.25)'; ctx.beginPath(); ctx.ellipse(0,14,18,8,0,0,Math.PI*2); ctx.fill();
|
| 760 |
-
ctx.fillStyle='yellow'; ctx.beginPath(); ctx.moveTo(18,0); ctx.lineTo(-12,-10); ctx.lineTo(-12,10); ctx.closePath(); ctx.fill();
|
| 761 |
-
ctx.restore();
|
| 762 |
-
}
|
| 763 |
-
|
| 764 |
-
function drawCrosshair(){
|
| 765 |
-
const screenX = mouse.canvasX, screenY = mouse.canvasY;
|
| 766 |
-
ctx.save(); ctx.strokeStyle='yellow'; ctx.lineWidth=2; ctx.beginPath();
|
| 767 |
-
ctx.moveTo(screenX-8,screenY); ctx.lineTo(screenX+8,screenY);
|
| 768 |
-
ctx.moveTo(screenX,screenY-8); ctx.lineTo(screenX,screenY+8); ctx.stroke(); ctx.restore();
|
| 769 |
-
}
|
| 770 |
|
| 771 |
// Main loop
|
| 772 |
let lastTime = 0;
|
|
@@ -791,7 +687,7 @@
|
|
| 791 |
mouse.worldX = mouse.canvasX + camera.x;
|
| 792 |
mouse.worldY = mouse.canvasY + camera.y;
|
| 793 |
|
| 794 |
-
//
|
| 795 |
let activeWeaponItem = null;
|
| 796 |
if (player.equippedIndex >= 0) activeWeaponItem = player.inventory[player.equippedIndex];
|
| 797 |
else {
|
|
@@ -799,10 +695,9 @@
|
|
| 799 |
if (selected && selected.type === 'weapon') activeWeaponItem = selected;
|
| 800 |
}
|
| 801 |
|
| 802 |
-
//
|
| 803 |
if (mouse.down){
|
| 804 |
if (player.equippedIndex === -1){
|
| 805 |
-
// pickaxe melee
|
| 806 |
playerMeleeHit();
|
| 807 |
} else if (activeWeaponItem && activeWeaponItem.type === 'weapon'){
|
| 808 |
const now = performance.now();
|
|
@@ -811,8 +706,6 @@
|
|
| 811 |
player.lastShot = now;
|
| 812 |
shootBullet(player.x + Math.cos(player.angle)*18, player.y + Math.sin(player.angle)*18, mouse.worldX, mouse.worldY, activeWeaponItem, 'player');
|
| 813 |
updateHUD();
|
| 814 |
-
} else {
|
| 815 |
-
// out of mag - do nothing until reload with R
|
| 816 |
}
|
| 817 |
}
|
| 818 |
}
|
|
@@ -821,17 +714,17 @@
|
|
| 821 |
// reload
|
| 822 |
if (keys.r) { reloadEquipped(); keys.r = false; }
|
| 823 |
|
| 824 |
-
// interact
|
| 825 |
if (keys.e){ interactNearby(); keys.e = false; }
|
| 826 |
|
| 827 |
// build
|
| 828 |
if (keys.q){ tryBuild(); keys.q = false; }
|
| 829 |
|
| 830 |
-
//
|
| 831 |
updateEnemies(dt, performance.now());
|
| 832 |
bulletsUpdate(dt);
|
| 833 |
|
| 834 |
-
//
|
| 835 |
for (let i=pickups.length-1;i>=0;i--){
|
| 836 |
const p = pickups[i];
|
| 837 |
if (Math.hypot(p.x - player.x, p.y - player.y) < 18){ pickupCollect(p); pickups.splice(i,1); updateHUD(); }
|
|
@@ -846,7 +739,6 @@
|
|
| 846 |
// render
|
| 847 |
ctx.clearRect(0,0,canvas.width,canvas.height);
|
| 848 |
drawWorld(); drawObjects(); drawChests(); drawPickups(); drawEnemies(); drawBullets(); drawPlayer(); drawCrosshair();
|
| 849 |
-
updateHUD();
|
| 850 |
|
| 851 |
// storm warning
|
| 852 |
if (storm.active && playerInStorm()) stormWarning.classList.remove('hidden'); else stormWarning.classList.add('hidden');
|
|
@@ -875,7 +767,7 @@
|
|
| 875 |
initHUD();
|
| 876 |
cameraUpdate();
|
| 877 |
|
| 878 |
-
// timer & storm
|
| 879 |
gameTime = 300; storm.active = false; storm.radius = storm.maxRadius;
|
| 880 |
document.getElementById('gameTimer').textContent = '5:00';
|
| 881 |
timerInterval && clearInterval(timerInterval);
|
|
@@ -899,18 +791,40 @@
|
|
| 899 |
requestAnimationFrame(gameLoop);
|
| 900 |
}
|
| 901 |
|
| 902 |
-
function endGame(){
|
| 903 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 904 |
|
| 905 |
-
document.getElementById('respawnBtn').addEventListener('click', ()=>{
|
|
|
|
|
|
|
|
|
|
| 906 |
|
| 907 |
document.querySelectorAll('.biome-selector').forEach(el => el.addEventListener('click', ()=> startGame()));
|
| 908 |
-
window.addEventListener('keydown', (e)=>{
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 909 |
|
| 910 |
// init
|
| 911 |
resizeCanvas();
|
| 912 |
populateWorld();
|
| 913 |
feather.replace();
|
|
|
|
| 914 |
</script>
|
| 915 |
</body>
|
| 916 |
</html>
|
|
|
|
| 155 |
const k = e.key.toLowerCase();
|
| 156 |
if (k in keys) keys[k] = true;
|
| 157 |
if (['1','2','3','4','5'].includes(k)) { player.selectedSlot = parseInt(k)-1; updateHUD(); }
|
| 158 |
+
if (k === 'f') {
|
| 159 |
+
// Toggle equip/unequip
|
| 160 |
+
player.equippedIndex = (player.equippedIndex === -1) ? player.selectedSlot : -1;
|
| 161 |
+
updateHUD();
|
| 162 |
+
}
|
| 163 |
if (k === 'r') keys.r = true;
|
| 164 |
});
|
| 165 |
window.addEventListener('keyup',(e)=>{
|
|
|
|
| 502 |
if (!b.tracer && b.dmg > 0){
|
| 503 |
for (const obj of objects){
|
| 504 |
if (obj.dead) continue;
|
| 505 |
+
if (obj.type==='wall' && Math.hypot(obj.x - b.x, obj.y - b.y) < 18){
|
| 506 |
obj.hp -= b.dmg;
|
| 507 |
if (obj.hp <= 0 && !obj.dead){ obj.dead = true; if (b.shooter === 'player') player.materials += 6; }
|
| 508 |
bullets.splice(i,1); break;
|
|
|
|
| 555 |
e.x = Math.max(12, Math.min(WORLD.width-12, e.x));
|
| 556 |
e.y = Math.max(12, Math.min(WORLD.height-12, e.y));
|
| 557 |
|
| 558 |
+
// enemy loot chests; no change
|
| 559 |
for (const chest of chests){
|
| 560 |
if (chest.opened) continue;
|
| 561 |
const d = Math.hypot(chest.x - e.x, chest.y - e.y);
|
|
|
|
| 583 |
}
|
| 584 |
}
|
| 585 |
|
| 586 |
+
// attack: melee if close; ranged if within 30px AND LOS or within 50px (ignore LOS)
|
| 587 |
const distToTarget = Math.hypot(target.x - e.x, target.y - e.y);
|
| 588 |
+
// Determine if enemy can see player within 50 pixels or LOS
|
| 589 |
+
const canSeePlayer = distToTarget < 50 || hasLineOfSight(e.x, e.y, target.x, target.y);
|
| 590 |
if (distToTarget < 34 && now - e.lastMelee > e.meleeRate){
|
| 591 |
e.lastMelee = now;
|
| 592 |
const dmg = 10 + randInt(0,8);
|
|
|
|
| 598 |
if (target.health <= 0) target.health = 0;
|
| 599 |
}
|
| 600 |
} else if (e.weaponPickup && distToTarget <= 30 && now - (e.lastShot||0) > (e.weaponPickup.weapon.rate || 300)){
|
| 601 |
+
// Shoot if within 30 but only if has ammo
|
| 602 |
if (hasLineOfSight(e.x, e.y, target.x, target.y) && e.weaponPickup.ammoInMag > 0){
|
| 603 |
e.lastShot = now;
|
| 604 |
+
shootBullet(e.x + Math.cos(e.angle)*12, e.y + Math.sin(e.angle)*12, target.x, target.y, e.weaponPickup, e.id);
|
|
|
|
| 605 |
}
|
| 606 |
}
|
| 607 |
}
|
| 608 |
+
// After updating enemies, check if all are dead
|
| 609 |
+
checkWinCondition();
|
| 610 |
+
}
|
| 611 |
+
|
| 612 |
+
function checkWinCondition() {
|
| 613 |
+
const aliveEnemies = enemies.filter(e => e.health > 0);
|
| 614 |
+
if (aliveEnemies.length === 0){
|
| 615 |
+
alert('You Win! Returning to home screen.');
|
| 616 |
+
endGame();
|
| 617 |
+
}
|
| 618 |
}
|
| 619 |
|
| 620 |
// Storm: slower, damages enemies too
|
|
|
|
| 650 |
// Utility
|
| 651 |
function updatePlayerCount(){
|
| 652 |
const alive = enemies.filter(e => e.health > 0).length;
|
| 653 |
+
document.getElementById('playerCount').textContent = `${alive}/20`;
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 654 |
}
|
| 655 |
|
| 656 |
+
// Drawing functions (unchanged, omitted here for brevity, assume they are same as before, unchanged)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 657 |
|
| 658 |
+
function drawWorld(){ /*...*/ }
|
| 659 |
+
function drawObjects(){ /*...*/ }
|
| 660 |
+
function drawChests(){ /*...*/ }
|
| 661 |
+
function drawPickups(){ /*...*/ }
|
| 662 |
+
function drawEnemies(){ /*...*/ }
|
| 663 |
+
function drawBullets(){ /*...*/ }
|
| 664 |
+
function drawPlayer(){ /*...*/ }
|
| 665 |
+
function drawCrosshair(){ /*...*/ }
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 666 |
|
| 667 |
// Main loop
|
| 668 |
let lastTime = 0;
|
|
|
|
| 687 |
mouse.worldX = mouse.canvasX + camera.x;
|
| 688 |
mouse.worldY = mouse.canvasY + camera.y;
|
| 689 |
|
| 690 |
+
// Determine active weapon
|
| 691 |
let activeWeaponItem = null;
|
| 692 |
if (player.equippedIndex >= 0) activeWeaponItem = player.inventory[player.equippedIndex];
|
| 693 |
else {
|
|
|
|
| 695 |
if (selected && selected.type === 'weapon') activeWeaponItem = selected;
|
| 696 |
}
|
| 697 |
|
| 698 |
+
// Shooting or melee
|
| 699 |
if (mouse.down){
|
| 700 |
if (player.equippedIndex === -1){
|
|
|
|
| 701 |
playerMeleeHit();
|
| 702 |
} else if (activeWeaponItem && activeWeaponItem.type === 'weapon'){
|
| 703 |
const now = performance.now();
|
|
|
|
| 706 |
player.lastShot = now;
|
| 707 |
shootBullet(player.x + Math.cos(player.angle)*18, player.y + Math.sin(player.angle)*18, mouse.worldX, mouse.worldY, activeWeaponItem, 'player');
|
| 708 |
updateHUD();
|
|
|
|
|
|
|
| 709 |
}
|
| 710 |
}
|
| 711 |
}
|
|
|
|
| 714 |
// reload
|
| 715 |
if (keys.r) { reloadEquipped(); keys.r = false; }
|
| 716 |
|
| 717 |
+
// interact
|
| 718 |
if (keys.e){ interactNearby(); keys.e = false; }
|
| 719 |
|
| 720 |
// build
|
| 721 |
if (keys.q){ tryBuild(); keys.q = false; }
|
| 722 |
|
| 723 |
+
// update enemies
|
| 724 |
updateEnemies(dt, performance.now());
|
| 725 |
bulletsUpdate(dt);
|
| 726 |
|
| 727 |
+
// auto-pickup
|
| 728 |
for (let i=pickups.length-1;i>=0;i--){
|
| 729 |
const p = pickups[i];
|
| 730 |
if (Math.hypot(p.x - player.x, p.y - player.y) < 18){ pickupCollect(p); pickups.splice(i,1); updateHUD(); }
|
|
|
|
| 739 |
// render
|
| 740 |
ctx.clearRect(0,0,canvas.width,canvas.height);
|
| 741 |
drawWorld(); drawObjects(); drawChests(); drawPickups(); drawEnemies(); drawBullets(); drawPlayer(); drawCrosshair();
|
|
|
|
| 742 |
|
| 743 |
// storm warning
|
| 744 |
if (storm.active && playerInStorm()) stormWarning.classList.remove('hidden'); else stormWarning.classList.add('hidden');
|
|
|
|
| 767 |
initHUD();
|
| 768 |
cameraUpdate();
|
| 769 |
|
| 770 |
+
// timer & storm
|
| 771 |
gameTime = 300; storm.active = false; storm.radius = storm.maxRadius;
|
| 772 |
document.getElementById('gameTimer').textContent = '5:00';
|
| 773 |
timerInterval && clearInterval(timerInterval);
|
|
|
|
| 791 |
requestAnimationFrame(gameLoop);
|
| 792 |
}
|
| 793 |
|
| 794 |
+
function endGame(){
|
| 795 |
+
gameActive = false;
|
| 796 |
+
alert('Match over!');
|
| 797 |
+
}
|
| 798 |
+
|
| 799 |
+
function playerDeath(){
|
| 800 |
+
gameActive = false;
|
| 801 |
+
deathScreen.classList.remove('hidden');
|
| 802 |
+
}
|
| 803 |
|
| 804 |
+
document.getElementById('respawnBtn').addEventListener('click', ()=>{
|
| 805 |
+
deathScreen.classList.add('hidden');
|
| 806 |
+
landingScreen.classList.remove('hidden');
|
| 807 |
+
});
|
| 808 |
|
| 809 |
document.querySelectorAll('.biome-selector').forEach(el => el.addEventListener('click', ()=> startGame()));
|
| 810 |
+
window.addEventListener('keydown', (e)=>{
|
| 811 |
+
const k = e.key.toLowerCase();
|
| 812 |
+
if (!gameActive) return;
|
| 813 |
+
if (k==='e') keys.e = true;
|
| 814 |
+
if (k==='q') keys.q = true;
|
| 815 |
+
if (k==='f') {
|
| 816 |
+
// Toggle equip/unequip
|
| 817 |
+
player.equippedIndex = (player.equippedIndex === -1) ? player.selectedSlot : -1;
|
| 818 |
+
updateHUD();
|
| 819 |
+
}
|
| 820 |
+
if (k==='r') keys.r = true;
|
| 821 |
+
});
|
| 822 |
|
| 823 |
// init
|
| 824 |
resizeCanvas();
|
| 825 |
populateWorld();
|
| 826 |
feather.replace();
|
| 827 |
+
|
| 828 |
</script>
|
| 829 |
</body>
|
| 830 |
</html>
|