Files
doom-clone/js/weapons.js
T
aharrison-fullerandClaude Opus 4.6 c55d7a7475 Add browser-based Doom clone with raycasting engine
Built a complete FPS game playable in the browser using vanilla JS and
HTML5 Canvas. Features a raycasting renderer with 8 textured wall types,
3 weapons (pistol, shotgun, plasma gun), 3 enemy types with AI
(imp, demon, baron), 3 levels of increasing difficulty, HUD with
minimap, pickups, doors, explosive barrels, and procedural audio.
Includes GitHub Actions workflow for GitHub Pages deployment.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-16 21:27:43 +00:00

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 && map[checkY][checkX] !== WALL_DOOR) {
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();
}
}