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>
254 lines
7.1 KiB
JavaScript
254 lines
7.1 KiB
JavaScript
// 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;
|
|
}
|
|
};
|