Files
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

401 lines
9.8 KiB
JavaScript

// Procedural sprite generation for enemies and pickups
const SpriteGen = {
createCanvas(w, h) {
const canvas = document.createElement('canvas');
canvas.width = w;
canvas.height = h;
return canvas;
},
// Imp enemy - brown/tan demon
impSprite() {
const canvas = this.createCanvas(64, 64);
const ctx = canvas.getContext('2d');
// Body
ctx.fillStyle = '#8B4513';
ctx.fillRect(20, 20, 24, 30);
// Head
ctx.fillStyle = '#A0522D';
ctx.beginPath();
ctx.arc(32, 16, 10, 0, Math.PI * 2);
ctx.fill();
// Eyes (glowing red)
ctx.fillStyle = '#f00';
ctx.fillRect(27, 13, 3, 3);
ctx.fillRect(34, 13, 3, 3);
// Horns
ctx.fillStyle = '#654321';
ctx.beginPath();
ctx.moveTo(24, 8); ctx.lineTo(22, 0); ctx.lineTo(27, 8);
ctx.fill();
ctx.beginPath();
ctx.moveTo(37, 8); ctx.lineTo(42, 0); ctx.lineTo(40, 8);
ctx.fill();
// Arms
ctx.fillStyle = '#8B4513';
ctx.fillRect(12, 22, 8, 6);
ctx.fillRect(44, 22, 8, 6);
// Claws
ctx.fillStyle = '#654321';
ctx.fillRect(10, 28, 3, 5);
ctx.fillRect(14, 28, 3, 5);
ctx.fillRect(47, 28, 3, 5);
ctx.fillRect(51, 28, 3, 5);
// Legs
ctx.fillStyle = '#7B3F00';
ctx.fillRect(22, 50, 8, 14);
ctx.fillRect(34, 50, 8, 14);
return canvas;
},
// Demon - pink/red bigger enemy
demonSprite() {
const canvas = this.createCanvas(64, 64);
const ctx = canvas.getContext('2d');
// Body (larger)
ctx.fillStyle = '#C0392B';
ctx.fillRect(14, 16, 36, 34);
// Head
ctx.fillStyle = '#E74C3C';
ctx.beginPath();
ctx.arc(32, 14, 12, 0, Math.PI * 2);
ctx.fill();
// Jaw
ctx.fillStyle = '#C0392B';
ctx.fillRect(22, 18, 20, 8);
// Teeth
ctx.fillStyle = '#fff';
for (let i = 0; i < 5; i++) {
ctx.fillRect(24 + i * 4, 22, 2, 4);
}
// Eyes
ctx.fillStyle = '#ff0';
ctx.fillRect(25, 10, 4, 4);
ctx.fillRect(35, 10, 4, 4);
ctx.fillStyle = '#f00';
ctx.fillRect(26, 11, 2, 2);
ctx.fillRect(36, 11, 2, 2);
// Arms/claws
ctx.fillStyle = '#C0392B';
ctx.fillRect(6, 20, 8, 20);
ctx.fillRect(50, 20, 8, 20);
// Legs
ctx.fillStyle = '#922B21';
ctx.fillRect(18, 50, 10, 14);
ctx.fillRect(36, 50, 10, 14);
return canvas;
},
// Baron of Hell - tall, green/brown
baronSprite() {
const canvas = this.createCanvas(64, 64);
const ctx = canvas.getContext('2d');
// Body
ctx.fillStyle = '#2E4A1E';
ctx.fillRect(16, 10, 32, 38);
// Head
ctx.fillStyle = '#3D5E2A';
ctx.beginPath();
ctx.arc(32, 10, 12, 0, Math.PI * 2);
ctx.fill();
// Horns (large)
ctx.fillStyle = '#5A3E1B';
ctx.beginPath();
ctx.moveTo(22, 2); ctx.lineTo(16, -8); ctx.lineTo(26, 4);
ctx.fill();
ctx.beginPath();
ctx.moveTo(42, 2); ctx.lineTo(48, -8); ctx.lineTo(38, 4);
ctx.fill();
// Eyes (green fire)
ctx.fillStyle = '#0f0';
ctx.fillRect(26, 7, 4, 4);
ctx.fillRect(34, 7, 4, 4);
// Chest detail
ctx.fillStyle = '#4A6E2E';
ctx.fillRect(22, 18, 20, 4);
// Arms (muscular)
ctx.fillStyle = '#2E4A1E';
ctx.fillRect(6, 14, 10, 24);
ctx.fillRect(48, 14, 10, 24);
// Fists (glowing)
ctx.fillStyle = '#0f0';
ctx.fillRect(6, 38, 10, 8);
ctx.fillRect(48, 38, 10, 8);
// Legs
ctx.fillStyle = '#1E3A0E';
ctx.fillRect(20, 48, 10, 16);
ctx.fillRect(34, 48, 10, 16);
// Hooves
ctx.fillStyle = '#5A3E1B';
ctx.fillRect(18, 60, 14, 4);
ctx.fillRect(32, 60, 14, 4);
return canvas;
},
// Health pickup
healthPickup() {
const canvas = this.createCanvas(32, 32);
const ctx = canvas.getContext('2d');
// Vial
ctx.fillStyle = '#06f';
ctx.fillRect(10, 8, 12, 20);
// Cross
ctx.fillStyle = '#fff';
ctx.fillRect(13, 12, 6, 2);
ctx.fillRect(15, 10, 2, 6);
// Cap
ctx.fillStyle = '#888';
ctx.fillRect(10, 6, 12, 3);
// Glow
ctx.fillStyle = 'rgba(0, 100, 255, 0.3)';
ctx.beginPath();
ctx.arc(16, 18, 12, 0, Math.PI * 2);
ctx.fill();
return canvas;
},
// Armor pickup
armorPickup() {
const canvas = this.createCanvas(32, 32);
const ctx = canvas.getContext('2d');
// Shield
ctx.fillStyle = '#080';
ctx.beginPath();
ctx.moveTo(16, 4);
ctx.lineTo(26, 10);
ctx.lineTo(24, 24);
ctx.lineTo(16, 28);
ctx.lineTo(8, 24);
ctx.lineTo(6, 10);
ctx.closePath();
ctx.fill();
// Inner shield
ctx.fillStyle = '#0a0';
ctx.beginPath();
ctx.moveTo(16, 8);
ctx.lineTo(22, 12);
ctx.lineTo(21, 22);
ctx.lineTo(16, 25);
ctx.lineTo(11, 22);
ctx.lineTo(10, 12);
ctx.closePath();
ctx.fill();
return canvas;
},
// Ammo pickup
ammoPickup() {
const canvas = this.createCanvas(32, 32);
const ctx = canvas.getContext('2d');
// Box
ctx.fillStyle = '#8B7355';
ctx.fillRect(6, 10, 20, 16);
// Label
ctx.fillStyle = '#ff0';
ctx.font = 'bold 8px monospace';
ctx.fillText('AMMO', 7, 21);
// Bullets poking out
ctx.fillStyle = '#da0';
ctx.fillRect(8, 6, 3, 6);
ctx.fillRect(13, 4, 3, 8);
ctx.fillRect(18, 6, 3, 6);
// Bullet tips
ctx.fillStyle = '#a80';
ctx.fillRect(8, 4, 3, 3);
ctx.fillRect(13, 2, 3, 3);
ctx.fillRect(18, 4, 3, 3);
return canvas;
},
// Shotgun pickup
shotgunPickup() {
const canvas = this.createCanvas(48, 32);
const ctx = canvas.getContext('2d');
// Barrel
ctx.fillStyle = '#555';
ctx.fillRect(4, 12, 30, 4);
// Stock
ctx.fillStyle = '#7a5230';
ctx.fillRect(34, 10, 12, 8);
// Pump
ctx.fillStyle = '#444';
ctx.fillRect(14, 10, 8, 8);
// Glow
ctx.fillStyle = 'rgba(255, 200, 0, 0.3)';
ctx.beginPath();
ctx.arc(24, 16, 14, 0, Math.PI * 2);
ctx.fill();
return canvas;
},
// Plasma gun pickup
plasmaPickup() {
const canvas = this.createCanvas(48, 32);
const ctx = canvas.getContext('2d');
// Body
ctx.fillStyle = '#336';
ctx.fillRect(6, 10, 28, 10);
// Barrel
ctx.fillStyle = '#448';
ctx.fillRect(2, 12, 6, 6);
// Energy cell
ctx.fillStyle = '#08f';
ctx.fillRect(34, 8, 10, 14);
ctx.fillStyle = '#0af';
ctx.fillRect(36, 10, 6, 10);
// Glow
ctx.fillStyle = 'rgba(0, 150, 255, 0.3)';
ctx.beginPath();
ctx.arc(24, 16, 14, 0, Math.PI * 2);
ctx.fill();
return canvas;
},
// Barrel (explosive)
barrelSprite() {
const canvas = this.createCanvas(32, 48);
const ctx = canvas.getContext('2d');
// Barrel body
ctx.fillStyle = '#4a4';
ctx.fillRect(6, 8, 20, 36);
// Top
ctx.fillStyle = '#5b5';
ctx.beginPath();
ctx.ellipse(16, 10, 10, 4, 0, 0, Math.PI * 2);
ctx.fill();
// Hazard symbol
ctx.fillStyle = '#ff0';
ctx.beginPath();
ctx.moveTo(16, 18);
ctx.lineTo(22, 30);
ctx.lineTo(10, 30);
ctx.closePath();
ctx.fill();
ctx.fillStyle = '#000';
ctx.font = 'bold 8px sans-serif';
ctx.fillText('!', 14, 29);
// Bands
ctx.fillStyle = '#383';
ctx.fillRect(6, 14, 20, 2);
ctx.fillRect(6, 36, 20, 2);
return canvas;
},
// Lamp sprite
lampSprite() {
const canvas = this.createCanvas(16, 48);
const ctx = canvas.getContext('2d');
// Pole
ctx.fillStyle = '#666';
ctx.fillRect(6, 16, 4, 32);
// Base
ctx.fillStyle = '#555';
ctx.fillRect(2, 44, 12, 4);
// Light
ctx.fillStyle = '#ff8';
ctx.beginPath();
ctx.arc(8, 12, 6, 0, Math.PI * 2);
ctx.fill();
// Glow
ctx.fillStyle = 'rgba(255, 255, 100, 0.3)';
ctx.beginPath();
ctx.arc(8, 12, 10, 0, Math.PI * 2);
ctx.fill();
return canvas;
},
// Dead enemy sprite
deadSprite(color) {
const canvas = this.createCanvas(64, 32);
const ctx = canvas.getContext('2d');
ctx.fillStyle = color;
ctx.fillRect(8, 10, 48, 12);
// Blood pool
ctx.fillStyle = 'rgba(150, 0, 0, 0.6)';
ctx.beginPath();
ctx.ellipse(32, 22, 24, 8, 0, 0, Math.PI * 2);
ctx.fill();
return canvas;
}
};
const Sprites = {};
function initSprites() {
Sprites.imp = SpriteGen.impSprite();
Sprites.demon = SpriteGen.demonSprite();
Sprites.baron = SpriteGen.baronSprite();
Sprites.health = SpriteGen.healthPickup();
Sprites.armor = SpriteGen.armorPickup();
Sprites.ammo = SpriteGen.ammoPickup();
Sprites.shotgun_pickup = SpriteGen.shotgunPickup();
Sprites.plasma_pickup = SpriteGen.plasmaPickup();
Sprites.barrel = SpriteGen.barrelSprite();
Sprites.lamp = SpriteGen.lampSprite();
Sprites.deadImp = SpriteGen.deadSprite('#8B4513');
Sprites.deadDemon = SpriteGen.deadSprite('#C0392B');
Sprites.deadBaron = SpriteGen.deadSprite('#2E4A1E');
}