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>
This commit is contained in:
+253
@@ -0,0 +1,253 @@
|
||||
// Procedural audio using Web Audio API
|
||||
const Audio = {
|
||||
ctx: null,
|
||||
enabled: true,
|
||||
|
||||
init() {
|
||||
try {
|
||||
this.ctx = new (window.AudioContext || window.webkitAudioContext)();
|
||||
} catch (e) {
|
||||
this.enabled = false;
|
||||
}
|
||||
},
|
||||
|
||||
resume() {
|
||||
if (this.ctx && this.ctx.state === 'suspended') {
|
||||
this.ctx.resume();
|
||||
}
|
||||
},
|
||||
|
||||
// Pistol shot - short, sharp
|
||||
playPistol() {
|
||||
if (!this.enabled || !this.ctx) return;
|
||||
const ctx = this.ctx;
|
||||
const now = ctx.currentTime;
|
||||
|
||||
const osc = ctx.createOscillator();
|
||||
const gain = ctx.createGain();
|
||||
const noise = this.createNoise(0.08);
|
||||
|
||||
osc.type = 'square';
|
||||
osc.frequency.setValueAtTime(150, now);
|
||||
osc.frequency.exponentialRampToValueAtTime(50, now + 0.08);
|
||||
|
||||
gain.gain.setValueAtTime(0.3, now);
|
||||
gain.gain.exponentialRampToValueAtTime(0.001, now + 0.1);
|
||||
|
||||
osc.connect(gain);
|
||||
gain.connect(ctx.destination);
|
||||
noise.connect(ctx.destination);
|
||||
|
||||
osc.start(now);
|
||||
osc.stop(now + 0.1);
|
||||
},
|
||||
|
||||
// Shotgun blast - loud, wide
|
||||
playShotgun() {
|
||||
if (!this.enabled || !this.ctx) return;
|
||||
const ctx = this.ctx;
|
||||
const now = ctx.currentTime;
|
||||
|
||||
const noise = this.createNoise(0.2);
|
||||
const osc = ctx.createOscillator();
|
||||
const gain = ctx.createGain();
|
||||
|
||||
osc.type = 'sawtooth';
|
||||
osc.frequency.setValueAtTime(100, now);
|
||||
osc.frequency.exponentialRampToValueAtTime(30, now + 0.15);
|
||||
|
||||
gain.gain.setValueAtTime(0.4, now);
|
||||
gain.gain.exponentialRampToValueAtTime(0.001, now + 0.2);
|
||||
|
||||
osc.connect(gain);
|
||||
gain.connect(ctx.destination);
|
||||
noise.connect(ctx.destination);
|
||||
|
||||
osc.start(now);
|
||||
osc.stop(now + 0.2);
|
||||
},
|
||||
|
||||
// Plasma shot - sci-fi energy
|
||||
playPlasma() {
|
||||
if (!this.enabled || !this.ctx) return;
|
||||
const ctx = this.ctx;
|
||||
const now = ctx.currentTime;
|
||||
|
||||
const osc = ctx.createOscillator();
|
||||
const osc2 = ctx.createOscillator();
|
||||
const gain = ctx.createGain();
|
||||
|
||||
osc.type = 'sine';
|
||||
osc.frequency.setValueAtTime(800, now);
|
||||
osc.frequency.exponentialRampToValueAtTime(200, now + 0.15);
|
||||
|
||||
osc2.type = 'sine';
|
||||
osc2.frequency.setValueAtTime(1200, now);
|
||||
osc2.frequency.exponentialRampToValueAtTime(300, now + 0.12);
|
||||
|
||||
gain.gain.setValueAtTime(0.2, now);
|
||||
gain.gain.exponentialRampToValueAtTime(0.001, now + 0.15);
|
||||
|
||||
osc.connect(gain);
|
||||
osc2.connect(gain);
|
||||
gain.connect(ctx.destination);
|
||||
|
||||
osc.start(now);
|
||||
osc.stop(now + 0.15);
|
||||
osc2.start(now);
|
||||
osc2.stop(now + 0.15);
|
||||
},
|
||||
|
||||
// Enemy hurt
|
||||
playEnemyHurt() {
|
||||
if (!this.enabled || !this.ctx) return;
|
||||
const ctx = this.ctx;
|
||||
const now = ctx.currentTime;
|
||||
|
||||
const osc = ctx.createOscillator();
|
||||
const gain = ctx.createGain();
|
||||
|
||||
osc.type = 'sawtooth';
|
||||
osc.frequency.setValueAtTime(200, now);
|
||||
osc.frequency.exponentialRampToValueAtTime(80, now + 0.15);
|
||||
|
||||
gain.gain.setValueAtTime(0.15, now);
|
||||
gain.gain.exponentialRampToValueAtTime(0.001, now + 0.15);
|
||||
|
||||
osc.connect(gain);
|
||||
gain.connect(ctx.destination);
|
||||
osc.start(now);
|
||||
osc.stop(now + 0.15);
|
||||
},
|
||||
|
||||
// Enemy death
|
||||
playEnemyDeath() {
|
||||
if (!this.enabled || !this.ctx) return;
|
||||
const ctx = this.ctx;
|
||||
const now = ctx.currentTime;
|
||||
|
||||
const osc = ctx.createOscillator();
|
||||
const gain = ctx.createGain();
|
||||
|
||||
osc.type = 'sawtooth';
|
||||
osc.frequency.setValueAtTime(300, now);
|
||||
osc.frequency.exponentialRampToValueAtTime(30, now + 0.4);
|
||||
|
||||
gain.gain.setValueAtTime(0.2, now);
|
||||
gain.gain.exponentialRampToValueAtTime(0.001, now + 0.4);
|
||||
|
||||
osc.connect(gain);
|
||||
gain.connect(ctx.destination);
|
||||
osc.start(now);
|
||||
osc.stop(now + 0.4);
|
||||
},
|
||||
|
||||
// Player hurt
|
||||
playPlayerHurt() {
|
||||
if (!this.enabled || !this.ctx) return;
|
||||
const ctx = this.ctx;
|
||||
const now = ctx.currentTime;
|
||||
|
||||
const osc = ctx.createOscillator();
|
||||
const gain = ctx.createGain();
|
||||
|
||||
osc.type = 'square';
|
||||
osc.frequency.setValueAtTime(120, now);
|
||||
osc.frequency.exponentialRampToValueAtTime(60, now + 0.2);
|
||||
|
||||
gain.gain.setValueAtTime(0.2, now);
|
||||
gain.gain.exponentialRampToValueAtTime(0.001, now + 0.2);
|
||||
|
||||
osc.connect(gain);
|
||||
gain.connect(ctx.destination);
|
||||
osc.start(now);
|
||||
osc.stop(now + 0.2);
|
||||
},
|
||||
|
||||
// Pickup sound
|
||||
playPickup() {
|
||||
if (!this.enabled || !this.ctx) return;
|
||||
const ctx = this.ctx;
|
||||
const now = ctx.currentTime;
|
||||
|
||||
const osc = ctx.createOscillator();
|
||||
const gain = ctx.createGain();
|
||||
|
||||
osc.type = 'sine';
|
||||
osc.frequency.setValueAtTime(400, now);
|
||||
osc.frequency.setValueAtTime(600, now + 0.05);
|
||||
osc.frequency.setValueAtTime(800, now + 0.1);
|
||||
|
||||
gain.gain.setValueAtTime(0.15, now);
|
||||
gain.gain.exponentialRampToValueAtTime(0.001, now + 0.15);
|
||||
|
||||
osc.connect(gain);
|
||||
gain.connect(ctx.destination);
|
||||
osc.start(now);
|
||||
osc.stop(now + 0.15);
|
||||
},
|
||||
|
||||
// Door open
|
||||
playDoorOpen() {
|
||||
if (!this.enabled || !this.ctx) return;
|
||||
const ctx = this.ctx;
|
||||
const now = ctx.currentTime;
|
||||
|
||||
const osc = ctx.createOscillator();
|
||||
const gain = ctx.createGain();
|
||||
|
||||
osc.type = 'triangle';
|
||||
osc.frequency.setValueAtTime(80, now);
|
||||
osc.frequency.linearRampToValueAtTime(120, now + 0.3);
|
||||
|
||||
gain.gain.setValueAtTime(0.1, now);
|
||||
gain.gain.exponentialRampToValueAtTime(0.001, now + 0.3);
|
||||
|
||||
osc.connect(gain);
|
||||
gain.connect(ctx.destination);
|
||||
osc.start(now);
|
||||
osc.stop(now + 0.3);
|
||||
},
|
||||
|
||||
// Explosion
|
||||
playExplosion() {
|
||||
if (!this.enabled || !this.ctx) return;
|
||||
const ctx = this.ctx;
|
||||
const now = ctx.currentTime;
|
||||
|
||||
const noise = this.createNoise(0.4);
|
||||
const osc = ctx.createOscillator();
|
||||
const gain = ctx.createGain();
|
||||
|
||||
osc.type = 'sawtooth';
|
||||
osc.frequency.setValueAtTime(60, now);
|
||||
osc.frequency.exponentialRampToValueAtTime(20, now + 0.4);
|
||||
|
||||
gain.gain.setValueAtTime(0.3, now);
|
||||
gain.gain.exponentialRampToValueAtTime(0.001, now + 0.4);
|
||||
|
||||
osc.connect(gain);
|
||||
gain.connect(ctx.destination);
|
||||
noise.connect(ctx.destination);
|
||||
|
||||
osc.start(now);
|
||||
osc.stop(now + 0.4);
|
||||
},
|
||||
|
||||
createNoise(duration) {
|
||||
const ctx = this.ctx;
|
||||
const bufferSize = ctx.sampleRate * duration;
|
||||
const buffer = ctx.createBuffer(1, bufferSize, ctx.sampleRate);
|
||||
const data = buffer.getChannelData(0);
|
||||
for (let i = 0; i < bufferSize; i++) {
|
||||
data[i] = (Math.random() * 2 - 1) * Math.max(0, 1 - i / bufferSize);
|
||||
}
|
||||
const source = ctx.createBufferSource();
|
||||
source.buffer = buffer;
|
||||
const gain = ctx.createGain();
|
||||
gain.gain.value = 0.15;
|
||||
source.connect(gain);
|
||||
source.start();
|
||||
return gain;
|
||||
}
|
||||
};
|
||||
Reference in New Issue
Block a user