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>
325 lines
9.8 KiB
JavaScript
325 lines
9.8 KiB
JavaScript
// Procedural texture generation
|
|
const TextureGen = {
|
|
size: 64,
|
|
|
|
createCanvas() {
|
|
const canvas = document.createElement('canvas');
|
|
canvas.width = this.size;
|
|
canvas.height = this.size;
|
|
return canvas;
|
|
},
|
|
|
|
// Stone wall texture
|
|
stoneWall() {
|
|
const canvas = this.createCanvas();
|
|
const ctx = canvas.getContext('2d');
|
|
ctx.fillStyle = '#555';
|
|
ctx.fillRect(0, 0, 64, 64);
|
|
|
|
// Stone blocks
|
|
const blockColors = ['#4a4a4a', '#505050', '#585858', '#464646'];
|
|
for (let y = 0; y < 4; y++) {
|
|
for (let x = 0; x < 4; x++) {
|
|
const offset = (y % 2) ? 8 : 0;
|
|
ctx.fillStyle = blockColors[Math.floor(Math.random() * blockColors.length)];
|
|
ctx.fillRect(x * 16 + offset, y * 16, 15, 15);
|
|
ctx.fillStyle = '#333';
|
|
ctx.fillRect(x * 16 + offset, y * 16 + 15, 16, 1);
|
|
ctx.fillRect(x * 16 + offset + 15, y * 16, 1, 16);
|
|
}
|
|
}
|
|
// Noise
|
|
this.addNoise(ctx, 0.15);
|
|
return canvas;
|
|
},
|
|
|
|
// Brick wall texture
|
|
brickWall() {
|
|
const canvas = this.createCanvas();
|
|
const ctx = canvas.getContext('2d');
|
|
ctx.fillStyle = '#8b4513';
|
|
ctx.fillRect(0, 0, 64, 64);
|
|
|
|
for (let row = 0; row < 8; row++) {
|
|
const offset = (row % 2) ? 16 : 0;
|
|
for (let col = -1; col < 3; col++) {
|
|
const shade = 0.85 + Math.random() * 0.3;
|
|
const r = Math.floor(139 * shade);
|
|
const g = Math.floor(69 * shade);
|
|
const b = Math.floor(19 * shade);
|
|
ctx.fillStyle = `rgb(${r},${g},${b})`;
|
|
ctx.fillRect(col * 32 + offset + 1, row * 8 + 1, 30, 6);
|
|
}
|
|
// Mortar lines
|
|
ctx.fillStyle = '#666';
|
|
ctx.fillRect(0, row * 8, 64, 1);
|
|
}
|
|
this.addNoise(ctx, 0.1);
|
|
return canvas;
|
|
},
|
|
|
|
// Metal wall texture
|
|
metalWall() {
|
|
const canvas = this.createCanvas();
|
|
const ctx = canvas.getContext('2d');
|
|
ctx.fillStyle = '#667';
|
|
ctx.fillRect(0, 0, 64, 64);
|
|
|
|
// Panels
|
|
ctx.fillStyle = '#778';
|
|
ctx.fillRect(2, 2, 28, 28);
|
|
ctx.fillRect(34, 2, 28, 28);
|
|
ctx.fillRect(2, 34, 28, 28);
|
|
ctx.fillRect(34, 34, 28, 28);
|
|
|
|
// Rivets
|
|
ctx.fillStyle = '#99a';
|
|
for (const [x, y] of [[4,4],[28,4],[4,28],[28,28],[36,4],[60,4],[36,28],[60,28],[4,36],[28,36],[4,60],[28,60],[36,36],[60,36],[36,60],[60,60]]) {
|
|
ctx.fillRect(x, y, 2, 2);
|
|
}
|
|
|
|
// Highlights
|
|
ctx.fillStyle = 'rgba(255,255,255,0.1)';
|
|
ctx.fillRect(2, 2, 28, 1);
|
|
ctx.fillRect(34, 2, 28, 1);
|
|
ctx.fillRect(2, 34, 28, 1);
|
|
ctx.fillRect(34, 34, 28, 1);
|
|
|
|
this.addNoise(ctx, 0.08);
|
|
return canvas;
|
|
},
|
|
|
|
// Tech wall texture
|
|
techWall() {
|
|
const canvas = this.createCanvas();
|
|
const ctx = canvas.getContext('2d');
|
|
ctx.fillStyle = '#334';
|
|
ctx.fillRect(0, 0, 64, 64);
|
|
|
|
// Circuit pattern
|
|
ctx.strokeStyle = '#0a5';
|
|
ctx.lineWidth = 1;
|
|
ctx.beginPath();
|
|
ctx.moveTo(10, 0); ctx.lineTo(10, 20); ctx.lineTo(30, 20); ctx.lineTo(30, 40);
|
|
ctx.moveTo(50, 0); ctx.lineTo(50, 15); ctx.lineTo(35, 15); ctx.lineTo(35, 50);
|
|
ctx.moveTo(0, 45); ctx.lineTo(20, 45); ctx.lineTo(20, 64);
|
|
ctx.moveTo(45, 35); ctx.lineTo(64, 35);
|
|
ctx.moveTo(45, 55); ctx.lineTo(64, 55);
|
|
ctx.stroke();
|
|
|
|
// Nodes
|
|
ctx.fillStyle = '#0f8';
|
|
for (const [x, y] of [[10, 20],[30, 40],[50, 15],[35, 50],[20, 45]]) {
|
|
ctx.fillRect(x - 1, y - 1, 3, 3);
|
|
}
|
|
|
|
// Screen
|
|
ctx.fillStyle = '#041';
|
|
ctx.fillRect(42, 42, 18, 18);
|
|
ctx.fillStyle = '#0f4';
|
|
ctx.font = '8px monospace';
|
|
ctx.fillText('OK', 46, 54);
|
|
|
|
this.addNoise(ctx, 0.05);
|
|
return canvas;
|
|
},
|
|
|
|
// Door texture
|
|
doorTexture() {
|
|
const canvas = this.createCanvas();
|
|
const ctx = canvas.getContext('2d');
|
|
ctx.fillStyle = '#654';
|
|
ctx.fillRect(0, 0, 64, 64);
|
|
|
|
// Door panels
|
|
ctx.fillStyle = '#543';
|
|
ctx.fillRect(4, 4, 24, 26);
|
|
ctx.fillRect(36, 4, 24, 26);
|
|
ctx.fillRect(4, 34, 24, 26);
|
|
ctx.fillRect(36, 34, 24, 26);
|
|
|
|
// Door frame highlights
|
|
ctx.fillStyle = '#876';
|
|
ctx.fillRect(0, 0, 64, 2);
|
|
ctx.fillRect(0, 0, 2, 64);
|
|
ctx.fillStyle = '#432';
|
|
ctx.fillRect(0, 62, 64, 2);
|
|
ctx.fillRect(62, 0, 2, 64);
|
|
|
|
// Handle
|
|
ctx.fillStyle = '#aa8';
|
|
ctx.fillRect(52, 30, 4, 6);
|
|
|
|
this.addNoise(ctx, 0.1);
|
|
return canvas;
|
|
},
|
|
|
|
// Exit texture
|
|
exitTexture() {
|
|
const canvas = this.createCanvas();
|
|
const ctx = canvas.getContext('2d');
|
|
ctx.fillStyle = '#300';
|
|
ctx.fillRect(0, 0, 64, 64);
|
|
|
|
// Frame
|
|
ctx.fillStyle = '#f00';
|
|
ctx.fillRect(0, 0, 64, 3);
|
|
ctx.fillRect(0, 61, 64, 3);
|
|
ctx.fillRect(0, 0, 3, 64);
|
|
ctx.fillRect(61, 0, 3, 64);
|
|
|
|
// EXIT text
|
|
ctx.fillStyle = '#f00';
|
|
ctx.font = 'bold 14px monospace';
|
|
ctx.textAlign = 'center';
|
|
ctx.fillText('EXIT', 32, 37);
|
|
|
|
// Glow
|
|
ctx.fillStyle = 'rgba(255, 0, 0, 0.15)';
|
|
ctx.fillRect(5, 5, 54, 54);
|
|
|
|
return canvas;
|
|
},
|
|
|
|
// Wood texture
|
|
woodWall() {
|
|
const canvas = this.createCanvas();
|
|
const ctx = canvas.getContext('2d');
|
|
ctx.fillStyle = '#7a5230';
|
|
ctx.fillRect(0, 0, 64, 64);
|
|
|
|
// Wood grain
|
|
ctx.strokeStyle = 'rgba(0,0,0,0.15)';
|
|
ctx.lineWidth = 1;
|
|
for (let i = 0; i < 20; i++) {
|
|
const y = Math.random() * 64;
|
|
ctx.beginPath();
|
|
ctx.moveTo(0, y);
|
|
ctx.bezierCurveTo(20, y + (Math.random() - 0.5) * 8, 40, y + (Math.random() - 0.5) * 8, 64, y + (Math.random() - 0.5) * 4);
|
|
ctx.stroke();
|
|
}
|
|
|
|
// Plank dividers
|
|
ctx.fillStyle = '#5a3a1a';
|
|
ctx.fillRect(0, 15, 64, 2);
|
|
ctx.fillRect(0, 31, 64, 2);
|
|
ctx.fillRect(0, 47, 64, 2);
|
|
|
|
this.addNoise(ctx, 0.1);
|
|
return canvas;
|
|
},
|
|
|
|
// Hell texture
|
|
hellWall() {
|
|
const canvas = this.createCanvas();
|
|
const ctx = canvas.getContext('2d');
|
|
ctx.fillStyle = '#411';
|
|
ctx.fillRect(0, 0, 64, 64);
|
|
|
|
// Lava cracks
|
|
ctx.strokeStyle = '#f50';
|
|
ctx.lineWidth = 2;
|
|
ctx.beginPath();
|
|
ctx.moveTo(10, 0); ctx.lineTo(15, 20); ctx.lineTo(8, 40); ctx.lineTo(20, 64);
|
|
ctx.moveTo(40, 0); ctx.lineTo(35, 25); ctx.lineTo(50, 45); ctx.lineTo(45, 64);
|
|
ctx.stroke();
|
|
|
|
ctx.strokeStyle = '#fa0';
|
|
ctx.lineWidth = 1;
|
|
ctx.beginPath();
|
|
ctx.moveTo(10, 0); ctx.lineTo(15, 20); ctx.lineTo(8, 40); ctx.lineTo(20, 64);
|
|
ctx.moveTo(40, 0); ctx.lineTo(35, 25); ctx.lineTo(50, 45); ctx.lineTo(45, 64);
|
|
ctx.stroke();
|
|
|
|
// Skulls/faces suggestion
|
|
ctx.fillStyle = '#633';
|
|
ctx.beginPath();
|
|
ctx.arc(32, 32, 8, 0, Math.PI * 2);
|
|
ctx.fill();
|
|
ctx.fillStyle = '#200';
|
|
ctx.fillRect(28, 30, 3, 3);
|
|
ctx.fillRect(34, 30, 3, 3);
|
|
ctx.fillRect(30, 36, 5, 2);
|
|
|
|
this.addNoise(ctx, 0.15);
|
|
return canvas;
|
|
},
|
|
|
|
// Floor texture
|
|
floorTexture() {
|
|
const canvas = this.createCanvas();
|
|
const ctx = canvas.getContext('2d');
|
|
ctx.fillStyle = '#333';
|
|
ctx.fillRect(0, 0, 64, 64);
|
|
|
|
// Tile pattern
|
|
ctx.fillStyle = '#3a3a3a';
|
|
ctx.fillRect(0, 0, 31, 31);
|
|
ctx.fillRect(33, 33, 31, 31);
|
|
ctx.fillStyle = '#2e2e2e';
|
|
ctx.fillRect(33, 0, 31, 31);
|
|
ctx.fillRect(0, 33, 31, 31);
|
|
|
|
ctx.fillStyle = '#282828';
|
|
ctx.fillRect(31, 0, 2, 64);
|
|
ctx.fillRect(0, 31, 64, 2);
|
|
|
|
this.addNoise(ctx, 0.1);
|
|
return canvas;
|
|
},
|
|
|
|
// Ceiling texture
|
|
ceilingTexture() {
|
|
const canvas = this.createCanvas();
|
|
const ctx = canvas.getContext('2d');
|
|
ctx.fillStyle = '#222';
|
|
ctx.fillRect(0, 0, 64, 64);
|
|
|
|
// Light panel effect
|
|
ctx.fillStyle = '#2a2a2a';
|
|
ctx.fillRect(8, 8, 48, 48);
|
|
ctx.fillStyle = '#252525';
|
|
ctx.fillRect(16, 16, 32, 32);
|
|
|
|
this.addNoise(ctx, 0.08);
|
|
return canvas;
|
|
},
|
|
|
|
addNoise(ctx, intensity) {
|
|
const imageData = ctx.getImageData(0, 0, this.size, this.size);
|
|
const data = imageData.data;
|
|
for (let i = 0; i < data.length; i += 4) {
|
|
const noise = (Math.random() - 0.5) * 255 * intensity;
|
|
data[i] = Math.max(0, Math.min(255, data[i] + noise));
|
|
data[i + 1] = Math.max(0, Math.min(255, data[i + 1] + noise));
|
|
data[i + 2] = Math.max(0, Math.min(255, data[i + 2] + noise));
|
|
}
|
|
ctx.putImageData(imageData, 0, 0);
|
|
}
|
|
};
|
|
|
|
// Generate all textures at startup
|
|
const Textures = {};
|
|
|
|
function initTextures() {
|
|
Textures[WALL_STONE] = TextureGen.stoneWall();
|
|
Textures[WALL_BRICK] = TextureGen.brickWall();
|
|
Textures[WALL_METAL] = TextureGen.metalWall();
|
|
Textures[WALL_TECH] = TextureGen.techWall();
|
|
Textures[WALL_DOOR] = TextureGen.doorTexture();
|
|
Textures[WALL_EXIT] = TextureGen.exitTexture();
|
|
Textures[WALL_WOOD] = TextureGen.woodWall();
|
|
Textures[WALL_HELL] = TextureGen.hellWall();
|
|
Textures.floor = TextureGen.floorTexture();
|
|
Textures.ceiling = TextureGen.ceilingTexture();
|
|
|
|
// Pre-extract image data for fast pixel access
|
|
Textures.data = {};
|
|
for (const key of Object.keys(Textures)) {
|
|
if (key === 'data') continue;
|
|
const canvas = Textures[key];
|
|
const ctx = canvas.getContext('2d');
|
|
Textures.data[key] = ctx.getImageData(0, 0, 64, 64).data;
|
|
}
|
|
}
|