Closed doors now block line of sight for both enemy AI and player weapons. Previously, door tiles were excluded from collision checks, allowing attacks through them. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
322 lines
9.5 KiB
JavaScript
322 lines
9.5 KiB
JavaScript
// Weapon system
|
|
const WeaponDefs = {
|
|
pistol: {
|
|
name: 'PISTOL',
|
|
damage: 15,
|
|
fireRate: 300, // ms between shots
|
|
ammoPerShot: 1,
|
|
spread: 0.02,
|
|
projectiles: 1,
|
|
range: 15,
|
|
color: '#ff0',
|
|
muzzleColor: '#ff8',
|
|
autoFire: false,
|
|
},
|
|
shotgun: {
|
|
name: 'SHOTGUN',
|
|
damage: 10,
|
|
fireRate: 700,
|
|
ammoPerShot: 2,
|
|
spread: 0.08,
|
|
projectiles: 6,
|
|
range: 10,
|
|
color: '#fa0',
|
|
muzzleColor: '#ff4',
|
|
autoFire: false,
|
|
},
|
|
plasma: {
|
|
name: 'PLASMA GUN',
|
|
damage: 20,
|
|
fireRate: 120,
|
|
ammoPerShot: 1,
|
|
spread: 0.03,
|
|
projectiles: 1,
|
|
range: 18,
|
|
color: '#0af',
|
|
muzzleColor: '#0ff',
|
|
autoFire: true,
|
|
}
|
|
};
|
|
|
|
class WeaponSystem {
|
|
constructor() {
|
|
this.weapons = {
|
|
pistol: { unlocked: true, def: WeaponDefs.pistol },
|
|
shotgun: { unlocked: false, def: WeaponDefs.shotgun },
|
|
plasma: { unlocked: false, def: WeaponDefs.plasma },
|
|
};
|
|
this.current = 'pistol';
|
|
this.lastFireTime = 0;
|
|
this.firing = false;
|
|
this.animFrame = 0;
|
|
this.animTimer = 0;
|
|
this.muzzleFlash = 0;
|
|
}
|
|
|
|
get currentWeapon() {
|
|
return this.weapons[this.current];
|
|
}
|
|
|
|
get currentDef() {
|
|
return this.currentWeapon.def;
|
|
}
|
|
|
|
switchTo(weaponName) {
|
|
if (this.weapons[weaponName] && this.weapons[weaponName].unlocked) {
|
|
this.current = weaponName;
|
|
this.animFrame = 0;
|
|
return true;
|
|
}
|
|
return false;
|
|
}
|
|
|
|
unlock(weaponName) {
|
|
if (this.weapons[weaponName]) {
|
|
this.weapons[weaponName].unlocked = true;
|
|
}
|
|
}
|
|
|
|
canFire(ammo) {
|
|
const now = performance.now();
|
|
return ammo >= this.currentDef.ammoPerShot &&
|
|
now - this.lastFireTime >= this.currentDef.fireRate;
|
|
}
|
|
|
|
fire(player, enemies, ammo) {
|
|
if (!this.canFire(ammo)) return { hit: false, ammoUsed: 0 };
|
|
|
|
this.lastFireTime = performance.now();
|
|
this.muzzleFlash = 4;
|
|
this.animFrame = 1;
|
|
|
|
// Play sound
|
|
switch (this.current) {
|
|
case 'pistol': Audio.playPistol(); break;
|
|
case 'shotgun': Audio.playShotgun(); break;
|
|
case 'plasma': Audio.playPlasma(); break;
|
|
}
|
|
|
|
const def = this.currentDef;
|
|
let totalHits = [];
|
|
|
|
for (let p = 0; p < def.projectiles; p++) {
|
|
const spread = (Math.random() - 0.5) * def.spread;
|
|
const rayAngle = player.angle + spread;
|
|
|
|
// Check each enemy for hit
|
|
for (const enemy of enemies) {
|
|
if (!enemy.active || enemy.health <= 0) continue;
|
|
|
|
const dx = enemy.x - player.x;
|
|
const dy = enemy.y - player.y;
|
|
const dist = Math.sqrt(dx * dx + dy * dy);
|
|
|
|
if (dist > def.range) continue;
|
|
|
|
// Check if enemy is in the firing cone
|
|
let enemyAngle = Math.atan2(dy, dx);
|
|
let angleDiff = enemyAngle - rayAngle;
|
|
while (angleDiff > Math.PI) angleDiff -= 2 * Math.PI;
|
|
while (angleDiff < -Math.PI) angleDiff += 2 * Math.PI;
|
|
|
|
// Hit detection - wider at closer range
|
|
const hitWidth = 0.4 / dist;
|
|
if (Math.abs(angleDiff) < hitWidth) {
|
|
// Check line of sight (simplified - check if wall is closer)
|
|
const blocked = this.isBlocked(player, enemy, player.currentMap);
|
|
if (!blocked) {
|
|
totalHits.push({ enemy, damage: def.damage, dist });
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
// Apply damage to closest hit per projectile
|
|
totalHits.sort((a, b) => a.dist - b.dist);
|
|
const hitEnemies = new Set();
|
|
let hitResults = [];
|
|
|
|
for (const hit of totalHits) {
|
|
if (!hitEnemies.has(hit.enemy) || this.current === 'shotgun') {
|
|
hit.enemy.takeDamage(hit.damage);
|
|
hitEnemies.add(hit.enemy);
|
|
hitResults.push(hit);
|
|
}
|
|
}
|
|
|
|
return { hit: hitResults.length > 0, ammoUsed: def.ammoPerShot, hits: hitResults };
|
|
}
|
|
|
|
isBlocked(player, enemy, map) {
|
|
if (!map) return false;
|
|
const dx = enemy.x - player.x;
|
|
const dy = enemy.y - player.y;
|
|
const dist = Math.sqrt(dx * dx + dy * dy);
|
|
const steps = Math.floor(dist * 4);
|
|
const stepX = dx / steps;
|
|
const stepY = dy / steps;
|
|
|
|
for (let i = 1; i < steps; i++) {
|
|
const checkX = Math.floor(player.x + stepX * i);
|
|
const checkY = Math.floor(player.y + stepY * i);
|
|
if (checkY >= 0 && checkY < map.length && checkX >= 0 && checkX < map[0].length) {
|
|
if (map[checkY][checkX] > 0) {
|
|
return true;
|
|
}
|
|
}
|
|
}
|
|
return false;
|
|
}
|
|
|
|
update(dt) {
|
|
if (this.muzzleFlash > 0) this.muzzleFlash--;
|
|
|
|
this.animTimer += dt;
|
|
if (this.animTimer > 50) {
|
|
this.animTimer = 0;
|
|
if (this.animFrame > 0) {
|
|
this.animFrame++;
|
|
if (this.animFrame > 4) this.animFrame = 0;
|
|
}
|
|
}
|
|
}
|
|
|
|
drawWeapon(ctx) {
|
|
const centerX = SCREEN_WIDTH / 2;
|
|
const baseY = SCREEN_HEIGHT;
|
|
const bobX = Math.sin(performance.now() / 200) * 3;
|
|
const bobY = Math.abs(Math.cos(performance.now() / 200)) * 3;
|
|
|
|
// Recoil animation
|
|
let recoilY = 0;
|
|
if (this.animFrame > 0) {
|
|
recoilY = this.animFrame === 1 ? -20 : this.animFrame === 2 ? -15 : this.animFrame === 3 ? -8 : 0;
|
|
}
|
|
|
|
ctx.save();
|
|
ctx.translate(bobX, bobY + recoilY);
|
|
|
|
switch (this.current) {
|
|
case 'pistol':
|
|
this.drawPistol(ctx, centerX, baseY);
|
|
break;
|
|
case 'shotgun':
|
|
this.drawShotgunWeapon(ctx, centerX, baseY);
|
|
break;
|
|
case 'plasma':
|
|
this.drawPlasmaGun(ctx, centerX, baseY);
|
|
break;
|
|
}
|
|
|
|
// Muzzle flash
|
|
if (this.muzzleFlash > 0) {
|
|
this.drawMuzzleFlash(ctx, centerX, baseY - 200);
|
|
}
|
|
|
|
ctx.restore();
|
|
}
|
|
|
|
drawPistol(ctx, cx, by) {
|
|
// Barrel
|
|
ctx.fillStyle = '#555';
|
|
ctx.fillRect(cx - 8, by - 200, 16, 80);
|
|
// Slide
|
|
ctx.fillStyle = '#444';
|
|
ctx.fillRect(cx - 10, by - 180, 20, 60);
|
|
// Grip
|
|
ctx.fillStyle = '#3a2a1a';
|
|
ctx.fillRect(cx - 12, by - 120, 24, 80);
|
|
// Trigger guard
|
|
ctx.fillStyle = '#444';
|
|
ctx.beginPath();
|
|
ctx.arc(cx, by - 115, 8, 0, Math.PI);
|
|
ctx.fill();
|
|
// Sight
|
|
ctx.fillStyle = '#666';
|
|
ctx.fillRect(cx - 3, by - 205, 6, 8);
|
|
// Highlight
|
|
ctx.fillStyle = '#666';
|
|
ctx.fillRect(cx - 8, by - 200, 2, 80);
|
|
}
|
|
|
|
drawShotgunWeapon(ctx, cx, by) {
|
|
// Barrels (double)
|
|
ctx.fillStyle = '#444';
|
|
ctx.fillRect(cx - 14, by - 240, 12, 120);
|
|
ctx.fillRect(cx + 2, by - 240, 12, 120);
|
|
// Barrel openings
|
|
ctx.fillStyle = '#222';
|
|
ctx.beginPath();
|
|
ctx.arc(cx - 8, by - 240, 5, 0, Math.PI * 2);
|
|
ctx.arc(cx + 8, by - 240, 5, 0, Math.PI * 2);
|
|
ctx.fill();
|
|
// Pump
|
|
ctx.fillStyle = '#7a5230';
|
|
ctx.fillRect(cx - 16, by - 160, 32, 30);
|
|
// Stock
|
|
ctx.fillStyle = '#5a3a1a';
|
|
ctx.fillRect(cx - 12, by - 130, 24, 100);
|
|
// Receiver
|
|
ctx.fillStyle = '#555';
|
|
ctx.fillRect(cx - 14, by - 120, 28, 20);
|
|
// Highlight
|
|
ctx.fillStyle = '#555';
|
|
ctx.fillRect(cx - 14, by - 240, 2, 120);
|
|
}
|
|
|
|
drawPlasmaGun(ctx, cx, by) {
|
|
// Main body
|
|
ctx.fillStyle = '#336';
|
|
ctx.fillRect(cx - 18, by - 200, 36, 80);
|
|
// Barrel
|
|
ctx.fillStyle = '#448';
|
|
ctx.fillRect(cx - 10, by - 240, 20, 50);
|
|
// Energy core (glowing)
|
|
const pulse = Math.sin(performance.now() / 100) * 0.3 + 0.7;
|
|
ctx.fillStyle = `rgba(0, 150, 255, ${pulse})`;
|
|
ctx.beginPath();
|
|
ctx.arc(cx, by - 160, 12, 0, Math.PI * 2);
|
|
ctx.fill();
|
|
ctx.fillStyle = `rgba(100, 200, 255, ${pulse * 0.5})`;
|
|
ctx.beginPath();
|
|
ctx.arc(cx, by - 160, 16, 0, Math.PI * 2);
|
|
ctx.fill();
|
|
// Barrel opening
|
|
ctx.fillStyle = '#0af';
|
|
ctx.beginPath();
|
|
ctx.arc(cx, by - 240, 6, 0, Math.PI * 2);
|
|
ctx.fill();
|
|
// Grip
|
|
ctx.fillStyle = '#224';
|
|
ctx.fillRect(cx - 14, by - 120, 28, 80);
|
|
// Details
|
|
ctx.fillStyle = '#08f';
|
|
ctx.fillRect(cx - 16, by - 190, 2, 40);
|
|
ctx.fillRect(cx + 14, by - 190, 2, 40);
|
|
}
|
|
|
|
drawMuzzleFlash(ctx, cx, y) {
|
|
const def = this.currentDef;
|
|
const size = this.current === 'shotgun' ? 40 : 25;
|
|
|
|
ctx.save();
|
|
ctx.globalCompositeOperation = 'lighter';
|
|
|
|
// Outer glow
|
|
const gradient = ctx.createRadialGradient(cx, y, 0, cx, y, size);
|
|
gradient.addColorStop(0, def.muzzleColor);
|
|
gradient.addColorStop(0.5, def.color);
|
|
gradient.addColorStop(1, 'transparent');
|
|
ctx.fillStyle = gradient;
|
|
ctx.fillRect(cx - size, y - size, size * 2, size * 2);
|
|
|
|
// Inner flash
|
|
ctx.fillStyle = '#fff';
|
|
ctx.beginPath();
|
|
ctx.arc(cx, y, size * 0.3, 0, Math.PI * 2);
|
|
ctx.fill();
|
|
|
|
ctx.restore();
|
|
}
|
|
}
|